block by fil 5724c516d6e51f4a2aaf0edae83e0dca

t-SNE, a force, and voronoi

Full Screen

This is a t-SNE representation of an array of (random) circles : red, green, blue, opacity and radius are 5 independent dimensions.

The t-SNE is computed by a javascript worker, using science.ai’s implementation.

A force is created that tries to:

The result is displayed with a Voronoi.

(A mix of t-SNE and a force and t-SNE Voronoi.)

index.html

<!DOCTYPE html>
<head>
  <meta charset="utf-8">
  <script src="https://d3js.org/d3.v4.min.js"></script>
  <style>
    body { margin:0;position:fixed;top:0;right:0;bottom:0;left:0; }
  </style>
</head>

<body>
  <script> 
const maxIter = 500,
    width = 400,
    x = d3.scaleLinear()
    .domain([-200, 200])
    .range([0, width]),
    area = d3.area()
       .x((d,i) => i)
       .y0(width+90)
       .y1((d,i) => width + 90 - parseInt(3 * d||0));


d3.queue()
    .defer(d3.text, 'tsne.min.js')
    .defer(d3.text, 'worker.js')
    .await(function (err, t, w) {

        const worker = new Worker(window.URL.createObjectURL(new Blob([t + w], {
            type: "text/javascript"
        })));

        const data = d3.range(300).map(d => [Math.random(), Math.random(), Math.random(), Math.random(), Math.random()]);

        const svg = d3.select('body').append('svg')
            .attr('width', width + 100)
            .attr('height', width + 100);

        const polygons = svg.selectAll('path')
            .data(data)
            .enter()
            .append('path')
            .attr('d', null)
            .attr('stroke-width', 0.3)
            .attr('fill', d => d3.rgb(d[0] * 256, d[1] * 256, d[2] * 256))
            .attr('stroke', d => d3.rgb(d[0] * 256, d[1] * 256, d[2] * 256))
            .attr('opacity', d => 0.5 + 0.5 * d[3]);
  
        const voronoi = d3.voronoi().extent([[-1, -1], [401, 401]]);

        const cost = svg.append('path')
            .attr('fill', '#aaa');            

        let pos = data.map(d => [Math.random() - 0.5, Math.random() - 0.5]);
        let costs = [];
  
        let s = 0, c = 1;

        const forcetsne = d3.forceSimulation(data)
        .alphaDecay(0)
        .alpha(0.1)
        .force('tsne', function(alpha){
          data.forEach((d,i) => {
            d.x += alpha * (150*pos[i][0] - d.x);
            d.y += alpha * (150*pos[i][1] - d.y);
          });
        })
        .force('collide', d3.forceCollide().radius(d => 1 + d.r))
        .on('tick', function () {
            let p = voronoi.polygons(data.map(d => [x(d.x), x(d.y)]));
            polygons.attr('d', (d, i) => p[i] ? "M" + (p[i]).join("L") + "Z" : null);
          // debug: show costs graph
          // cost.attr('d', area(costs));
        });

        
        worker.onmessage = function (e) {
            if (e.data.pos) pos = e.data.pos;
            if (e.data.iterations) { 
              costs[e.data.iterations] = e.data.cost;
            }
            if (e.data.stop) { 
              console.log("stopped with message", e.data);
              forcetsne.alphaDecay(0.02);
              worker.terminate();
            }
        };
        worker.postMessage({
            nIter: maxIter,
            // dim: 2,
            perplexity: 20.0,
            // earlyExaggeration: 4.0,
            // learningRate: 100.0,
            metric: 'manhattan', //'euclidean',
            data: data
        });

    });
  </script>
</body>

worker.js

self.onmessage = function(e){
  let msg = e.data,
      currcost = 100;
  
  let model = new TSNE({
  dim: msg.dim || 2,
  perplexity: msg.perplexity || 100.0,
  earlyExaggeration: msg.earlyExaggeration || 4.0,
  learningRate: msg.learningRate || 100.0,
  nIter: msg.nIter || 500,
  metric: msg.metric || 'euclidean'
  });

 model.init({
  data: msg.data,
  type: 'dense'
 });

 model.on('progressData', function(pos){
  self.postMessage({pos: model.getOutputScaled()});
 });

 model.on('progressIter', function (iter) {
  currcost = currcost * 0.9 + iter[1];
  self.postMessage({
    iterations: iter[0],
    cost: iter[1],
    stop: currcost < 20
   });
  });
 
  let run = model.run();

  self.postMessage({
    err: run[0],
    iterations: run[1],
    stop: true
  });

};