@ai-on-browser/data-analysis-models
Version:
Data analysis model package without any dependencies
184 lines (167 loc) • 4.44 kB
JavaScript
import Matrix from '../util/matrix.js'
const kernels = {
gaussian:
({ d = 1 }) =>
(a, b) => {
const r = a.reduce((acc, v, i) => acc + (v - b[i]) ** 2, 0)
return Math.exp(-r / (2 * d * d))
},
linear: () => (a, b) => a.reduce((acc, v, i) => acc + v * b[i], 0),
}
/**
* Semi-Supervised Support Vector Machine
*/
export default class S3VM {
// https://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.79.1969&rep=rep1&type=pdf
// https://is.mpg.de/fileadmin/user_upload/files/publications/SSL-spam_4162[0].pdf
// http://www.fabiangieseke.de/pdfs/neucom2013_draft.pdf
/**
* @param {'gaussian' | 'linear' | { name: 'gaussian', d?: number } | { name: 'linear' } | function (number[], number[]): number} kernel Kernel name
*/
constructor(kernel) {
this._b = 0
this._s = 3
this._gammas = null
this._rate = 0.1
this._C = 1
this._Cs = 1
if (typeof kernel === 'function') {
this._kernel = kernel
} else {
if (typeof kernel === 'string') {
kernel = { name: kernel }
}
this._kernel = kernels[kernel.name](kernel)
}
}
/**
* Initialize model.
* @param {Array<Array<number>>} x Training data
* @param {(1 | -1 | null)[]} y Target values
*/
init(x, y) {
this._x = x.map(r => r.concat())
this._t = y
const n = this._x.length
this._a = Matrix.randn(n, 1).value
this._m = Array(this._x[0].length).fill(0)
let us = 0
for (let i = 0; i < n; i++) {
if (this._t[i]) {
this._b += this._t[i]
} else {
for (let d = 0; d < this._x[i].length; d++) {
this._m[d] += this._x[i][d]
}
us++
}
}
this._m = this._m.map(v => v / us)
for (let i = 0; i < n; i++) {
for (let d = 0; d < this._x[i].length; d++) {
this._x[i][d] -= this._m[d]
}
}
this._b /= this._x.length - us
this._k = []
for (let i = 0; i < n; this._k[i++] = []);
for (let i = 0; i < n; i++) {
for (let j = i; j < n; j++) {
this._k[i][j] = this._k[j][i] = this._kernel(this._x[i], this._x[j])
}
}
}
/**
* Fit model.
*/
fit() {
this._fit_continuation()
}
_fit_continuation() {
const n = this._x.length
if (!this._gammas) {
const ux = Matrix.zeros(n, n)
let xnmax = 0
for (let i = 0; i < n; i++) {
if (this._t[i]) {
continue
}
const xi = new Matrix(n, 1, this._k[i])
const xx = xi.dot(xi.t)
const xn = xi.norm()
xx.div(xn ** 3)
ux.add(xx)
if (xnmax < xn) {
xnmax = xn
}
}
const [lmax] = ux.eigenPowerIteration()
const g0 = Math.cbrt((this._Cs * lmax) ** 2 / (2 * this._s))
const gend = 1 / (20 * this._s * xnmax ** 2)
this._gammas = []
const gn = 11
for (let i = 0; i <= 10; i++) {
this._gammas[i] = (gend / g0) ** (i / (gn - 1)) * g0
}
this._gamma_idx = 0
}
const gamma = this._gammas[this._gamma_idx]
const pred = []
const a = []
const e = []
for (let i = 0; i < n; i++) {
const xn2 = this._k[i].reduce((s, v) => s + v ** 2, 0)
pred[i] = this._k[i].reduce((s, v, j) => s + this._a[j] * v, 0) + this._b
a[i] = 1 + 2 * gamma * this._s * xn2
if (this._t[i]) {
e[i] = (this._t[i] * pred[i] - 1) / Math.sqrt(2 * gamma * xn2)
}
}
const newa = this._a.concat()
for (let i = 0; i < n; i++) {
let d
if (this._t[i]) {
d = (this._C / 2) * this._erfc(e[i]) * this._t[i]
} else {
d = this._Cs * ((2 * this._s * pred[i]) / a[i] ** (3 / 2)) * Math.exp((-this._s * pred[i] ** 2) / a[i])
}
for (let k = 0; k < n; k++) {
newa[k] -= d * this._k[i][k]
}
}
const err = newa.reduce((s, v) => s + v ** 2, 0)
this._a = this._a.map((v, i) => v - this._rate * newa[i])
if (err < 1.0e-4 && this._gamma_idx < this._gammas.length - 1) {
this._gamma_idx++
}
}
_erfc(z) {
const p = 0.3275911
const a1 = 0.254829592
const a2 = -0.284496736
const a3 = 1.421413741
const a4 = -1.453152027
const a5 = 1.061405429
const sign = z < 0 ? -1 : 1
const x = Math.abs(z)
const t = 1 / (1 + p * x)
const erf = 1 - ((((a5 * t + a4) * t + a3) * t + a2) * t + a1) * t * Math.exp(-x * x)
return 1 - sign * erf
}
/**
* Returns predicted values.
* @param {Array<Array<number>>} data Sample data
* @returns {number[]} Predicted values
*/
predict(data) {
const n = this._x.length
return data.map(v => {
v = v.map((v, d) => v - this._m[d])
let y = 0
for (let k = 0; k < n; k++) {
y += this._a[k] * this._kernel(v, this._x[k])
}
return y + this._b
})
}
}