资讯详情 周志华《机器学习》代码练习:从公式到可运行代码的避坑指南
📅 2026/10/10 19:59:57
简介这份资源是周志华《机器学习》一书的配套代码练习包面向正在系统学习机器学习算法原理、希望动手复现经典模型的高校学生与算法入门者。内容围绕线性模型、决策树、集成学习与支持向量机等核心章节展开涵盖逻辑回归、LDA、Friedman 决策树、Iterated Bagging、MultiBoosting、偏差方差分解以及简化SVM、SVR 等典型实验帮助读者把书本公式落到可运行的代码上。压缩包共16个文件以15个 Python 脚本为主另附1份 README 说明文档整体约20KB体量轻便便于逐文件阅读与调试。目前已有508人学习下载适合作为课程作业、算法复现或面试复习的参考。读者可借助这些脚本理解各模型的训练流程、参数设置与评估思路并对照教材章节梳理从监督学习到集成方法的实现脉络在动手实践中加深对机器学习理论的认识。1. 周志华《机器学习》代码练习从公式到可运行代码的那道坎很多人学机器学习卡住的地方不是看不懂公式而是看完公式不知道代码该怎么写。周志华老师的《机器学习》俗称“西瓜书”把算法原理讲得很透但书里的伪代码和数学推导离能跑起来的 Python 代码之间还隔着一段距离。这个代码练习包要解决的就是这段距离。它适合两类人一类是正在跟着西瓜书自学、想用代码验证每一个公式的入门者另一类是准备机器学习期末复习、需要把线性回归、决策树、神经网络这些算法亲手实现一遍的学生。热搜里“机器学习线性回归实验”“机器学习头歌”这些词频繁出现说明大量人的真实需求就是——把书上的算法变成能跑、能调参、能看到结果的代码。这篇笔记就按这个思路把代码练习的落地路径拆开讲清楚。2. 代码练习包的结构拆解与运行环境搭建2.1 先搞清楚练习包通常包含什么拿到一个《机器学习》代码练习包第一件事不是急着跑代码而是先看目录结构。常见的组织方式是按章节分文件夹每个文件夹里放对应算法的实现脚本和一份数据。比如第3章线性模型对应线性回归和对数几率回归第4章决策树对应ID3或C4.5的实现第5章神经网络对应BP算法的前向和反向传播。你需要确认三件事数据文件是什么格式csv、txt还是mat代码是纯NumPy实现还是调用了sklearn有没有依赖特定的绘图库。这三件事决定了你后面要装什么、改什么。如果练习包里用的是纯NumPy那说明作者希望你理解每一步矩阵运算如果直接调sklearn那重点就在调参和对比实验上。两种方式没有优劣但你的学习路径完全不同。2.2 环境搭建最小依赖清单我一般会先建一个干净的虚拟环境避免和系统里已有的包版本打架。下面这套命令在Windows和macOS上都能用Python版本建议3.8以上。# 创建虚拟环境名字叫ml_practice python -m venv ml_practice # 激活环境Windows ml_practice\Scripts\activate # 激活环境macOS/Linux source ml_practice/bin/activate # 安装核心依赖 pip install numpy pandas matplotlib scikit-learn这里解释一下每个包的角色。NumPy是所有矩阵运算的底座西瓜书里的公式几乎都能翻译成NumPy的数组操作。pandas主要用来读csv数据、做简单的数据清洗。matplotlib负责画图比如线性回归的拟合直线、决策树的分类边界。scikit-learn不是必须的但建议装上因为你可以用它来交叉验证自己手写实现的结果对不对。注意不要一上来就装TensorFlow或PyTorch。西瓜书里的神经网络练习用NumPy手写一遍BP算法比直接调框架收获大得多。框架可以后面再学。2.3 跑通第一个脚本以线性回归为例环境装好后找一个最简单的脚本先跑通建立信心。线性回归通常是练习包里的第一个算法因为它涉及的数学最少代码也最短。下面是一个典型的纯NumPy实现你可以对照西瓜书第3章的公式看。import numpy as np import matplotlib.pyplot as plt # 生成模拟数据y 3x 2 加上一些噪声 np.random.seed(42) X np.random.rand(100, 1) * 10 y 3 * X 2 np.random.randn(100, 1) * 2 # 在X前面加一列1用来对应截距项b X_b np.c_[np.ones((100, 1)), X] # 用正规方程直接求解theta (X^T X)^(-1) X^T y theta np.linalg.inv(X_b.T.dot(X_b)).dot(X_b.T).dot(y) print(截距b , theta[0][0]) print(斜率w , theta[1][0]) # 画图看看拟合效果 plt.scatter(X, y, s10) plt.plot(X, X_b.dot(theta), colorred) plt.xlabel(x) plt.ylabel(y) plt.show()这段代码的核心就一行np.linalg.inv(X_b.T.dot(X_b)).dot(X_b.T).dot(y)。它对应西瓜书里的正规方程解。np.c_用来拼接列把全1列加到X前面这样theta的第一个元素就是截距。np.linalg.inv求逆矩阵dot做矩阵乘法。跑完之后你应该看到截距接近2、斜率接近3图上的红线穿过散点中心。如果跑出来结果差很远先检查数据生成时的随机种子有没有设再检查X_b的维度对不对。常见错误是忘了加全1列导致求出来的theta只有一个元素截距被强行当成0。3. 从线性回归到决策树核心算法的代码实现要点3.1 对数几率回归sigmoid和梯度下降的手写细节线性回归之后练习包通常会进入对数几率回归也就是Logistic Regression。这里的关键是理解sigmoid函数和交叉熵损失。西瓜书里的公式看起来复杂但代码实现其实很直接。import numpy as np def sigmoid(z): # 防止溢出对z做截断 z np.clip(z, -500, 500) return 1 / (1 np.exp(-z)) def train_logistic(X, y, lr0.01, epochs1000): m, n X.shape X_b np.c_[np.ones((m, 1)), X] # 加截距列 theta np.zeros((n 1, 1)) losses [] for i in range(epochs): z X_b.dot(theta) h sigmoid(z) # 交叉熵损失 loss -np.mean(y * np.log(h 1e-9) (1 - y) * np.log(1 - h 1e-9)) losses.append(loss) # 梯度计算 gradient X_b.T.dot(h - y) / m theta - lr * gradient return theta, losses这里有几个参数需要你亲手调一下感受变化。lr是学习率设太大损失会震荡甚至发散设太小收敛太慢。我一般先用0.01跑1000轮看损失曲线是不是平滑下降。epochs是迭代次数如果损失还在降就继续加。np.clip那行是血泪经验不加的话当z很大或很小时np.exp会溢出程序直接报inf。跑完训练后你可以用losses画一条损失曲线确认模型确实在收敛。如果曲线是一条水平线说明学习率太小或者梯度算错了。如果曲线往上走学习率太大了。3.2 决策树信息增益和递归分裂的代码骨架决策树是西瓜书第4章的重点也是练习包里代码量比较大的一个。核心是计算信息增益然后递归地选最优特征做分裂。下面是一个简化版的骨架帮你理解流程。import numpy as np def calc_entropy(y): # 计算香农熵 values, counts np.unique(y, return_countsTrue) probs counts / len(y) return -np.sum(probs * np.log2(probs 1e-9)) def split_dataset(X, y, feature_idx, value): # 按某个特征的某个值切分数据 mask X[:, feature_idx] value return X[mask], y[mask], X[~mask], y[~mask] def choose_best_feature(X, y): # 遍历所有特征选信息增益最大的 base_entropy calc_entropy(y) best_gain 0 best_feature -1 n_features X.shape[1] for i in range(n_features): values np.unique(X[:, i]) new_entropy 0 for v in values: _, sub_y, _, _ split_dataset(X, y, i, v) prob len(sub_y) / len(y) new_entropy prob * calc_entropy(sub_y) gain base_entropy - new_entropy if gain best_gain: best_gain gain best_feature i return best_feature这段代码只实现了选最优特征的部分递归建树的逻辑需要你自己补上。练习包里通常会有一个完整的build_tree函数你可以对照它的实现看它是怎么处理递归终止条件的。常见的终止条件有三个当前节点所有样本属于同一类、特征用完了、信息增益小于某个阈值。参数方面calc_entropy里的1e-9是为了防止log2(0)的情况。choose_best_feature里遍历的是离散特征如果数据里有连续特征需要先做二分离散化这是练习包里容易翻车的地方。很多人在西瓜书的西瓜数据集上跑得好好的换到自己的连续特征数据上就报错原因就在这里。3.3 神经网络BP算法的前向与反向西瓜书第5章的神经网络核心是BP算法。练习包里通常会让你手写一个单隐层的网络在西瓜数据集或鸢尾花数据集上做分类。下面是一个最小实现的前向和反向部分。import numpy as np def forward(X, W1, b1, W2, b2): # 隐层用sigmoid输出层用softmax Z1 X.dot(W1) b1 A1 sigmoid(Z1) Z2 A1.dot(W2) b2 A2 softmax(Z2) return Z1, A1, Z2, A2 def backward(X, y, Z1, A1, Z2, A2, W2): m X.shape[0] # 输出层梯度 dZ2 A2 - y dW2 A1.T.dot(dZ2) / m db2 np.sum(dZ2, axis0, keepdimsTrue) / m # 隐层梯度 dA1 dZ2.dot(W2.T) dZ1 dA1 * A1 * (1 - A1) # sigmoid导数 dW1 X.T.dot(dZ1) / m db1 np.sum(dZ1, axis0, keepdimsTrue) / m return dW1, db1, dW2, db2这里的关键参数是隐层节点数。设太少欠拟合设太多过拟合。我一般从10个节点开始试在西瓜数据集上通常够用。学习率还是从0.01起步配合批量梯度下降。softmax函数需要你自己实现注意减去最大值防止溢出这个技巧和sigmoid的clip是一个道理。反向传播里最容易错的是维度。dZ2的形状应该和A2一致dW2的形状应该和W2一致。如果你跑的时候报维度不匹配先打印每一步的shape对照公式检查。另一个常见问题是忘了除以m导致梯度太大参数直接飞掉。4. 避坑与排查代码练习里最容易翻车的五个地方4.1 数据读取后没有检查维度和缺失值现象代码跑起来不报错但结果完全不对准确率接近随机猜。 原因csv文件里可能有表头行被当成了数据或者某一列有缺失值被读成了NaN导致矩阵运算结果全是NaN。 解决读数据后立刻打印data.shape和data.head()用data.isnull().sum()检查缺失值。如果有缺失用dropna()或fillna()处理掉再送进模型。4.2 矩阵乘法用错顺序现象报错shapes not aligned或者结果维度对但数值离谱。 原因NumPy的dot不检查你的数学意图只检查维度是否匹配。比如把X.dot(theta)写成了theta.dot(X)维度碰巧能过但结果全错。 解决在每次矩阵乘法前打印两个操作数的shape心里默念一遍公式里的维度关系。西瓜书里的公式X是m×ntheta是n×1结果应该是m×1。4.3 学习率设太大导致损失爆炸现象训练几个epoch后损失变成inf或nan参数全是nan。 原因学习率太大梯度更新一步跨太远直接跳出了损失函数的有效区域。 解决先把学习率降到0.001试确认损失在下降后再慢慢往上调。同时加上梯度裁剪把梯度的范数限制在一个范围内。4.4 决策树递归没有终止条件现象程序跑着跑着报RecursionError或者内存爆掉。 原因递归建树时没有正确判断终止条件比如特征用完了还在继续分裂或者某个子集为空还在递归。 解决在递归函数开头加三个判断样本全同一类就返回叶节点特征集为空就返回多数类子集为空就返回父节点多数类。这三个条件缺一不可。4.5 用sklearn的结果去“验证”手写代码时标准不一致现象手写实现准确率0.85sklearn跑出来0.92怀疑自己写错了。 原因sklearn默认做了特征标准化、正则化、不同的初始化策略和你手写的裸算法不在同一个起跑线上。 解决对比时先把sklearn的fit_intercept、C、penalty这些参数调成和你手写实现一致或者干脆只对比损失函数的下降趋势不对比最终准确率。5. 进阶技巧用交叉验证和可视化验证你的实现5.1 手写K折交叉验证的代码模板当你把单个算法跑通后下一步是验证它的稳定性。最简单的办法是手写一个K折交叉验证不依赖sklearn。下面这个模板可以直接套用到任何分类或回归算法上。import numpy as np def k_fold_cv(X, y, model_fn, k5): X, y: 完整数据集 model_fn: 一个函数接收(X_train, y_train)返回训练好的模型 k: 折数 indices np.arange(len(X)) np.random.shuffle(indices) fold_size len(X) // k scores [] for i in range(k): # 划分验证集索引 val_idx indices[i*fold_size:(i1)*fold_size] train_idx np.setdiff1d(indices, val_idx) X_train, y_train X[train_idx], y[train_idx] X_val, y_val X[val_idx], y[val_idx] model model_fn(X_train, y_train) score evaluate(model, X_val, y_val) scores.append(score) return np.mean(scores), np.std(scores)这个模板的关键在于model_fn的设计。它把训练过程封装成一个函数接收训练数据返回模型。这样你可以把线性回归、对数几率回归、决策树的训练函数都传进去一套交叉验证代码复用。evaluate函数需要你自己实现分类问题返回准确率回归问题返回均方误差。跑完K折后看均值和标准差。如果标准差很大说明模型对数据划分敏感可能需要更多数据或正则化。如果均值远低于训练集准确率说明过拟合了。5.2 用决策边界图直观检查分类器数字指标有时候会骗人画一张决策边界图能让你一眼看出模型是欠拟合还是过拟合。下面这段代码适用于二维特征的数据集。import matplotlib.pyplot as plt import numpy as np def plot_decision_boundary(model, X, y): # 生成网格点 x_min, x_max X[:, 0].min() - 0.5, X[:, 0].max() 0.5 y_min, y_max X[:, 1].min() - 0.5, X[:, 1].max() 0.5 xx, yy np.meshgrid(np.arange(x_min, x_max, 0.02), np.arange(y_min, y_max, 0.02)) # 对每个网格点预测 grid np.c_[xx.ravel(), yy.ravel()] Z model.predict(grid) Z Z.reshape(xx.shape) # 画等高线和散点 plt.contourf(xx, yy, Z, alpha0.3, cmapcoolwarm) plt.scatter(X[:, 0], X[:, 1], cy, edgecolorsk, cmapcoolwarm) plt.xlabel(feature 1) plt.ylabel(feature 2) plt.show()这段代码的核心是np.meshgrid生成网格然后对每个网格点调用模型的predict方法。如果你的模型没有predict方法需要自己写一个接收二维数组返回预测标签。画出来的图里背景颜色代表模型的决策区域散点代表真实标签。如果决策边界是一条直线说明模型是线性的如果边界很曲折说明模型复杂度高可能过拟合了。我一般会把这个图用在两个地方一是对比不同算法的决策边界比如对数几率回归和决策树的边界差异二是调参时看边界变化比如神经网络隐层节点数从5增加到50边界会从平滑变得非常扭曲。5.3 一个我常用的调试习惯每次手写实现完一个算法我会先在一个极小的数据集上跑比如只有10个样本、2个特征。这样我可以手动算出每一步的期望值然后打印代码里的中间结果逐个对比。如果小数据集上结果对了再换大数据集。这个习惯帮我省了很多时间因为在大数据集上调试维度错误和梯度错误就像在黑匣子里找问题。另外我会把每次实验的超参数和结果记在一个表格里哪怕只是简单的文本文件。这样当我想复现某个好结果时不会忘记当时的学习率、迭代次数和随机种子。机器学习里随机性太多没有记录就没有后悔药。希望这些代码练习的思路和踩坑记录能帮你把西瓜书里的公式真正跑起来。本文还有配套的精品资源点击获取