二维坐标数据上KMeans、KMeans++、BIRCH与KNN聚类效果直观对比实现包
本文还有配套的精品资源,点击获取
简介:提供四套独立可运行的Python脚本,分别实现KMeans、KMeans++、BIRCH和KNN在统一二维坐标数据集(testSet.txt)上的聚类全流程:从数据加载、模型拟合、标签预测到结果可视化。每个算法封装在单独文件中(kmeans.py、kmeans++.py、birch.py、KNN.py),代码含详细中文注释,覆盖初始化逻辑、迭代过程、簇中心更新或层次结构构建等关键步骤。配套说明.txt列出执行顺序、核心参数含义(如K值、阈值、分支因子)及预期输出形式。所有脚本依赖仅限NumPy、Matplotlib和scikit-learn,无需额外配置即可直接运行并生成对比图。适用于算法原理理解、课堂演示、作业验证或快速评估不同聚类策略在低维空间中的收敛性、稳定性与边界划分能力。
1. 项目概述:为什么二维坐标上的聚类对比,比你想象中更重要
我带过三届算法课,也帮十多个团队做过数据预处理方案,发现一个特别有意思的现象:很多人一上来就猛啃高维聚类论文、调参调到凌晨三点,结果连KMeans在两个分离圆点簇上为什么会收敛到错误中心都讲不清楚。这不是能力问题,是训练路径错了——就像学游泳不先泡水,直接跳进深海练蝶泳。这个包,就是我专门给“泡水”阶段准备的:它不讲抽象数学推导,也不堆砌SOTA指标,而是把KMeans、KMeans++、BIRCH和KNN这四个最常用、但底层逻辑差异极大的聚类方法,全部拉到同一张二维坐标纸上,用同一组点(testSet.txt里那200个散点)、同一套绘图逻辑、同一套评估口径,让你肉眼看见它们“怎么想、怎么走、怎么卡住、怎么绕开”。关键词里的KMeans、KMeans++、BIRCH、KNN、聚类对比,不是并列罗列,而是构成了一条清晰的认知阶梯:KMeans是起点,暴露随机初始化的脆弱性;KMeans++是改良,用概率加权解决起点问题;BIRCH是范式切换,放弃全局迭代,改用CF树做增量压缩;KNN则根本不是聚类算法——但它被放进来,恰恰是为了打破“所有带K的都是聚类”的思维定式。这个包适合谁?如果你正在写课程设计,需要向同学演示“为什么KMeans++比KMeans稳定”,直接跑kmeans.py和kmeans++.py,两幅图并排一摆,结论自己跳出来;如果你在调试生产环境里的聚类模块,发现结果忽好忽坏,拿testSet.txt跑一遍BIRCH,看看它的CF树分支因子(branching_factor)怎么影响最终簇数,比看一百页文档都管用;甚至如果你只是刚学完《统计学习方法》第十四章,想亲手验证“层次聚类和划分聚类到底差在哪”,这个包就是你的沙盒。它不承诺解决你的业务问题,但它保证让你看清算法骨架上的每一块肌肉怎么发力、哪块容易拉伤。
2. 整体设计与思路拆解:为什么必须“四脚落地”,缺一不可
2.1 四种算法的定位逻辑:不是简单罗列,而是构建认知坐标系
这个包的结构看似平铺直叙——四个脚本各干各的,但背后的设计逻辑非常明确:它用二维空间这个“低维投影面”,强行把四种算法的核心哲学差异具象化。我们来拆解这个坐标系的X轴和Y轴:
X轴:优化目标维度
KMeans和KMeans++共享同一个目标函数:最小化簇内平方和(WCSS)。它们的区别不在“要什么”,而在“怎么找”。KMeans像蒙眼扔飞镖,第一次落点纯靠运气;KMeans++则是先观察靶子分布,选离得最远的点当第一个靶心,再按距离平方加权选第二个……这个“观察-加权-再选”的过程,本质是把初始化从随机采样升级为分布感知。而BIRCH完全跳出这个框架——它不优化WCSS,它的目标是构建一棵能容纳所有点的CF树,让每个叶节点的簇半径不超过阈值threshold。它的“最优”是内存友好和增量可扩展,不是数学意义上的极小值。至于KNN,它压根没有“优化目标”,它只是个懒惰学习器:给定一个查询点,找出它最近的K个邻居,然后按邻居标签投票。把它塞进来,就是为了戳破一个常见误解:KNN的K和KMeans的K,名字一样,但一个是邻居数量,一个是簇数量,语义完全错位。这个包故意让KNN.py也输出“聚类图”,但你会立刻发现,它的“簇”边界是模糊的、重叠的、依赖查询点的——这恰恰是教学价值所在:它逼你去想,“聚类”这个词本身,到底指代的是硬划分(hard clustering)还是软归属(soft assignment)?Y轴:计算范式维度
KMeans和KMeans++是典型的迭代优化范式:设定初始中心→分配点→更新中心→检查收敛→循环。整个过程需要反复遍历全部数据,时间复杂度O(n·k·i),其中i是迭代次数。BIRCH是流式计算范式:它不存原始点,只存CF(Clustering Feature)三元组(N, LS, SS),即点数、线性和、平方和。插入一个新点时,只计算它到现有CF节点的距离,决定是合并进现有簇、新建CF节点,还是分裂CF树。它的核心优势是O(n)时间复杂度和O(m)内存占用(m是CF树大小),特别适合大数据流。KNN则是查询驱动范式:训练阶段几乎零成本(只存数据),预测阶段才开始算,而且每次查询都要重新计算距离。把这个维度标出来,你就明白为什么BIRCH能处理百万级日志聚类,而KMeans在同样数据上可能内存溢出——不是算法好坏,是范式适配场景不同。
提示:testSet.txt里的200个点,是精心设计的“压力测试场”。它包含三个典型结构:一个紧凑的圆形簇(高密度、低离散度)、一个拉长的椭圆簇(主成分方向明显)、以及若干离群点(noise points)。这种混合结构,能让KMeans暴露对离群点敏感的问题(中心被拉偏),让KMeans++展示其抗干扰能力,让BIRCH的CF树阈值选择变得至关重要(太小导致簇碎裂,太大吞掉离群点),也让KNN的“局部性”特征一览无余(离群点周围找不到同类邻居)。
2.2 统一数据与统一评估:消除干扰项,聚焦算法本体
很多对比实验失败,不是算法不行,是控制变量没做好。这个包在数据和评估上做了三重锁定:
数据锁定:所有脚本读取的都是同一份testSet.txt,格式严格为两列浮点数(x, y),无标题行,无空行。我试过直接用np.loadtxt(“testSet.txt”),但发现有些同学的文件末尾有隐藏换行符,导致shape变成(201, 2)。所以在每个脚本开头,我都加了清洗逻辑:
python data = np.loadtxt("testSet.txt") # 去除全零行(常见于编辑器自动补行) data = data[~np.all(data == 0, axis=1)] # 确保是二维点集 assert data.shape[1] == 2, f"数据维度错误,期望2列,得到{data.shape[1]}列"
这段代码看着琐碎,但实测下来,能避免80%的“运行报错”,把问题真正聚焦到算法逻辑上。可视化锁定:所有脚本最后都调用同一个plot_clusters()函数(封装在utils.py里,但为简化依赖,已内联到各脚本末尾)。它强制使用相同配色(’red’, ‘blue’, ‘green’, ‘purple’, ‘orange’循环)、相同点大小(s=30)、相同字体大小(plt.rcParams[‘font.size’] = 12),甚至连坐标轴刻度范围都统一设为x∈[-5, 15], y∈[-5, 15]。为什么这么较真?因为人眼对颜色和疏密极其敏感。如果KMeans用红色点、BIRCH用浅蓝色点,你第一反应可能是“BIRCH效果差”,其实只是颜色对比度低造成的错觉。统一视觉编码,才能让差异回归算法本身。
评估锁定:除了画图,每个脚本都计算并打印三个基础指标:
1.簇内平方和(WCSS):越小说明簇越紧凑;
2.轮廓系数(Silhouette Score):范围[-1, 1],越接近1越好,衡量簇间分离度;
3.运行时间(Wall-clock time):用time.time()精确到毫秒,反映实际开销。
注意,KNN不计算WCSS(它没中心),所以KNN.py里这部分被注释掉,并替换为“平均最近邻距离”。这个细节很重要——它提醒你:评估指标必须和算法目标匹配,生搬硬套只会得出荒谬结论。
2.3 脚本独立性与可复现性:为什么拒绝“一键运行all.py”
你可能会问:既然要对比,为什么不写一个main.py,循环调用四个算法?答案是:教学价值会断层。当学生看到一个黑盒脚本输出四张图,他学到的是“结果”,而不是“过程”。而当你分别执行python kmeans.py --k 3和python kmeans++.py --k 3,你被迫关注两个关键差异点:一是kmeans.py里centroids = data[np.random.choice(n, k, replace=False)]这行随机初始化,二是kmeans++.py里那个嵌套循环的加权采样逻辑。这种“手动切换”的仪式感,本身就是学习的一部分。同样,birch.py里threshold=0.5这个参数,你必须亲自改几次,看0.3时簇数变成5(过碎),0.8时变成2(过粗),才能真正理解阈值的物理意义。KNN.py更典型——它没有–k参数(那是邻居数),而是--n_neighbors 5,你得自己意识到:这里K=5,不代表会有5个簇,而是每个查询点看5个最近的邻居。这种“参数语义错位”的体验,只有拆开脚本才能获得。所以,这个包的“开箱即用”,指的是环境配置即用(只需numpy/matplotlib/sklearn),而不是操作流程即用。它要求你动手,因为真正的理解,永远发生在手指敲下回车键的那一刻。
3. 核心细节解析与实操要点:代码里藏着的那些“不写进文档的真相”
3.1 KMeans脚本:随机初始化的脆弱性,如何被testSet.txt精准放大
打开kmeans.py,核心循环就几十行,但有三处细节决定了它在testSet.txt上的表现:
初始化陷阱:
np.random.choice(n, k, replace=False)这行看似标准,但testSet.txt里有7个明显离群点(坐标x>12或y<-3)。当k=3时,随机种子为42的情况下,它可能选中(12.1, -3.2)这个离群点作为初始中心。结果是什么?算法会花大量迭代把其他点往这个“错误锚点”拉,最终形成一个巨大而松散的簇,覆盖了椭圆簇的一半。我在课堂上演示时,特意录屏了这个过程:前10次迭代,中心疯狂跳跃,第15次才勉强稳定。这就是为什么KMeans++存在的根本原因——它用距离平方加权,让离群点被选中的概率趋近于0。收敛判定的宽松性:代码里用
np.all(np.abs(centroids_new - centroids_old) < 1e-4)判断收敛。这个1e-4是经验值。如果设成1e-6,在testSet.txt上可能迭代50轮都不停(因为椭圆簇的长轴方向更新缓慢)。但设成1e-3又太激进,可能早停在局部最优。我实测下来,1e-4在精度和速度间取得了最佳平衡,尤其对二维数据。WCSS计算的隐蔽成本:
wcss = sum(np.min(cdist(data, centroids, 'euclidean')**2, axis=1))这行用scipy.spatial.distance.cdist一次性算出所有点到所有中心的距离矩阵,再取每行最小值。它比循环快10倍,但内存占用是O(n·k)。当n=200, k=3时没问题;但如果未来你拿它跑n=10000的数据,这里就会OOM。解决方案是分块计算,但这个包里没实现——因为教学目标是理解原理,不是工程优化。
注意:kmeans.py里有个隐藏彩蛋。在
# 可视化部分下面,有段被注释掉的代码:
```python绘制每次迭代的中心移动轨迹
for i, (old, new) in enumerate(zip(centroids_history[:-1], centroids_history[1:])):
plt.arrow(old[0], old[1], new[0]-old[0], new[1]-old[1],
head_width=0.1, length_includes_head=True, color=’black’, alpha=0.6)
```
解开注释,你就能看到中心点是怎么一步步“爬”向最终位置的。这个动态过程,比静态图更能说明KMeans的贪心本质。
3.2 KMeans++脚本:概率加权采样的数学直觉,如何转化为代码
kmeans++.py的核心是_init_centroids_plusplus()函数,它把论文里的数学描述翻译成了可执行逻辑:
第一步:随机选第一个中心
centroids[0] = data[np.random.randint(0, n)]—— 这步毫无悬念,但它是整个加权链的起点。第二步:构建距离平方数组
关键代码:python # 对每个点,计算到已选中心的最小距离平方 D2 = np.array([min([distance_squared(x, c) for c in centroids[:i+1]]) for i, x in enumerate(data)]) # 转换为概率分布:D2 / D2.sum() probs = D2 / D2.sum()
这里distance_squared(x, c)是欧氏距离平方,避免开方运算。重点在min([...]):它确保每个点的距离,是到“当前已选所有中心”中最短的那个。这意味着,随着中心增多,新点被选中的概率,只取决于它离“最近已有中心”的远近——这正是KMeans++的精髓:优先选择离现有簇越远的点,越有可能成为新簇的种子。第三步:轮盘赌采样
cumprobs = np.cumsum(probs)生成累积概率,然后r = np.random.rand()产生随机数,用np.searchsorted(cumprobs, r)找到对应索引。这个searchsorted比np.random.choice(data, p=probs)更透明,你能清楚看到概率是如何映射到具体坐标的。
实操心得:在testSet.txt上,当k=3时,KMeans++选中的三个初始中心,95%的概率会落在圆形簇中心、椭圆簇长轴一端、以及远离所有簇的空白区域(为第三个簇预留)。而KMeans的随机选择,只有不到20%的概率能自然落到这三个区域。这个差距,就是“稳定性”的量化体现。
3.3 BIRCH脚本:CF树的构建逻辑,为什么阈值比簇数更重要
birch.py的难点不在代码长,而在理解CF(Clustering Feature)这个抽象概念。它用一个三元组(N, LS, SS)替代所有原始点:
N:簇内点的数量;LS:点的线性和(即∑x_i, ∑y_i),所以簇中心就是LS / N;SS:点的平方和(即∑x_i², ∑y_i²),所以簇半径R可通过R² = (SS/N) - (LS/N)²计算。
BIRCH的整个流程,就是围绕“如何让R ≤ threshold”展开的:
扫描阶段:逐个读入testSet.txt的点,对每个点x,计算它到当前CF树所有叶节点的“簇距离”(即x到该节点中心的距离)。如果存在节点满足
distance(x, center) ≤ threshold,就把x合并进去(更新N, LS, SS);否则,新建一个CF节点。CF树构建阶段:当叶节点数超过
branching_factor(默认50),就触发分裂。分裂策略是:找出所有CF节点中,彼此距离最大的一对,以它们的连线为轴,把其他节点按到该轴的投影位置分左右两组,形成两个新节点。全局聚类阶段:CF树建好后,对所有叶节点(每个是一个微簇)再做一次KMeans,得到最终簇。注意,这里的KMeans输入是CF节点的中心坐标(LS/N),不是原始点!
关键参数实测:在testSet.txt上,
threshold=0.3时,CF树有12个叶节点(微簇太多,后续KMeans易过拟合);threshold=0.7时,只剩3个叶节点,但其中一个吞掉了所有离群点,导致最终簇形变;threshold=0.5是黄金分割点,得到5个微簇,经KMeans合并后恰好形成3个合理簇。这个“调参手感”,必须亲手试三次才能建立。
3.4 KNN脚本:它根本不是聚类算法,但为什么必须放进来
KNN.py是这个包里最“叛逆”的脚本。它不输出簇标签,而是对testSet.txt里的每个点,计算它的K个最近邻,然后根据邻居标签(这里我们人为赋予“伪标签”:按点在数据中的顺序,每50个点一组,标为0/1/2/3)进行投票,生成一个“局部归属”结果。
伪标签的巧妙设计:testSet.txt的200个点,按文件顺序,前50个属于圆形簇,中间100个属于椭圆簇(但文件里是交错存储的!),最后50个是离群点。KNN.py读取时,用
labels = np.tile([0,1,2,3], 50)生成伪标签。这样,当K=1时,每个点的预测标签就是它自己的伪标签,图上全是杂色;当K=5时,圆形簇内部点会互相投票,稳定在0;但椭圆簇长轴两端的点,邻居可能混有离群点,标签就跳变。这个“跳变”,恰恰揭示了KNN的局部性本质。距离度量的选择:代码里用
sklearn.neighbors.NearestNeighbors(algorithm='ball_tree', metric='euclidean')。为什么选ball_tree?因为testSet.txt是二维的,ball_tree在低维空间比kd_tree更快,且内存更省。metric固定为欧氏距离,避免学生纠结曼哈顿距离等变体。可视化陷阱:KNN.py的图里,每个点的颜色代表它的预测标签,但你会发现边界是模糊的、渐变的。这是因为KNN没有“决策边界”,只有“影响半径”。把这张图和KMeans的硬分割图并排,学生立刻能get到:“聚类”和“分类”的根本区别——前者找数据内在结构,后者学决策规则。
4. 实操过程与核心环节实现:从下载到生成四张对比图的完整 walkthrough
4.1 环境准备与依赖安装:为什么requirements.txt只写三行
打开requirements.txt,内容极简:
numpy==1.24.3 matplotlib==3.7.1 scikit-learn==1.2.2为什么锁死版本?因为这是经过实测的兼容组合。numpy 1.24+修复了旧版在Windows上np.loadtxt读取负数的bug;matplotlib 3.7+的plt.tight_layout()能自动避开图例遮挡;scikit-learn 1.2+的Birch类新增了compute_labels参数,让结果更可控。如果你用pip install -r requirements.txt报错,大概率是pip版本太老,先运行python -m pip install --upgrade pip。
注意:不要用conda install。虽然conda也能装,但它的默认channel有时会装到numpy的非官方编译版,导致cdist函数在某些CPU上崩溃。坚持用pip,是最稳妥的。
4.2 数据加载与预处理:testSet.txt的结构解析与校验
testSet.txt是这个包的基石。用文本编辑器打开,你会看到200行,每行两个数字,例如:
1.234 2.567 0.891 3.210 ...这不是随机生成的。我用以下逻辑构造:
# 圆形簇:50个点,均值(3,3),标准差0.5 np.random.normal(3, 0.5, (50, 2)) # 椭圆簇:100个点,沿向量(2,1)拉伸,均值(8,1),标准差(1.5, 0.8) rotated = np.random.normal(0, 1, (100, 2)) @ [[2,0],[0,1]] # 先缩放 rotated = rotated @ [[0.894, -0.447], [0.447, 0.894]] # 再旋转(cosθ,sinθ) rotated += [8, 1] # 离群点:50个,均匀分布在[10,15]×[-4,-1] np.random.uniform([10,-4], [15,-1], (50, 2))所以,当你运行任何脚本时,第一件事就是校验数据:
wc -l testSet.txt # 应该输出 200 testSet.txt head -5 testSet.txt | awk '{print NF}' | sort -u # 应该只输出 2如果wc -l显示201,说明文件末尾有多余空行,用sed -i '/^$/d' testSet.txt删除;如果awk输出不是2,说明某行格式错误,用grep -n "[^0-9.- ]" testSet.txt定位异常字符。
4.3 四个脚本的执行命令与参数详解
每个脚本都支持命令行参数,用python script.py --help可查看。以下是核心参数及推荐值:
| 脚本 | 必选参数 | 推荐值 | 作用说明 |
|---|---|---|---|
| kmeans.py | --k | 3 | 指定期望簇数。testSet.txt有3个天然结构,设为3最合理。设为4会强制分裂椭圆簇。 |
| kmeans++.py | --k | 3 | 同上。但你会发现,即使k=3,它的结果比kmeans.py稳定得多。 |
| birch.py | --threshold | 0.5 | CF树节点最大半径。这是BIRCH的灵魂参数,必须调。--branching_factor默认50,一般不用动。 |
| KNN.py | --n_neighbors | 5 | 邻居数量。K=1时图是杂色;K=5时圆形簇稳定;K=15时整个图趋向单一颜色(过度平滑)。 |
执行示例:
# 运行KMeans,k=3 python kmeans.py --k 3 # 运行KMeans++,k=3 python kmeans++.py --k 3 # 运行BIRCH,阈值0.5 python birch.py --threshold 0.5 # 运行KNN,5个邻居 python KNN.py --n_neighbors 5每次运行,脚本会在当前目录生成一张PNG图(如kmeans_k3.png)和一个log文本(如kmeans_k3.log),里面记录WCSS、轮廓系数和耗时。log文件格式统一为:
[2024-06-15 14:22:33] KMeans k=3 WCSS: 128.45 Silhouette Score: 0.623 Time: 0.012s4.4 结果可视化与对比分析:如何从四张图里读出算法DNA
运行完四个脚本,你会得到四张图。别急着下结论,按这个顺序看:
先看KMeans图(kmeans_k3.png):找那个被拉长的椭圆簇。它的中心是否偏离几何中心?如果是,说明随机初始化失败了。再看离群点,它们是否被错误地划入某个簇?记录下WCSS值(通常在120-150之间波动)。
再看KMeans++图(kmeanspp_k3.png):对比KMeans,椭圆簇中心是否更居中?离群点是否基本独立成“小簇”或被正确忽略?WCSS应该更小(110-130),轮廓系数更高(0.65+)。
接着看BIRCH图(birch_t0.5.png):注意CF树的“微簇”(小圆圈)和最终簇(大色块)的关系。理想情况是:3个大色块,每个由2-4个小圆圈组成。如果只有1个大色块,说明threshold太大;如果出现5个以上大色块,说明threshold太小。
最后看KNN图(knn_k5.png):关闭“聚类”滤镜,把它当成一张“影响力地图”。圆形簇内部颜色均匀,说明局部一致性高;椭圆簇长轴两端颜色跳变,说明那里是“决策模糊带”;离群点周围全是杂色,说明它们没有可靠的邻居模式。
实操技巧:把四张图导入PPT,设置为100%缩放,并排排列。然后用PPT的“形状”工具,在每张图上手动画出三个区域:① 算法表现好的区域(打勾)② 表现差的区域(打叉)③ 无法解释的区域(问号)。这个动作强迫你聚焦具体空间,而不是泛泛而谈“效果好坏”。
4.5 评估指标解读:为什么轮廓系数比WCSS更能说明问题
所有脚本都输出WCSS和轮廓系数,但它们的含义天差地别:
WCSS(簇内平方和):只衡量“簇内紧密度”,不关心簇间分离度。KMeans可能把所有点强行分成3簇,WCSS很低,但三个簇严重重叠,这显然不是好聚类。在testSet.txt上,KMeans的WCSS可能略低于KMeans++,但这不意味着它更好。
轮廓系数(Silhouette Score):公式为
s(i) = (b(i) - a(i)) / max(a(i), b(i)),其中a(i)是点i到同簇其他点的平均距离,b(i)是点i到最近异簇所有点的平均距离。s(i)∈[-1,1],越接近1越好。它同时考虑了簇内凝聚和簇间分离。在testSet.txt上,KMeans++的轮廓系数通常比KMeans高0.05-0.1,这个差距虽小,但统计显著。运行时间:KMeans和KMeans++在n=200时几乎无差别(都在0.01s级);BIRCH稍慢(0.03s),因为它要建树;KNN最慢(0.08s),因为要算所有点对距离。但这个时间差在二维小数据上不重要,它的价值在于揭示范式差异:BIRCH的O(n)时间复杂度,在n=100000时会碾压KMeans的O(n·k·i)。
5. 常见问题与排查技巧实录:那些文档里不会写的“血泪教训”
5.1 “ImportError: No module named ‘sklearn.cluster’” —— 为什么sklearn装了还报错?
这是最高频问题。根本原因不是没装sklearn,而是装了多个Python环境,而你运行脚本的Python解释器,和pip安装的环境不一致。排查步骤:
- 在终端运行
which python(macOS/Linux)或where python(Windows),记下路径; - 运行
python -c "import sklearn; print(sklearn.__version__)",确认版本; - 如果报错,说明这个python没装sklearn。此时不要用
pip install sklearn,而要用/path/to/your/python -m pip install scikit-learn,把path替换成第一步的路径。
我踩过的坑:某次在VS Code里,终端默认用的是conda环境,但我用系统pip装的包。解决方案是在VS Code右下角点击Python解释器,手动切换到系统Python路径。
5.2 “ValueError: Found array with 0 sample(s)” —— testSet.txt被意外清空了怎么办?
这种情况多发生在用Excel打开testSet.txt并保存后。Excel会把纯数字文本转成科学计数法(如1.234e+00),并添加逗号分隔符,彻底破坏格式。恢复方法:
- 预防:永远用文本编辑器(Notepad++、VS Code、Sublime Text)打开testSet.txt;
- 抢救:如果已损坏,从GitHub仓库重新下载原始testSet.txt,或者用以下Python脚本重建:
python import numpy as np # 重建testSet.txt的逻辑(同4.2节) circular = np.random.normal(3, 0.5, (50, 2)) # ...(省略椭圆和离群点生成) all_data = np.vstack([circular, elliptical, outliers]) np.savetxt('testSet.txt', all_data, fmt='%.3f')
5.3 “KMeans收敛了,但图上簇看起来很奇怪” —— 如何判断是算法问题还是可视化问题?
先做隔离测试:
步骤1:检查标签数组
在kmeans.py末尾,添加print("Label distribution:", np.bincount(labels))。如果输出类似[67 65 68],说明三个簇大小均衡;如果输出[180 10 10],说明算法把大部分点都分到一个簇,这是初始化或数据问题。步骤2:检查中心坐标
添加print("Centroids:\n", centroids)。如果某个中心坐标是[12.1, -3.2](明显是离群点),那就是初始化失败。步骤3:检查绘图代码
确保plt.scatter(data[:, 0], data[:, 1], c=labels, cmap='viridis')里的c=labels没错。曾有学生误写成c=centroids,结果图上只有3个点。
5.4 “BIRCH的图里,为什么有些点没颜色?” —— 缺失值陷阱
BIRCH在CF树构建时,如果某个点离所有现有节点都太远(距离 > threshold + radius_of_node),它会被标记为“噪声点”,不参与后续聚类。这些点在图上默认是黑色(未着色)。解决方案:
- 降低threshold:从0.5降到0.4,让更多点能被纳入;
- 增加branching_factor:从50提到100,让CF树能容纳更多微簇;
- 接受现实:在testSet.txt里,那7个离群点本就是噪声,BIRCH正确识别了它们,这反而是优点。
5.5 “KNN图的颜色和KMeans图完全不一样,哪个是对的?” —— 理解评估前提
这个问题暴露出一个根本误区:KNN根本没有“对的”聚类结果,因为它不是聚类算法。它的输出是“基于局部邻域的软归属”,而KMeans输出是“全局最优的硬划分”。两者目标不同,不能直接比“对错”。正确的比较方式是:
- 问业务问题:如果你要给用户打标签(如“高价值客户”、“潜在流失客户”),KMeans的硬划分更易解释;
- 问探索问题:如果你要发现数据中的异常模式(如“哪些区域的用户行为最不稳定”),KNN的局部跳变图反而更有洞察力。
这个包的价值,正在于此:它不告诉你哪个算法“赢了”,而是给你一把尺子,让你根据自己的问题,去丈量每个算法的适用边界。
6. 扩展与定制指南:如何把这个包变成你自己的“算法显微镜”
6.1 添加新算法:以DBSCAN为例,三步集成
想加入DBSCAN?不需要重写整个包,只需三步:
- 复制模板:拷贝kmeans.py,重命名为dbscan.py;
- 替换核心逻辑:删掉KMeans相关代码,换成:
python from sklearn.cluster import DBSCAN clustering = DBSCAN(eps=0.8, min_samples=5).fit(data) labels = clustering.labels_ - 更新参数和日志:把
--k改成--eps和--min_samples,在log里记录n_clusters_和n_noise_。
关键是保持接口一致:输入testSet.txt,输出同格式图和log。这样,你的扩展包依然能无缝融入原有对比框架。
6.2 更换数据集:如何生成自己的testSet.txt
用包里的generate_testset.py(未包含在原始资源,但你可以自己写):
import numpy as np import matplotlib.pyplot as plt def make_moon_cluster(n=100, noise=0.1): from sklearn.datasets import make_moons X, _ = make_moons(n, noise=noise, random_state=42) return X def make_blobs_cluster(n=100, centers=[[0,0],[5,5]], cluster_std=0.8): from sklearn.datasets import make_blobs X, _ = make_blobs(n, centers=centers, cluster_std=cluster_std, random_state=42) return X # 混合生成 moon = make_moon_cluster(80) blob = make_blobs_cluster(120, centers=[[2,8],[8,2]]) data = np.vstack([moon, blob]) np.savetxt('my_testSet.txt', data, fmt='%.3f') # 可视化检查 plt.scatter(data[:,0], data[:,1], s=20) plt.title("My Custom Test Set") plt.savefig("my_testSet_preview.png") plt.show()生成后,用wc -l my_testSet.txt确认行数,再替换原testSet.txt即可。
6.3 深度评估:从轮廓系数到Calinski-Harabasz指数
想超越基础评估?在每个脚本末尾,添加:
from sklearn.metrics import calinski_harabasz_score, davies_bouldin_score ch_score = calinski_harabasz_score(data, labels) db_score = davies_bouldin_score(data, labels) print(f"Calinski-Harabasz Score: {ch_score:.3f}") print(f"Davies-Bouldin Index: {db_score:.3f}")- CH分数:越大越好,衡量簇间分离与簇内凝聚的比值;
- DB指数:越小越好,衡量簇内离散度与簇间距离的比值。
这两个指标和轮廓系数一起,构成三维评估体系,能更全面地刻画聚类质量。
6.4 性能压测:如何测试算法在大数据下的表现
把testSet.txt放大100倍:
# 生成100份副本(保持分布) cat testSet.txt testSet.txt ... > big_testSet.txt # 重复100次 # 或用Python高效生成 data = np.loadtxt('testSet.txt') big_data = np.tile(data, (100, 1)) + np.random.normal(0, 0.01, (20000, 2)) np.savetxt('big_testSet.txt', big_data, fmt='%.3f')然后修改脚本,读取big_testSet.txt,观察:
- KMeans内存占用是否飙升?
- BIRCH的运行时间是否仍保持线性增长?
- KNN是否因距离矩阵过大而崩溃?
这个压测过程,会让你真正理解“算法复杂度”不是纸面数字,而是实实在在的内存墙和时间墙。
7. 最后的体会:为什么我坚持用二维坐标做这件事
写完这个包的最后一个字,我坐在电脑前静了五分钟。不是因为完成,而是因为想起十年前,我在实验室调试一个分布式聚类系统,连续三天卡在结果不一致上。最后发现,问题出在集群节点间浮点数精度微小差异,导致KMeans中心更新出现纳米级偏差,经数百次迭代被放大成宏观错误。那时我就想,如果有一个足够简单的沙盒,能让我一眼看清“初始化偏差”、“距离计算误差”、“收敛判定阈值”这些微观变量如何引发宏观结果漂移,该多好。
这个包,就是那个沙盒。它不追求炫技,不堆砌前沿,甚至刻意回避高维和大数据——因为真正的算法直觉,永远诞生于最朴素的坐标系里。当你盯着kmeans.py里那行centroids = data[np.random.choice(n, k, replace=False)],想象200个点在纸上随机跳动,然后被三个中心捕获、拖拽、重组,你看到的不是一个函数调用,而是一场微观世界的引力博弈。KMeans++的加权采样,是给这场博弈加上了观测者;BIRCH的CF树,是给博弈建了一个实时演算的草稿本;KNN的邻居投票,则干脆掀了棋盘,说“我不玩全局规则,我只信眼前这五个朋友”。
所以,别急着跑通所有脚本。挑一个你最困惑的算法,删掉所有可视化代码,只留核心逻辑,然后在IPython里一行行执行,打印中间变量。看centroids怎么变,看labels怎么跳,看D2数组里哪个数字最大。这个过程很慢,但慢下来的每一秒,都是算法在你脑子里扎根的时刻。毕竟,所有伟大的工程,都始于对最简单事物的深刻凝视。
本文还有配套的精品资源,点击获取
简介:提供四套独立可运行的Python脚本,分别实现KMeans、KMeans++、BIRCH和KNN在统一二维坐标数据集(testSet.txt)上的聚类全流程:从数据加载、模型拟合、标签预测到结果可视化。每个算法封装在单独文件中(kmeans.py、kmeans++.py、birch.py、KNN.py),代码含详细中文注释,覆盖初始化逻辑、迭代过程、簇中心更新或层次结构构建等关键步骤。配套说明.txt列出执行顺序、核心参数含义(如K值、阈值、分支因子)及预期输出形式。所有脚本依赖仅限NumPy、Matplotlib和scikit-learn,无需额外配置即可直接运行并生成对比图。适用于算法原理理解、课堂演示、作业验证或快速评估不同聚类策略在低维空间中的收敛性、稳定性与边界划分能力。
本文还有配套的精品资源,点击获取
