|
GNG = function() { |
|
this.init(); |
|
|
|
this.α = 0.5; // error factor on split |
|
this.β = 0.9995; // error factor each step |
|
this.λ = 300; // add neuron every λ steps |
|
this.max_n = 100; // maximum neuron count |
|
this.ε_b = 0.05; // best learning rate |
|
this.ε_n = 0.001; // neighbor learning rate |
|
this.max_age = 10; // max link age |
|
|
|
this.params = [ |
|
{ name: 'α', desc: 'error factor on split', min: 0, max: 1, step: 0.001 } |
|
, { name: 'β', desc: 'error factor each step', min: 0.5, max: 1, step: 0.0005 } |
|
, { name: 'λ', desc: 'add neuron every λ steps', min: 1, max: 1000, step: 1 } |
|
, { name: 'max_n', desc: 'maximum neuron count', min: 2, max: 1000, step: 1 } |
|
, { name: 'ε_b', desc: 'learning rate best neuron', min: 0, max: 1, step: 0.001 } |
|
, { name: 'ε_n', desc: 'learning rate best neuron neighbors', min: 0, max: 1, step: 0.001 } |
|
, { name: 'max_age', desc: 'maximum link age', min: 0, max: 1000, step: 1 } |
|
]; |
|
} |
|
|
|
GNG.prototype.init = function() { |
|
this.neurons = []; |
|
this.links = []; |
|
this.createNeuron(); |
|
this.createNeuron(); |
|
this.createLink(this.neurons[0], this.neurons[1]); |
|
this.step = 0; |
|
} |
|
|
|
GNG.prototype.input = function(x, y) { |
|
this.step++; |
|
|
|
var b1 = this.getClosestNeuronExcept(x, y, null); |
|
var b2 = this.getClosestNeuronExcept(x, y, b1); |
|
|
|
b1.error += b1.dist2(x, y); |
|
b1.move_towards(x, y, this.ε_b); |
|
|
|
var connected = false; |
|
b1.links.reduceRight((prev_l, l) => { |
|
var b1n = l.get_other(b1); |
|
b1n.move_towards(x, y, this.ε_n); |
|
if (b1n === b2) { |
|
connected = true; |
|
l.age = 0; |
|
} else { |
|
l.age++; |
|
if (l.age > this.max_age) this.removeLink(l); |
|
} |
|
}, null); |
|
if (!connected) this.createLink(b1, b2); |
|
this.neurons = this.neurons.filter(n => n.links.length > 0); |
|
|
|
if (this.step % this.λ === 0) this.addNeuron(); |
|
|
|
this.neurons.forEach(n => n.error *= this.β); |
|
} |
|
|
|
GNG.prototype.addNeuron = function() { |
|
if (this.neurons.length >= this.max_n) return; |
|
var w1 = this.getWorstNeuron(this.neurons); |
|
var w2 = this.getWorstNeuron(w1.links.map(l => l.get_other(w1))); |
|
var w3 = this.createNeuron((w1.x+w2.x)/2, (w1.y+w2.y)/2); |
|
this.createLink(w1, w3); |
|
this.createLink(w2, w3); |
|
this.removeLink(this.getLink(w1, w2)); |
|
w1.error *= this.α; |
|
w2.error *= this.α; |
|
w3.error = (w1.error + w2.error) / 2; |
|
} |
|
|
|
GNG.prototype.getClosestNeuronExcept = function (x, y, except) { |
|
return _.minBy(this.neurons, n => n===except ? Infinity : n.dist2(x, y)); |
|
}; |
|
|
|
GNG.prototype.getWorstNeuron = function (ns) { |
|
return _.maxBy(ns, n => n.error); |
|
}; |
|
|
|
|
|
GNG.prototype.createNeuron = function(x, y) { |
|
if (arguments.length < 2) { x = Math.random(); y = Math.random() } |
|
var n = new Neuron(x, y); |
|
this.neurons.push(n); |
|
return n; |
|
} |
|
|
|
GNG.prototype.createLink = function(n1, n2) { |
|
var l = new Link(n1, n2); |
|
this.links.push(l); |
|
n2.links.push(l); |
|
n1.links.push(l); |
|
} |
|
|
|
GNG.prototype.removeLink = function(l) { |
|
this.links.splice(this.links.indexOf(l), 1); |
|
l.n1.links.splice(l.n1.links.indexOf(l), 1); |
|
l.n2.links.splice(l.n2.links.indexOf(l), 1); |
|
} |
|
|
|
GNG.prototype.getLink = function(n1, n2) { |
|
return _.find(n1.links, l => (l.n1 === n1 && l.n2 === n2) || (l.n1 === n2 && l.n2 === n1)); |
|
} |
|
|
|
Neuron = function(x, y) { |
|
this.error = 0; |
|
this.x = x; |
|
this.y = y; |
|
this.links = []; |
|
} |
|
|
|
Neuron.prototype.dist2 = function(x, y) { |
|
var dx = this.x-x, dy = this.y-y; |
|
return dx*dx + dy*dy; |
|
} |
|
|
|
Neuron.prototype.move_towards = function(x, y, s) { |
|
this.x += s*(x-this.x); |
|
this.y += s*(y-this.y); |
|
} |
|
|
|
Link = function(n1, n2) { |
|
this.age = 0; |
|
this.n1 = n1; |
|
this.n2 = n2; |
|
} |
|
|
|
Link.prototype.get_other = function(n) { |
|
return this.n1 === n ? this.n2 : this.n1; |
|
} |