|
|
|
function animated_learning() { |
|
|
|
var svg = d3.select("body").select("svg") |
|
|
|
var x = d3.scaleLinear().range([200,500]).domain([0,1]) |
|
|
|
var learning_rate = 0.2 |
|
|
|
var n_As = 10 |
|
var n_Bs = 10 |
|
var n_pairs = 8 |
|
|
|
// create elements with latent values and initial positions |
|
|
|
var As = [] |
|
for (var i=0; i < n_As; i++){ |
|
As.push({id: i, latent_value: Math.random(), current_position: 0.5, next_position: 0.5}) |
|
} |
|
|
|
var Bs = [] |
|
for (var i=0; i < n_Bs; i++){ |
|
Bs.push({id: i, latent_value: Math.random(), current_position: 0.5, next_position: 0.5}) |
|
} |
|
|
|
// construct circles |
|
|
|
svg.selectAll(".A").data(As, function(d){ return d.id; }) |
|
.enter().append("circle") |
|
.attr("class", "A A-color") |
|
.attr("cx", 350) |
|
.attr("cy", 230) |
|
.attr("r", 3) |
|
|
|
svg.selectAll(".B").data(Bs, function(d){ return d.id; }) |
|
.enter().append("circle") |
|
.attr("class", "B B-color") |
|
.attr("cx", 350) |
|
.attr("cy", 270) |
|
.attr("r", 3) |
|
|
|
|
|
// simulation / animation loop |
|
|
|
d3.interval(function(){ |
|
|
|
// *** SIMULATION *** |
|
|
|
var pairs = [] |
|
|
|
for (var i=0; i < n_pairs; i++) { |
|
var A_id = Math.floor(Math.random()*n_As) |
|
|
|
// pair selection a stochastic function of distance from respective current positions |
|
var weights = Bs.map(function(d){ return d.current_position - As[A_id].current_position; }) |
|
var cum_weights = [] |
|
weights.reduce(function(a,b,i) { cum_weights[i] = {v: a+b, id:i}; return a + b; },0) |
|
cum_weights = cum_weights.sort(function(a,b){ return a.v > b.v; }) |
|
var B_id = Math.floor(Math.random()*n_Bs) |
|
if (cum_weights[cum_weights.length - 1].v != 0) { |
|
var sel_random = Math.random() * cum_weights[cum_weights.length - 1].v |
|
var sel = cum_weights.find(function(d){ return d.v >= sel_random; }) |
|
if (!(sel == null)) { |
|
B_id = sel.id |
|
} |
|
} |
|
|
|
pairs.push({A_id: A_id, B_id: B_id}) |
|
|
|
// big = 1, small = -1 |
|
var feedback = -1 + 2 * (As[A_id].latent_value > Bs[B_id].latent_value) |
|
|
|
// use feedback if it contradicts current |
|
if ((feedback == -1) && (As[A_id].current_position <= Bs[B_id].current_position)) { |
|
As[A_id].next_position = As[A_id].current_position + Math.random() * learning_rate |
|
Bs[B_id].next_position = Bs[B_id].current_position - Math.random() * learning_rate |
|
} |
|
|
|
if ((feedback == 1) && (As[A_id].current_position >= Bs[B_id].current_position)) { |
|
As[A_id].next_position = As[A_id].current_position - Math.random() * learning_rate |
|
Bs[B_id].next_position = Bs[B_id].current_position + Math.random() * learning_rate |
|
} |
|
|
|
As[A_id].next_position = Math.min(1, Math.max(0, As[A_id].next_position)) |
|
Bs[B_id].next_position = Math.min(1, Math.max(0, Bs[B_id].next_position)) |
|
|
|
} |
|
|
|
// *** SVG ANIMATION *** |
|
|
|
var delay = 400 |
|
var move = 500 |
|
|
|
// draw and animate pair lines |
|
|
|
svg.selectAll(".pair").remove() |
|
|
|
svg.selectAll(".pair").data(pairs).enter().append("line") |
|
.attr("class", "pair") |
|
.attr("y1", 230) |
|
.attr("y2", 270) |
|
.style("stroke", "#000") |
|
.style("stroke-width", 0.25) |
|
.style("fill", "none") |
|
.attr("x1", function(d){ return x(As[d.A_id].current_position); }) |
|
.attr("x2", function(d){ return x(Bs[d.B_id].current_position); }) |
|
.transition().delay(delay).duration(move) |
|
.attr("x1", function(d){ return x(As[d.A_id].next_position); }) |
|
.attr("x2", function(d){ return x(Bs[d.B_id].next_position); }) |
|
|
|
// animate circles |
|
|
|
svg.selectAll(".A") |
|
.transition().delay(delay).duration(move) |
|
.attr("cx", function(d){ return x(d.next_position); }) |
|
|
|
svg.selectAll(".B") |
|
.transition().delay(delay).duration(move) |
|
.attr("cx", function(d){ return x(d.next_position); }) |
|
|
|
// *** end of svg animation code *** |
|
|
|
// prep next timestep |
|
|
|
As.forEach(function(d){ |
|
d.current_position = d.next_position |
|
}) |
|
|
|
Bs.forEach(function(d){ |
|
d.current_position = d.next_position |
|
}) |
|
|
|
}, 1200) |
|
|
|
} |
|
|