JAIN / DPC-MSTSC / EXECUTION TRACE

一个点,如何找到
它所属的簇?

随机选取 Jain 中的 p313,按真实函数调用顺序,查看每个中间集合、每条 MST 边和每一次分配依据。图中红环始终指向同一个样本。

样本索引 313 · 第 314 行K = 14 · λ = 0.3511 个函数阶段3 个过程滑块点击图片放大
执行约定:严格 K-FMNN 在这份数据上为空。本页先呈现这一事实,再明确启用 K-MNN 回退,以演示完整分配路径。所有图表均来自实际计算,真实类别未参与算法。
STEP 00

固定一次随机抽样,先看执行路线

fit_predict → fit → _validate

本页跟踪 p313:Python 索引 313,MATLAB / 人工行号 314。用 np.random.default_rng(20260926).integers(373) 从全部样本均匀抽取一次,没有按聚类结果筛选,也没有重抽。

输入 X:373 × 2 → K = 14,λ = 0.35,refine = True
prune_mean = "all" · vote_rule = "strict" · core_fallback = "kmnn"

_validate(X) 检查二维形状、有限数值、整数 K、1 ≤ K < N 和选项合法性。本例全部通过;此函数不改变样本,也不使用真实类别。所有后续点号均是 从 0 开始的索引。

校验 → 归一化 / 距离 → 邻域 → 核心集 → 局部 MST / 密度 → δ / γ → 骨架 → 吸引力分配 → 精修

这是带有全局依赖的算法:近邻需搜索全部点,δ 需比较全部密度,分配顺序需比较全部未标记点。本页在每一阶段突出 p313,同时保留必要的全局状态。

查看实际代码 · dpc_mstsc.py:32
    def _validate(self, X):
        X = np.asarray(X, dtype=float)
        if X.ndim != 2 or X.shape[0] < 2 or X.shape[1] < 1:
            raise ValueError("X must be a 2D matrix with at least two samples and one feature")
        if not np.isfinite(X).all():
            raise ValueError("X must contain only finite values")
        if isinstance(self.K, (bool, np.bool_)) or not isinstance(self.K, (int, np.integer)):
            raise ValueError("K must be an integer")
        if not 1 <= self.K < len(X):
            raise ValueError("K must satisfy 1 <= K < n_samples")
        if not np.isfinite(self.lam) or self.lam <= 0:
            raise ValueError("lam must be finite and positive (paper range: [0.25, 0.5])")
        if self.prune_mean not in ("all", "incident"):
            raise ValueError("prune_mean must be 'all' or 'incident'")
        if self.vote_rule not in ("strict", "plurality"):
            raise ValueError("vote_rule must be 'strict' or 'plurality'")
        if self.core_fallback not in ("error", "kmnn"):
            raise ValueError("core_fallback must be error or kmnn")
        return X
查看实际代码 · dpc_mstsc.py:209
    def fit_predict(self, X):
        return self.fit(X).labels_
查看实际代码 · dpc_mstsc.py:188
    def fit(self, X):
        X = self._validate(X)
        # Clear prior fitted results even if the new fit fails during core discovery.
        self.labels_ = self.centers_ = self.skeletons_ = self.n_clusters_ = None
        self.X_normalized_ = self._minmax_normalize(X)
        self.D_ = cdist(self.X_normalized_, self.X_normalized_)
        self._build_neighborhoods(self.D_)
        self._find_kfmnn()
        self._pruned_mst_density(self.D_)
        self._compute_delta_gamma(self.D_)
        self._build_skeletons()
        labels = np.full(len(X), -1, dtype=int)
        for c, skeleton in enumerate(self.skeletons_):
            labels[skeleton] = c
        self.skeleton_labels_ = labels.copy()
        self.labels_before_refinement_ = self._attractiveness_assignment(self.D_, labels)
        self.labels_ = (self._label_refinement(self.labels_before_refinement_) if self.refine
                        else self.labels_before_refinement_.copy())
        self.n_output_clusters_ = len(np.unique(self.labels_))
        return self
STEP 01

从原始坐标得到实际计算空间

_minmax_normalize(X) + fit 中的 cdist

分别缩放每个特征。所有近邻、MST 边权和吸引力距离,都来自归一化后的欧氏空间;它与原始空间的邻居关系不必完全相同。

x′ik = (xik − min X:k) / (max X:k − min X:k)
d(i,j) = √[(x′i1 − x′j1)² + (x′i2 − x′j2)²]
特征全局最小值全局最大值跨度p313 原值归一化结果
10.7541.340.5536.70.88655980
22.9527.8524.99.150.24899598
两幅图均保持各自坐标单位的等比例显示。红色圆环是同一个样本;缩放会改变两个特征对距离的相对贡献。

cdist 生成 373 × 373 距离矩阵。对角线 d(i,i)=0;选近邻时才在临时行副本中将自身距离置为 ∞,原距离矩阵不被改变。

