@ai-on-browser/data-analysis-models
Version:
Data analysis model package without any dependencies
241 lines (219 loc) • 5.99 kB
JavaScript
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),
}
/**
* Support vector machine
*/
export default class SVM {
/**
* @param {'gaussian' | 'linear' | { name: 'gaussian', d?: number } | { name: 'linear' } | function (number[], number[]): number} kernel Kernel name
*/
constructor(kernel) {
this._n = 0
this._a = []
this._x = []
this._t = []
this._b = 0
this._C = 1000
this._eps = 0.001
this._tolerance = 0.001
this._err = []
if (typeof kernel === 'function') {
this._kernel = kernel
} else {
if (typeof kernel === 'string') {
kernel = { name: kernel }
}
this._kernel = kernels[kernel.name](kernel)
}
}
/**
* Initialize this model.
* @param {Array<Array<number>>} train_x Training data
* @param {Array<1 | -1>} train_y Target values
*/
init(train_x, train_y) {
this._n = train_x.length
this._a = Array(this._n).fill(0)
this._x = train_x.map(x => x)
this._t = train_y
this._err = Array(this._n).fill(0)
this._alldata = true
}
/**
* Fit model.
*/
fit() {
const changed = this._fitOnce(this._alldata)
if (this._alldata) {
this._alldata = false
if (changed === 0) {
return
}
} else if (changed === 0) {
this._alldata = true
}
}
_fitOnce(all = false) {
let change = 0
const between_eps = v => this._eps < v && v < this._C - this._eps
for (let i = 0; i < this._n; i++) {
let ei = 0
if (between_eps(this._a[i])) {
ei = this._err[i]
} else if (all) {
ei = this._predict1(this._x[i]) - this._t[i]
} else {
continue
}
const yfi = ei * this._t[i]
if (
(this._a[i] >= this._C - this._eps || yfi >= -this._tolerance) &&
(this._a[i] <= this._eps || yfi <= this._tolerance)
) {
continue
}
let max_e = 0
let max_j = -1
const offset = Math.floor(Math.random() * (this._n + 1))
const in_eps = []
const out_eps = []
for (let j = 0; j < this._n; j++) {
const p = (j + offset) % this._n
if (p === i) {
continue
}
if (between_eps(this._a[p])) {
const ej = this._err[p]
if (Math.abs(ei - ej) > max_e) {
max_e = Math.abs(ei - ej)
if (max_j >= 0) in_eps.push(max_j)
max_j = p
} else {
in_eps.push(p)
}
} else {
out_eps.push(p)
}
}
const checks = max_j >= 0 ? [].concat(max_j, in_eps, out_eps) : [].concat(in_eps, out_eps)
for (let ck = 0; ck < checks.length; ck++) {
const j = checks[ck]
const ai_old = this._a[i]
const aj_old = this._a[j]
let u, v
if (this._t[i] !== this._t[j]) {
u = Math.max(0, ai_old - aj_old)
v = Math.min(this._C, this._C + ai_old - aj_old)
} else {
u = Math.max(0, ai_old + aj_old - this._C)
v = Math.min(this._C, ai_old + aj_old)
}
if (u === v) {
continue
}
const kii = this._kernel(this._x[i], this._x[i])
const kjj = this._kernel(this._x[j], this._x[j])
const kij = this._kernel(this._x[i], this._x[j])
const k = kii + kjj - 2 * kij
const ej = between_eps(this._a[j]) ? this._err[j] : this._predict1(this._x[j]) - this._t[j]
let bClip = false
let ai_new = 0,
aj_new = 0
if (k <= 0) {
const lh = [u, v].map(t => {
const ai_n = t
const aj_n = aj_old + this._t[i] * this._t[j] * (ai_old - ai_n)
this._a[i] = ai_n
this._a[j] = aj_n
const v1 =
this._predict1(this._x[j]) + this._b - this._t[j] * aj_old * kjj - this._t[i] * ai_old * kij
const v2 =
this._predict1(this._x[i]) + this._b - this._t[j] * aj_old * kij - this._t[i] * ai_old * kii
const lobj =
aj_n +
ai_n -
(kjj * aj_n ** 2) / 2 -
(kii * ai_n ** 2) / 2 -
this._t[j] * this._t[i] * kij * aj_n * ai_n -
this._t[j] * aj_n * v1 -
this._t[i] * ai_n * v2
return lobj
})
this._a[i] = ai_old
this._a[j] = aj_old
ai_new = lh[0] > lh[1] + this._eps ? u : lh[0] < lh[1] - this._eps ? v : ai_old
bClip = true
} else {
ai_new = ai_old + (this._t[i] * (ej - ei)) / k
if (ai_new > v) {
bClip = true
ai_new = v
} else if (ai_new < u) {
bClip = true
ai_new = u
}
}
if (Math.abs(ai_new - ai_old) < this._eps * (ai_new + ai_old + this._eps)) {
continue
}
aj_new = aj_old + this._t[i] * this._t[j] * (ai_old - ai_new)
const b_old = this._b
if (between_eps(this._a[i])) {
this._b += ei + (ai_new - ai_old) * this._t[i] * kii + (aj_new - aj_old) * this._t[j] * kij
} else if (between_eps(this._a[j])) {
this._b += ej + (ai_new - ai_old) * this._t[i] * kij + (aj_new - aj_old) * this._t[j] * kjj
} else {
this._b +=
(ei +
ej +
(ai_new - ai_old) * this._t[i] * (kii + kij) +
(aj_new - aj_old) * this._t[j] * (kij + kjj)) /
2
}
for (let m = 0; m < this._n; m++) {
if (m === i || m === j) {
continue
}
this._err[m] +=
this._t[j] * (aj_new - aj_old) * this._kernel(this._x[j], this._x[m]) +
this._t[i] * (ai_new - ai_old) * this._kernel(this._x[i], this._x[m]) +
b_old -
this._b
}
this._a[i] = ai_new
this._a[j] = aj_new
if (!bClip) {
this._err[i] = 0
} else if (between_eps(ai_new)) {
this._err[i] = this._predict1(this._x[i]) - this._t[i]
}
this._err[j] = this._predict1(this._x[j]) - this._t[j]
change++
break
}
}
return change
}
_predict1(data) {
let y = 0
for (let n = 0; n < this._n; n++) {
if (this._a[n]) y += this._a[n] * this._t[n] * this._kernel(data, this._x[n])
}
return y - this._b
}
/**
* Returns predicted values.
* @param {Array<Array<number>>} data Sample data
* @returns {number[]} Predicted values
*/
predict(data) {
return data.map(v => this._predict1(v))
}
}