Skip to content

Commit e3567c9

Browse files
authored
Add ordered logistic regression and rename ordered probit model (#732)
* Add ordered logistic regression and rename ordered probit model * Small modification
1 parent 7b24d02 commit e3567c9

File tree

10 files changed

+213
-14
lines changed

10 files changed

+213
-14
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ for (let i = 0; i < n; i++) {
122122
| task | model |
123123
| ---- | ----- |
124124
| clustering | (Soft / Kernel / Genetic / Weighted) k-means, k-means++, k-medois, k-medians, x-means, G-means, LBG, ISODATA, Fuzzy c-means, Possibilistic c-means, Agglomerative (complete linkage, single linkage, group average, Ward's, centroid, weighted average, median), DIANA, Monothetic, Mutual kNN, Mean shift, DBSCAN, OPTICS, HDBSCAN, DENCLUE, DBCLASD, CLUES, PAM, CLARA, CLARANS, BIRCH, CURE, ROCK, C2P, PLSA, Latent dirichlet allocation, GMM, VBGMM, Affinity propagation, Spectral clustering, Mountain, (Growing) SOM, GTM, (Growing) Neural gas, Growing cell structures, LVQ, ART, SVC, CAST, CHAMELEON, COLL, CLIQUE, PROCLUS, ORCLUS, FINDIT, NMF, Autoencoder |
125-
| classification | (Fisher's) Linear discriminant, Quadratic discriminant, Mixture discriminant, Least squares, (Multiclass / Kernel) Ridge, (Complement / Negation / Universal-set / Selective) Naive Bayes (gaussian), AODE, (Fuzzy / Weighted) k-nearest neighbor, Radius neighbor, Nearest centroid, ENN, ENaN, NNBCA, ADAMENN, DANN, IKNN, Decision tree, Random forest, Extra trees, GBDT, XGBoost, ALMA, (Aggressive) ROMMA, (Bounded) Online gradient descent, (Budgeted online) Passive aggressive, RLS, (Selective-sampling) Second order perceptron, AROW, NAROW, Confidence weighted, CELLIP, IELLIP, Normal herd, Stoptron, (Kernelized) Pegasos, MIRA, Forgetron, Projectron, Projectron++, Banditron, Ballseptron, (Multiclass) BSGD, ILK, SILK, (Multinomial) Logistic regression, (Multinomial) Probit, Ordinal regression, SVM, Gaussian process, HMM, CRF, Bayesian Network, LVQ, (Average / Multiclass / Voted / Kernelized / Selective-sampling / Margin / Shifting / Budget / Tighter / Tightest) Perceptron, PAUM, RBP, ADALINE, MADALINE, MLP, LMNN |
125+
| classification | (Fisher's) Linear discriminant, Quadratic discriminant, Mixture discriminant, Least squares, (Multiclass / Kernel) Ridge, (Complement / Negation / Universal-set / Selective) Naive Bayes (gaussian), AODE, (Fuzzy / Weighted) k-nearest neighbor, Radius neighbor, Nearest centroid, ENN, ENaN, NNBCA, ADAMENN, DANN, IKNN, Decision tree, Random forest, Extra trees, GBDT, XGBoost, ALMA, (Aggressive) ROMMA, (Bounded) Online gradient descent, (Budgeted online) Passive aggressive, RLS, (Selective-sampling) Second order perceptron, AROW, NAROW, Confidence weighted, CELLIP, IELLIP, Normal herd, Stoptron, (Kernelized) Pegasos, MIRA, Forgetron, Projectron, Projectron++, Banditron, Ballseptron, (Multiclass) BSGD, ILK, SILK, (Multinomial) Logistic regression, (Multinomial) Probit, Ordered logistic, Ordered probit, SVM, Gaussian process, HMM, CRF, Bayesian Network, LVQ, (Average / Multiclass / Voted / Kernelized / Selective-sampling / Margin / Shifting / Budget / Tighter / Tightest) Perceptron, PAUM, RBP, ADALINE, MADALINE, MLP, LMNN |
126126
| semi-supervised classification | k-nearest neighbor, Radius neighbor, Label propagation, Label spreading, k-means, GMM, S3VM, Ladder network |
127127
| regression | Least squares, Ridge, Lasso, Elastic net, RLS, Bayesian linear, Poisson, Least absolute deviations, Huber, Tukey, Least trimmed squares, Least median squares, Lp norm linear, SMA, Deming, Segmented, LOWESS, LOESS, spline, Naive Bayes, Gaussian process, Principal components, Partial least squares, Projection pursuit, Quantile regression, k-nearest neighbor, Radius neighbor, IDW, Nadaraya Watson, Priestley Chao, Gasser Muller, RBF Network, RVM, Decision tree, Random forest, Extra trees, GBDT, XGBoost, SVR, MLP, GMR, Isotonic, Ramer Douglas Peucker, Theil-Sen, Passing-Bablok, Repeated median |
128128
| interpolation | Nearest neighbor, IDW, (Spherical) Linear, Brahmagupta, Logarithmic, Cosine, (Inverse) Smoothstep, Cubic, (Centripetal) Catmull-Rom, Hermit, Polynomial, Lagrange, Trigonometric, Spline, RBF Network, Akima, Natural neighbor, Delaunay |

js/model_selector.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -207,13 +207,16 @@ const AIMethods = [
207207
{ value: 'mlp', title: 'Multi-layer perceptron' },
208208
{ value: 'neuralnetwork', title: 'Neuralnetwork' },
209209
],
210+
Ranking: [
211+
{ value: 'ordered_logistic', title: 'Ordered logistic regression' },
212+
{ value: 'ordered_probit', title: 'Ordered probit regression' },
213+
],
210214
'': [
211215
{ value: 'least_square', title: 'Least squares' },
212216
{ value: 'ridge', title: 'Ridge' },
213217
{ value: 'nearest_centroid', title: 'Nearest Centroid' },
214218
{ value: 'logistic', title: 'Logistic regression' },
215219
{ value: 'probit', title: 'Probit' },
216-
{ value: 'ordinal_regression', title: 'Ordinal regression' },
217220
{ value: 'svm', title: 'Support vector machine' },
218221
{ value: 'gaussian_process', title: 'Gaussian Process' },
219222
{ value: 'hmm', title: 'HMM' },

js/view/ordered_logistic.js

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import OrderedLogisticRegression from '../../lib/model/ordered_logistic.js'
2+
import Controller from '../controller.js'
3+
4+
export default function (platform) {
5+
platform.setting.ml.usage =
6+
'Click and add data point. Next, click "Initialize". Finally, click "Fit" button repeatedly.'
7+
platform.setting.ml.reference = {
8+
title: 'Ordinal regression (Wikipedia)',
9+
url: 'https://en.wikipedia.org/wiki/Ordinal_regression',
10+
}
11+
const controller = new Controller(platform)
12+
13+
let model = null
14+
const fitModel = () => {
15+
if (!model) {
16+
return
17+
}
18+
19+
model.fit(
20+
platform.trainInput,
21+
platform.trainOutput.map(v => v[0])
22+
)
23+
const pred = model.predict(platform.testInput(4))
24+
platform.testResult(pred)
25+
}
26+
27+
const rate = controller.input.number({ label: ' Learning rate ', value: 0.1, min: 0, max: 100, step: 0.1 })
28+
controller
29+
.stepLoopButtons()
30+
.init(() => {
31+
model = new OrderedLogisticRegression(rate.value)
32+
platform.init()
33+
})
34+
.step(fitModel)
35+
.epoch()
36+
}

js/view/ordinal_regression.js renamed to js/view/ordered_probit.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import OrdinalRegression from '../../lib/model/ordinal_regression.js'
1+
import OrderedProbitRegression from '../../lib/model/ordered_probit.js'
22
import Controller from '../controller.js'
33

44
export default function (platform) {
@@ -24,11 +24,11 @@ export default function (platform) {
2424
platform.testResult(pred)
2525
}
2626

27-
const rate = controller.input.number({ label: ' Learning rate ', value: 0.001, min: 0, max: 100, step: 0.001 })
27+
const rate = controller.input.number({ label: ' Learning rate ', value: 0.1, min: 0, max: 100, step: 0.1 })
2828
controller
2929
.stepLoopButtons()
3030
.init(() => {
31-
model = new OrdinalRegression(rate.value)
31+
model = new OrderedProbitRegression(rate.value)
3232
platform.init()
3333
})
3434
.step(fitModel)

lib/model/ordered_logistic.js

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import Matrix from '../util/matrix.js'
2+
3+
/**
4+
* Ordered logistic regression
5+
*/
6+
export default class OrderedLogisticRegression {
7+
// https://en.wikipedia.org/wiki/Ordered_logit
8+
// https://en.wikipedia.org/wiki/Ordinal_regression
9+
/**
10+
* @param {number} [rate=0.1] Learning rate
11+
*/
12+
constructor(rate = 0.1) {
13+
this._w = null
14+
this._a = rate
15+
}
16+
17+
_s(x) {
18+
return 1 / (1 + Math.exp(-x))
19+
}
20+
21+
/**
22+
* Fit model.
23+
*
24+
* @param {Array<Array<number>>} x Training data
25+
* @param {Array<number>} y Target values
26+
*/
27+
fit(x, y) {
28+
x = Matrix.fromArray(x)
29+
if (!this._response) {
30+
this._response = [...new Set(y)]
31+
this._response.sort((a, b) => a - b)
32+
this._t = Array.from(this._response, (_, i) => i - 1)
33+
this._t[0] = -Infinity
34+
this._t.push(Infinity)
35+
this._w = Matrix.randn(x.cols, 1, 0, 0.1)
36+
}
37+
38+
const pstar = x.dot(this._w).value
39+
const dt = Array(this._t.length).fill(0)
40+
const dw = Matrix.zeros(this._w.rows, this._w.cols)
41+
for (let i = 0; i < y.length; i++) {
42+
const k = this._response.indexOf(y[i])
43+
const pk0 = this._s(this._t[k] - pstar[i])
44+
const pk1 = this._s(this._t[k + 1] - pstar[i])
45+
if (pk0 === pk1) {
46+
continue
47+
}
48+
49+
dt[k] += (pk0 * (1 - pk0)) / (pk0 - pk1)
50+
dt[k + 1] -= (pk1 * (1 - pk1)) / (pk0 - pk1)
51+
52+
const dwi = x.row(i).t
53+
dwi.mult((pk1 * (1 - pk1) - pk0 * (1 - pk0)) / (pk0 - pk1))
54+
dw.add(dwi)
55+
}
56+
dw.mult(this._a / y.length)
57+
58+
this._w.add(dw)
59+
for (let i = 1; i < this._t.length - 1; i++) {
60+
this._t[i] += (this._a / y.length) * dt[i]
61+
}
62+
this._t.sort((a, b) => a - b)
63+
}
64+
65+
/**
66+
* Returns predicted values.
67+
*
68+
* @param {Array<Array<number>>} x Sample data
69+
* @returns {Array<number>} Predicted values
70+
*/
71+
predict(x) {
72+
x = Matrix.fromArray(x)
73+
const pstar = x.dot(this._w).value
74+
const p = []
75+
for (let i = 0; i < pstar.length; i++) {
76+
p[i] = null
77+
for (let k = 0; k < this._response.length; k++) {
78+
if (this._t[k] < pstar[i] && pstar[i] <= this._t[k + 1]) {
79+
p[i] = this._response[k]
80+
break
81+
}
82+
}
83+
}
84+
return p
85+
}
86+
}

lib/model/ordinal_regression.js renamed to lib/model/ordered_probit.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import Matrix from '../util/matrix.js'
22

33
/**
4-
* Ordinal regression
4+
* Ordered probit regression
55
*/
6-
export default class OrdinalRegression {
6+
export default class OrderedProbitRegression {
77
// https://en.wikipedia.org/wiki/Ordinal_regression
88
/**
99
* @param {number} [rate=0.001] Learning rate
@@ -76,11 +76,11 @@ export default class OrdinalRegression {
7676
dwi.mult((this._gaussian(this._t[k + 1] - pstar[i]) - this._gaussian(this._t[k] - pstar[i])) / (pk0 - pk1))
7777
dw.add(dwi)
7878
}
79-
dw.mult(this._a)
79+
dw.mult(this._a / y.length)
8080

8181
this._w.add(dw)
8282
for (let i = 1; i < this._t.length - 1; i++) {
83-
this._t[i] += this._a * dt[i]
83+
this._t[i] += (this._a / y.length) * dt[i]
8484
}
8585
this._t.sort((a, b) => a - b)
8686
}

tests/gui/view/ordinal_regression.test.js renamed to tests/gui/view/ordered_logistic.test.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,19 +15,19 @@ describe('classification', () => {
1515
const taskSelectBox = await page.waitForSelector('#ml_selector dl:first-child dd:nth-child(5) select')
1616
await taskSelectBox.selectOption('CF')
1717
const modelSelectBox = await page.waitForSelector('#ml_selector .model_selection #mlDisp')
18-
await modelSelectBox.selectOption('ordinal_regression')
18+
await modelSelectBox.selectOption('ordered_logistic')
1919
const methodMenu = await page.waitForSelector('#ml_selector #method_menu')
2020
const buttons = await methodMenu.waitForSelector('.buttons')
2121

2222
const rate = await buttons.waitForSelector('input:nth-of-type(1)')
23-
await expect((await rate.getProperty('value')).jsonValue()).resolves.toBe('0.001')
23+
await expect((await rate.getProperty('value')).jsonValue()).resolves.toBe('0.1')
2424
})
2525

2626
test('learn', async () => {
2727
const taskSelectBox = await page.waitForSelector('#ml_selector dl:first-child dd:nth-child(5) select')
2828
await taskSelectBox.selectOption('CF')
2929
const modelSelectBox = await page.waitForSelector('#ml_selector .model_selection #mlDisp')
30-
await modelSelectBox.selectOption('ordinal_regression')
30+
await modelSelectBox.selectOption('ordered_logistic')
3131
const methodMenu = await page.waitForSelector('#ml_selector #method_menu')
3232
const buttons = await methodMenu.waitForSelector('.buttons')
3333

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import { getPage } from '../helper/browser'
2+
3+
describe('classification', () => {
4+
/** @type {Awaited<ReturnType<getPage>>} */
5+
let page
6+
beforeEach(async () => {
7+
page = await getPage()
8+
})
9+
10+
afterEach(async () => {
11+
await page?.close()
12+
})
13+
14+
test('initialize', async () => {
15+
const taskSelectBox = await page.waitForSelector('#ml_selector dl:first-child dd:nth-child(5) select')
16+
await taskSelectBox.selectOption('CF')
17+
const modelSelectBox = await page.waitForSelector('#ml_selector .model_selection #mlDisp')
18+
await modelSelectBox.selectOption('ordered_probit')
19+
const methodMenu = await page.waitForSelector('#ml_selector #method_menu')
20+
const buttons = await methodMenu.waitForSelector('.buttons')
21+
22+
const rate = await buttons.waitForSelector('input:nth-of-type(1)')
23+
await expect((await rate.getProperty('value')).jsonValue()).resolves.toBe('0.1')
24+
})
25+
26+
test('learn', async () => {
27+
const taskSelectBox = await page.waitForSelector('#ml_selector dl:first-child dd:nth-child(5) select')
28+
await taskSelectBox.selectOption('CF')
29+
const modelSelectBox = await page.waitForSelector('#ml_selector .model_selection #mlDisp')
30+
await modelSelectBox.selectOption('ordered_probit')
31+
const methodMenu = await page.waitForSelector('#ml_selector #method_menu')
32+
const buttons = await methodMenu.waitForSelector('.buttons')
33+
34+
const epoch = await buttons.waitForSelector('[name=epoch]')
35+
await expect(epoch.evaluate(el => el.textContent)).resolves.toBe('0')
36+
const methodFooter = await page.waitForSelector('#method_footer', { state: 'attached' })
37+
await expect(methodFooter.evaluate(el => el.textContent)).resolves.toBe('')
38+
39+
const initButton = await buttons.waitForSelector('input[value=Initialize]')
40+
await initButton.evaluate(el => el.click())
41+
const stepButton = await buttons.waitForSelector('input[value=Step]:enabled')
42+
await stepButton.evaluate(el => el.click())
43+
44+
await expect(epoch.evaluate(el => el.textContent)).resolves.toBe('1')
45+
await expect(methodFooter.evaluate(el => el.textContent)).resolves.toMatch(/^Accuracy:[0-9.]+$/)
46+
})
47+
})

tests/lib/model/ordinal_regression.test.js renamed to tests/lib/model/ordered_logistic.test.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import Matrix from '../../../lib/util/matrix.js'
2-
import OrdinalRegression from '../../../lib/model/ordinal_regression.js'
2+
import OrderedLogisticRegression from '../../../lib/model/ordered_logistic.js'
33

44
import { rmse } from '../../../lib/evaluate/regression.js'
55

66
describe('ordinal', () => {
77
test('fit', () => {
8-
const model = new OrdinalRegression()
8+
const model = new OrderedLogisticRegression()
99
const x = Matrix.concat(
1010
Matrix.concat(Matrix.randn(50, 2, 0, 0.2), Matrix.randn(50, 2, 2, 0.2)),
1111
Matrix.randn(50, 2, 4, 0.2)
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { jest } from '@jest/globals'
2+
jest.retryTimes(3)
3+
4+
import Matrix from '../../../lib/util/matrix.js'
5+
import OrderedProbitRegression from '../../../lib/model/ordered_probit.js'
6+
7+
import { rmse } from '../../../lib/evaluate/regression.js'
8+
9+
describe('ordinal', () => {
10+
test('fit', () => {
11+
const model = new OrderedProbitRegression()
12+
const x = Matrix.concat(
13+
Matrix.concat(Matrix.randn(50, 2, 0, 0.2), Matrix.randn(50, 2, 2, 0.2)),
14+
Matrix.randn(50, 2, 4, 0.2)
15+
).toArray()
16+
const t = []
17+
for (let i = 0; i < x.length; i++) {
18+
t[i] = Math.floor(i / 50)
19+
}
20+
for (let i = 0; i < 100; i++) {
21+
model.fit(x, t)
22+
}
23+
const y = model.predict(x)
24+
const err = rmse(y, t)
25+
expect(err).toBeLessThan(0.7)
26+
})
27+
})

0 commit comments

Comments
 (0)