展开 p313 到全部样本的距离(按距离排序)
距离排序点索引距离
03130.00000000
13100.02130112
23150.02459598
33140.02622065
43110.02648052
53040.03303350
63090.03595552
73170.04083640
83120.04140252
93200.04203038
103160.04238922
113210.04362044
123030.04468932
133080.04479654
142970.05317267
153020.05349964
162960.06155280
173180.06302573
183060.06711725
193220.06778780
203190.06828423
213010.07045398
223230.07070515
232950.07441092
243050.07561966
253000.07631518
262940.08074532
273240.08242329
282980.08526856
293250.09177221
302920.09255057
312910.09273017
322930.09765733
332990.09842447
342900.10277487
353260.10467944
362680.10717415
372890.11075054
383310.11273032
392670.11423877
403320.11444872
413300.11486436
423290.12124299
432880.12183167
443270.12251478
453460.12686374
462870.12696493
473330.13028519
482690.13427399
493280.13699640
503450.14253725
512700.14304925
522840.14373216
532660.14453302
543470.14471890
552850.14553119
562830.14767661
573500.14795275
582860.14937718
592710.15204935
603350.15532485
613340.15594146
623440.16049891
632650.16251974
642720.16282417
653480.16297332
662820.16304241
673510.16325677
682730.16904012
693490.17011033
703360.17181914
713430.17225675
722640.17323094
732810.17428794
742800.17857476
753520.18660251
762630.19081320
772790.19365370
782470.19545685
792780.19826983
802740.19856414
812620.19956060
822770.20765635
833410.20957427
843420.21270535
853400.21276258
862610.21359644
873390.21543930
882460.21646023
893370.21672303
902760.22159370
913380.22240363
922750.22412742
933530.23391630
942570.23907920
952450.24196338
963540.24276952
972440.24600317
982540.24946138
993550.25062837
1002600.25105021
1012550.25192468
1022560.25248724
1032590.25452361
1042580.25707748
1053660.26047354
1063650.26265911
1073560.26318299
1082530.26404462
1092430.26437531
1103070.26656560
1112220.26829392
1122240.27121026
1133570.27179354
1142520.27187751
1152230.27534952
1162480.27560009
1172510.27746454
1182250.27815813
1192490.28159681
1202420.28267692
1213580.28316314
1222030.28491486
1232500.28596863
1242210.28933895
1253640.29232952
1263630.29392447
1272260.29573031
1283670.29780623
1293620.30167296
1302270.30194545
1312020.30221233
1323690.30452568
1332280.30886211
1343610.30932313
1352040.31254026
1362410.31312836
1373680.31334212
1382010.31748806
1392400.32047863
1403710.32077317
1412290.32162128
1421900.32250132
1432000.32523529
1443720.32693613
1453590.32707805
1462300.32917091
1471890.33121630
1483700.33152823
1493600.33413256
1502380.33694116
1512390.34042553
1522200.34044909
1532050.34065837
154920.34344423
1551990.34424688
1562320.34552337
1572370.34785667
1582330.34824537
1592310.34966062
1601980.35112115
1612190.35427959
1621840.35986005
1631870.36137198
1642150.36390838
1652170.36408203
1662180.36452825
1671960.36670417
1681830.36697019
1691970.36707512
1701820.36714376
1712360.36746960
1721880.37033147
1732160.37121777
1742350.37145437
1752340.37178134
1762060.37193267
177960.37443194
1781950.37508270
1791720.37757369
1801850.37972830
1812130.38037373
1822070.38236850
1831710.38250250
1841930.38392901
1852140.38729000
1861730.38747257
187950.38813030
1881940.39067532
1892110.39411102
1901920.39479309
1911810.39548418
1922120.39553868
1932080.39810959
1942100.39824068
1952090.39829023
1961650.39852170
197930.40048954
1981800.40287585
1991910.40352523
2001640.40456357
2011790.40607623
202940.40620345
2031740.41114197
2041630.41195960
2051860.41393699
2061780.41444508
2071620.41447842
2081750.41503087
2091490.41800597
2101560.42680387
2111760.42788373
2121550.42910458
2131610.42956449
214910.42981446
2151770.43173886
2161500.43432924
2171570.43543182
2181680.43770107
2191580.43973975
2201690.44375213
2211700.44429210
2221600.44796541
2231590.44940545
2241400.45183717
2251420.45273981
226810.45337034
2271390.45415185
2281670.45420858
2291540.45735676
230820.45988523
2311660.46107232
2321380.46619739
233900.46706799
2341410.46770279
2351530.46927829
2361450.46939992
237800.47023506
2381440.47273243
2391370.47300034
2401520.47413540
2411510.47471847
2421430.47736949
2431460.47893133
244790.48117001
2451360.48545385
2461470.48755363
247890.49037765
2481480.49239482
249780.49352890
2501350.49430095
251880.49739100
2521330.49942118
2531340.50142739
2541290.50372265
2551280.50451349
2561270.50584060
257830.50614321
2581250.50743623
2591240.50830449
2601260.51163731
2611300.51298893
262870.51387422
2631230.51544490
264840.51936298
2651320.52108612
2661220.52112367
2671310.52195287
2681180.52415901
2691170.53123731
2701200.53141932
2711190.53284851
272770.53331436
2731210.53331805
274850.53749648
2751160.53808957
2761100.53811692
2771120.54000923
2781150.54011490
2791110.54405478
280860.54674161
2811140.54810791
2821090.55003788
2831130.55151166
2841070.55336916
2851080.55551010
2861050.56115490
2871060.56779229
2881040.57303462
289760.57380097
290710.57644155
291700.58227475
2921010.58502011
2931020.58974687
2941030.59060437
2951000.59170363
296690.61152806
297990.61270515
298680.62471001
299750.63006931
300980.63209198
301620.63557512
302670.64620797
303970.64629796
304720.65048761
305610.66124481
306600.66512425
307650.67119519
308660.67189979
309730.67448168
310740.68712582
311640.68994591
312590.69134455
313630.70112085
314580.71113380
315490.74433357
316480.75278449
317140.75856202
318470.76187765
319560.76476041
320440.76592560
321400.76663307
322570.76953209
323460.77225582
324430.77293490
325150.77482896
326410.78137834
327550.78140801
328450.78159415
329510.78555973
330420.78564708
331500.79138043
332520.79906657
33330.80166425
334380.80483043
335310.81642926
336300.81675927
33750.81860612
338390.82075090
339160.82365537
34040.82652307
341280.83080946
342290.83424921
343170.84913826
34460.85485131
345530.86099904
34620.86165813
34770.87190431
348540.87555694
349180.88792435
35080.89272373
351370.89460314
352200.90051558
353360.90410636
354130.90890804
355190.92201112
356210.92207511
35710.92362763
35890.92406736
359270.93427348
360350.93792748
361320.94176462
36200.94484539
363340.94710689
364330.95062724
365120.95625544
366100.96282985
367220.96486384
368110.97300049
369250.98722066
370230.98970202
371240.99115065
372260.99181098
查看实际代码 · dpc_mstsc.py:52
    @staticmethod
    def _minmax_normalize(X):
        span = np.ptp(X, axis=0)
        return (X - X.min(axis=0)) / np.where(span == 0, 1, span)
