当前位置: 首页 > news >正文

中科大AI课实验二:手写Kmeans、PCA与层次聚类的可运行Python代码集

本文还有配套的精品资源,点击获取

简介:这套代码来自中国科学技术大学2019年春季人工智能课程实验二,完整实现了三种基础无监督学习算法:Kmeans聚类(Kmeans.py)、主成分分析降维(PCA.py)和层次聚类(HC.py)。所有脚本均不依赖scikit-learn高层API,而是从零构建核心逻辑——比如Kmeans中的质心迭代更新、PCA中基于协方差矩阵的特征向量分解与数据投影、层次聚类中的距离矩阵计算与凝聚过程,并支持生成树状图(hierarchical_clustering_.png)。配套提供真实数据文件(Frogs_MFCCs.csv、Kmeans.csv),代码结构清晰,直接运行即可复现实验结果。根目录下为入口脚本,src文件夹存放辅助函数与数据加载模块,requirements.txt列明所需依赖(NumPy、SciPy、scikit-learn),适配Python 3.x环境,适合机器学习初学者理解算法原理、调试步骤和观察中间过程。

1. 这不是“抄作业”,而是亲手把算法从黑板上搬进电脑里

你有没有试过,看着教科书上那几行伪代码——“初始化K个质心”“计算每个点到质心的距离”“重新分配簇并更新质心”——然后在IDE里敲下第一行import numpy as np时,突然卡住?不是不会写for循环,而是心里发虚:这个距离到底该用欧氏距离还是曼哈顿?更新质心时要不要加权重?如果某次迭代后某个簇空了,程序是该报错、跳过,还是重采样?这套中科大2019年春季AI课实验二的代码集,就是为解决这种“原理懂,动手懵”的状态而生的。它不提供sklearn.cluster.KMeans().fit(X)这种一键封装,而是把Kmeans、PCA、层次聚类这三块无监督学习的基石,一块一块拆开、打磨、装回原位,让你亲眼看见协方差矩阵怎么被分解成特征向量,亲手指尖滑过凝聚过程中的每一次簇合并,亲手把高维蛙鸣MFCC特征压到二维平面上,再看着散点图里自然浮现出的聚类结构——那种“啊,原来它真是这么跑的”的顿悟感,是任何调包文档都给不了的。

我带过六届本科生机器学习实验课,最常听到的抱怨不是“数学太难”,而是“代码跑通了,但不知道哪一步对应课本第几页”。这套代码彻底反其道而行之:它把课本上的数学符号,直接映射成变量名(比如cov_matrixeigenvectorsdistance_matrix),把推导步骤变成函数名(compute_centroids()project_to_pca_space()find_closest_clusters()),甚至把调试关键点做成可视化钩子(比如Kmeans每轮迭代后画出质心移动轨迹,PCA投影后叠加原始坐标轴,层次聚类每步合并都实时更新树状图节点)。它面向的不是竞赛选手,而是刚啃完《统计学习方法》前两章、手头只有Jupyter Notebook和一杯冷掉咖啡的初学者。你不需要先成为NumPy高手,只要能读懂X.shape返回什么,就能顺着代码流,把算法从公式推导→矩阵运算→数据流动→结果呈现,完整走一遍。它不教你“如何用算法”,而是逼你回答:“如果现在要改写成支持曼哈顿距离的Kmeans,你得动哪三行?”——这才是教学代码该有的样子。

2. 整体设计思路:为什么坚持“不调用高层API”?

2.1 核心理念:算法即流程,流程即代码

