| // Javascript implementation pf Kohonen's Self Organizing Map |
| // Based on http://www.ai-junkie.com/ann/som/som1.html |
| |
| var mapWidth = 800, |
| mapHeight = 800; |
| |
| function getDistance(weight, inputVector) { |
| var distance = 0; |
| for (var i = 0; i <weight.length; i++) { |
| distance += (inputVector[i] - weight[i]) * (inputVector[i] - weight[i]); |
| } |
| return Math.sqrt(distance); |
| } |
| |
| function makeRandomWeights(vSize, eSize) { |
| var weights = []; |
| if(typeof ArrayBuffer === 'undefined') { |
| // lacking browser support |
| while (weights.length < vSize) { |
| var arr = new Array(eSize); |
| for(var i = 0; i < eSize; i++) { arr[i]= Math.random(); } |
| weights.push(arr); |
| } |
| } else { |
| while (weights.length < vSize) { |
| var arr = new Float64Array(eSize); |
| for(var i = 0; i < eSize; i++) { arr[i]= Math.random(); } |
| weights.push(arr); |
| } |
| } |
| return weights; |
| } |
| |
| function getBMUIndex(weights, target) { |
| var BMUIndex = 0; |
| var bestScore = 99999; |
| |
| for (i=0; i < weights.length; i++) { |
| distance = getDistance(weights[i], target); |
| if (distance < bestScore) { |
| bestScore = distance; |
| BMUIndex = i; |
| } |
| } |
| return BMUIndex; |
| } |
| |
| function convertIndexToXY(idx, dimW) { |
| var x = parseInt(idx % dimW,10); |
| var y = parseInt((idx / dimW),10); |
| return [x,y]; |
| } |
| |
| |
| function getEucledianDistance(coord1, coord2) { |
| return (coord1[0] - coord2[0]) * (coord1[0] - coord2[0]) + (coord1[1] - coord2[1]) * (coord1[1] - coord2[1]); |
| } |
| |
| // utilitity that creates contiguous vector of zeros of size n |
| var zeros = function(n) { |
| if(typeof(n)==='undefined' || isNaN(n)) { return []; } |
| if(typeof ArrayBuffer === 'undefined') { |
| // lacking browser support |
| var arr = new Array(n); |
| for(var i=0;i<n;i++) { arr[i]= 0; } |
| return arr; |
| } else { |
| return new Float64Array(n); // typed arrays are faster |
| } |
| } |
| |
| // compute L2 distance between two vectors |
| var L2 = function(x1, x2) { |
| var D = x1.length; |
| var d = 0; |
| for(var i=0;i<D;i++) { |
| var x1i = x1[i]; |
| var x2i = x2[i]; |
| d += (x1i-x2i)*(x1i-x2i); |
| } |
| return d; |
| } |
| |
| // compute pairwise distance in all vectors in X |
| var xtod = function(X) { |
| var N = X.length; |
| var dist = zeros(N * N); // allocate contiguous array |
| for(var i=0;i<N;i++) { |
| for(var j=i+1;j<N;j++) { |
| var d = L2(X[i], X[j]); |
| dist[i*N+j] = d; |
| dist[j*N+i] = d; |
| } |
| } |
| return dist; |
| } |
| |
| function dotproduct(a,b) { |
| var n = 0, lim = Math.min(a.length,b.length); |
| for (var i = 0; i < lim; i++) n += a[i] * b[i]; |
| return n; |
| } |
| |
| function vecsum(a,b) { |
| var lim = a.length; |
| var sum = new Array(lim); |
| for (var i = 0; i < lim; i++) sum[i] = a[i] + b[i]; |
| return sum; |
| } |
| |
| function norm2(a) {var sumsqr = 0; for (var i = 0; i < a.length; i++) sumsqr += a[i]*a[i]; return Math.sqrt(sumsqr);} |
| |
| function cosine_sim(x, y) { |
| xnorm = norm2(x); |
| if(!xnorm) return 0; |
| ynorm = norm2(y); |
| if(!ynorm) return 0; |
| return dotproduct(x, y) / (xnorm * ynorm); |
| } |
| |
| function makeSOM(data, training_iterations) { |
| var dimW = 6; |
| var dimH = 6; |
| |
| var radius = (dimW * dimH) / 2; |
| var learning_rate = 1; |
| var time_constant = training_iterations / Math.log(radius); |
| var inputs = xtod(data.vecs); |
| var dimI = data.vecs.length; |
| var weights = makeRandomWeights(dimW * dimH, dimI); |
| var radius_decaying = 0; |
| var learning_rate_decaying = 0; |
| var svg; |
| var no_targets = (data.target.match(/.[ |]+./g) || []).length+1; |
| // var avg, avgsim1, avgsim2, minsim; |
| var refIndex; |
| var colorScale; |
| var urlprefix = new URLSearchParams(window.location.search); |
| urlprefix.delete("word"); |
| urlprefix.append("word",""); |
| |
| if(no_targets > 1) { |
| refIndex=1; |
| colorScale = d3.scale.linear() |
| .range(['green', 'yellow', 'red']) // or use hex values |
| .domain([-1, 0, 1]); |
| |
| // avg = vecsum(inputs.slice(0, dimI), inputs.slice(dimI, 2*dimI)); |
| // avgsim1 = cosine_sim(inputs.slice(0, dimI), avg); |
| // avgsim2 = cosine_sim(inputs.slice(dimI, 2*dimI), avg); |
| |
| $("#somcolor2").css("background-color", colorScale(0)); |
| $("#somcolor1").css("background-color", colorScale(-1)); |
| $("#somcolor3").css("background-color", colorScale(1)); |
| } else { |
| refIndex = data.words.length-1; |
| colorScale = d3.scale.linear() |
| .range(['white', 'red']) |
| .domain([-1, 1]); |
| $("#somcolor1").css("background-color", colorScale(1)); |
| $("#somcolor3").css("background-color", colorScale(-1)); |
| } |
| |
| $("#somword1").html(data.words[0]); |
| $("#somword2").html(data.words[refIndex]); |
| minsim = cosine_sim(inputs.slice(0, dimI), inputs.slice(refIndex*dimI, (refIndex+1)*dimI)); |
| |
| var itdom = document.getElementById("iterations"); |
| |
| var div = d3.select("#som2"); |
| |
| data.coords = []; |
| for(var i=0; i< data.words.length; i++) { |
| data.coords[i] = [Math.floor(dimW/2), Math.floor(dimH/2)]; |
| } |
| |
| svg = div.append("svg") |
| .attr("width", mapWidth) |
| .attr("height", mapHeight); |
| |
| var rects = svg.selectAll(".r") |
| .data(weights) |
| .enter().append("rect") |
| .attr("class", "r") |
| .attr("width", mapWidth/dimW) |
| .attr("height", mapHeight/dimH) |
| .attr("fill", "white") |
| .attr("z-index", "-1") |
| .attr("x", function(d, i) { return (i % dimW) * (mapWidth/dimW);}) |
| .attr("y", function(d, i) { return (Math.floor(i / dimW) * (mapWidth/dimW)); }) |
| |
| |
| var g = svg.selectAll(".b") |
| .data(data.words) |
| .enter().append("g") |
| .attr("class", "u"); |
| g.append("a") |
| .attr("xlink:href", function(word) {return "?"+urlprefix+word;}) |
| .attr("title", function(d, i) { |
| return "rank: "+i +" "+"freq. rank: "+data.ranks[i].toString().replace(/\B(?=(\d{3})+(?!\d))/g, ","); |
| }) |
| .append("text") |
| .attr("text-anchor", "bottom") |
| .attr("font-size", 12) |
| .attr("fill", function(d) { |
| if(data.target.indexOf(" "+d+" ") >= 0) { |
| return "blue"; |
| } else { |
| return "#333" |
| } |
| }) |
| .text(function(d) { return d; }); |
| |
| var som_interval = setInterval(somStep, 0); |
| var it=0; |
| |
| function updateSOM() { |
| var oc = []; |
| for(var x = 0; x < dimW; x++) { |
| for(var y = 0; y < dimH; y++) { |
| oc[y*dimW+x]=1; |
| } |
| } |
| svg.selectAll('.u') |
| .data(data.coords) |
| .transition() |
| .attr("transform", function(d, i) { |
| return "translate(" + |
| (d[0]*(mapWidth/dimW)+4) + "," + |
| (d[1]*(mapHeight/dimH)+oc[d[1]*dimW+d[0]]++*14+4) + ")"; }); |
| |
| var colorFun = function(d, i) { |
| var sim1=cosine_sim(d, inputs.slice(0, dimI)); |
| var sim2=cosine_sim(d, inputs.slice(dimI, 2*dimI)); |
| var col; |
| // col = (sim1-avgsim1)/(1-avgsim1)-(sim2-avgsim2)/(1-avgsim2); |
| col = (sim2-sim1)/(1-minsim); |
| // console.log(Math.floor(i/dimW)+","+i%dimW+":"+(sim1-minsim)/(1-minsim)+ " " + (sim2-minsim)/(1-minsim) + "--> "+ col); |
| if(col > 1) col=1; |
| if(col < -1) col=-1; |
| return colorScale(col); |
| }; |
| |
| if(it>training_iterations*.6) { |
| svg.selectAll(".r") |
| .data(weights) |
| .transition() |
| .attr("fill", colorFun); |
| } |
| } |
| |
| function somStep() { |
| if(it++ >= training_iterations) { |
| updateSOM(); |
| clearInterval(som_interval); |
| return; |
| } |
| itdom.innerHTML = it; |
| radius_decaying = radius * Math.exp(-it/time_constant); |
| learning_rate_decaying = learning_rate * Math.exp(-it/time_constant); |
| //learning_rate_decaying = learning_rate * Math.exp(-it/training_iterations); |
| |
| //pick a random input to train |
| var current=Math.floor(Math.random()*dimI) |
| var iv = inputs.slice(current*dimI, (current+1)*dimI); |
| // Determine the BMU |
| BMUIdx = getBMUIndex(weights, iv); |
| var coord1 = convertIndexToXY(BMUIdx, dimW); |
| data.coords[current] = coord1; |
| var widthSq = radius_decaying * radius_decaying; |
| for (var v in weights) { |
| var coord2 = convertIndexToXY(v, dimW); |
| var dist = getEucledianDistance(coord1, coord2); |
| // Determine if the weight is within the training radius |
| if (dist < widthSq) { |
| // console.log(dist, learning_rate_decaying, radius_decaying, it); |
| influence = Math.exp(-dist/(2*widthSq)); |
| for (vidx = 0;vidx<weights[v].length;vidx++) { |
| weights[v][vidx] += influence * learning_rate_decaying * (iv[vidx] - weights[v][vidx]); |
| } |
| } |
| } |
| // } |
| if(it % 10 == 0) { |
| updateSOM(); |
| } |
| } |
| |
| } |