STEP 02

得到 KNN、互近邻、二阶邻居和共享计数

_build_neighborhoods(D)

先排除自身,选最小的 14 个距离;边界平票按点索引打破。对每个邻居 j,再检查 p313 是否也出现在 j 的 KNN 中。两边都成立,才是互近邻。

14一阶近邻
13互近邻
32二阶近邻(去自身)
左侧局部放大展示一阶关系;右侧在完整数据上显示二阶集合。二阶邻居与一阶邻居可以重叠,不是简单的“第 15–28 近邻”。

唯一的单向邻居为 p297:它属于 p313 的 KNN,但它的 14 个近邻不包含 p313。因此 |MNN(p313)| = 13。

KNN 排名jd(i,j)互近邻?|MNN(j)|j ∈ S?SNN(i,j)
13100.02130112是12否12
23150.02459598是14是9
33140.02622065是14是9
43110.02648052是14是11
53040.03303350是14是6
63090.03595552是10否11
73170.04083640是14是8
83120.04140252是12否9
93200.04203038是11否8
103160.04238922是14是8
113210.04362044是7否9
123030.04468932是13否3
133080.04479654是10否9
142970.05317267否13否3
KNN²(i) = ⋃j ∈ KNN(i) KNN(j),本实现去除 i
SNN(i,j) = |KNN(i) ∩ KNN(j)|

SNN 是共享样本的数量,不要求 i 与 j 本身互为近邻。代码用稀疏 0/1 邻接矩阵 B 构建 B @ B.T;每个元素等于一个集合交集大小。

以目标 p313 与 p310 为例,紫色的 12 个点同时属于双方的 KNN,故共享计数为 12。红环/方框标出配对点;一个配对点可能属于对方的 KNN,但不属于自身 KNN。
展开二阶邻居的具体到达路径 i → j → k
二阶点 k可作为中间点的 j
290297
291297
292303, 297
293297
294304, 303, 308, 297
295304, 303, 308, 297
296304, 309, 303, 308, 297
297310, 304, 309, 303, 308
298303, 297
300304, 303, 297
301304, 303, 297
302304, 303, 308, 297
303310, 304, 309, 308, 297
304310, 311, 309, 321, 303, 308, 297
305303
306304, 303
308310, 311, 304, 309, 312, 303, 297
309310, 315, 314, 311, 304, 317, 312, 308
310315, 314, 311, 304, 309, 317, 312, 320, 316, 321, 308
311310, 315, 314, 304, 309, 317, 312, 320, 316, 321, 308
312310, 315, 314, 311, 309, 317, 320, 316, 321, 308
314310, 315, 311, 309, 317, 312, 320, 316, 321, 308
315310, 314, 311, 309, 317, 312, 320, 316, 321, 308
316310, 315, 314, 311, 309, 317, 312, 320, 321
317310, 315, 314, 311, 309, 312, 320, 316, 321
318310, 315, 314, 311, 309, 317, 312, 320, 316, 321
319315, 314, 311, 317, 312, 320, 316, 321
320310, 315, 314, 311, 317, 312, 316, 321
321315, 314, 311, 320, 316
322315, 314, 317, 320, 316, 321
323315, 314, 317, 312, 320, 316, 321
324317, 312, 320, 316
展开 14 个近邻各自与目标点共享的样本
邻居 j共享样本索引
310297, 303, 304, 308, 309, 311, 312, 314, 315, 316, 317, 320
315309, 310, 311, 312, 314, 316, 317, 320, 321
314309, 310, 311, 312, 315, 316, 317, 320, 321
311304, 308, 309, 310, 312, 314, 315, 316, 317, 320, 321
304297, 303, 308, 309, 310, 311
309297, 303, 304, 308, 310, 311, 312, 314, 315, 316, 317
317309, 310, 311, 312, 314, 315, 316, 320
312308, 309, 310, 311, 314, 315, 316, 317, 320
320310, 311, 312, 314, 315, 316, 317, 321
316310, 311, 312, 314, 315, 317, 320, 321
321304, 310, 311, 312, 314, 315, 316, 317, 320
303297, 304, 308
308297, 303, 304, 309, 310, 311, 312, 314, 315
297303, 304, 308
查看实际代码 · dpc_mstsc.py:57
    def _build_neighborhoods(self, D):
        n, k = len(D), self.K
        self.K_eff_ = k
        knn = np.empty((n, k), dtype=int)
        for i in range(n):
            row = D[i].copy()
            row[i] = np.inf  # Exclude identity, not zero-distance duplicates.
            cutoff = np.partition(row, k - 1)[k - 1]
            closer = np.flatnonzero(row < cutoff)
            tied = np.flatnonzero(row == cutoff)[:k - len(closer)]
            chosen = np.concatenate((closer, tied))
            knn[i] = chosen[np.lexsort((chosen, row[chosen]))]
        self.knn_ = knn
        adjacency = csr_matrix((np.ones(n*k, dtype=np.int64),
                                (np.repeat(np.arange(n), k), knn.ravel())),
                               shape=(n, n))
        mutual = adjacency.multiply(adjacency.T).tocsr()
        self.mnn_ = [mutual.indices[mutual.indptr[i]:mutual.indptr[i+1]].copy()
                     for i in range(n)]
        self.knn2_ = [set(knn[knn[i]].ravel().tolist()) - {i} for i in range(n)]
        # Sparse product avoids the original dense O(n^3) integer product.
        self.snn_ = (adjacency @ adjacency.T).toarray()