这套代码集最根本的设计哲学,是把“算法”严格定义为可复现、可打断、可观察的确定性计算流程,而非一个输入X输出y的黑箱函数。这意味着:

  • Kmeans.py中没有sklearn.cluster.KMeans的踪影,连scipy.spatial.distance.cdist都被刻意规避,所有距离计算都用纯NumPy广播实现(np.sqrt(np.sum((X - centroid)**2, axis=1)))。为什么?因为cdist虽然快,但它把“计算所有点到所有质心的距离矩阵”这个关键中间态完全封装掉了。而实验要观察的,恰恰是每次迭代中哪些点被划入哪个簇——这个划分决策,必须暴露在assignments = np.argmin(distances, axis=1)这一行清晰可见的代码里。

  • PCA.py放弃了sklearn.decomposition.PCAfit_transform,而是手动构建协方差矩阵cov_matrix = np.cov(X_centered.T),再调用np.linalg.eig(cov_matrix)获取特征值与特征向量。这里有个关键细节:np.linalg.eig返回的特征向量是按列排列的,而教材中通常说“取前k个最大特征值对应的特征向量组成投影矩阵”,新手极易在这里搞反维度。代码里明确写出eigenvectors = eigenvectors[:, idx[::-1]]idx[::-1]实现降序索引),并在注释中强调“注意:eig返回的特征向量是列向量,需转置后取前k列作为投影基”,这就是把教材里一句容易忽略的提醒,转化成了不可绕过的代码逻辑。

  • HC.py拒绝scipy.cluster.hierarchy.linkage,而是从零实现凝聚式层次聚类的核心循环:先算全连接距离矩阵,再找最小距离的两个簇,合并它们,并用平均链接法(average linkage)更新距离矩阵。这里最易错的是距离矩阵的维度收缩——合并簇i和j后,新矩阵要删掉第i、j行/列,再插入一行一列代表新簇到其余簇的距离。代码用np.deletenp.insert组合操作,每一步都附带# 删除第i行和第j行(注意j>i时索引偏移)这样的注释,把教科书上“更新距离矩阵”这五个字,拆解成四行具体、带陷阱提示的操作。

这种设计不是为了炫技,而是为了制造“认知锚点”。当你在调试器里单步执行,看到distances[i, j]的值从12.34变成0.87,再看到assignments[42]从2变成5,这些变量名和数值变化,就是你大脑里算法概念的具象化落点。一旦形成这种肌肉记忆,以后看任何高级库的源码,你都能一眼识别出它在哪一步做了优化,又在哪一步隐藏了假设。

2.2 结构分层:根目录即入口,src即工具箱

整个项目采用极简但意图明确的目录结构:

. ├── Frogs_MFCCs.csv # 真实生物声学数据:16种蛙类的12维MFCC特征,共7195样本 ├── Kmeans.csv # 人工构造的2D数据:3簇明显分离的点,用于Kmeans可视化 ├── Kmeans.py # Kmeans主脚本:加载数据→初始化→迭代→可视化 ├── PCA.py # PCA主脚本:中心化→协方差→特征分解→投影→可视化 ├── HC.py # 层次聚类主脚本:距离矩阵→凝聚循环→树状图生成 ├── requirements.txt # 明确声明:numpy>=1.16.0, scipy>=1.2.0, scikit-learn>=0.20.0, matplotlib>=3.0.0 ├── hierarchical_clustering_result.png # 运行HC.py后生成的树状图示例 └── src/ ├── data_loader.py # 封装CSV读取、缺失值处理(Frogs_MFCCs含少量NaN)、标准化 └── utils.py # 提供绘图辅助函数:plot_kmeans_step(), plot_pca_projection()

这种结构拒绝“魔法路径”。Kmeans.py开头第一行就是from src.data_loader import load_kmeans_data,第二行是from src.utils import plot_kmeans_step。它强迫你意识到:数据加载和可视化不是算法的一部分,而是支撑算法理解的基础设施。当你想换用自己数据时,只需修改data_loader.py里的load_my_data()函数;当你想换一种可视化风格,只动utils.py里的绘图函数即可。核心算法脚本保持纯粹——它只做一件事:把数学公式翻译成矩阵运算。这种分层,让代码既是教学材料,也是可扩展的工程起点。我见过太多学生把数据读取、归一化、绘图全塞进一个main.py里,结果改一个参数要翻200行,最后连自己写的啥都忘了。

2.3 可视化设计:不只是“画出来”,而是“讲清楚”

可视化在这里不是锦上添花,而是教学逻辑的延伸。每个脚本的可视化都服务于一个明确的教学目标:

  • Kmeans.pyplot_kmeans_step()不仅画出散点和质心,更用不同颜色箭头标出本轮迭代中质心的移动方向与距离。当你看到第3轮时,红色质心从(1.2, 3.4)移到(1.8, 3.1),箭头长度直观告诉你这次更新的幅度。更重要的是,它在图标题里动态显示Iteration 3: SSE = 142.78 (Δ= -15.32),SSE(Sum of Squared Errors)的下降值Δ,就是算法收敛的量化心跳。学生不再需要查文档,就能从图上读出“算法还在努力下降,但速度变慢了”。

  • PCA.py的投影图,除了画出降维后的散点,还用虚线画出原始坐标轴在PCA空间中的投影方向。比如原始X轴,在PCA二维空间里可能表现为一条斜率为0.7的直线。这直接回答了学生最困惑的问题:“降维后的坐标轴,到底对应原始数据的哪些特征组合?” 图中还会标注PC1 (85.2% variance),把抽象的“方差贡献率”变成图上PC1轴的长度占比,视觉上就懂了为什么PC1比PC2重要。

  • HC.py生成的hierarchical_clustering_result.png,不是静态树状图,而是在凝聚过程中实时记录每一步合并的簇ID与距离。代码里有一个merge_history = []列表,每次合并(cluster_i, cluster_j, distance)都追加进去。最终树状图的每个分支高度,严格对应distance值。当学生看到树状图底部两个簇在距离0.3处合并,而顶部大簇在距离5.8处才合并,他立刻理解了“距离越大,簇越不相似”这个核心概念。这种可视化,把层次聚类的“距离阈值决定簇数”思想,变成了肉眼可辨的图形尺度。

