@ai-on-browser/data-analysis-models
Version:
Data analysis model package without any dependencies
193 lines (175 loc) • 4.68 kB
JavaScript
import Matrix from '../util/matrix.js'
/**
* Generative topographic mapping
*/
export default class GTM {
// https://datachemeng.com/generativetopographicmapping/
// https://github.com/hkaneko1985/dcekit
/**
* @param {number} input_size Input size
* @param {number} output_size Output size
* @param {number} [k] Grid size
* @param {number} [q] Grid size for basis function
*/
constructor(input_size, output_size, k = 20, q = 10) {
this.in_size = input_size
this.out_size = output_size
this._k = k
this._lambda = 0.001
this._w = null
this._b = 1
this._init_method = 'PCA'
this._fit_method = 'mean'
this._epoch = 0
this._z = this._make_grid(Array(output_size).fill(this._k))
this._t = this._make_grid(Array(output_size).fill(q))
}
_make_grid(n) {
const g = []
const g0 = Array(n.length).fill(0)
do {
g.push(g0.map((v, i) => (2 * v) / (n[i] - 1) - 1))
for (let i = n.length - 1; i >= 0; i--) {
g0[i]++
if (g0[i] < n[i]) break
g0[i] = 0
}
} while (g0.reduce((a, v) => a + v, 0) > 0)
return g
}
_phi(z, i = null, s = Math.SQRT2) {
if (i === null) {
const p = []
for (let k = 0; k < this._t.length; k++) {
p.push(this._phi(z, k, s))
}
return p
}
return Math.exp(-this._t[i].reduce((a, v, k) => a + (v - z[k]) ** 2, 0) / (2 * s ** 2))
}
_prob(x, z) {
const probs = []
const phi = Matrix.fromArray(this._phi(z))
const phiw = phi.tDot(this._w).value
const c = (this._b / (2 * Math.PI)) ** (x[0].length / 2)
for (let n = 0; n < x.length; n++) {
const norm = phiw.reduce((s, v, k) => s + (v - x[n][k]) ** 2, 0)
probs[n] = c * Math.exp((-this._b * norm) / 2)
}
return probs
}
/**
* Returns probabilities.
* @param {Array<Array<number>>} x Sample data
* @returns {number[]} Predicted values
*/
probability(x) {
const probs = Array(x.length).fill(0)
for (let i = 0; i < this._z.length; i++) {
const p = this._prob(x, this._z[i])
for (let k = 0; k < p.length; k++) {
probs[k] += p[k]
}
}
return probs.map(v => v / this._z.length)
}
/**
* Returns responsibility.
* @param {Array<Array<number>>} x Sample data
* @returns {Matrix} Responsibility
*/
responsibility(x) {
const r = new Matrix(x.length, this._z.length)
for (let k = 0; k < this._z.length; k++) {
const p = this._prob(x, this._z[k])
for (let i = 0; i < p.length; i++) {
r.set(i, k, p[i])
}
}
const rsum = r.sum(1)
for (let i = 0; i < x.length; i++) {
if (rsum.at(i, 0) === 0) {
rsum.set(i, 0, 1)
for (let k = 0; k < this._z.length; k++) {
r.set(i, k, 1 / this._z.length)
}
}
}
r.div(rsum)
return r
}
/**
* Fit model.
* @param {Array<Array<number>>} data Training data
*/
fit(data) {
const x = data
const n = x.length
const dim = this.in_size
if (!this._w) {
if (this._init_method === 'random') {
this._w = Matrix.randn(this._t.length, dim)
} else if (this._init_method === 'PCA') {
const x0 = new Matrix(n, dim, data)
const xd = x0.cov()
const [l, pca] = xd.eigen()
const expl = new Matrix(
1,
l.length,
l.map(v => Math.sqrt(v))
)
expl.repeat(this._t.length, 0)
expl.mult(x0.block(0, 0, this._t.length, l.length))
this._w = expl.dot(pca.t)
}
}
const r = this.responsibility(x)
const phi = Matrix.fromArray(this._z.map(v => this._phi(v)))
const pp = Matrix.mult(phi, r.sum(0).t).tDot(phi)
pp.add(Matrix.eye(pp.cols, pp.cols, this._lambda / this._b))
const xmat = Matrix.fromArray(x)
const w1 = pp.solve(phi.tDot(r.tDot(xmat)))
const d = new Matrix(n, this._z.length)
const phiw = phi.dot(w1)
for (let i = 0; i < n; i++) {
const di = Matrix.sub(phiw, xmat.row(i))
di.mult(di)
d.set(i, 0, di.sum(1).t)
}
this._b = (n * dim) / Matrix.mult(r, d).sum()
this._w = w1.slice(0, this._t.length)
this._epoch++
}
/**
* Returns best indexes.
* @param {Array<Array<number>>} x Sample data
* @returns {number[]} Predicted values
*/
predictIndex(x) {
return this.responsibility(x).argmax(1).value
}
/**
* Returns predicted values.
* @param {Array<Array<number>>} x Sample data
* @returns {Array<Array<number>>} Predicted values
*/
predict(x) {
const r = this.responsibility(x)
if (this._fit_method === 'mode') {
return r.argmax(1).value.map(v => this._z[v])
} else {
r.div(r.sum(1))
const p = []
for (let i = 0; i < x.length; i++) {
const v = Array(this._z[0].length).fill(0)
for (let k = 0; k < this._z.length; k++) {
for (let d = 0; d < v.length; d++) {
v[d] += this._z[k][d] * r.at(i, k)
}
}
p.push(v)
}
return p
}
}
}