STEP 03

核心筛选失败在哪里?为何需要显式回退?

_find_kfmnn()

S = {i : |MNN(i)| = 14}
SF = {i ∈ S : MNN(i) ⊆ S}

第一层筛选要求一个点的全部 14 个近邻都是互近邻。第二层还要求这些互近邻本身都通过第一层筛选。p313 只有 13 个互近邻,第一层就未通过,所以不属于 S,也不属于严格 SF。

全局 S 有 114 个点,但没有任何点同时满足严格 K-FMNN 的第二层条件。这里不能把 S 的点直接称作 K-FMNN 点。
默认严格模式到这里终止。

No K-FMNN core points for this K; paper skeleton initialization is undefined. Choose another K; no K-MNN fallback is applied.

为了演示后续完整路径,本页显式启用 core_fallback="kmnn",实际初始化集合改为 S(114 个点)。这是扩展策略,不能等同于论文严格 K-FMNN 流程已经复现成功。p313 仍不是核心点。

展开所有 K-MNN 候选及其未满足条件的互近邻
S 中候选其互近邻中不属于 S 的点
60, 1, 2, 4, 5, 13, 15, 16
70, 1, 2, 4, 5, 13, 15, 16
80, 4, 5, 11, 12, 13, 15, 16
90, 11, 12, 13, 16, 19, 20, 21
1011, 12, 13, 19, 20, 21, 22, 23
1711, 12, 13, 16, 19, 20, 21, 28
1811, 12, 13, 16, 19, 20, 21, 28
3029, 31, 38, 39, 40, 41, 43
3726, 27, 32, 33, 34, 35, 36, 38, 39, 50, 51, 52, 53, 54
4228, 29, 31, 40, 41, 43
4429, 31, 40, 41, 43
4529, 31, 38, 40, 41, 43
4629, 31, 38, 41, 43
4729, 31, 40, 41, 43
4829, 31, 40, 41, 43
4931, 40, 41, 43, 60
5843, 60, 62, 63, 64
5960, 62, 63, 64, 65, 66
6160, 62, 63, 64, 65, 66, 67, 69, 70, 71, 72
6860, 62, 63, 64, 65, 66, 67, 69, 70, 71, 72
7667, 69, 70, 71, 72, 77, 84, 85, 86, 87, 88
8370, 71, 77, 78, 79, 80, 82, 84, 85, 86, 91
8977, 78, 79, 80, 82, 84, 85, 86, 87, 88, 91
9077, 78, 79, 80, 82, 84, 85, 86, 87, 88, 91, 94
107100, 101, 106, 109, 113, 114, 115, 118
108100, 101, 109, 113, 114, 115, 118
116106, 113, 114, 115, 118, 123, 124
117109, 113, 114, 115, 118, 123, 124
119109, 114, 115, 118, 123, 124, 125
120109, 114, 118, 123, 124, 125, 126
121109, 110, 123, 124, 125, 126, 127
122109, 110, 118, 123, 124, 125, 126, 127
129110, 111, 126, 127, 128, 131, 132, 137, 138, 139
130110, 111, 112, 127, 128, 131, 132, 137, 138
133112, 132, 140, 145, 146, 147, 148
134111, 112, 131, 132, 137, 140
135111, 112, 131, 132, 137, 140
136132, 137, 139, 140, 142, 145
141137, 139, 140, 142, 145, 146, 147, 148
143140, 142, 145, 146, 147, 148, 149
144140, 142, 145, 146, 147, 148, 149
150142, 145, 146, 149, 154
155142, 145, 149, 154, 171
156149, 154, 171, 172
157149, 151, 152, 154
158151, 152, 154, 160
159151, 152, 153, 154, 160, 166, 167, 168, 169
161152, 153, 154, 160, 173
162154, 160, 171, 172, 173
163149, 171, 172, 173, 182
164149, 171, 172, 173, 182
165171, 172, 173, 182, 183
176166, 167, 168, 169, 170, 174, 175, 177, 178, 179, 185, 186
180168, 169, 170, 173, 174, 175, 178, 179, 184, 185, 187, 188
181168, 169, 170, 173, 174, 175, 179, 183, 184, 185, 187, 188
191177, 178, 186, 192, 193, 195, 196, 209, 210
194186, 192, 193, 195, 196, 209, 210
197188, 192, 193, 195, 196, 198, 199, 205
206192, 193, 195, 196, 205, 209, 210
207192, 193, 195, 196, 209, 210, 211
208186, 192, 193, 195, 209, 210, 211
213209, 210, 211, 212, 214
215205, 209, 210, 211, 220
216209, 210, 211, 212, 214, 220, 231, 232, 234
217211, 212, 214, 220, 231, 232, 234, 235
218211, 212, 214, 230, 231, 232, 234, 235, 236
219211, 214, 220, 229, 230, 231, 232, 234
233228, 229, 230, 231, 232, 234, 235, 236, 237, 238, 239
242240, 241, 243, 249, 250
251241, 243, 248, 249, 250
252243, 248, 249, 250, 257
253225, 243, 244, 250, 257
254225, 243, 244, 245, 257, 261
255243, 244, 245, 257, 261
256243, 250, 257, 260, 261
258248, 249, 250, 257, 260
259248, 249, 250, 257, 260, 275
270264, 265, 266, 268, 269, 271, 283, 287
272262, 263, 264, 265, 266, 269, 271, 283
273262, 264, 265, 269, 271, 278, 283, 285
274257, 261, 262, 264, 275, 276, 277, 278, 279, 280
281276, 277, 278, 279, 280, 283, 285, 286, 287
282271, 277, 278, 280, 283, 285, 286, 287
284265, 269, 271, 283, 285, 286, 287
289268, 283, 285, 287, 288, 294
290268, 269, 287, 288, 294
291268, 269, 287, 288, 294, 297, 302
292268, 287, 288, 294, 297, 301, 302
293268, 287, 288, 294, 297, 299, 301
295268, 294, 297, 302, 303, 308
296268, 294, 297, 301, 302, 303, 308
298288, 297, 299, 300, 301, 302, 303
304294, 297, 300, 301, 302, 303, 306, 308, 309, 310, 313
307243, 244, 249, 250, 257
311308, 309, 310, 312, 313, 319, 320, 321
314309, 310, 312, 313, 319, 320, 321, 322
315309, 310, 312, 313, 319, 320, 321, 322
316310, 312, 313, 319, 320, 321, 322, 324
317309, 310, 312, 313, 319, 320, 322, 324
318309, 310, 312, 319, 322, 324, 325, 326
323319, 320, 322, 324, 325, 326
330324, 325, 326, 327, 328, 329, 334, 335, 346
331324, 325, 326, 327, 328, 329, 334, 346, 347
332324, 325, 326, 327, 329, 346, 347, 350
333326, 327, 328, 329, 334, 346, 347, 348
343334, 335, 336, 340, 341, 342, 347, 348, 349, 351, 352
344328, 334, 335, 336, 346, 347, 348, 349, 351, 352
345334, 346, 347, 348, 349, 350, 351, 352
356338, 342, 353, 354, 355, 357, 358, 359, 361
362357, 358, 359, 360, 361, 368, 369, 371, 372
363358, 359, 360, 361, 367, 368, 369, 371, 372
364358, 359, 360, 361, 367, 368, 369, 371, 372
365338, 340, 353, 354, 355, 357, 358, 367, 369
366337, 338, 339, 340, 341, 354, 355, 367, 369
查看实际代码 · dpc_mstsc.py:80
    def _find_kfmnn(self):
        full = np.array([len(v) == self.K for v in self.mnn_])
        self.S_ = np.flatnonzero(full)
        self.SF_ = np.array([i for i in self.S_ if full[self.mnn_[i]].all()], dtype=int)
        self.strict_SF_ = self.SF_.copy()
        self.used_fallback_ = False
        if not len(self.SF_) and self.core_fallback == "kmnn" and len(self.S_):
            self.SF_ = self.S_.copy()
            self.used_fallback_ = True
        if not len(self.SF_):
            raise ValueError("No K-FMNN core points for this K; paper skeleton initialization "
                             "is undefined. Choose another K; no K-MNN fallback is applied.")
