Skip to content

Commit 03de4b9

Browse files
authored
Add STING (#970)
1 parent e23ca29 commit 03de4b9

File tree

6 files changed

+212
-38
lines changed

6 files changed

+212
-38
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ for (let i = 0; i < n; i++) {
121121

122122
| task | model |
123123
| ---- | ----- |
124-
| clustering | (Soft / Kernel / Genetic / Weighted / Bisecting) k-means, k-means++, k-medois, k-medians, x-means, G-means, LBG, ISODATA, Fuzzy c-means, Possibilistic c-means, k-harmonic means, MacQueen, Hartigan-Wong, Elkan, Hamelry, Drake, Yinyang, Agglomerative (complete linkage, single linkage, group average, Ward's, centroid, weighted average, median), DIANA, Monothetic, Mutual kNN, Mean shift, DBSCAN, OPTICS, DTSCAN, HDBSCAN, DENCLUE, DBCLASD, BRIDGE, 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, DOC, FastDOC, DiSH, LMCLUS, NMF, Autoencoder |
124+
| clustering | (Soft / Kernel / Genetic / Weighted / Bisecting) k-means, k-means++, k-medois, k-medians, x-means, G-means, LBG, ISODATA, Fuzzy c-means, Possibilistic c-means, k-harmonic means, MacQueen, Hartigan-Wong, Elkan, Hamelry, Drake, Yinyang, Agglomerative (complete linkage, single linkage, group average, Ward's, centroid, weighted average, median), DIANA, Monothetic, Mutual kNN, Mean shift, DBSCAN, OPTICS, DTSCAN, HDBSCAN, DENCLUE, DBCLASD, BRIDGE, CLUES, PAM, CLARA, CLARANS, BIRCH, CURE, ROCK, C2P, STING, 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, DOC, FastDOC, DiSH, LMCLUS, NMF, Autoencoder |
125125
| 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, 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, ELM, 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, MARS, MLP, ELM, GMR, Isotonic, Ramer Douglas Peucker, Theil-Sen, Passing-Bablok, Repeated median |

js/model_selector.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,7 @@ const AIMethods = [
146146
'': [
147147
{ value: 'mutual_knn', title: 'Mutual kNN' },
148148
{ value: 'art', title: 'Adaptive resonance theory' },
149-
//{ value: "sting", title: "STING" },
149+
{ value: 'sting', title: 'STING' },
150150
{ value: 'svc', title: 'Support vector clustering' },
151151
{ value: 'affinity_propagation', title: 'Affinity Propagation' },
152152
{ value: 'cast', title: 'CAST' },

js/view/sting.js

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,16 @@ export default function (platform) {
1010
}
1111
const controller = new Controller(platform)
1212
const fitModel = () => {
13-
const model = new STING()
13+
const model = new STING(c.value)
1414
model.fit(platform.trainInput)
15-
//const pred = model.predict(platform.trainInput);
16-
//platform.trainResult = pred.map(v => v + 1)
17-
//clusters.value = new Set(pred).size
15+
const pred = model.predict(platform.trainInput)
16+
platform.trainResult = pred.map(v => v + 1)
17+
clusters.value = new Set(pred.filter(v => v >= 0)).size
18+
const tilePred = model.predict(platform.testInput(4))
19+
platform.testResult(tilePred.map(v => (v < 0 ? -1 : v + 1)))
1820
}
1921

20-
const stepButton = controller.input.button('Fit').on('click', fitModel)
22+
const c = controller.input.number({ label: 'c', min: 0, max: 10000, value: 500 })
23+
controller.input.button('Fit').on('click', fitModel)
2124
const clusters = controller.text({ label: ' Clusters: ' })
2225
}

lib/model/sting.js

Lines changed: 137 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
11
/**
22
* STatistical INformation Grid-based method
3-
* @deprecated Not implemented
43
*/
54
export default class STING {
65
// https://en.wikipedia.org/wiki/Cluster_analysis
76
// "STING : A Statistical Information Grid Approach to Spatial Data Mining"
8-
constructor() {
7+
/**
8+
* @param {number} c specified density
9+
*/
10+
constructor(c) {
11+
this._c = c
912
this._cells = null
13+
this._t = 0.05
1014
}
1115

1216
/**
@@ -24,14 +28,14 @@ export default class STING {
2428
ranges: ranges,
2529
children: [],
2630
}
27-
let stack = [this._cells]
31+
let layer = [this._cells]
2832
const spl_size = 2 ** dim
29-
const average_number = 20
33+
const average_number = 5
3034
const max_depth = Math.log(n / average_number) / Math.log(spl_size)
31-
const cells = [stack]
35+
const cells = [layer]
3236
for (let a = 0; a < max_depth; a++) {
3337
const new_stack = []
34-
for (const c of stack) {
38+
for (const c of layer) {
3539
const rng = c.ranges
3640
for (let i = 0; i < spl_size; i++) {
3741
let p = i
@@ -53,53 +57,141 @@ export default class STING {
5357
c.children.push(t)
5458
}
5559
}
56-
stack = new_stack
57-
cells.push(stack)
60+
layer = new_stack
61+
cells.push(layer)
62+
}
63+
64+
let bottomSpace = 1
65+
for (let d = 0; d < dim; d++) {
66+
const range = layer[0].ranges[d]
67+
bottomSpace *= range[1] - range[0]
5868
}
59-
for (let i = 0; i < stack.length; i++) {
60-
const c = stack[i]
69+
for (let i = 0; i < layer.length; i++) {
70+
const c = layer[i]
6171
const d = x.filter(v => {
6272
return c.ranges.every((r, i) => r[0] <= v[i] && (r[1] === maxs[i] ? v[i] <= r[1] : v[i] < r[1]))
6373
})
64-
const n = (c.n = d.length)
65-
const m = Array(dim).fill(0)
66-
const min = (c.min = Array(dim).fill(Infinity))
67-
const max = (c.max = Array(dim).fill(-Infinity))
68-
for (let j = 0; j < n; j++) {
74+
c.n = d.length
75+
c.min = Array(dim).fill(Infinity)
76+
c.max = Array(dim).fill(-Infinity)
77+
const sum = Array(dim).fill(0)
78+
for (let j = 0; j < c.n; j++) {
6979
for (let k = 0; k < dim; k++) {
70-
m[k] += d[j][k]
71-
min[k] = Math.min(min[k], d[j][k])
72-
max[k] = Math.max(max[k], d[j][k])
80+
sum[k] += d[j][k]
81+
c.min[k] = Math.min(c.min[k], d[j][k])
82+
c.max[k] = Math.max(c.max[k], d[j][k])
7383
}
7484
}
75-
c.m = m.map(v => (n > 0 ? v / n : 0))
85+
c.m = sum.map(v => (c.n > 0 ? v / c.n : 0))
7686
const s = Array(dim).fill(0)
77-
for (let j = 0; j < n; j++) {
87+
for (let j = 0; j < c.n; j++) {
7888
for (let k = 0; k < dim; k++) {
79-
s[k] += (d[j][k] - m[k]) ** 2
89+
s[k] += (d[j][k] - c.m[k]) ** 2
8090
}
8191
}
82-
c.s = s.map(v => (n > 0 ? Math.sqrt(v / n) : 0))
92+
c.s = s.map(v => (c.n > 0 ? Math.sqrt(v / c.n) : 0))
93+
c.dist = Array(dim).fill('normal')
94+
95+
c.area = bottomSpace
8396
}
8497
for (let k = cells.length - 2; k >= 0; k--) {
8598
for (let i = 0; i < cells[k].length; i++) {
86-
let n = 0
87-
const m = Array(dim).fill(0)
99+
let nki = 0
100+
let aki = 0
101+
const sum = Array(dim).fill(0)
88102
const min = (cells[k][i].min = Array(dim).fill(Infinity))
89103
const max = (cells[k][i].max = Array(dim).fill(-Infinity))
90104
const s = Array(dim).fill(0)
91-
for (const ccell of cells[k + 1].slice(i * spl_size, (i + 1) * spl_size)) {
92-
n += ccell.n
105+
const ccells = cells[k + 1].slice(i * spl_size, (i + 1) * spl_size)
106+
for (const ccell of ccells) {
107+
nki += ccell.n
108+
aki += ccell.area
93109
for (let p = 0; p < dim; p++) {
94-
m[p] += ccell.m[p] * ccell.n
110+
sum[p] += ccell.m[p] * ccell.n
95111
min[p] = Math.min(min[p], ccell.min[p])
96112
max[p] = Math.max(max[p], ccell.max[p])
97113
s[p] += (ccell.s[p] ** 2 + ccell.m[p] ** 2) * ccell.n
98114
}
99115
}
100-
cells[k][i].n = n
101-
cells[k][i].m = m.map(v => (n > 0 ? v / n : 0))
102-
cells[k][i].s = s.map((v, p) => (n > 0 ? Math.sqrt(v / n - m[p] ** 2) : 0))
116+
cells[k][i].n = nki
117+
cells[k][i].m = sum.map(v => (nki > 0 ? v / nki : 0))
118+
cells[k][i].s = s.map((v, p) => (nki > 0 ? Math.sqrt(v / nki - (sum[p] / nki) ** 2) : 0))
119+
const eps = 0.1
120+
cells[k][i].dist = Array(dim).fill('normal')
121+
cells[k][i].area = aki
122+
for (let d = 0; d < dim; d++) {
123+
let confl = 0
124+
let dist = 'normal'
125+
for (const ccell of ccells) {
126+
let mdiff = 0
127+
let sdiff = 0
128+
if (cells[k][i].m[d] !== 0) {
129+
mdiff += Math.abs((cells[k][i].m[d] - ccell.m[d]) / cells[k][i].m[d])
130+
} else if (ccell.m[d] !== 0) {
131+
mdiff += Math.abs((cells[k][i].m[d] - ccell.m[d]) / ccell.m[d])
132+
}
133+
if (cells[k][i].s[d] !== 0) {
134+
sdiff += Math.abs((cells[k][i].s[d] - ccell.s[d]) / cells[k][i].s[d])
135+
} else if (ccell.s[d] !== 0) {
136+
sdiff += Math.abs((cells[k][i].s[d] - ccell.s[d]) / ccell.s[d])
137+
}
138+
if (dist !== ccell.dist && mdiff < eps && sdiff < eps) {
139+
confl += ccell.n
140+
} else if (mdiff >= eps || sdiff >= eps) {
141+
confl = nki
142+
}
143+
}
144+
if (nki > 0 && confl / nki > this._t) {
145+
dist = 'none'
146+
}
147+
cells[k][i].dist[d] = dist
148+
}
149+
}
150+
}
151+
152+
let relevantCells = [this._cells]
153+
for (let k = 1; k < cells.length; k++) {
154+
const childRelevantCells = []
155+
for (let i = 0; i < relevantCells.length; i++) {
156+
for (const child of relevantCells[i].children) {
157+
if (child.n < child.area * this._c) {
158+
continue
159+
}
160+
childRelevantCells.push(child)
161+
}
162+
}
163+
relevantCells = childRelevantCells
164+
}
165+
166+
this._clusters = []
167+
const stack = []
168+
while (true) {
169+
if (stack.length === 0) {
170+
if (relevantCells.length === 0) {
171+
break
172+
}
173+
stack.push(relevantCells.pop())
174+
this._clusters.push([])
175+
}
176+
const curcell = stack.pop()
177+
this._clusters[this._clusters.length - 1].push(curcell)
178+
179+
for (let k = relevantCells.length - 1; k >= 0; k--) {
180+
const c = relevantCells[k]
181+
let adjointCnt = 0
182+
for (let d = 0; d < dim && adjointCnt < 2; d++) {
183+
if (curcell.ranges[d][0] === c.ranges[d][0] && curcell.ranges[d][1] === c.ranges[d][1]) {
184+
continue
185+
} else if (curcell.ranges[d][0] === c.ranges[d][1] || curcell.ranges[d][1] === c.ranges[d][0]) {
186+
adjointCnt++
187+
} else {
188+
adjointCnt = Infinity
189+
}
190+
}
191+
if (adjointCnt === 1) {
192+
stack.push(c)
193+
relevantCells.splice(k, 1)
194+
}
103195
}
104196
}
105197
}
@@ -109,5 +201,19 @@ export default class STING {
109201
* @param {Array<Array<number>>} datas Sample data
110202
* @returns {number[]} Predicted values
111203
*/
112-
predict(datas) {}
204+
predict(datas) {
205+
const p = []
206+
for (let i = 0; i < datas.length; i++) {
207+
p[i] = -1
208+
for (let k = 0; k < this._clusters.length && p[i] < 0; k++) {
209+
for (const cell of this._clusters[k]) {
210+
if (datas[i].every((v, d) => cell.ranges[d][0] <= v && v <= cell.ranges[d][1])) {
211+
p[i] = k
212+
break
213+
}
214+
}
215+
}
216+
}
217+
return p
218+
}
113219
}

tests/gui/view/sting.test.js

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { getPage } from '../helper/browser'
2+
3+
describe('clustering', () => {
4+
/** @type {Awaited<ReturnType<getPage>>} */
5+
let page
6+
beforeEach(async () => {
7+
page = await getPage()
8+
const taskSelectBox = page.locator('#ml_selector dl:first-child dd:nth-child(5) select')
9+
await taskSelectBox.selectOption('CT')
10+
const modelSelectBox = page.locator('#ml_selector .model_selection #mlDisp')
11+
await modelSelectBox.selectOption('sting')
12+
})
13+
14+
afterEach(async () => {
15+
await page?.close()
16+
})
17+
18+
test('initialize', async () => {
19+
const methodMenu = page.locator('#ml_selector #method_menu')
20+
const buttons = methodMenu.locator('.buttons')
21+
22+
const c = buttons.locator('input:nth-of-type(1)')
23+
await expect(c.inputValue()).resolves.toBe('500')
24+
})
25+
26+
test('learn', async () => {
27+
const methodMenu = page.locator('#ml_selector #method_menu')
28+
const buttons = methodMenu.locator('.buttons')
29+
30+
const clusters = buttons.locator('span:last-child')
31+
await expect(clusters.textContent()).resolves.toBe('')
32+
33+
const fitButton = buttons.locator('input[value=Fit]')
34+
await fitButton.dispatchEvent('click')
35+
36+
const svg = page.locator('#plot-area svg')
37+
await expect(svg.locator('.datas circle').count()).resolves.toBe(300)
38+
await expect(clusters.textContent()).resolves.toMatch(/^[0-9]+$/)
39+
})
40+
})

tests/lib/model/sting.test.js

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import Matrix from '../../../lib/util/matrix.js'
2+
import STING from '../../../lib/model/sting.js'
3+
4+
import { randIndex } from '../../../lib/evaluate/clustering.js'
5+
6+
test('clustering', () => {
7+
const model = new STING(1)
8+
const n = 50
9+
const x0 = Matrix.concat(
10+
Matrix.concat(Matrix.randn(n, 2, 0, 0.1), Matrix.randn(n, 2, 5, 0.1)),
11+
Matrix.randn(n, 2, [0, 5], 0.1)
12+
)
13+
const x = x0.toArray()
14+
15+
model.fit(x)
16+
const y = model.predict(x)
17+
expect(y).toHaveLength(x.length)
18+
19+
const t = []
20+
for (let i = 0; i < x.length; i++) {
21+
t[i] = Math.floor(i / n)
22+
}
23+
const ri = randIndex(y, t)
24+
expect(ri).toBeGreaterThan(0.9)
25+
})

0 commit comments

Comments
 (0)