Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion js/view/label_propagation.js
Original file line number Diff line number Diff line change
Expand Up @@ -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])
Expand Down
2 changes: 1 addition & 1 deletion js/view/label_spreading.js
Original file line number Diff line number Diff line change
Expand Up @@ -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])
Expand Down
2 changes: 1 addition & 1 deletion js/view/laplacian_eigenmaps.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
})
Expand Down
4 changes: 2 additions & 2 deletions js/view/spectral.js
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
28 changes: 15 additions & 13 deletions lib/model/label_propagation.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -31,23 +31,25 @@ 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)
}
}
con.add(con.t)
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))
}
}
Expand Down
28 changes: 15 additions & 13 deletions lib/model/label_spreading.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand All @@ -32,23 +32,25 @@ 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)
}
}
con.add(con.t)
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))
}
}
Expand Down
26 changes: 14 additions & 12 deletions lib/model/laplacian_eigenmaps.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand All @@ -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)
}
}
Expand All @@ -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
Expand Down
11 changes: 3 additions & 8 deletions lib/model/spectral.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

/**
Expand All @@ -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
Expand Down
37 changes: 17 additions & 20 deletions tests/lib/model/label_propagation.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
})
41 changes: 21 additions & 20 deletions tests/lib/model/label_spreading.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
})
4 changes: 2 additions & 2 deletions tests/lib/model/laplacian_eigenmaps.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
4 changes: 2 additions & 2 deletions tests/lib/model/spectral.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)),
Expand Down