STEP 04

在这个点的局部完全图上逐步构建 MST

_pruned_mst_density(D) 内调用 _mst(sub)

局部节点按 [i] + KNN(i) 排列,共 15 个节点。完全图有 15×14/2 = 105 条候选无向边,但 MST 最终只有 14 条边。边权是归一化欧氏距离,不是目标点到所有邻居的星形连线。

_mst 使用 Prim:从 p313 开始;key[v] 是当前树到未入树节点 v 的最短边长,parent[v] 保存该边在树中的端点。每次选择 key 最小的节点,加入边,再更新剩余节点的 key 与 parent。

拖动滑块观察真实选边顺序。蓝绿色实线是已经加入树的边;橙色虚线是尚未入树节点的当前最短连接候选。下方表中的边序与滑块一致。

展开 14 次选边记录
加边顺序父节点 → 新节点边权
1313 → 3100.02130112
2310 → 3110.01229799
3310 → 3090.01493218
4311 → 3120.01534886
5311 → 3140.01597584
6314 → 3150.00766594
7314 → 3170.01489644
8315 → 3160.01823623
9309 → 3080.01885116
10316 → 3200.02862198
11320 → 3210.01178197
12313 → 3040.03303350
13304 → 3030.01211113
14303 → 2970.01493218
查看实际代码 · dpc_mstsc.py:93
    @staticmethod
    def _mst(sub):
        """Dense Prim; returns local endpoint pairs and weights, including zero edges."""
        n = len(sub)
        used = np.zeros(n, dtype=bool)
        key = np.full(n, np.inf)
        parent = np.full(n, -1, dtype=int)
        key[0] = 0
        edges, weights = [], []
        for _ in range(n):
            u = int(np.argmin(np.where(used, np.inf, key)))
            used[u] = True
            if parent[u] >= 0:
                edges.append((parent[u], u))
                weights.append(key[u])
            improve = (~used) & (sub[u] < key)
            key[improve] = sub[u, improve]
            parent[improve] = u
        return np.asarray(edges, dtype=int).reshape(-1, 2), np.asarray(weights)
