UNPKG

@ai-on-browser/data-analysis-models

Version:

Data analysis model package without any dependencies

189 lines (173 loc) 4.03 kB
/** * Growing neural gas */ export default class GrowingNeuralGas { // http://www.thothchildren.com/chapter/5c02328c41f88f267249e72b // https://en.wikipedia.org/wiki/Neural_gas /** * @param {number} l Neughborhood range * @param {number} m Decreasing factor of `l` */ constructor(l, m) { this._nodes = [] this._edges = [] this._err = [] this._l = l this._m = m this._k = 0 this._eps = 0.01 this._err_fact = 0.99 this._max_age = 5 this._inserted_iteration = 20 } /** * Number of clusters * @type {number} */ get size() { const c = [] const s = new Set() for (let i = 0; i < this._nodes.length; i++) { if (s.has(i)) { continue } const n = this._topological_neighbor(i) c.push(i) for (let k = 0; k < n.length; k++) { s.add(n[k]) } } return c.length } _init(x) { const n = x.length const n1 = Math.floor(Math.random() * n) const c1 = x[n1] const c2 = x[(n1 + Math.floor(n / 2)) % n] this._nodes = [c1, c2] this._edges = [[], []] this._err = [0, 0] } _distance(a, b) { return Math.sqrt(a.reduce((s, v, i) => s + (v - b[i]) ** 2, 0)) } _topological_neighbor(n) { const tn = [] const stack = [n] while (stack.length > 0) { const i = stack.pop() if (tn.includes(i)) { continue } tn.push(i) for (let j = 0; j < this._edges[i].length; j++) { if (Number.isFinite(this._edges[i][j])) { stack.push(j) } } } return tn } /** * Update parameter. * @param {number[]} x Training data */ update(x) { const s = this._nodes.map((n, i) => [this._distance(x, n), i]) s.sort((a, b) => a[0] - b[0]) const s1 = s[0][1] const s2 = s[1][1] this._err[s1] += s[0][0] // const tn = this._topological_neighbor(s1) const tn = [s1] s.sort((a, b) => a[1] - b[1]) for (let i = 0; i < tn.length; i++) { const c = this._nodes[tn[i]] for (let j = 0; j < c.length; j++) { c[j] += (x[j] - c[j]) * this._eps * Math.exp(-s[tn[i]][0] / this._l) } } for (let j = 0; j < this._edges[s1].length; j++) { if (Number.isFinite(this._edges[s1][j])) { this._edges[s1][j]++ this._edges[j][s1]++ if (this._edges[s1][j] >= this._max_age) { this._edges[s1][j] = null this._edges[j][s1] = null } } } this._edges[s1][s2] = 0 this._edges[s2][s1] = 0 for (let i = this._edges.length - 1; i >= 0; i--) { if (this._edges[i].some(Number.isFinite)) { continue } this._edges.splice(i, 1) for (let j = 0; j < this._edges.length; j++) { this._edges[j].splice(i, 1) } this._nodes.splice(i, 1) this._err.splice(i, 1) } this._k++ this._l *= this._m if (this._k % this._inserted_iteration === 0) { const en = this._err.map((v, i) => [v, i]) en.sort((a, b) => b[0] - a[0]) const ei = en[0][1] let min_i = -1 let min_d = Infinity for (let i = 0; i < this._nodes.length; i++) { if (i === ei) { continue } const d = this._distance(this._nodes[i], this._nodes[ei]) if (d < min_d) { min_d = d min_i = i } } const n = this._nodes[min_i].map((v, i) => (v + this._nodes[ei][i]) / 2) this._nodes.push(n) this._edges.push([]) this._err.push(0) } this._err = this._err.map(v => v * this._err_fact) } /** * Fit model. * @param {Array<Array<number>>} x Training data */ fit(x) { if (this._nodes.length === 0) { this._init(x) } for (let i = 0; i < x.length; i++) { this.update(x[i]) } } /** * Returns predicted categories. * @param {Array<Array<number>>} datas Sample data * @returns {number[]} Predicted values */ predict(datas) { if (this._nodes.length === 0) { throw new Error('Call fit before predict.') } return datas.map(value => { let min_d = Infinity let min_i = -1 for (let i = 0; i < this._nodes.length; i++) { const d = this._distance(this._nodes[i], value) if (d < min_d) { min_d = d min_i = i } } const n = this._topological_neighbor(min_i) return Math.min(...n) }) } }