diff --git a/js/view/label_propagation.js b/js/view/label_propagation.js index f3b82f70a..8ca26a76c 100644 --- a/js/view/label_propagation.js +++ b/js/view/label_propagation.js @@ -7,7 +7,7 @@ export default function (platform) { let model = null const fitModel = () => { if (!model) { - model = new LabelPropagation(method.value, sigma.value, k.value) + model = new LabelPropagation({ name: method.value, sigma: sigma.value, k: k.value }) model.init( platform.trainInput, platform.trainOutput.map(v => v[0]) diff --git a/js/view/label_spreading.js b/js/view/label_spreading.js index 2a5b504c9..fa418e8b0 100644 --- a/js/view/label_spreading.js +++ b/js/view/label_spreading.js @@ -7,7 +7,7 @@ export default function (platform) { let model = null const fitModel = () => { if (!model) { - model = new LabelSpreading(alpha.value, method.value, sigma.value, k.value) + model = new LabelSpreading(alpha.value, { name: method.value, sigma: sigma.value, k: k.value }) model.init( platform.trainInput, platform.trainOutput.map(v => v[0]) diff --git a/js/view/laplacian_eigenmaps.js b/js/view/laplacian_eigenmaps.js index f45a9f57b..89d6db574 100644 --- a/js/view/laplacian_eigenmaps.js +++ b/js/view/laplacian_eigenmaps.js @@ -16,7 +16,7 @@ export default function (platform) { const k = controller.input.number({ label: 'k =', name: 'k_nearest', min: 1, max: 100, value: 10 }) controller.input.button('Fit').on('click', () => { const dim = platform.dimension - const model = new LaplacianEigenmaps(dim, method.value, k.value, sigma.value) + const model = new LaplacianEigenmaps(dim, { name: method.value, k: k.value, sigma: sigma.value }) const pred = model.predict(platform.trainInput) platform.trainResult = pred }) diff --git a/js/view/spectral.js b/js/view/spectral.js index 2c0b6f0a3..fa013c577 100644 --- a/js/view/spectral.js +++ b/js/view/spectral.js @@ -29,8 +29,8 @@ export default function (platform) { knnSpan.element.style.display = 'none' const slbConf = controller.stepLoopButtons().init(() => { - const param = { sigma: sigma.value, k: k.value } - model = new SpectralClustering(method.value, param) + const param = { name: method.value, sigma: sigma.value, k: k.value } + model = new SpectralClustering(param) model.init(platform.trainInput) clusters.value = model.size runSpan.element.querySelectorAll('input').forEach(elm => (elm.disabled = null)) diff --git a/lib/model/label_propagation.js b/lib/model/label_propagation.js index df58c95aa..7af3584ba 100644 --- a/lib/model/label_propagation.js +++ b/lib/model/label_propagation.js @@ -9,14 +9,14 @@ export default class LabelPropagation { // http://yamaguchiyuto.hatenablog.com/entry/2016/09/22/014202 // https://github.com/scikit-learn/scikit-learn/blob/15a949460/sklearn/semi_supervised/_label_propagation.py /** - * @param {'rbf' | 'knn'} [method] Method name - * @param {number} [sigma] Sigma of normal distribution - * @param {number} [k] Number of neighborhoods + * @param {'rbf' | 'knn' | { name: 'rbf', sigma?: number, k?: number } | { name: 'knn', k?: number }} [method] Method name */ - constructor(method = 'rbf', sigma = 0.1, k = Infinity) { - this._k = k - this._sigma = sigma - this._affinity = method + constructor(method = 'rbf') { + if (typeof method === 'string') { + this._affinity = { name: method } + } else { + this._affinity = method + } } _affinity_matrix(x) { @@ -31,13 +31,14 @@ export default class LabelPropagation { } const con = Matrix.zeros(n, n) - if (this._k >= n) { + const k = this._affinity.k ?? Infinity + if (k >= n) { con.fill(1) - } else if (this._k > 0) { + } else if (k > 0) { for (let i = 0; i < n; i++) { const di = distances.row(i).value.map((v, i) => [v, i]) di.sort((a, b) => a[0] - b[0]) - for (let j = 1; j < Math.min(this._k + 1, di.length); j++) { + for (let j = 1; j < Math.min(k + 1, di.length); j++) { con.set(i, di[j][1], 1) } } @@ -45,9 +46,10 @@ export default class LabelPropagation { con.div(2) } - if (this._affinity === 'rbf') { - return Matrix.map(distances, (v, i) => (con.at(i) > 0 ? Math.exp(-(v ** 2) / this._sigma ** 2) : 0)) - } else if (this._affinity === 'knn') { + if (this._affinity.name === 'rbf') { + const sigma = this._affinity.sigma ?? 0.1 + return Matrix.map(distances, (v, i) => (con.at(i) > 0 ? Math.exp(-(v ** 2) / sigma ** 2) : 0)) + } else if (this._affinity.name === 'knn') { return Matrix.map(con, v => (v > 0 ? 1 : 0)) } } diff --git a/lib/model/label_spreading.js b/lib/model/label_spreading.js index c850fd532..3c58a218f 100644 --- a/lib/model/label_spreading.js +++ b/lib/model/label_spreading.js @@ -8,14 +8,14 @@ export default class LabelSpreading { // https://github.com/scikit-learn/scikit-learn/blob/15a949460/sklearn/semi_supervised/_label_propagation.py /** * @param {number} [alpha] Clamping factor - * @param {'rbf' | 'knn'} [method] Method name - * @param {number} [sigma] Sigma of normal distribution - * @param {number} [k] Number of neighborhoods + * @param {'rbf' | 'knn' | { name: 'rbf', sigma?: number, k?: number } | { name: 'knn', k?: number }} [method] Method name */ - constructor(alpha = 0.2, method = 'rbf', sigma = 0.1, k = Infinity) { - this._k = k - this._sigma = sigma - this._affinity = method + constructor(alpha = 0.2, method = 'rbf') { + if (typeof method === 'string') { + this._affinity = { name: method } + } else { + this._affinity = method + } this._alpha = alpha } @@ -32,13 +32,14 @@ export default class LabelSpreading { } const con = Matrix.zeros(n, n) - if (this._k >= n) { + const k = this._affinity.k ?? Infinity + if (k >= n) { con.fill(1) - } else if (this._k > 0) { + } else if (k > 0) { for (let i = 0; i < n; i++) { const di = distances.row(i).value.map((v, i) => [v, i]) di.sort((a, b) => a[0] - b[0]) - for (let j = 1; j < Math.min(this._k + 1, di.length); j++) { + for (let j = 1; j < Math.min(k + 1, di.length); j++) { con.set(i, di[j][1], 1) } } @@ -46,9 +47,10 @@ export default class LabelSpreading { con.div(2) } - if (this._affinity === 'rbf') { - return Matrix.map(distances, (v, i) => (con.at(i) > 0 ? Math.exp(-(v ** 2) / this._sigma ** 2) : 0)) - } else if (this._affinity === 'knn') { + if (this._affinity.name === 'rbf') { + const sigma = this._affinity.sigma ?? 0.1 + return Matrix.map(distances, (v, i) => (con.at(i) > 0 ? Math.exp(-(v ** 2) / sigma ** 2) : 0)) + } else if (this._affinity.name === 'knn') { return Matrix.map(con, v => (v > 0 ? 1 : 0)) } } diff --git a/lib/model/laplacian_eigenmaps.js b/lib/model/laplacian_eigenmaps.js index e0594c442..c8b6a9205 100644 --- a/lib/model/laplacian_eigenmaps.js +++ b/lib/model/laplacian_eigenmaps.js @@ -9,16 +9,16 @@ export default class LaplacianEigenmaps { // https://scikit-learn.org/stable/modules/generated/sklearn.manifold.SpectralEmbedding.html /** * @param {number} rd Reduced dimension - * @param {'rbf' | 'knn'} [affinity] Affinity type name - * @param {number} [k] Number of neighborhoods - * @param {number} [sigma] Sigma of normal distribution + * @param {'rbf' | 'knn' | { name: 'rbf', sigma?: number, k?: number } | { name: 'knn', k?: number }} [affinity] Affinity type name * @param {'unnormalized' | 'normalized'} [laplacian] Normalized laplacian matrix or not */ - constructor(rd, affinity = 'rbf', k = 10, sigma = 1, laplacian = 'unnormalized') { + constructor(rd, affinity = 'rbf', laplacian = 'unnormalized') { this._rd = rd - this._affinity = affinity - this._k = k - this._sigma = sigma + if (typeof affinity === 'string') { + this._affinity = { name: affinity } + } else { + this._affinity = affinity + } this._laplacian = laplacian } @@ -41,11 +41,12 @@ export default class LaplacianEigenmaps { } const con = Matrix.zeros(n, n) - if (this._k > 0) { + const k = this._affinity.k ?? 10 + if (k > 0) { for (let i = 0; i < n; i++) { const di = distances.row(i).value.map((v, i) => [v, i]) di.sort((a, b) => a[0] - b[0]) - for (let j = 1; j < Math.min(this._k + 1, di.length); j++) { + for (let j = 1; j < Math.min(k + 1, di.length); j++) { con.set(i, di[j][1], 1) } } @@ -54,9 +55,10 @@ export default class LaplacianEigenmaps { } let W - if (this._affinity === 'rbf') { - W = Matrix.map(distances, (v, i) => (con.at(i) > 0 ? Math.exp(-(v ** 2) / this._sigma ** 2) : 0)) - } else if (this._affinity === 'knn') { + if (this._affinity.name === 'rbf') { + const sigma = this._affinity.sigma ?? 1 + W = Matrix.map(distances, (v, i) => (con.at(i) > 0 ? Math.exp(-(v ** 2) / sigma ** 2) : 0)) + } else if (this._affinity.name === 'knn') { W = Matrix.map(con, v => (v > 0 ? 1 : 0)) } let d = W.sum(1).value diff --git a/lib/model/spectral.js b/lib/model/spectral.js index 80f2caaa7..ee957dda7 100644 --- a/lib/model/spectral.js +++ b/lib/model/spectral.js @@ -7,18 +7,13 @@ import LaplacianEigenmaps from './laplacian_eigenmaps.js' export default class SpectralClustering { // https://mr-r-i-c-e.hatenadiary.org/entry/20121214/1355499195 /** - * @param {'rbf' | 'knn'} [affinity] Affinity type name - * @param {object} [param] Config - * @param {number} [param.sigma] Sigma of normal distribution - * @param {number} [param.k] Number of neighborhoods + * @param {'rbf' | 'knn' | { name: 'rbf', sigma?: number, k?: number } | { name: 'knn', k?: number }} [affinity] Affinity type name */ - constructor(affinity = 'rbf', param = {}) { + constructor(affinity = 'rbf') { this._size = 0 this._epoch = 0 this._clustering = new KMeanspp() this._affinity = affinity - this._sigma = param.sigma || 1.0 - this._k = param.k || 10 } /** @@ -44,7 +39,7 @@ export default class SpectralClustering { init(datas) { const n = datas.length this._n = n - const le = new LaplacianEigenmaps(datas[0].length, this._affinity, this._k, this._sigma, 'normalized') + const le = new LaplacianEigenmaps(datas[0].length, this._affinity, 'normalized') this.ready = false le.predict(datas) this._ev = le._ev diff --git a/tests/lib/model/label_propagation.test.js b/tests/lib/model/label_propagation.test.js index 686a429f1..9025b8c7a 100644 --- a/tests/lib/model/label_propagation.test.js +++ b/tests/lib/model/label_propagation.test.js @@ -3,25 +3,22 @@ import LabelPropagation from '../../../lib/model/label_propagation.js' import { accuracy } from '../../../lib/evaluate/classification.js' -test.each([{}, { method: 'rbf', sigma: 0.2 }, { method: 'knn', k: 10 }])( - 'semi-classifier %p', - ({ method, sigma, k }) => { - const model = new LabelPropagation(method, sigma, k) - const x = Matrix.concat(Matrix.randn(50, 2, 0, 0.2), Matrix.randn(50, 2, 5, 0.2)).toArray() - const t = [] - const t_org = [] - for (let i = 0; i < x.length; i++) { - t_org[i] = t[i] = String.fromCharCode('a'.charCodeAt(0) + Math.floor(i / 50)) - if (Math.random() < 0.5) { - t[i] = null - } +test.each([undefined, 'rbf', { name: 'rbf', sigma: 0.2 }, { name: 'knn', k: 10 }])('semi-classifier %p', method => { + const model = new LabelPropagation(method) + const x = Matrix.concat(Matrix.randn(50, 2, 0, 0.2), Matrix.randn(50, 2, 5, 0.2)).toArray() + const t = [] + const t_org = [] + for (let i = 0; i < x.length; i++) { + t_org[i] = t[i] = String.fromCharCode('a'.charCodeAt(0) + Math.floor(i / 50)) + if (Math.random() < 0.5) { + t[i] = null } - model.init(x, t) - for (let i = 0; i < 20; i++) { - model.fit() - } - const y = model.predict(x) - const acc = accuracy(y, t_org) - expect(acc).toBeGreaterThan(0.95) } -) + model.init(x, t) + for (let i = 0; i < 20; i++) { + model.fit() + } + const y = model.predict(x) + const acc = accuracy(y, t_org) + expect(acc).toBeGreaterThan(0.95) +}) diff --git a/tests/lib/model/label_spreading.test.js b/tests/lib/model/label_spreading.test.js index 47acb1544..e21c51b26 100644 --- a/tests/lib/model/label_spreading.test.js +++ b/tests/lib/model/label_spreading.test.js @@ -3,25 +3,26 @@ import LabelSpreading from '../../../lib/model/label_spreading.js' import { accuracy } from '../../../lib/evaluate/classification.js' -test.each([{}, { alpha: 0.5, method: 'rbf', sigma: 0.2 }, { alpha: 0.8, method: 'knn', k: 10 }])( - 'semi-classifier %s %p', - ({ alpha, method, sigma, k }) => { - const model = new LabelSpreading(alpha, method, sigma, k) - const x = Matrix.concat(Matrix.randn(50, 2, 0, 0.2), Matrix.randn(50, 2, 5, 0.2)).toArray() - const t = [] - const t_org = [] - for (let i = 0; i < x.length; i++) { - t_org[i] = t[i] = String.fromCharCode('a'.charCodeAt(0) + Math.floor(i / 50)) - if (Math.random() < 0.5) { - t[i] = null - } +test.each([ + [undefined, undefined], + [0.5, { name: 'rbf', sigma: 0.2 }], + [0.8, { name: 'knn', k: 10 }], +])('semi-classifier %p %p', (alpha, method) => { + const model = new LabelSpreading(alpha, method) + const x = Matrix.concat(Matrix.randn(50, 2, 0, 0.2), Matrix.randn(50, 2, 5, 0.2)).toArray() + const t = [] + const t_org = [] + for (let i = 0; i < x.length; i++) { + t_org[i] = t[i] = String.fromCharCode('a'.charCodeAt(0) + Math.floor(i / 50)) + if (Math.random() < 0.5) { + t[i] = null } - model.init(x, t) - for (let i = 0; i < 20; i++) { - model.fit() - } - const y = model.predict(x) - const acc = accuracy(y, t_org) - expect(acc).toBeGreaterThan(0.95) } -) + model.init(x, t) + for (let i = 0; i < 20; i++) { + model.fit() + } + const y = model.predict(x) + const acc = accuracy(y, t_org) + expect(acc).toBeGreaterThan(0.95) +}) diff --git a/tests/lib/model/laplacian_eigenmaps.test.js b/tests/lib/model/laplacian_eigenmaps.test.js index 8b4277715..0e75ae109 100644 --- a/tests/lib/model/laplacian_eigenmaps.test.js +++ b/tests/lib/model/laplacian_eigenmaps.test.js @@ -6,10 +6,10 @@ import LaplacianEigenmaps from '../../../lib/model/laplacian_eigenmaps.js' import { coRankingMatrix } from '../../../lib/evaluate/dimensionality_reduction.js' -describe.each([undefined, 'knn'])('dimensionality reduction affinity:%p', affinity => { +describe.each([undefined, 'knn', { name: 'rbf' }])('dimensionality reduction affinity:%p', affinity => { test.each([undefined, 'normalized'])('laplacian: %p', laplacian => { const x = Matrix.concat(Matrix.randn(30, 5, 0, 0.2), Matrix.randn(30, 5, 5, 0.2)).toArray() - const model = new LaplacianEigenmaps(2, affinity, undefined, undefined, laplacian) + const model = new LaplacianEigenmaps(2, affinity, laplacian) const y = model.predict(x) expect(y[0]).toHaveLength(2) diff --git a/tests/lib/model/spectral.test.js b/tests/lib/model/spectral.test.js index 6d2375a5c..83a79b8c8 100644 --- a/tests/lib/model/spectral.test.js +++ b/tests/lib/model/spectral.test.js @@ -3,8 +3,8 @@ import SpectralClustering from '../../../lib/model/spectral.js' import { randIndex } from '../../../lib/evaluate/clustering.js' -test('clustering', () => { - const model = new SpectralClustering() +test.each([undefined, 'rbf', { name: 'rbf', sigma: 0.5 }, { name: 'knn', k: 4 }])('clustering %p', affinity => { + const model = new SpectralClustering(affinity) const n = 5 const x = Matrix.concat( Matrix.concat(Matrix.randn(n, 2, 0, 0.1), Matrix.randn(n, 2, 5, 0.1)),