STEP 05

剪除异常边,再将保留边转换成密度

_pruned_mst_density(D)

本页采用 prune_mean="all":先计算全部 14 条 MST 边的平均权重,再检查每条边的相对偏差。不是只删除长边:异常短的边也可能被删除。

平均边权 w̄ = 0.01714189
保留条件:|w − w̄| / w̄ < 0.35
等价区间:0.01114223 < w < 0.02314156
ρ313 = (1 / 14) × Σ保留边 exp(−w) = 0.77362114
左:完整局部 MST。右:蓝绿色实线为保留边,红色虚线为剪除边。剪枝作用在边上,样本没有从整个数据集中删除。
条形图显示每条边为何被保留或删除,以及它最终对目标点密度的贡献。分母始终为 K=14,不是保留边数。

本点保留 11 条边,删除 3 条。所有保留边都参与求和,即使剪枝后不再与目标点相连;代码没有额外截取目标点所在的连通分量。

边序边端点w|w−w̄|/w̄判断exp(−w)/14 的实际贡献
1313–3100.021301120.24263537保留0.06992315
2310–3110.012297990.28257695保留0.07055552
3310–3090.014932180.12890720保留0.07036991
4311–3120.015348860.10459954保留0.07034060
5311–3140.015975840.06802347保留0.07029651
6314–3150.007665940.55279500删除0.00000000
7314–3170.014896440.13099226保留0.07037243
8315–3160.018236230.06383977保留0.07013779
9309–3080.018851160.09971251保留0.07009467
10316–3200.028621980.66970930删除0.00000000
11320–3210.011781970.31267968保留0.07059194
12313–3040.033033500.92706235删除0.00000000
13304–3030.012111130.29347801保留0.07056871
14303–2970.014932180.12890720保留0.07036991

论文式(6)对均值的文字表述存在歧义。本例采用“全部局部 MST 边均值”的解释;另一可用选项是 incident。本页的数字严格对应当前 all 配置,没有混合两种定义。

查看实际代码 · dpc_mstsc.py:113
    def _pruned_mst_density(self, D):
        rho = np.zeros(len(D))
        for i in range(len(D)):
            nodes = np.r_[i, self.knn_[i]]
            edges, weights = self._mst(D[np.ix_(nodes, nodes)])
            reference = weights if self.prune_mean == "all" else weights[(edges == 0).any(axis=1)]
            mean = reference.mean()
            # Eq. 6 is undefined at mean=0: retain zero edges, reject positive ones.
            keep = (np.abs(weights - mean) < self.lam * mean) if mean > 0 else (weights == 0)
            rho[i] = np.exp(-weights[keep]).sum() / self.K
        self.rho_ = rho
STEP 06

计算严格高密度距离 δ 和决策值 γ

_compute_delta_gamma(D)

先对全部 373 个样本重复上一阶段,得到完整密度向量。再从满足 rho[j] > rho[i] 的样本中选距离最近者;相同密度不算更高密度。

ρ313 = 0.77362114
最近的严格高密度点:p309,ρ309 = 0.84351529
δ313 = d(313, 309) = 0.03595552
γ313 = ρ313 × δ313 = 0.02781595
左图色值是全局计算后的密度,连线指向最近的严格高密度点;右图显示该点在决策图中的位置。

本点有 36 个严格高密度候选。若一个点没有任何严格高密度候选,它的 δ 才取该行最大距离。

最近的高密度候选 jρ(j)d(i,j)
3090.843515290.03595552
3170.842678480.04083640
3120.842762650.04140252
3200.842900510.04203038
3160.842900510.04238922
3180.773910110.06302573
3260.844183590.10467944
3300.843785750.11486436
δ 和 γ 在本实现中用于返回结果与解释,不驱动后续标签分配。骨架中心按 rho 选取,非核心点按吸引力分配,不能理解为“跟随 p309 就完成聚类”。
查看实际代码 · dpc_mstsc.py:125
    def _compute_delta_gamma(self, D):
        n = len(D)
        self.delta_ = np.zeros(n)
        self.nneigh_ = np.full(n, -1, dtype=int)
        for i in range(n):
            higher = np.flatnonzero(self.rho_ > self.rho_[i])
            if len(higher):
                j = higher[np.argmin(D[i, higher])]
                self.delta_[i], self.nneigh_[i] = D[i, j], j
            else:
                self.delta_[i] = D[i].max()
        self.gamma_ = self.rho_ * self.delta_
