w-cluster 1.0.19 → 1.0.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/ci-test.yml +3 -3
- package/README.md +4 -6
- package/babel.config.js +4 -3
- package/dist/w-cluster.umd.js +2 -2
- package/dist/w-cluster.umd.js.map +1 -1
- package/dist/w-cluster.wk.umd.js +1 -1
- package/docs/DenseMatrix.html +202 -0
- package/docs/DistancePair.html +202 -0
- package/docs/LowerTriangle.html +202 -0
- package/docs/Rec.html +202 -0
- package/docs/Reco.html +202 -0
- package/docs/WCluster.mjs.html +8 -4
- package/docs/examples/ex-PCA.html +1 -1
- package/docs/examples/ex-cluster-webworker.html +1 -1
- package/docs/examples/ex-cluster.html +1 -1
- package/docs/global.html +6799 -164
- package/docs/index.html +2 -2
- package/docs/k-medoids_alternating.mjs.html +144 -0
- package/docs/k-medoids_arrayadapter.mjs.html +107 -0
- package/docs/k-medoids_dynmsc.mjs.html +255 -0
- package/docs/k-medoids_fastermsc.mjs.html +411 -0
- package/docs/k-medoids_fasterpam.mjs.html +305 -0
- package/docs/k-medoids_fastmsc.mjs.html +166 -0
- package/docs/k-medoids_fastpam1.mjs.html +132 -0
- package/docs/k-medoids_initialization.mjs.html +107 -0
- package/docs/k-medoids_pam.mjs.html +275 -0
- package/docs/k-medoids_pammedsil.mjs.html +307 -0
- package/docs/k-medoids_pamsil.mjs.html +225 -0
- package/docs/k-medoids_par_fasterpam.mjs.html +134 -0
- package/docs/k-medoids_par_silhouette.mjs.html +126 -0
- package/docs/k-medoids_silhouette.mjs.html +193 -0
- package/docs/k-medoids_util.mjs.html +135 -0
- package/g-PCA-nodeworker.mjs +1 -1
- package/g-PCA.mjs +1 -1
- package/g-cluster-kMeans-large.mjs +83 -0
- package/g-cluster-kMeans-nodeworker.mjs +181 -0
- package/g-cluster-kMeans.mjs +178 -0
- package/g-cluster-kMedoids-large-suggest.mjs +95 -0
- package/g-cluster-kMedoids-large.mjs +104 -0
- package/{g-cluster-nodeworker.mjs → g-cluster-kMedoids-nodeworker.mjs} +1 -2
- package/{g-cluster.mjs → g-cluster-kMedoids-simple.mjs} +1 -1
- package/package.json +7 -7
- package/script.txt +18 -0
- package/src/WCluster.mjs +6 -2
- package/src/WClusterCore.mjs +30 -21
- package/src/WClusterMat.mjs +98 -24
- package/src/WPCAMat.mjs +2 -2
- package/src/jaccardBitset.mjs +63 -0
- package/src/k-medoids/README.md +124 -0
- package/src/k-medoids/alternating.mjs +72 -0
- package/src/k-medoids/arrayadapter.mjs +35 -0
- package/src/k-medoids/dynmsc.mjs +183 -0
- package/src/k-medoids/fastermsc.mjs +339 -0
- package/src/k-medoids/fasterpam.mjs +233 -0
- package/src/k-medoids/fastmsc.mjs +94 -0
- package/src/k-medoids/fastpam1.mjs +60 -0
- package/src/k-medoids/index.mjs +37 -0
- package/src/k-medoids/initialization.mjs +35 -0
- package/src/k-medoids/package.json +8 -0
- package/src/k-medoids/pam.mjs +203 -0
- package/src/k-medoids/pammedsil.mjs +235 -0
- package/src/k-medoids/pamsil.mjs +153 -0
- package/src/k-medoids/par_fasterpam.mjs +62 -0
- package/src/k-medoids/par_silhouette.mjs +54 -0
- package/src/k-medoids/silhouette.mjs +121 -0
- package/src/k-medoids/test.mjs +357 -0
- package/src/k-medoids/util.mjs +63 -0
- package/test/jaccardBitset.test.mjs +141 -0
- package/test/kMeans.test.mjs +278 -0
- package/test/kMedoids-large-suggest.test.mjs +140 -0
- package/test/kMedoids-large.test.mjs +142 -0
- package/test/{cluster.test.mjs → kMedoids-simple.test.mjs} +5 -5
- package/toolg/gDocsExams.mjs +1 -1
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
|
|
5
|
+
<meta charset="utf-8">
|
|
6
|
+
<title>k-medoids/fastermsc.mjs - Documentation</title>
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
<script src="scripts/prettify/prettify.js"></script>
|
|
10
|
+
<script src="scripts/prettify/lang-css.js"></script>
|
|
11
|
+
<!--[if lt IE 9]>
|
|
12
|
+
<script src="//html5shiv.googlecode.com/svn/trunk/html5.js"></script>
|
|
13
|
+
<![endif]-->
|
|
14
|
+
<link type="text/css" rel="stylesheet" href="styles/prettify.css">
|
|
15
|
+
<link type="text/css" rel="stylesheet" href="styles/jsdoc.css">
|
|
16
|
+
<script src="scripts/nav.js" defer></script>
|
|
17
|
+
|
|
18
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
19
|
+
</head>
|
|
20
|
+
<body>
|
|
21
|
+
|
|
22
|
+
<input type="checkbox" id="nav-trigger" class="nav-trigger" />
|
|
23
|
+
<label for="nav-trigger" class="navicon-button x">
|
|
24
|
+
<div class="navicon"></div>
|
|
25
|
+
</label>
|
|
26
|
+
|
|
27
|
+
<label for="nav-trigger" class="overlay"></label>
|
|
28
|
+
|
|
29
|
+
<nav >
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
<h2><a href="index.html">Home</a></h2><h3>Classes</h3><ul><li><a href="DenseMatrix.html">DenseMatrix</a></li><li><a href="DistancePair.html">DistancePair</a></li><li><a href="LowerTriangle.html">LowerTriangle</a></li><li><a href="Rec.html">Rec</a></li><li><a href="Reco.html">Reco</a></li></ul><h3>Global</h3><ul><li><a href="global.html#PCA">PCA</a></li><li><a href="global.html#WCluster">WCluster</a></li><li><a href="global.html#alternating">alternating</a></li><li><a href="global.html#arrayAdapter">arrayAdapter</a></li><li><a href="global.html#assign_nearest">assign_nearest</a></li><li><a href="global.html#choose_medoid_within_partition">choose_medoid_within_partition</a></li><li><a href="global.html#cluster">cluster</a></li><li><a href="global.html#do_swap">do_swap</a></li><li><a href="global.html#do_swap_k2">do_swap_k2</a></li><li><a href="global.html#dynmsc">dynmsc</a></li><li><a href="global.html#fastermsc">fastermsc</a></li><li><a href="global.html#fastermsc_k2">fastermsc_k2</a></li><li><a href="global.html#fasterpam">fasterpam</a></li><li><a href="global.html#fastmsc">fastmsc</a></li><li><a href="global.html#fastmsc_k2">fastmsc_k2</a></li><li><a href="global.html#fastpam1">fastpam1</a></li><li><a href="global.html#find_best_swap">find_best_swap</a></li><li><a href="global.html#find_best_swap_k2">find_best_swap_k2</a></li><li><a href="global.html#find_best_swap_pam">find_best_swap_pam</a></li><li><a href="global.html#find_best_swap_pammedsil">find_best_swap_pammedsil</a></li><li><a href="global.html#find_best_swap_pammedsil_k2">find_best_swap_pammedsil_k2</a></li><li><a href="global.html#find_max">find_max</a></li><li><a href="global.html#find_min">find_min</a></li><li><a href="global.html#first_k">first_k</a></li><li><a href="global.html#initial_assignment">initial_assignment</a></li><li><a href="global.html#initial_assignment_k2">initial_assignment_k2</a></li><li><a href="global.html#medoid_silhouette">medoid_silhouette</a></li><li><a href="global.html#pam">pam</a></li><li><a href="global.html#pam_build">pam_build</a></li><li><a href="global.html#pam_build_initialize">pam_build_initialize</a></li><li><a href="global.html#pam_optimize">pam_optimize</a></li><li><a href="global.html#pam_swap">pam_swap</a></li><li><a href="global.html#pammedsil">pammedsil</a></li><li><a href="global.html#pammedsil_build_initialize">pammedsil_build_initialize</a></li><li><a href="global.html#pammedsil_optimize">pammedsil_optimize</a></li><li><a href="global.html#pammedsil_swap">pammedsil_swap</a></li><li><a href="global.html#pamsil">pamsil</a></li><li><a href="global.html#pamsil_build_initialize">pamsil_build_initialize</a></li><li><a href="global.html#pamsil_optimize">pamsil_optimize</a></li><li><a href="global.html#pamsil_swap">pamsil_swap</a></li><li><a href="global.html#par_fasterpam">par_fasterpam</a></li><li><a href="global.html#par_silhouette">par_silhouette</a></li><li><a href="global.html#rand_fasterpam">rand_fasterpam</a></li><li><a href="global.html#random_initialization">random_initialization</a></li><li><a href="global.html#remove_med">remove_med</a></li><li><a href="global.html#sample">sample</a></li><li><a href="global.html#shuffle">shuffle</a></li><li><a href="global.html#silhouette">silhouette</a></li><li><a href="global.html#update_removal_loss">update_removal_loss</a></li><li><a href="global.html#update_second_nearest">update_second_nearest</a></li><li><a href="global.html#update_third_nearest">update_third_nearest</a></li><li><a href="global.html#update_third_nearest_without_new">update_third_nearest_without_new</a></li></ul>
|
|
33
|
+
|
|
34
|
+
</nav>
|
|
35
|
+
|
|
36
|
+
<div id="main">
|
|
37
|
+
|
|
38
|
+
<h1 class="page-title">k-medoids/fastermsc.mjs</h1>
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
<section>
|
|
47
|
+
<article>
|
|
48
|
+
<pre class="prettyprint source linenums"><code>// Ported 1:1 from src/fastermsc.rs (FasterMSC algorithm).
|
|
49
|
+
import { arrayAdapter } from './arrayadapter.mjs';
|
|
50
|
+
import { Reco, DistancePair, U32_MAX, find_min, find_max, choose_medoid_within_partition } from './util.mjs';
|
|
51
|
+
|
|
52
|
+
// _loss(a, b): 0 if a or b is zero, else a / b.
|
|
53
|
+
export function _loss(a, b) {
|
|
54
|
+
if (a === 0 || b === 0) { return 0; } else { return a / b; }
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Run the FasterMSC algorithm.
|
|
59
|
+
* @param {object} mat - pairwise distance matrix (wrapped)
|
|
60
|
+
* @param {number[]} med - the list of medoids (mutated in place)
|
|
61
|
+
* @param {number} maxiter - maximum number of iterations
|
|
62
|
+
* returns { loss, assi, nIter, nSwaps }
|
|
63
|
+
*/
|
|
64
|
+
export function fastermsc(mat, med, maxiter) {
|
|
65
|
+
mat = arrayAdapter(mat);
|
|
66
|
+
const n = mat.len(), k = med.length;
|
|
67
|
+
if (k === 1) {
|
|
68
|
+
const assi = new Array(n).fill(0);
|
|
69
|
+
const [swapped, loss] = choose_medoid_within_partition(mat, assi, med, 0);
|
|
70
|
+
return { loss, assi, nIter: 1, nSwaps: swapped ? 1 : 0 };
|
|
71
|
+
}
|
|
72
|
+
if (k === 2) { // special hadling, as there is no third
|
|
73
|
+
return fastermsc_k2(mat, med, maxiter);
|
|
74
|
+
}
|
|
75
|
+
let [loss, data] = initial_assignment(mat, med);
|
|
76
|
+
|
|
77
|
+
let removal_loss = new Array(k).fill(0);
|
|
78
|
+
update_removal_loss(data, removal_loss);
|
|
79
|
+
let lastswap = n, n_swaps = 0, iter = 0;
|
|
80
|
+
while (iter < maxiter) {
|
|
81
|
+
iter += 1;
|
|
82
|
+
const swaps_before = n_swaps, lastloss = loss;
|
|
83
|
+
for (let j = 0; j < n; j++) {
|
|
84
|
+
if (j === lastswap) {
|
|
85
|
+
break;
|
|
86
|
+
}
|
|
87
|
+
if (j === med[data[j].near.i]) {
|
|
88
|
+
continue; // This already is a medoid
|
|
89
|
+
}
|
|
90
|
+
const [change, b] = find_best_swap(mat, removal_loss, data, j);
|
|
91
|
+
if (change <= 0) {
|
|
92
|
+
continue; // No improvement
|
|
93
|
+
}
|
|
94
|
+
n_swaps += 1;
|
|
95
|
+
lastswap = j;
|
|
96
|
+
// perform the swap
|
|
97
|
+
loss = do_swap(mat, med, data, b, j);
|
|
98
|
+
update_removal_loss(data, removal_loss);
|
|
99
|
+
}
|
|
100
|
+
if (n_swaps === swaps_before || loss >= lastloss) {
|
|
101
|
+
break; // converged
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
const assi = data.map((x) => x.near.i);
|
|
105
|
+
loss = 1 - loss / n;
|
|
106
|
+
return { loss, assi, nIter: iter, nSwaps: n_swaps };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Perform the initial assignment to medoids. Returns [loss, data] (data = Reco[]). */
|
|
110
|
+
export function initial_assignment(mat, med) {
|
|
111
|
+
const n = mat.len(), k = med.length;
|
|
112
|
+
if (!mat.isSquare()) throw new Error('Dissimilarity matrix is not square');
|
|
113
|
+
if (!(n <= U32_MAX)) throw new Error('N is too large');
|
|
114
|
+
if (!(k > 0 && k < U32_MAX)) throw new Error('invalid N');
|
|
115
|
+
if (!(k <= n)) throw new Error('k must be at most N');
|
|
116
|
+
const data = new Array(mat.len());
|
|
117
|
+
for (let _i = 0; _i < data.length; _i++) data[_i] = Reco.empty();
|
|
118
|
+
const firstcenter = med[0];
|
|
119
|
+
let loss = 0;
|
|
120
|
+
for (let i = 0; i < data.length; i++) {
|
|
121
|
+
// Rust: *cur = Reco::new(...) overwrites the slot in place.
|
|
122
|
+
const cur = new Reco(0, mat.get(i, firstcenter), U32_MAX, 0, U32_MAX, 0);
|
|
123
|
+
data[i] = cur;
|
|
124
|
+
for (let m = 1; m < med.length; m++) {
|
|
125
|
+
const me = med[m];
|
|
126
|
+
const d = mat.get(i, me);
|
|
127
|
+
if (d < cur.near.d || i === me) {
|
|
128
|
+
cur.third = cur.seco.clone();
|
|
129
|
+
cur.seco = cur.near.clone();
|
|
130
|
+
cur.near = new DistancePair(m, d);
|
|
131
|
+
} else if (cur.seco.i === U32_MAX || d < cur.seco.d) {
|
|
132
|
+
cur.third = cur.seco.clone();
|
|
133
|
+
cur.seco = new DistancePair(m, d);
|
|
134
|
+
} else if (cur.third.i === U32_MAX || d < cur.third.d) {
|
|
135
|
+
cur.third = new DistancePair(m, d);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
loss += _loss(cur.near.d, cur.seco.d);
|
|
139
|
+
}
|
|
140
|
+
return [loss, data];
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** Find the best swap for object j - FastMSC version. Returns [change, b]. */
|
|
144
|
+
export function find_best_swap(mat, removal_loss, data, j) {
|
|
145
|
+
const ploss = removal_loss.slice();
|
|
146
|
+
// Improvement from the journal version:
|
|
147
|
+
let acc = 0;
|
|
148
|
+
for (let o = 0; o < data.length; o++) {
|
|
149
|
+
const reco = data[o];
|
|
150
|
+
const doj = mat.get(o, j);
|
|
151
|
+
if (doj < reco.near.d) {
|
|
152
|
+
acc += _loss(reco.near.d, reco.seco.d) - _loss(doj, reco.near.d);
|
|
153
|
+
// loss already includes (dt - ds) - (ds - dn), remove
|
|
154
|
+
ploss[reco.near.i] += _loss(doj, reco.near.d) + _loss(reco.seco.d, reco.third.d) - _loss(reco.near.d + doj, reco.seco.d);
|
|
155
|
+
ploss[reco.seco.i] += _loss(reco.near.d, reco.third.d) - _loss(reco.near.d, reco.seco.d);
|
|
156
|
+
} else if (doj < reco.seco.d) {
|
|
157
|
+
acc += _loss(reco.near.d, reco.seco.d) - _loss(reco.near.d, doj);
|
|
158
|
+
ploss[reco.near.i] += _loss(reco.near.d, doj) + _loss(reco.seco.d, reco.third.d) - _loss(reco.near.d + doj, reco.seco.d);
|
|
159
|
+
// loss already includes (dt - ds) - (ds - dn), adjust to 2*d(xo) - ds - dt
|
|
160
|
+
// loss already includes (dt - ds), adjust to 2*d(xo) - ds - dt
|
|
161
|
+
ploss[reco.seco.i] += _loss(reco.near.d, reco.third.d) - _loss(reco.near.d, reco.seco.d);
|
|
162
|
+
} else if (doj < reco.third.d) {
|
|
163
|
+
// loss already includes (dt - ds) - (ds - dn), adjust to d(xo)- dt
|
|
164
|
+
ploss[reco.near.i] += _loss(reco.seco.d, reco.third.d) - _loss(reco.seco.d, doj);
|
|
165
|
+
// loss already includes (dt - ds), adjust to d(xo)- dt
|
|
166
|
+
ploss[reco.seco.i] += _loss(reco.near.d, reco.third.d) - _loss(reco.near.d, doj);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
const [b, bloss] = find_max(ploss);
|
|
170
|
+
return [bloss + acc, b]; // add the shared accumulator
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** Update the loss when removing each medoid. Mutates loss in place. */
|
|
174
|
+
export function update_removal_loss(data, loss) {
|
|
175
|
+
loss.fill(0); // stable since 1.50
|
|
176
|
+
for (let r = 0; r < data.length; r++) {
|
|
177
|
+
const rec = data[r];
|
|
178
|
+
loss[rec.near.i] += _loss(rec.near.d, rec.seco.d) - _loss(rec.seco.d, rec.third.d);
|
|
179
|
+
loss[rec.seco.i] += _loss(rec.near.d, rec.seco.d) - _loss(rec.near.d, rec.third.d);
|
|
180
|
+
// as N might be unsigned
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** Update the third nearest medoid information. Called after each swap. Returns fresh DistancePair. */
|
|
185
|
+
export function update_third_nearest(mat, med, n, s, b, o, doj) {
|
|
186
|
+
let dist = new DistancePair(b, doj);
|
|
187
|
+
for (let i = 0; i < med.length; i++) {
|
|
188
|
+
const mi = med[i];
|
|
189
|
+
if (i === n || i === b || i === s) {
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
const d = mat.get(o, mi);
|
|
193
|
+
if (d < dist.d) {
|
|
194
|
+
dist = new DistancePair(i, d);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return dist;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** Perform a single swap. Returns RAW loss. */
|
|
201
|
+
export function do_swap(mat, med, data, b, j) {
|
|
202
|
+
const n = mat.len();
|
|
203
|
+
if (!(b < med.length)) throw new Error('invalid medoid number');
|
|
204
|
+
if (!(j < n)) throw new Error('invalid object number');
|
|
205
|
+
med[b] = j;
|
|
206
|
+
let acc = 0;
|
|
207
|
+
for (let o = 0; o < data.length; o++) {
|
|
208
|
+
const reco = data[o];
|
|
209
|
+
if (o === j) {
|
|
210
|
+
if (reco.near.i !== b) {
|
|
211
|
+
if (reco.seco.i !== b) {
|
|
212
|
+
reco.third = reco.seco.clone();
|
|
213
|
+
}
|
|
214
|
+
reco.seco = reco.near.clone();
|
|
215
|
+
}
|
|
216
|
+
reco.near = new DistancePair(b, 0);
|
|
217
|
+
acc += 0;
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
const doj = mat.get(o, j);
|
|
221
|
+
// Nearest medoid is gone:
|
|
222
|
+
if (reco.near.i === b) {
|
|
223
|
+
if (doj < reco.seco.d) {
|
|
224
|
+
reco.near = new DistancePair(b, doj);
|
|
225
|
+
} else if (reco.third.i === U32_MAX || doj < reco.third.d) {
|
|
226
|
+
reco.near = reco.seco.clone();
|
|
227
|
+
reco.seco = new DistancePair(b, doj);
|
|
228
|
+
} else {
|
|
229
|
+
reco.near = reco.seco.clone();
|
|
230
|
+
reco.seco = reco.third.clone();
|
|
231
|
+
reco.third = update_third_nearest(mat, med, reco.near.i, reco.seco.i, b, o, doj);
|
|
232
|
+
}
|
|
233
|
+
} else if (reco.seco.i === b) {
|
|
234
|
+
// second nearest was replaced
|
|
235
|
+
if (doj < reco.near.d) {
|
|
236
|
+
reco.seco = reco.near.clone();
|
|
237
|
+
reco.near = new DistancePair(b, doj);
|
|
238
|
+
} else if (reco.third.i === U32_MAX || doj < reco.third.d) {
|
|
239
|
+
reco.seco = new DistancePair(b, doj);
|
|
240
|
+
} else {
|
|
241
|
+
reco.seco = reco.third.clone();
|
|
242
|
+
reco.third = update_third_nearest(mat, med, reco.near.i, reco.seco.i, b, o, doj);
|
|
243
|
+
}
|
|
244
|
+
} else {
|
|
245
|
+
// nearest not removed
|
|
246
|
+
if (doj < reco.near.d) {
|
|
247
|
+
reco.third = reco.seco.clone();
|
|
248
|
+
reco.seco = reco.near.clone();
|
|
249
|
+
reco.near = new DistancePair(b, doj);
|
|
250
|
+
} else if (doj < reco.seco.d) {
|
|
251
|
+
reco.third = reco.seco.clone();
|
|
252
|
+
reco.seco = new DistancePair(b, doj);
|
|
253
|
+
} else if (reco.third.i === U32_MAX || doj < reco.third.d) {
|
|
254
|
+
reco.third = new DistancePair(b, doj);
|
|
255
|
+
} else if (reco.third.i === b) {
|
|
256
|
+
reco.third = update_third_nearest(mat, med, reco.near.i, reco.seco.i, b, o, doj);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
acc += _loss(reco.near.d, reco.seco.d);
|
|
260
|
+
}
|
|
261
|
+
return acc;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/** Special case k=2 of the FasterMSC algorithm. Returns { loss, assi, nIter, nSwaps }. */
|
|
265
|
+
export function fastermsc_k2(mat, med, maxiter) {
|
|
266
|
+
const n = mat.len(), k = med.length;
|
|
267
|
+
if (!(k === 2)) throw new Error('Only valid for k=2');
|
|
268
|
+
let [loss, assi, data] = initial_assignment_k2(mat, med);
|
|
269
|
+
let lastswap = n, n_swaps = 0, iter = 0;
|
|
270
|
+
while (iter < maxiter) {
|
|
271
|
+
iter += 1;
|
|
272
|
+
const swaps_before = n_swaps, lastloss = loss;
|
|
273
|
+
for (let j = 0; j < n; j++) {
|
|
274
|
+
if (j === lastswap) {
|
|
275
|
+
break;
|
|
276
|
+
}
|
|
277
|
+
if (j === med[assi[j]]) {
|
|
278
|
+
continue; // This already is a medoid
|
|
279
|
+
}
|
|
280
|
+
const [newloss, b] = find_best_swap_k2(mat, data, j); // assi not used, see below
|
|
281
|
+
if (newloss >= loss) {
|
|
282
|
+
continue; // No improvement
|
|
283
|
+
}
|
|
284
|
+
n_swaps += 1;
|
|
285
|
+
lastswap = j;
|
|
286
|
+
// perform the swap
|
|
287
|
+
loss = do_swap_k2(mat, med, assi, data, b, j);
|
|
288
|
+
}
|
|
289
|
+
if (n_swaps === swaps_before || loss >= lastloss) {
|
|
290
|
+
break; // converged
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
loss = 1 - loss / n;
|
|
294
|
+
return { loss, assi, nIter: iter, nSwaps: n_swaps };
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/** Perform the initial assignment to medoids, for k=2 only. Returns [loss, assi, data] (data = [d0,d1] pairs). */
|
|
298
|
+
export function initial_assignment_k2(mat, med) {
|
|
299
|
+
const n = mat.len(), k = med.length;
|
|
300
|
+
if (!mat.isSquare()) throw new Error('Dissimilarity matrix is not square');
|
|
301
|
+
if (!(n <= U32_MAX)) throw new Error('N is too large');
|
|
302
|
+
if (!(k === 2)) throw new Error('k must be 2');
|
|
303
|
+
const assi = new Array(mat.len()).fill(0);
|
|
304
|
+
const data = new Array(mat.len());
|
|
305
|
+
for (let _i = 0; _i < data.length; _i++) data[_i] = [0, 0];
|
|
306
|
+
let loss = 0;
|
|
307
|
+
for (let i = 0; i < data.length; i++) {
|
|
308
|
+
const d = data[i];
|
|
309
|
+
d[0] = mat.get(i, med[0]);
|
|
310
|
+
d[1] = mat.get(i, med[1]);
|
|
311
|
+
if (d[0] < d[1]) {
|
|
312
|
+
assi[i] = 0;
|
|
313
|
+
loss += _loss(d[0], d[1]); // return
|
|
314
|
+
} else {
|
|
315
|
+
assi[i] = 1;
|
|
316
|
+
loss += _loss(d[1], d[0]); // return
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
return [loss, assi, data];
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/** Find the best swap for object j - FastMSC version, k=2. Returns [loss, b]. */
|
|
323
|
+
export function find_best_swap_k2(mat, data, j) {
|
|
324
|
+
const ploss = [0, 0];
|
|
325
|
+
for (let o = 0; o < data.length; o++) {
|
|
326
|
+
const d = data[o];
|
|
327
|
+
const doj = mat.get(o, j);
|
|
328
|
+
// We do not use the assignment here, because we stored d0/d1 by medoid position, not closeness
|
|
329
|
+
ploss[0] += (doj < d[1]) ? _loss(doj, d[1]) : _loss(d[1], doj);
|
|
330
|
+
ploss[1] += (doj < d[0]) ? _loss(doj, d[0]) : _loss(d[0], doj);
|
|
331
|
+
}
|
|
332
|
+
const [b, bloss] = find_min(ploss);
|
|
333
|
+
return [bloss, b];
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/** Perform a single swap, k=2. Returns RAW loss. */
|
|
337
|
+
export function do_swap_k2(mat, med, assi, data, b, j) {
|
|
338
|
+
const n = mat.len();
|
|
339
|
+
if (!(b < med.length)) throw new Error('invalid medoid number');
|
|
340
|
+
if (!(j < n)) throw new Error('invalid object number');
|
|
341
|
+
med[b] = j;
|
|
342
|
+
// Its nicer to have the if outside, even though this looks duplicated
|
|
343
|
+
if (b === 0) {
|
|
344
|
+
let acc = 0;
|
|
345
|
+
for (let o = 0; o < data.length; o++) {
|
|
346
|
+
const d = data[o];
|
|
347
|
+
if (o === j) {
|
|
348
|
+
assi[o] = 0;
|
|
349
|
+
d[0] = 0;
|
|
350
|
+
acc += 0;
|
|
351
|
+
continue;
|
|
352
|
+
}
|
|
353
|
+
const doj = mat.get(o, j);
|
|
354
|
+
d[0] = doj;
|
|
355
|
+
if (doj < d[1] || (doj === d[1] && assi[o] === 0)) {
|
|
356
|
+
assi[o] = 0;
|
|
357
|
+
acc += _loss(doj, d[1]); // return
|
|
358
|
+
} else {
|
|
359
|
+
assi[o] = 1;
|
|
360
|
+
acc += _loss(d[1], doj); // return
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
return acc;
|
|
364
|
+
} else { // b == 1
|
|
365
|
+
let acc = 0;
|
|
366
|
+
for (let o = 0; o < data.length; o++) {
|
|
367
|
+
const d = data[o];
|
|
368
|
+
if (o === j) {
|
|
369
|
+
assi[o] = 1;
|
|
370
|
+
d[1] = 0;
|
|
371
|
+
acc += 0;
|
|
372
|
+
continue;
|
|
373
|
+
}
|
|
374
|
+
const doj = mat.get(o, j);
|
|
375
|
+
d[1] = doj;
|
|
376
|
+
if (doj < d[0] || (doj === d[0] && assi[o] === 1)) {
|
|
377
|
+
assi[o] = 1;
|
|
378
|
+
acc += _loss(doj, d[0]); // return
|
|
379
|
+
} else {
|
|
380
|
+
assi[o] = 0;
|
|
381
|
+
acc += _loss(d[0], doj); // return
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
return acc;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
</code></pre>
|
|
388
|
+
</article>
|
|
389
|
+
</section>
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
</div>
|
|
397
|
+
|
|
398
|
+
<br class="clear">
|
|
399
|
+
|
|
400
|
+
<footer>
|
|
401
|
+
Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Sun Jun 14 2026 10:59:21 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
|
|
402
|
+
</footer>
|
|
403
|
+
|
|
404
|
+
<script>prettyPrint();</script>
|
|
405
|
+
<script src="scripts/polyfill.js"></script>
|
|
406
|
+
<script src="scripts/linenumber.js"></script>
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
</body>
|
|
411
|
+
</html>
|