简介:本资源是武汉大学《机器学习与模式识别》课程配套的Python实践项目合集,面向计算机、人工智能、自动化等专业的本科生及初阶学习者,覆盖监督学习、无监督学习、神经网络等核心模块,可支撑课程实验、课程设计、毕设开发与自学进阶。压缩包共337个文件,含145个Python源码(实现算法与模型训练)、72个YAML配置文件(用于超参管理与环境定义)、18个Markdown文档(含实验说明、运行指南与原理简析)、17张PNG/JPG示意图及9个Jupyter Notebook交互式案例,整体大小60.22MB,结构清晰、模块分明,便于按主题分步学习与二次开发。已有153人下载学习,所有代码均经实机测试通过,答辩平均分96分,附完整README与多场景运行说明,支持基础复现、参数调优及功能拓展,亦可作为教学参考或项目原型快速启动。
1. 这不是一份“课程作业打包”,而是一套可直接复用的机器学习教学实验骨架
武汉大学《机器学习与模式识别》课程配套的 Python 实验源码,常被误认为是仅供交差的课堂练习。但实际翻看其结构会发现:它用清晰分层的模块组织(supervised/,unsupervised/,neural_networks/),统一的数据加载接口(datasets/下含 Iris、Wine、MNIST 等标准数据集预处理脚本),以及每份实验都附带.md文档说明——从数学推导(如 SVM 的拉格朗日对偶问题求解步骤)、代码逻辑(梯度下降中 learning_rate 与 epoch 的耦合关系)、到可视化输出(决策边界热力图、聚类轮廓系数曲线)全部闭环。它解决的不是“如何跑通一个 sklearn 示例”,而是“如何在无框架封装前提下,亲手实现并验证算法内核”。适合高校助教快速搭建实验课环境、自学者系统补全手写算法能力、或工程师回溯经典模型底层细节。你不需要重造轮子,但必须理解每个for循环里更新的是哪个参数、每次np.dot()背后的几何意义。
2. 用原生 NumPy 实现监督学习分类器:从感知机到支持向量机
监督学习实验的核心价值,在于剥离 scikit-learn 的黑盒封装,暴露模型训练过程中的关键决策点。该课程实验包中supervised/perceptron.py和supervised/svm.py是典型代表——它们不调用sklearn.svm.SVC,而是基于 NumPy 手动实现梯度更新与约束优化。
2.1 感知机的权重更新逻辑与收敛性验证
感知机作为最基础的线性分类器,其实现难点不在公式本身,而在样本遍历顺序对收敛速度的影响。课程代码采用随机打乱(np.random.shuffle)+ 批量更新策略:
# supervised/perceptron.py 关键片段 def train(self, X, y, max_iter=1000): self.w = np.zeros(X.shape[1]) self.b = 0.0 for _ in range(max_iter): # 随机打乱索引,避免周期性震荡 indices = np.random.permutation(len(X)) misclassified = 0 for i in indices: if y[i] * (np.dot(self.w, X[i]) + self.b) <= 0: self.w += self.lr * y[i] * X[i] self.b += self.lr * y[i] misclassified += 1 if misclassified == 0: break注意:
self.lr(学习率)并非固定值。实验文档明确指出:当X未归一化时,lr=0.01可能导致权重爆炸;若使用StandardScaler预处理,则lr=1.0更稳定。这正是课程强调的“数据预处理与超参强耦合”原则——没有脱离数据分布谈学习率的合理性。
2.2 SVM 对偶问题的手动求解与核函数注入点
SVM 实验的精髓在于将原始优化问题转化为对偶问题,并通过 SMO(序列最小优化)思想简化求解。课程代码supervised/svm.py中solve_dual_problem()函数不依赖cvxopt,而是用坐标上升法迭代更新 α:
# supervised/svm.py 片段:SMO 核心循环 def solve_dual_problem(self, X, y): n_samples = X.shape[0] alphas = np.zeros(n_samples) b = 0.0 for _ in range(self.max_iter): alpha_pairs_changed = 0 for i in range(n_samples): # 计算预测误差 E_i E_i = self._predict(X[i], alphas, y, X, b) - y[i] # 启发式选择 j(非 i 的最大 |E_i - E_j|) j = self._select_j(i, E_i, alphas, y, X, b) # 计算 L, H 边界(满足 0≤α≤C 约束) L, H = self._compute_bounds(alphas[i], alphas[j], y[i], y[j]) if L == H: continue # 计算 η = K_ii + K_jj - 2*K_ij eta = self._kernel(X[i], X[i]) + self._kernel(X[j], X[j]) - 2 * self._kernel(X[i], X[j]) if eta <= 0: continue # 更新 α_j alpha_j_new = alphas[j] + y[j] * (E_i - E_j) / eta alpha_j_new = np.clip(alpha_j_new, L, H) # 更新 α_i(保证 ∑α_i y_i = 0) alpha_i_new = alphas[i] + y[i] * y[j] * (alphas[j] - alpha_j_new) # 更新 b(根据支持向量条件) b = self._update_b(alphas, y, X, b, i, j, alpha_i_new, alpha_j_new, E_i, E_j) alphas[i], alphas[j] = alpha_i_new, alpha_j_new if abs(alpha_j_new - alphas[j]) > 1e-5: alpha_pairs_changed += 1 if alpha_pairs_changed == 0: break return alphas, b2.2.1 核函数替换的三个关键位置
课程文档特别标注:若要将线性 SVM 改为 RBF SVM,需同时修改三处:
self._kernel()方法:从lambda x1,x2: np.dot(x1,x2)替换为lambda x1,x2: np.exp(-gamma * np.linalg.norm(x1-x2)**2)gamma参数初始化:在__init__()中增加self.gamma = gammasolve_dual_problem()内eta计算:self._kernel()调用已自动适配新核函数
提示:RBF 核的
gamma值直接影响决策边界曲率。实验建议先用GridSearchCV在小数据集(如 Iris)上粗筛gamma ∈ [0.001, 10],再代入手写 SVM 验证——这比盲目调参更高效。
2.3 多分类策略的实现差异:OvR vs OvO
课程实验对比了 One-vs-Rest(OvR)和 One-vs-One(OvO)两种策略在supervised/logistic_regression.py中的实现:
| 策略 | 分类器数量 | 训练数据量 | 决策逻辑 | 适用场景 |
|---|---|---|---|---|
| OvR | n_classes | 全量数据(每次取一类正样本+其余负样本) | argmax(各分类器输出概率) | 类别不平衡时更鲁棒 |
| OvO | n_classes*(n_classes-1)/2 | 每次仅用两类样本子集 | 投票制(得票最多者胜出) | 小样本、高维数据收敛更快 |
代码中MultiClassifier类通过strategy='ovr'或'ovo'切换,且fit()方法内部会动态生成子分类器列表。这种设计让学习者直观看到:多分类不是“单个模型的简单扩展”,而是分类器组合架构的设计问题。
3. 无监督学习实验:K-Means 初始化陷阱与层次聚类距离度量选择
无监督学习实验的价值,在于揭示“没有标签”的情况下,算法如何依赖数据内在结构做出假设。课程中unsupervised/kmeans.py和unsupervised/hierarchical.py并非简单调用sklearn.cluster,而是暴露了 K-Means 的“初始中心敏感性”与层次聚类的“距离度量歧义性”。
3.1 K-Means 的 k-means++ 初始化与收敛判断
标准 K-Means 对初始质心敏感,易陷入局部最优。课程实验强制实现 k-means++ 初始化:
# unsupervised/kmeans.py def _initialize_centers_kmeans_plusplus(self, X): n_samples, n_features = X.shape centers = np.zeros((self.n_clusters, n_features)) # 第一个中心随机选取 centers[0] = X[np.random.randint(n_samples)] # 后续中心按距离平方概率选取 for c in range(1, self.n_clusters): distances = np.array([min([np.linalg.norm(x-center)**2 for center in centers[:c]]) for x in X]) probabilities = distances / distances.sum() cumulative_probabilities = np.cumsum(probabilities) r = np.random.rand() for i, p in enumerate(cumulative_probabilities): if r < p: centers[c] = X[i] break return centers3.1.1 收敛判定的双重标准
课程代码不采用简单的“质心移动距离 < ε”,而是叠加簇内平方和(WCSS)变化率:
# 迭代中计算 wcss_old = self._calculate_wcss(X, labels, centers) # ... 更新质心与标签 ... wcss_new = self._calculate_wcss(X, labels, centers) if abs(wcss_new - wcss_old) / wcss_old < 1e-4 and np.all(np.linalg.norm(centers_new - centers_old, axis=1) < 1e-4): break注意:
1e-4是经验阈值。当X维度 > 50 时,建议将 WCSS 变化率阈值放宽至1e-3,否则可能因浮点精度导致无限循环。
3.2 层次聚类的三种距离度量与树状图截断逻辑
层次聚类实验unsupervised/hierarchical.py提供euclidean、manhattan、cosine三种距离选项,并要求用户手动指定截断高度threshold:
# 构建距离矩阵 if self.metric == 'euclidean': dist_matrix = squareform(pdist(X, metric='euclidean')) elif self.metric == 'manhattan': dist_matrix = squareform(pdist(X, metric='cityblock')) elif self.metric == 'cosine': dist_matrix = squareform(pdist(X, metric='cosine')) # 执行凝聚层次聚类(单连接) linkage_matrix = linkage(dist_matrix, method='single') # 截断获取簇标签 labels = fcluster(linkage_matrix, t=self.threshold, criterion='distance')3.2.1 距离度量选择指南
| 度量类型 | 适用数据特征 | 数学表达 | 课程实验建议场景 |
|---|---|---|---|
| Euclidean | 连续型特征、各维度量纲一致 | √Σ(x_i-y_i)² | Iris 花瓣长度/宽度(单位统一) |
| Manhattan | 高维稀疏数据、存在异常值 | `Σ | x_i-y_i |
| Cosine | 方向敏感、忽略向量模长 | `1 - (x·y)/( | x |
实验文档强调:同一数据集切换距离度量,可能导致树状图分支结构完全改变。例如在 Wine 数据集上,Euclidean 距离下前 3 层合并的样本组,Cosine 距离下可能分散在不同子树——这正是无监督学习“无唯一正确答案”的体现。
4. 神经网络实验:从全连接到卷积层的手动反向传播推导
神经网络实验是课程难度跃升的关键环节。neural_networks/mlp.py和neural_networks/cnn.py不使用 PyTorch/TensorFlow,而是用 NumPy 实现前向传播与反向传播,强制学习者理解链式法则在每一层的具象化。
4.1 全连接网络的梯度计算与权重更新
MLP 实验中backward()方法需逐层计算梯度。以 ReLU 激活的隐藏层为例:
# neural_networks/mlp.py def backward(self, X, y_true): # 输出层梯度(MSE loss) d_loss_d_out = (self.output - y_true) / len(y_true) # 均值损失 # 输出层到隐藏层权重梯度 d_loss_d_w2 = self.hidden.T @ d_loss_d_out d_loss_d_b2 = np.sum(d_loss_d_out, axis=0) # 隐藏层激活函数梯度(ReLU) d_relu = (self.hidden > 0).astype(float) # 导数:x>0 时为1,否则为0 # 隐藏层输入梯度 d_loss_d_hidden = d_loss_d_out @ self.W2.T * d_relu # 输入层到隐藏层权重梯度 d_loss_d_w1 = X.T @ d_loss_d_hidden d_loss_d_b1 = np.sum(d_loss_d_hidden, axis=0) # 更新权重(带动量) self.v_W1 = self.momentum * self.v_W1 - self.lr * d_loss_d_w1 self.W1 += self.v_W1 self.v_b1 = self.momentum * self.v_b1 - self.lr * d_loss_d_b1 self.b1 += self.v_b1 # ... 同理更新 W2, b24.1.1 动量项的物理意义与调试技巧
课程文档解释:self.momentum=0.9并非随意设定,而是模拟“惯性”——使权重更新方向更平滑,减少震荡。调试时若发现 loss 曲线剧烈波动,应检查:
self.lr是否过大(尝试0.001 → 0.0001)self.momentum是否过小(低于0.8时惯性不足)d_loss_d_w1计算是否遗漏X.T(矩阵维度错误会导致梯度爆炸)
4.2 卷积层的 im2col 优化与反向传播映射
CNN 实验neural_networks/cnn.py的核心是conv_forward()中的im2col技术:
# 将卷积操作转为矩阵乘法 def im2col(self, x, filter_h, filter_w, stride=1, pad=0): n, c, h, w = x.shape out_h = (h + 2*pad - filter_h) // stride + 1 out_w = (w + 2*pad - filter_w) // stride + 1 # 补零 col = np.pad(x, [(0,0), (0,0), (pad,pad), (pad,pad)], 'constant') # 按滑动窗口展开 col = np.array([ col[:, :, i:i+filter_h, j:j+filter_w].reshape(n, -1) for i in range(0, out_h * stride, stride) for j in range(0, out_w * stride, stride) ]).transpose(1, 0, 2).reshape(n, -1, filter_h * filter_w * c) return col.reshape(n * out_h * out_w, -1) # 前向传播 col = self.im2col(x, self.fh, self.fw, self.stride, self.pad) out = col @ self.W.T + self.b # 矩阵乘法替代循环卷积 out = out.reshape(n, out_h, out_w, -1).transpose(0, 3, 1, 2)4.2.1 反向传播中 col2im 的维度还原
conv_backward()必须将d_out(形状(n, f, out_h, out_w))还原为d_col,再映射回输入梯度d_x:
# d_out 形状:(n, f, out_h, out_w) d_out_reshaped = d_out.transpose(0, 2, 3, 1).reshape(-1, self.f) d_col = d_out_reshaped @ self.W # (n*out_h*out_w, c*fh*fw) # col2im:将 d_col 按 im2col 逆序还原 d_x = np.zeros_like(x) for i in range(out_h): for j in range(out_w): d_x[:, :, i*stride:i*stride+self.fh, j*stride:j*stride+self.fw] += \ d_col[i*out_w+j].reshape(c, self.fh, self.fw)提示:
d_col的 reshape 维度必须与im2col输出严格一致。若d_col.shape != (n*out_h*out_w, c*fh*fw),说明d_outtranspose 顺序错误——这是 CNN 手写实现中最常见的维度 bug。
5. 实验环境配置与常见报错定位:从 Python 安装到 NumPy 版本兼容
课程实验对运行环境有明确要求,但文档未提供一键安装脚本。以下是基于 Ubuntu 22.04 和 macOS 13 的实操配置路径,覆盖 Python 安装、依赖管理及典型报错修复。
5.1 Python 环境的最小化安装(避开系统 Python 冲突)
课程实验要求Python >= 3.8,但禁止使用系统自带 Python(Ubuntu 的/usr/bin/python3或 macOS 的/usr/bin/python3),因其包管理受限且升级风险高。推荐方案:
# Ubuntu 22.04:使用 deadsnakes PPA 安装 Python 3.9 sudo apt update sudo apt install -y software-properties-common sudo add-apt-repository ppa:deadsnakes/ppa sudo apt update sudo apt install -y python3.9 python3.9-venv python3.9-dev # macOS:使用 pyenv 管理多版本(避免 Homebrew Python 权限问题) brew install pyenv pyenv install 3.9.18 pyenv global 3.9.18注意:
python3.9-venv(Ubuntu)或pyenv virtualenv(macOS)是必须安装的组件。课程实验依赖venv创建隔离环境,而非conda——后者在 NumPy BLAS 后端兼容性上易出问题。
5.2 依赖包的精确版本锁定与冲突解决
实验requirements.txt明确指定:
numpy==1.23.5 scipy==1.10.1 matplotlib==3.7.1 pandas==1.5.3但直接pip install -r requirements.txt可能失败,原因在于numpy 1.23.5与较新pip的 wheel 兼容性问题。解决方案:
# 步骤1:降级 pip 到兼容版本 python3.9 -m pip install pip==22.3.1 # 步骤2:强制从 PyPI 源安装(避免镜像缓存旧 wheel) python3.9 -m pip install --index-url https://pypi.org/simple/ --no-cache-dir numpy==1.23.5 # 步骤3:逐个安装(防止依赖解析冲突) python3.9 -m pip install --no-cache-dir scipy==1.10.1 python3.9 -m pip install --no-cache-dir matplotlib==3.7.1 python3.9 -m pip install --no-cache-dir pandas==1.5.35.2.1 典型报错与修复对照表
| 报错信息 | 根本原因 | 修复命令 |
|---|---|---|
ImportError: numpy.core.multiarray failed to import | NumPy 安装损坏或 ABI 不匹配 | python3.9 -m pip uninstall numpy -y && python3.9 -m pip install --no-cache-dir numpy==1.23.5 |
ModuleNotFoundError: No module named 'sklearn' | 课程实验未依赖 scikit-learn,但某些.py文件误导入 | 删除import sklearn行,改用from supervised.svm import SVM等本地模块 |
ValueError: operands could not be broadcast together with shapes (100,4) (100,) | y_true未 reshape 为列向量 | 在mlp.py的train()中添加y_true = y_true.reshape(-1, 1) |
OSError: [Errno 24] Too many open files | Linux 系统默认文件描述符限制过低(影响pandas.read_csv) | ulimit -n 65536(临时)或echo "* soft nofile 65536" >> /etc/security/limits.conf(永久) |
5.3 VS Code 调试配置:设置 Python 解释器路径与断点验证
课程实验需在 VS Code 中调试main.py,关键配置如下:
- 打开命令面板(
Ctrl+Shift+P),输入Python: Select Interpreter - 选择路径为
~/.pyenv/versions/3.9.18/bin/python(macOS)或/usr/bin/python3.9(Ubuntu) - 在
.vscode/settings.json中添加:
{ "python.defaultInterpreterPath": "/home/user/.pyenv/versions/3.9.18/bin/python", "python.testing.pytestArgs": ["tests/"], "python.formatting.provider": "autopep8" }- 在
mlp.py的forward()函数首行设断点,运行调试(F5),观察self.W1.shape是否匹配X.shape[1]——这是验证数据流完整性的最快方式。
提示:若断点无法命中,检查 VS Code 左下角 Python 解释器路径是否显示为
3.9.18。曾有用户因未重启 VS Code 导致仍使用旧解释器,造成ModuleNotFoundError。
本文还有配套的精品资源,点击获取