STEP 07

先在全局构建骨架,目标点暂时保持未标记

_build_skeletons()

实际候选集合为回退后的 114 个 K-MNN 点。每次选剩余候选中密度最高者作为新中心,从该中心开始 BFS;只把一阶或二阶邻域内仍在候选集合中的点加入队列。入队时就从剩余集合删除,避免重复入队。

星号是骨架中心,实心彩色点是初始化核心点。红色圆环 p313 不属于核心集合,因此不会被 BFS 直接吸收。
骨架标签中心索引中心密度初始骨架点数
C01160.9180526190
C1100.6920848624

骨架建成后,skeleton_labels_[313] = −1。灰色 −1 表示尚未分配,不是算法最终判定的噪声。114 个核心点已有标签,剩余 259 个点进入吸引力传播。

查看实际代码 · dpc_mstsc.py:138
    def _build_skeletons(self):
        remaining = set(self.SF_.tolist())
        skeletons, centers = [], []
        while remaining:
            cen = min(remaining, key=lambda i: (-self.rho_[i], i))
            centers.append(cen)
            queue, group = deque([cen]), []
            remaining.remove(cen)
            while queue:
                p = queue.popleft()
                group.append(p)
                reachable = (set(self.knn_[p]) | self.knn2_[p]) & remaining
                for q in sorted(reachable):
                    remaining.remove(q)
                    queue.append(q)
            skeletons.append(np.asarray(group, dtype=int))
        self.skeletons_ = skeletons
        self.centers_ = np.asarray(centers, dtype=int)
        self.n_clusters_ = len(skeletons)
STEP 08

比较每个簇的吸引力,等待全局轮到这个点

_attractiveness_assignment(D, skeleton_labels)

对每个未标记点 i 与每个簇 c,累加该簇所有已标记点 j 的贡献。j 可以是初始骨架点,也可以是刚刚被吸收的非核心点。共享邻居为零的点对,贡献就是零。

w(i,j) = SNN(i,j) × exp(−|ρi−ρj|) × exp(−d(i,j))
A(i,c) = Σj ∈ Cc w(i,j)
(i*,c*) = arg max全部未标记 i、全部簇 c A(i,c)

目标点并不是按输入行号、密度或距离次序被分配。它在全局第 93 轮才成为最大吸引力对应的点,被分给 C0。

阶段A(i,C0)A(i,C1)
仅有初始骨架67.544138460.00000000
第 93 轮分配之前115.200150890.00000000

滑块 t 表示已完成 t 次分配:t=0 只有骨架,t=92 是目标点分配前,t=93 时它已获得标签。实线小图跟踪它等待期间的两个吸引力值;分配后不再把它作为候选评分。

在决定该点归属的一刻,共有 26 个已标记点对它提供正贡献。下面的图与表固定在目标点分配前,不随上方全局时间滑块改变。

左图连线连接提供正吸引力的点,线宽和点大小反映贡献;右图展示贡献最大的 12 个点。仅画出有限的正贡献,零贡献点仍属于各自的簇。
以贡献最大的 p310 为例:
SNN = 12,|Δρ| = 0.00073340,d = 0.02130112
w(313,310) = 12 × 0.99926687 × 0.97892414 = 11.73847757
展开分配时全部正贡献与共享样本明细
j所属簇SNN|Δρ|d(i,j)贡献共享样本索引
310C0120.000733400.0213011211.73847757297, 303, 304, 308, 309, 311, 312, 314, 315, 316, 317, 320
311C0110.000547770.0264805210.70667077304, 308, 309, 310, 312, 314, 315, 316, 317, 320, 321
309C0110.069894150.035955529.89515850297, 303, 304, 308, 310, 311, 312, 314, 315, 316, 317
315C090.000631930.024595988.77578887309, 310, 311, 312, 314, 316, 317, 320, 321
314C090.000631930.026220658.76154273309, 310, 311, 312, 315, 316, 317, 320, 321
318C080.000288970.063025737.50918423309, 310, 311, 312, 314, 315, 316, 317
308C090.139780330.044796547.48310422297, 303, 304, 309, 310, 311, 312, 314, 315
317C080.069057350.040836407.16743460309, 310, 311, 312, 314, 315, 316, 320
316C080.069279370.042389227.15472479310, 311, 312, 314, 315, 317, 320, 321
304C060.210494280.033033504.70314622297, 303, 308, 309, 310, 311
323C050.141609620.070705154.04355048314, 315, 316, 317, 320
306C040.283486260.067117252.81705172297, 303, 304, 321
295C040.280234180.074410922.80568935297, 303, 304, 308
296C040.350619310.061552802.64884116297, 303, 304, 308
305C030.212917550.075619662.24807676297, 303, 304
303C030.282023870.044689322.16387177297, 304, 308
302C030.279412320.053499642.15049992297, 303, 304
294C030.280234180.080745322.09097987297, 303, 304
297C030.350619310.053172672.00334905303, 304, 308
300C030.351605930.076315181.95558852297, 303, 304
301C030.421011740.070453981.83518734297, 303, 304
299C020.352037590.098424471.27466719297, 303
298C020.420940420.085268561.20555278297, 303
292C010.280423430.092550570.68868314297
291C010.280423430.092730170.68855946297
293C010.281015120.097657330.68476987297