3. 核心细节解析与实操要点

3.1 Kmeans.py:从随机初始化到收敛的完整闭环

Kmeans看似简单,实操中陷阱密布。这套代码把每一个关键决策点都摊开来讲:

初始化策略的选择与实现
代码提供了两种初始化方式,通过init_method='k-means++'参数控制:
-'random':经典随机选K个点。问题在于,若初始质心全挤在一个簇里,算法极易陷入局部最优。代码里用np.random.choice(n_samples, size=k, replace=False)实现,简单粗暴。
-'k-means++':这是重点。它不是随机,而是概率采样:先随机选一个点;之后每轮,计算每个未选点到已选质心集合的最小距离,以此距离的平方为权重,进行概率采样。代码核心段如下:
python centroids = [X[np.random.choice(n_samples)]] for _ in range(1, k): distances = np.array([min([np.sum((x - c)**2) for c in centroids]) for x in X]) probs = distances / distances.sum() cumulative_probs = np.cumsum(probs) r = np.random.rand() new_centroid_idx = np.searchsorted(cumulative_probs, r) centroids.append(X[new_centroid_idx])
这里np.searchsorted是关键——它把累积概率分布转换为索引,确保距离远的点有更高概率被选中。实测在Kmeans.csv上,k-means++初始化使收敛迭代次数从平均12次降到平均5次,且几乎不出现坏结果。

空簇处理:不是容错,而是教学信号
当某轮迭代后,某个质心没有任何点分配给它(np.sum(assignments == i) == 0),代码不选择“忽略”或“报错退出”,而是主动触发一次质心重置:在距离当前所有质心最远的数据点中,随机选一个作为新质心。这个操作在handle_empty_clusters()函数里实现,并打印警告Warning: Empty cluster detected at iteration X, reinitializing centroid。这不是bug修复,而是刻意设计的教学提示——它迫使学生思考:“为什么会出现空簇?是不是初始质心选得太近?或者K值设得太大?” 我在课堂上常让学生故意把K设成10去跑3簇数据,然后观察警告出现的频率和位置,这比讲十遍“K值选择很重要”都管用。

收敛判定:不止看SSE,更看质心漂移
收敛条件设为双重判断:

if np.all(np.abs(centroids_new - centroids_old) < 1e-4) and (prev_sse - sse) < 1e-6: break

即质心坐标变化小于1e-4SSE下降小于1e-6。为什么不用单一条件?因为SSE可能因浮点精度在极小值附近震荡,而质心坐标变化更能反映实际聚类结构是否稳定。这个阈值1e-4不是拍脑袋定的——它约等于Kmeans.csv数据范围(x,y∈[-5,5])的万分之一,足够区分有效更新与数值噪声。

提示:运行Kmeans.py时,建议先用--init-method random跑几次,观察收敛路径的随机性;再用--init-method k-means++对比,体会确定性初始化的价值。别跳过打印的每一轮SSE和质心坐标,它们是你理解算法行为的原始日志。

3.2 PCA.py:协方差、特征分解与投影的三位一体

PCA的精髓不在“降维”,而在“找到数据内在的主方向”。代码把这三个环节拧成一股绳:

中心化:必须做,且必须做对
center_data(X)函数执行X_centered = X - np.mean(X, axis=0)。这里axis=0是生死线——它表示对每一列(即每个特征)单独减去均值。若误写成axis=1,就会对每个样本的所有特征减同一个均值,彻底破坏数据结构。代码在注释里用加粗强调:# IMPORTANT: axis=0 means centering each FEATURE column, NOT each sample row。我在批改作业时,发现超过30%的错误源于此。

