周志华《机器学习》代码练习:从公式推导到Python手写实现
简介这份资源是周志华《机器学习》一书的配套代码练习包面向正在系统学习机器学习算法原理、希望动手复现经典模型的高校学生与自学者。内容围绕线性模型、决策树、集成学习与支持向量机等核心章节展开涵盖逻辑回归、LDA、Friedman 决策树、Iterated Bagging、MultiBoosting、偏差方差分解以及简化SVM、SVR 等典型算法的实现练习适合在理论学习之后通过编码加深对模型细节与调参过程的理解。压缩包共 16 个文件以 15 个 Python 脚本为主体另附 1 个 README 说明文档整体约 20KB体积轻量便于快速下载与本地运行。目前已有 508 人学习下载读者可借助这些脚本对照教材公式逐步调试观察不同算法在训练与验证中的表现差异并参考 README 梳理练习脉络从而形成从原理到代码的完整学习闭环。1. 周志华《机器学习》代码练习从公式到可运行代码的那道坎很多人学周志华《机器学习》时都会卡在同一个地方书上的公式推导能看懂但合上书打开编辑器面对一个真实数据集却不知道第一行该写什么。这本被大家叫作“西瓜书”的教材理论密度极高公式严谨但它本身不是一本代码书课后习题也大多停留在数学推导层面。于是“机器学习入门”这条路上最大的断层就出现在这里——从看懂公式到跑通代码之间缺了一套能对照章节、能直接运行、能改参数的代码练习。这个标题指向的正是填补这道断层的东西一套按《机器学习》章节组织的代码练习集合。它解决的不是“机器学习是什么”而是“第几章的哪个算法用 Python 怎么写出来参数怎么调结果怎么验证”。适合两类人一类是正在跟这本书自学、需要动手巩固的入门者另一类是准备“机器学习期末复习”或应对“机器学习知识考试”的学生需要把书上的算法真正跑一遍才能记住细节。下面我按自己带人做这套练习的经验把选型、环境、逐章实现和踩坑讲清楚。2. 环境与工具选型为什么用 Python NumPy 手写而不是直接调库2.1 手写实现和调库的分工边界做《机器学习》代码练习第一个要决定的事就是用 sklearn 还是自己手写我的经验是分两层。第一层核心算法必须手写比如线性回归的闭式解、逻辑回归的梯度下降、决策树的信息增益划分、K-Means 的迭代更新。原因是这本书的考点和面试点都在推导细节上你调一次sklearn.linear_model.LogisticRegression什么也学不到。第二层数据预处理、划分训练测试集、评估指标可以用 sklearn因为这些不是这本书的重点手写反而浪费时间。所以这套练习的典型依赖就三个NumPy 负责矩阵运算Matplotlib 负责可视化scikit-learn 只用来加载数据和算指标。不要一上来就装 PyTorch 或 TensorFlow这本书的算法用不到深度学习框架装了只会让环境变重、报错变多。2.2 最小可复现环境搭建我一般用 conda 建一个独立环境避免和系统里的包冲突。命令如下# 创建独立环境指定 python 版本 conda create -n ml-practice python3.10 -y conda activate ml-practice # 只装三个核心包版本不必锁死但 numpy 建议 1.24 以上 pip install numpy matplotlib scikit-learn装完后验证一下# check_env.py import numpy as np import matplotlib import sklearn print(numpy:, np.__version__) print(matplotlib:, matplotlib.__version__) print(sklearn:, sklearn.__version__) # 顺手验证矩阵运算是否正常 a np.array([[1, 2], [3, 4]]) b np.array([[1], [1]]) print(矩阵乘法结果:\n, a b)这段代码的逻辑很简单导入三个包并打印版本最后做一次矩阵乘法确认 NumPy 的底层线性代数库工作正常。参数上唯一要注意的是 Python 版本3.8 到 3.11 都可以但别用 3.12 刚发布时的早期版本部分科学计算包的预编译轮子还没跟上会触发源码编译新手容易在这里卡住。如果a b能正常输出环境就没问题了。提示不要用pip install numpy单独升级 NumPy 到最新大版本matplotlib 和 sklearn 对 NumPy 的 ABI 有依赖版本错配会报numpy.dtype size changed这类玄学错误。2.3 目录结构怎么组织代码练习最怕的是文件乱放过两周自己都找不到。我习惯按章节建目录每个算法一个文件数据统一放data/ml-practice/ ├── data/ # 数据集如西瓜数据集、iris ├── ch03_linear/ # 第3章 线性模型 │ ├── linear_regression.py │ └── logistic_regression.py ├── ch04_tree/ # 第4章 决策树 │ └── decision_tree.py ├── ch06_svm/ # 第6章 支持向量机 │ └── svm_smo.py └── utils/ └── metrics.py # 自己写的评估函数这样组织的好处是复习时按章节定位考试前想快速过一遍某个算法直接进对应目录跑就行。utils/metrics.py放自己实现的准确率、混淆矩阵避免每次都从 sklearn 导入也顺便练手。3. 逐章实现线性模型和决策树的最小可运行代码3.1 线性回归闭式解和梯度下降两种写法对照线性回归是“机器学习线性回归实验”里出现频率最高的也是理解后续一切模型的基础。西瓜书第 3 章给了两种解法最小二乘的闭式解和梯度下降的迭代解。两种都要写因为考试可能考推导工程上则常用梯度下降处理大规模数据。先看闭式解。对于y Xw b把 b 吸收进 X 变成增广矩阵后最优解是w (X^T X)^{-1} X^T yimport numpy as np def linear_regression_closed(X, y): 闭式解求解线性回归 X: shape (n_samples, n_features) y: shape (n_samples,) # 给 X 增加一列全 1用于吸收偏置 b X_aug np.hstack([X, np.ones((X.shape[0], 1))]) # 正规方程求解用 pinv 而非 inv避免矩阵奇异时报错 w np.linalg.pinv(X_aug.T X_aug) X_aug.T y return w # 最后一位是偏置 b # 造一组带噪声的数据验证 np.random.seed(42) X np.random.rand(100, 1) * 10 y 2.5 * X.squeeze() 1.3 np.random.randn(100) * 0.5 w linear_regression_closed(X, y) print(斜率 w:, w[0], 截距 b:, w[1])逻辑说明np.hstack把全 1 列拼到特征矩阵右边这样偏置 b 就变成权重向量的最后一位公式统一。参数上最关键的是用np.linalg.pinv而不是np.linalg.inv伪逆在矩阵接近奇异时也能给出数值稳定的解这是血泪经验——用inv遇到共线特征直接抛LinAlgError练习时数据小看不出来换成真实数据就翻车。再看梯度下降版本这个更能体现“机器学习应用流程”里的迭代思想def linear_regression_gd(X, y, lr0.01, epochs1000): 梯度下降求解线性回归 lr: 学习率控制每步更新幅度 epochs: 迭代轮数 n, d X.shape w np.zeros(d) b 0.0 losses [] for i in range(epochs): y_pred X w b error y_pred - y # 均方误差对 w 和 b 的梯度 grad_w (2 / n) * (X.T error) grad_b (2 / n) * np.sum(error) w - lr * grad_w b - lr * grad_b loss np.mean(error ** 2) losses.append(loss) return w, b, losses参数说明lr学习率是最容易出问题的设 0.01 在标准化后的数据上通常稳但如果特征没归一化、量纲差几个数量级梯度会爆炸loss 变成nan。epochs设 1000 一般够小数据收敛判断收敛不要只看轮数要看losses最后几十轮是否还在明显下降。我一般会把losses画出来曲线走平才算收敛这是最直观的验证方法。3.2 决策树信息增益划分和递归建树决策树是第 4 章的核心也是“机器学习算法”里最能体现分治思想的一个。手写决策树的关键有三块计算信息熵、选最优划分特征、递归建树。先写信息熵和条件熵import numpy as np from collections import Counter def entropy(y): 计算信息熵y 是标签数组 counts Counter(y) probs np.array([c / len(y) for c in counts.values()]) return -np.sum(probs * np.log2(probs)) def info_gain(X_col, y): 计算某特征划分带来的信息增益 base_ent entropy(y) values np.unique(X_col) cond_ent 0.0 for v in values: subset_y y[X_col v] cond_ent (len(subset_y) / len(y)) * entropy(subset_y) return base_ent - cond_ent逻辑说明entropy用标签的频率算信息熵info_gain遍历特征的每个取值按取值切分后算加权条件熵两者相减就是增益。参数上要注意np.log2的底数信息增益用 2 为底基尼指数不用对数别混。Counter统计频次比手写循环快数据量大时差别明显。建树用递归终止条件是节点样本全同类、或特征用完、或样本数小于阈值def build_tree(X, y, feature_names, depth0, max_depth5): # 终止条件1全同类 if len(np.unique(y)) 1: return {leaf: True, label: y[0]} # 终止条件2达到最大深度或没有特征 if depth max_depth or X.shape[1] 0: return {leaf: True, label: Counter(y).most_common(1)[0][0]} # 选信息增益最大的特征 gains [info_gain(X[:, i], y) for i in range(X.shape[1])] best int(np.argmax(gains)) tree {leaf: False, feature: feature_names[best], children: {}} for v in np.unique(X[:, best]): mask X[:, best] v # 递归时删掉已用特征 sub_X np.delete(X[mask], best, axis1) sub_names [n for i, n in enumerate(feature_names) if i ! best] tree[children][v] build_tree(sub_X, y[mask], sub_names, depth 1, max_depth) return tree参数说明max_depth是防过拟合的第一道闸设 5 到 10 之间比较常见不设的话树会一直长到每个叶子只有一个样本训练集准确率 100% 但测试集惨不忍睹。np.delete删特征这一步容易写错轴axis1是删列写成axis0就删成样本了这是新手高频翻车点。递归里传递的feature_names也要同步删否则叶子节点打印出来的特征名对不上。注意决策树对连续特征需要先离散化西瓜书里用的是二分法找划分点。练习时如果数据是连续的先做分箱否则np.unique会返回几百个取值树会爆炸。4. 避坑与排查代码练习里最容易翻车的 5 个地方4.1 现象loss 变成 nan 或 inf原因学习率过大或者特征没归一化导致梯度量级失控。线性回归、逻辑回归、神经网络都会遇到。解决先把特征标准化到均值 0 方差 1再把学习率降一个数量级试。判断方法是在训练循环里打印前几轮的 loss如果第一轮就 nan基本是学习率问题如果几十轮后突然 nan多半是数据里有异常值或除零。4.2 现象训练集准确率很高测试集一塌糊涂原因过拟合模型把训练数据的噪声也学进去了。决策树不设max_depth、KNN 的 K 设成 1、多项式回归阶数过高都会这样。解决决策树限制深度和最小叶节点样本数KNN 把 K 调大回归加正则项。验证方法是画学习曲线看训练和验证准确率的差距差距大就是过拟合。4.3 现象矩阵维度对不上报shapes not aligned原因NumPy 的广播规则没吃透或者特征矩阵和标签的维度搞反了。常见于手写线性回归时X w和w X写混。解决养成习惯在每个函数入口打印X.shape和y.shape确认第一个维度都是样本数。矩阵乘法记住“左矩阵的列等于右矩阵的行”对不上就转置。4.4 现象随机结果每次跑都不一样无法复现原因没设随机种子。数据划分、权重初始化、K-Means 初始中心都带随机性。解决在脚本开头统一设np.random.seed(42)sklearn 的train_test_split传random_state42。这样每次跑结果一致调参时才能判断改动是否真的有效否则你以为是参数起作用了其实只是随机波动。4.5 现象中文标签或特征名在图上显示成方框原因Matplotlib 默认字体不支持中文。解决在绘图前设置字体Windows 用SimHeiMac 用Arial Unicode MSLinux 装WenQuanYi。代码是plt.rcParams[font.sans-serif] [SimHei]加plt.rcParams[axes.unicode_minus] False后者解决负号显示成方块的问题。这个坑不致命但很影响看图早点设好省心。5. 进阶技巧用交叉验证和手写指标把练习变成真正的能力练完单个算法后怎么判断自己是真的掌握了我的标准是能不看书画出算法流程图能解释每个参数的作用能用交叉验证评估模型而不是只看一次划分的结果。这里给两个具体技巧。第一个是手写 K 折交叉验证不要直接调cross_val_score自己写一遍才能理解为什么它比单次划分可靠def k_fold_cv(X, y, model_fn, k5): K 折交叉验证 model_fn: 接收 (X_train, y_train) 返回预测函数的可调用对象 n len(y) indices np.random.permutation(n) # 打乱顺序 folds np.array_split(indices, k) scores [] for i in range(k): test_idx folds[i] train_idx np.concatenate([folds[j] for j in range(k) if j ! i]) predict model_fn(X[train_idx], y[train_idx]) y_pred predict(X[test_idx]) acc np.mean(y_pred y[test_idx]) scores.append(acc) return np.mean(scores), np.std(scores)逻辑说明先把索引打乱再切成 K 份每次拿一份当测试、其余当训练。返回均值和标准差标准差大说明模型对数据划分敏感稳定性差。参数k常用 5 或 10数据少时用留一法。这个函数配合前面的决策树或逻辑回归都能用是检验模型泛化能力的标配。第二个技巧是手写混淆矩阵和精确率召回率理解每个指标怎么来的指标公式关注场景准确率(TPTN)/(TPTNFPFN)类别均衡时精确率TP/(TPFP)误报代价高如垃圾邮件召回率TP/(TPFN)漏报代价高如疾病检测F12·P·R/(PR)精确率和召回率都要兼顾手写一遍这些指标比调classification_report印象深得多。尤其是类别不均衡时准确率会骗人——99% 的样本是负类全预测负类也有 99% 准确率但召回率是 0。这个认知在“机器学习检测”类任务里特别重要。最后说个我自己的习惯每写完一个算法我会故意把参数改坏看它怎么报错、怎么退化。比如把决策树深度设成 100 看它过拟合把学习率设成 10 看 loss 爆炸。这些“故意翻车”的实验比顺顺利利跑通一次学到的东西多得多。代码练习的价值不在于跑出正确结果而在于你能预判它在什么情况下会错。希望帮到你。本文还有配套的精品资源点击获取