每吸收一个点 p,只更新它所属簇的一列:A[:, c] += contribution[:, p]。这与重新计算各簇所有贡献之和等价。本页的跟踪重放已与实际函数输出逐项校验。

零距离不同样本仍可贡献吸引力,因为 exp(0)=1;只将自身对自身的对角贡献置零。本例没有重复坐标。

查看实际代码 · dpc_mstsc.py:158
    def _attractiveness_assignment(self, D, labels):
        labels = labels.copy()
        contribution = self.snn_ * np.exp(-np.abs(self.rho_[:, None] - self.rho_[None, :]) - D)
        np.fill_diagonal(contribution, 0)
        scores = np.column_stack([contribution[:, s].sum(axis=1) for s in self.skeletons_])
        pending = labels < 0
        self.zero_attraction_assignments_ = 0
        while pending.any():
            ids = np.flatnonzero(pending)
            sub = scores[ids]
            pos, cluster = np.unravel_index(np.argmax(sub), sub.shape)
            point = ids[pos]
            if sub[pos, cluster] == 0:
                self.zero_attraction_assignments_ += 1
            labels[point] = cluster
            pending[point] = False
            scores[:, cluster] += contribution[:, point]
        return labels
STEP 09

以传播后的邻居标签做一次同步多数投票

_label_refinement(labels)

所有非核心点都完成分配后,才开始精修。对每个点读取同一份传播结果,不把循环内刚改过的标签用于后面的点。本页设置 vote_rule="strict",要求唯一胜者的票数严格大于 K/2=7。

C0精修前标签
14 : 0C0 : C1 邻居票数
C0最终标签
左图是目标点 14 个邻居的传播后标签;右图是全体样本最终预测标签。颜色来自模型输出,未用真实类别参与算法。

本点的 14 个邻居全部投给 C0,满足严格多数;但它原本已属于 C0,因此标签不变。全数据本轮发生标签变化的点数为 0。

邻居 j传播完成后的标签
310C0
315C0
314C0
311C0
304C0
309C0
317C0
312C0
320C0
316C0
321C0
303C0
308C0
297C0

Algorithm 2 已为全部样本分配标签,因此这里没有遗留的 −1,也没有触发“未标记点归最近已标记点”的步骤。代码不会凭空额外执行一次噪声判定。

查看实际代码 · dpc_mstsc.py:177
    def _label_refinement(self, labels):
        result = labels.copy()
        for i, neighbors in enumerate(self.knn_):
            counts = np.bincount(labels[neighbors], minlength=self.n_clusters_)
            winners = np.flatnonzero(counts == counts.max())
            if len(winners) == 1:
                winner = winners[0]
                if self.vote_rule == "plurality" or counts[winner] > self.K / 2:
                    result[i] = winner
        return result
STEP 10

把这个点的整条计算路径串起来

fit / fit_predict 的最终输出

p313 → 14 个近邻 / 13 个互近邻 → 非核心点 → 局部 MST 保留 11 条边 → ρ = 0.773621 → 最近严格高密度点 p309 → 第 93 轮分配到 C0 → 14 票 C0 → 最终 C0

“非核心点”不意味着密度一定低;核心资格由互近邻结构决定。本点可以有较高的密度,却因一个不互惠的近邻而未通过 K-MNN 条件。密度、邻域结构与吸引力是不同阶段使用的不同信息。

中间变量p313 的结果
X[i][36.7, 9.15]
X_normalized_[i][0.8865598027127005, 0.24899598393574296]
|KNN(i)| / |MNN(i)| / |KNN²(i)|14 / 13 / 32
i ∈ S / i ∈ strict_SFFalse / False
rho_[i]0.77362114
delta_[i]0.03595552
gamma_[i]0.02781595
nneigh_[i]309
skeleton_labels_[i]-1
labels_before_refinement_[i]0
labels_[i]0
本页展示的是当前 Python 实现的一次可重现执行,包括明确启用的 K-MNN 回退。它帮助理解代码如何运行,但不消除论文严格 K-FMNN 定义与本地 Jain 数据之间尚未解决的差异。
追踪一致性检查与文件指纹
  • Prim endpoints and weights match _mst exactly
  • Retained-edge contributions equal rho[target]
  • BFS order and centers match _build_skeletons
  • Propagation replay equals labels_before_refinement_
  • Contributor sums equal target attractiveness at assignment
  • Refinement replay equals labels_

数据 SHA-256:c61fbbf821d5b20d6a9ab211a1fba1fb2a60feca3ff682903f998f6de73ba8f1

算法源文件 SHA-256:e48c6e0b6bb910ec8f3a817a30d9879215ce3f3470b2ddc209e24cebbe1e2d2a

复现命令:python build_walkthrough.py。HTML 自带全部图片、数值和交互脚本,离线可直接打开;walkthrough_trace.json 保存全部可审计状态,walkthrough_assets/ 保存独立 SVG 图。

原始矢量图 · 可横向/纵向滚动查看细节

放大的数据图