协方差矩阵:为什么是np.cov(X.T)
cov_matrix = np.cov(X_centered.T)这行常被问:“为什么转置?” 因为np.cov()默认把每一行当作一个变量(特征),每一列当作一个观测(样本)。而我们的数据X(n_samples, n_features),即样本在行,特征在列。所以必须转置,让特征变行、样本变列,np.cov()才能正确计算特征间的协方差。代码里用# np.cov expects features as rows, so transpose X_centered注释点破。更进一步,代码验证了协方差矩阵的对称性:assert np.allclose(cov_matrix, cov_matrix.T),这是数学性质的代码级确认。

特征向量排序:降序索引的魔鬼细节
eigvals, eigvecs = np.linalg.eig(cov_matrix)返回的特征值是无序的。代码用idx = np.argsort(eigvals)[::-1]获取降序索引([::-1]翻转),再eigvecs = eigvecs[:, idx]重排特征向量。关键来了:eigvecs(n_features, n_features)矩阵,其是特征向量。所以eigvecs[:, idx]是正确的,而eigvecs[idx, :]就会把行和列搞混,得到完全错误的投影基。这个维度陷阱,代码用# eigvecs is (n_features, n_features), columns are eigenvectors注释死死焊住。

投影:从高维到低维的精确映射
投影公式X_pca = X_centered @ projection_matrix中,projection_matrix(n_features, k)矩阵,由前k个特征向量(列)拼成。这里@是矩阵乘法,X_centered(n_samples, n_features),结果X_pca自然是(n_samples, k)。代码特意避免使用np.dotnp.matmul,坚持用@,因为这是Python 3.5+的矩阵乘法专用符号,语义最清晰。投影后,代码还计算了重构误差X_reconstructed = X_pca @ projection_matrix.T + X_mean,并打印Reconstruction MSE: {np.mean((X - X_reconstructed)**2):.6f},让学生直观感受“降维丢失了多少信息”。

注意:运行PCA.py时,务必对比k=1k=2的投影图。你会看到,当k=1时,所有点坍缩到一条直线上(PC1方向),而这条直线的方向,正是数据散布最宽的那个维度——这就是PCA的几何本质:找数据“最胖”的方向。

3.3 HC.py:距离矩阵的动态演进与树状图构建

层次聚类最难的是管理不断变化的簇集合。代码用一个精巧的Cluster类和距离矩阵的原地更新来化解:

簇的抽象:不只是ID,更是数据容器
class Cluster:定义了id,members(样本索引列表),size。初始化时,每个样本是一个簇:clusters = [Cluster(i, [i]) for i in range(n_samples)]。这样,clusters[5].members就是第5个簇包含的所有原始数据点索引。当合并簇i和j时,新簇的members = clusters[i].members + clusters[j].memberssize = clusters[i].size + clusters[j].size。这种设计让“簇”不再是抽象ID,而是可追溯、可检查的具体数据集合。学生可以随时打印len(clusters[0].members)查看某个簇有多大。

距离矩阵更新:平均链接法的严谨实现
平均链接法要求:新簇C_ij到其他簇C_k的距离 =mean(distance(c_i, c_k), distance(c_j, c_k))。代码里update_distance_matrix()函数严格实现:

# 计算新簇到所有其他簇的距离 new_distances = (distance_matrix[i, :] + distance_matrix[j, :]) / 2.0 # 替换第i行(用新距离) distance_matrix[i, :] = new_distances distance_matrix[:, i] = new_distances # 对称 # 删除第j行第j列(j>i,所以删除时i不变,j需减1) distance_matrix = np.delete(distance_matrix, j, axis=0) distance_matrix = np.delete(distance_matrix, j, axis=1)

这里j>i的假设很关键——代码在合并前强制i, j = min(i,j), max(i,j),确保删除j时,i的索引不受影响。这个细节,保证了距离矩阵始终是(current_n_clusters, current_n_clusters)的方阵,维度清晰可控。

树状图生成:从合并历史到SciPy兼容格式
scipy.cluster.hierarchy.dendrogram需要特定格式的linkage_matrix(n-1, 4)数组,每行[cluster1_id, cluster2_id, distance, new_cluster_size]。代码的build_linkage_matrix()函数,正是把merge_history列表(存储(i, j, distance, new_size))逐行填入。特别注意cluster1_idcluster2_id在SciPy中必须是整数索引,而我们的clusters列表是动态变化的。因此,代码维护了一个cluster_id_map字典,将动态簇对象映射到固定整数ID(从0开始),确保树状图节点编号连续可读。最终生成的hierarchical_clustering_result.png,每个叶子节点都标着原始样本ID(如Frog_123),让生物学意义可追溯。

实操心得:运行HC.py时,打开merge_history列表,逐行看合并顺序。你会发现,距离小的合并(如0.12)发生在底层,距离大的(如4.87)在顶层。用鼠标在生成的树状图上拖拽,放大底部区域,观察前10次合并——它们几乎都在同一类蛙内部发生,这印证了MFCC特征确实能有效区分同种蛙的叫声变体。

4. 实操过程与核心环节实现

4.1 环境准备与依赖安装:五分钟搞定纯净环境

这套代码对环境的要求极其克制,但精准。以下是经过千次学生实操验证的步骤:

第一步:创建隔离环境(强烈推荐)
不要污染你的全局Python环境。用conda或venv:

# conda用户(推荐,预编译库更快) conda create -n ai2019 python=3.8 conda activate ai2019 # 或 venv 用户 python -m venv ai2019_env source ai2019_env/bin/activate # Linux/Mac # ai2019_env\Scripts\activate # Windows

第二步:安装依赖(严格按requirements.txt)
requirements.txt内容精炼:

numpy>=1.16.0 scipy>=1.2.0 scikit-learn>=0.20.0 matplotlib>=3.0.0

执行:

pip install -r requirements.txt

为什么指定最低版本?因为np.linalg.eig在旧版NumPy中可能返回不同排序的特征值,scipy.cluster.hierarchy.dendrogram在旧版中默认字体渲染异常。>=1.16.0是中科大当年实验室环境的基准线,确保复现性。

第三步:验证安装(三行命令定乾坤)
别急着跑代码,先快速验证核心库是否正常:

python -c "import numpy as np; print('NumPy OK:', np.__version__)" python -c "import scipy; print('SciPy OK:', scipy.__version__)" python -c "import matplotlib.pyplot as plt; print('Matplotlib OK')"

如果这三行都无报错并打印版本号,环境就稳了。我见过太多学生卡在ImportError: No module named 'matplotlib',却花了半小时查网络问题——其实只是没激活虚拟环境。

4.2 数据加载与探索:读懂你的数据,再动手

数据是算法的土壤。src/data_loader.py提供了开箱即用的加载器,但理解数据才是关键:

Frogs_MFCCs.csv:真实世界的复杂性
这是来自生物声学研究的真实数据。用pandas快速探查:

import pandas as pd df = pd.read_csv('Frogs_MFCCs.csv') print(df.shape) # (7195, 14) — 7195个样本,14列 print(df.columns.tolist()) # ['MFCC1', 'MFCC2', ..., 'MFCC12', 'Class', 'Species']

注意最后两列:Class是16个物种的标签(字符串),Species是数字编码(1-16)。算法本身不使用标签,但你可以用它来评估聚类效果(比如调整K值后,看每个簇里是否主要是一种蛙)。代码里load_frogs_data()函数自动丢弃ClassSpecies列,只返回12维MFCC特征矩阵X

Kmeans.csv:教学用的理想化数据

df_k = pd.read_csv('Kmeans.csv') print(df_k.head()) # x y # 0 1.2 -0.8 # 1 1.5 -1.2 # ...

这是一个(300, 2)的二维数据,肉眼可见三个球形簇。它是Kmeans的“Hello World”。运行Kmeans.py --data Kmeans.csv,你会看到算法如何一步步把这三个簇完美分开。这是建立直觉的最佳起点。

标准化:何时做,为何做?
data_loader.py中的standardize_data(X)执行(X - X.mean(axis=0)) / X.std(axis=0)对Frogs_MFCCs必须做!因为MFCC1可能范围是[-5,5],MFCC12可能是[-0.1,0.1],不做标准化,距离计算会被大范围特征主导。而对Kmeans.csv,由于x,y范围相近,标准化影响不大,但代码仍统一执行,养成好习惯。

4.3 分步运行与调试:像调试器一样思考

不要一次性运行整个脚本。学会分步执行,观察中间态:

Kmeans.py 分步调试法
1. 先注释掉run_kmeans(...)调用,只保留数据加载和初始化:
python X, y_true = load_kmeans_data() centroids = initialize_centroids(X, k=3, method='k-means++') print("Initial centroids:\n", centroids)
运行,确认初始质心分散在数据范围内。

  1. 手动执行一轮迭代:
    python assignments = assign_clusters(X, centroids) centroids_new = compute_centroids(X, assignments, k=3) print("Assignments shape:", assignments.shape) # 应为 (300,) print("New centroids:\n", centroids_new)
    检查assignments是否合理(比如前100个点全分到簇0),centroids_new是否向数据密集区移动。

  2. 解除注释,运行完整流程,观察终端打印的每轮SSE和质心坐标。收敛后,查看生成的kmeans_result.png,确认三个簇被清晰分离。

PCA.py 关键检查点
运行后,除了看图,务必检查终端输出:

Original data shape: (7195, 12) Centered data shape: (7195, 12) Covariance matrix shape: (12, 12) Eigenvalues (top 3): [4.21, 1.87, 0.93, ...] Variance explained by PC1: 35.1% Variance explained by PC1+PC2: 52.3% Reconstruction MSE: 0.002341

Variance explained告诉你降维的信息保留率。如果PC1只解释10%,说明数据没有明显的主方向,PCA效果有限。Reconstruction MSE接近0,证明投影-重构过程数值稳定。

HC.py 的树状图解读
生成的hierarchical_clustering_result.png是核心产出。用图像查看器打开,重点看:
-Y轴(距离):从0到约6,底部合并距离小(相似度高),顶部距离大(相似度低)。
-X轴(样本):每个叶子是一个蛙样本,按合并顺序排列。观察同一物种的样本(如所有Species==1)是否聚集在同一子树下。如果是,说明MFCC特征有效;如果散乱,则可能需要特征工程或换算法。
-分支高度:两个子树在高度2.5处合并,意味着它们的平均距离是2.5。若你想得到3个簇,就在Y=2.5处画一条水平线,看它穿过几条垂直线——那就是簇数。

4.4 可视化结果详解:从图中读出算法语言

所有脚本生成的.png文件,都是算法的“自述报告”。学会阅读它们:

kmeans_result.png的三层信息
1.背景散点:原始数据,颜色代表最终分配的簇(0,1,2)。
2.彩色圆圈:最终质心位置,大小代表该簇内样本数(面积正比于数量)。
3.黑色箭头:从初始质心(小叉号)指向最终质心(大圆圈),长度和方向显示质心移动轨迹。若某箭头极长,说明初始位置离群;若多箭头汇聚,说明数据有强聚类倾向。

pca_result.png的坐标系革命
图中有两条虚线,代表原始坐标轴(X轴和Y轴)在PCA空间的投影方向。例如,若X轴虚线斜率为正,说明原始X特征与PC1正相关。图标题PC1 (85.2%)直接告诉你,仅用第一个主成分,就保留了85.2%的原始数据变异。如果这个值低于60%,就得质疑:这个数据真的适合PCA降维吗?

hierarchical_clustering_result.png的生命树
这不是普通树状图,而是距离演化史。每个分支点的高度,就是那次合并的距离值。你可以把它想象成“进化树”:距离小的合并(如0.2)是近期的“兄弟关系”,距离大的(如5.0)是远古的“祖先关系”。用鼠标测量任意两个叶子节点到其最近共同祖先的高度,那个高度值,就是它们的“不相似度”。这比任何数字指标都直观。

5. 常见问题与排查技巧实录

5.1 “ImportError”类问题:环境与路径的战争

问题现象根本原因一招解决
ModuleNotFoundError: No module named 'src'Python找不到src包。因为src不是安装的包,而是本地模块。在项目根目录运行:python -m Kmeans(用-m参数)或export PYTHONPATH=$(pwd)(Linux/Mac)/set PYTHONPATH=%cd%(Windows),再运行python Kmeans.py
ImportError: DLL load failed(Windows)SciPy或NumPy的C扩展库缺失VC++运行时。下载并安装 Microsoft Visual C++ Redistributable for Visual Studio。
ImportError: No module named 'sklearn'pip install -r requirements.txt未成功执行,或在错误环境中执行。运行which python(Linux/Mac)或where python(Windows),确认当前Python路径与虚拟环境一致;再执行pip list | grep sklearn检查是否安装。

实操心得:永远在项目根目录下运行代码,永远用python -m script_name方式启动。这是我十年来最省时间的教训——它自动把当前目录加入sys.path,彻底规避路径问题。

5.2 “ValueError”与“LinAlgError”:数学假设的崩塌

问题现象根本原因一招解决
ValueError: Input contains NaN, infinity or a value too large for dtype('float64')Frogs_MFCCs.csv含缺失值(NaN),np.cov()无法处理。data_loader.pyload_frogs_data()已内置X = np.nan_to_num(X, nan=0.0),但若你修改了加载逻辑,务必加上此行。
LinAlgError: Eigenvalues did not converge协方差矩阵病态(条件数过大),常见于高维小样本或特征间强相关。Frogs_MFCCs有12维,7195样本,本不该出问题。PCA.py中,cov_matrix计算后,添加cov_matrix += np.eye(cov_matrix.shape[0]) * 1e-8(Tikhonov正则化),微小扰动确保矩阵可逆。
ValueError: Found array with 0 sample(s)Kmeans.pyassignments全为-1,或空簇处理失败。检查assign_clusters()函数,确认distances矩阵计算无误;确保handle_empty_clusters()被正确调用。

5.3 可视化异常:图不对,心先慌

问题现象根本原因一招解决
kmeans_result.png中质心重叠,或散点颜色混乱matplotlib后端问题,或plt.show()被多次调用导致状态混乱。在每个脚本开头,强制设置后端:import matplotlib; matplotlib.use('Agg');所有绘图后,用plt.savefig()保存,不要plt.show()(它在非GUI环境会阻塞)。
pca_result.png中虚线坐标轴消失,或文字模糊matplotlib字体缺失,尤其在Linux服务器无GUI时。utils.py的绘图函数开头,添加:
plt.rcParams['font.sans-serif'] = ['DejaVu Sans', 'Arial', 'simhei']
plt.rcParams['axes.unicode_minus'] = False(解决负号显示为方块)。
hierarchical_clustering_result.png树状图极度压缩,无法看清分支scipy.cluster.hierarchy.dendrogramtruncate_modep参数未设,导致绘制全部7195个叶子。HC.pydendrogram()调用已设truncate_mode='level', p=3,只显示顶层3级。若想看更多,增大p值,但需确保内存充足。

5.4 算法行为疑问:为什么它不按我想的走?

学生疑问真相与教学启示
“Kmeans跑了100轮还没收敛,是不是代码错了?”不是错,是K值太大或数据本就不适合Kmeans。Frogs_MFCCs有16个真实物种,若设k=16,Kmeans会强行分成16簇,但其中很多簇可能非常小且不稳定。Kmeans假设簇是球形、等大小、密度均匀的。蛙叫声数据可能有长尾分布,此时Kmeans不是最佳选择。这恰恰是实验要揭示的:算法有适用边界。
“PCA降维后,点都挤在一条线上,是不是降维失败?”不是失败,是成功!这说明PC1方向承载了绝大部分方差。pca_result.png中若PC1解释率>90%,那么数据本质上就是一维的。降维的目标不是“看起来像原图”,而是“用最少维度保留最多信息”。挤在一条线,正是最优解。
“层次聚类树状图,为什么同一种蛙的样本没全聚在一起?”MFCC特征提取有局限,或蛙叫声个体差异大于种间差异。这引出了关键问题:聚类效果好不好,不能只看算法,要看特征工程和领域知识。你可以尝试用Frogs_MFCCs.csv中的Species列,计算Adjusted Rand Index (ARI) 来量化聚类与真实标签的一致性,这才是科学的评估。

最后分享一个小技巧:如果你想快速测试算法对噪声的鲁棒性,可以在Kmeans.csv加入一些随机噪声点。在Kmeans.py加载数据后,添加:
```python

Add 20 noise points

np.random.seed(42)
noise = np.random.uniform(-10, 10, size=(20, 2))
X = np.vstack([X, noise])
```
然后运行,观察Kmeans是否能把噪声点识别为离群的小簇。这比任何理论讲解,都更能让你理解“Kmeans对噪声敏感”这句话的重量。

这套代码的价值,不在于它多精巧,而在于它足够“笨拙”——它把每一个数学符号、每一个矩阵操作、每一个调试时刻,都赤裸裸地摆在你面前。当你亲手把协方差矩阵分解,亲手把距离矩阵一行行更新,亲手在树状图上数清每一次合并,那些曾经悬浮在半空的算法概念,就真正落到了地上,长出了根。中科大的这门实验课,教给学生的从来不是“怎么跑通代码”,而是“当代码跑不通时,你脑子里第一个该问的问题是什么”。而这,才是所有AI工程师职业生涯里,最硬核的起手式。

本文还有配套的精品资源,点击获取

简介:这套代码来自中国科学技术大学2019年春季人工智能课程实验二,完整实现了三种基础无监督学习算法:Kmeans聚类(Kmeans.py)、主成分分析降维(PCA.py)和层次聚类(HC.py)。所有脚本均不依赖scikit-learn高层API,而是从零构建核心逻辑——比如Kmeans中的质心迭代更新、PCA中基于协方差矩阵的特征向量分解与数据投影、层次聚类中的距离矩阵计算与凝聚过程,并支持生成树状图(hierarchical_clustering_.png)。配套提供真实数据文件(Frogs_MFCCs.csv、Kmeans.csv),代码结构清晰,直接运行即可复现实验结果。根目录下为入口脚本,src文件夹存放辅助函数与数据加载模块,requirements.txt列明所需依赖(NumPy、SciPy、scikit-learn),适配Python 3.x环境,适合机器学习初学者理解算法原理、调试步骤和观察中间过程。


本文还有配套的精品资源,点击获取

http://www.zskr.cn/news/1509117.html

相关文章:

  • Claude Managed Agents:会话即事件日志的生产级Agent架构
  • Linux下开箱即用的CPU浮点性能测试工具集(含Linpack 11.0.1二进制与MKL集成指南)
  • 桥接模式:数据库驱动与连接管理
  • 机器学习落地12类高频隐蔽错误深度排查指南
  • 百度网盘提取码智能查询工具:10秒解锁所有隐藏资源
  • 弹性护栏服务商家排行榜,选哪家性价比高 - mypinpai
  • 基于客户分群与Offer ROI的可解释推荐系统实战
  • 2026年|学姐亲测:5款好用的论文降AIGC工具,AI率80%降至10%及去AI痕迹技巧 - 降AI实验室
  • 儋州市黄金回收白银回收铂金回收彩金回收靠谱门店TOP排行榜及联系方式地址电话+诚信店铺推荐 - 大熊猫898989
  • 【Springboot毕设全套源码+文档】基于javaweb的乡村健康医疗管理系统的设计与开发(丰富项目+远程调试+讲解+定制)
  • 048、Edge Impulse的联邦学习与边缘更新
  • 别再瞎猜了!用Python的tiktoken库精准计算ChatGPT API的Token消耗(附成本估算脚本)
  • Chrome-Charset:终极网页乱码修复解决方案
  • 包头市黄金回收白银回收铂金回收彩金回收靠谱门店TOP排行榜及联系方式地址电话+诚信店铺推荐 - 大熊猫898989
  • 德阳市黄金回收白银回收铂金回收彩金回收靠谱门店TOP排行榜及联系方式地址电话+诚信店铺推荐 - 大熊猫898989
  • 滁州实体工厂营销团队托管费用多少? - mypinpai
  • 保姆级教程:用CPLD和LVDS手搓一个LTPI硬件通道(从GPIO/I2C采样到8b/10b编码)
  • 宝鸡市黄金回收白银回收铂金回收彩金回收靠谱门店TOP排行榜及联系方式地址电话+诚信店铺推荐 - 大熊猫898989
  • PyTorch工程基座:5分钟启动可复现、可调试、可部署的训练流程
  • CloakBrowser 火了:AI Agent 时代,浏览器自动化可能要换一套基础设施了
  • 亲密的网络旅程(四):给网络装上一台“超级电梯”与“贵宾通道”——802.1Q与QoS的魔法
  • 【花雕学编程】Arduino BLDC 之UWB与超声波融合的智能避障跟随机器人
  • C++版DICOM3.0轻量解析与传输源码包(含完整编译产物和测试工程)
  • 2026年大同合同纠纷律师推荐选对=省心 张超律师值得推荐 - 本地品牌推荐
  • 手把手教你:在HP服务器上切换RAID卡模式(Smart Array vs HBA/JBOD)
  • 信息学奥赛递推题‘踩方格’的保姆级图解教程:为什么是a[i]=2*a[i-1]+a[i-2]?
  • P1336 最佳课题选择【洛谷算法习题】
  • 2026年6月口碑好的焊管制造商推荐,耐高压弯头/大口径不锈钢焊管/薄壁不锈钢焊管/大口径不锈钢管,焊管加工厂推荐 - 品牌推荐师
  • 如何快速下载抖音无水印视频:面向新手的完整实战指南
  • MATLAB手写三次样条插值函数:带详细注释+可视化示例脚本