资讯动态

西瓜书习题代码化实战:从公式到手写机器学习算法

发布时间:2026/10/1 11:33:29 来源:尧图企业网站定制
简介这份资源是周志华《机器学习》西瓜书课后习题的代码实现合集面向正在系统学习机器学习理论、希望用代码验证公式推导的本科生、研究生及自学者。内容围绕书中各章节习题展开覆盖数据预处理、模型训练与评估等环节适合边读书边动手复现的读者对照练习。压缩包共224个文件约5.47MB其中30个py脚本承载核心算法实现19个md文档记录推导与说明18个html与127个png用于结果展示和图表呈现另有csv、data等数据文件支撑实验运行整体结构按章节与习题组织便于按需检索。目前已有1472人学习下载说明该实现具备一定参考价值。读者可借助这些代码理解习题背后的算法逻辑对照书中公式调试参数、观察输出并参考文档中的思路整理减少从理论到编码之间的摸索成本适合作为西瓜书配套的动手实践材料。1. 从《机器学习》西瓜书习题到可运行代码为什么值得动手很多人学周志华的《机器学习》书翻了三章公式抄了两页合上书却连一个能跑通的模型都没有。西瓜书的价值在于把算法讲透但它的习题大多是“推导证明”型真正落到代码实现时中间隔着一道不小的鸿沟。我见过太多人卡在“知道公式长什么样但不知道代码怎么写”这一步最后只能去搜别人的答案抄一遍交差学完还是不会。这篇笔记要解决的就是这道鸿沟。它面向的是正在啃西瓜书、准备机器学习期末复习、或者想从零把经典算法手写一遍的读者。核心思路是把西瓜书里最典型的几类习题——线性模型、决策树、神经网络、支持向量机、集成学习——拆成可运行的代码每一步都给出参数含义和踩坑记录。不是把书里的公式翻译成代码就完事而是让你理解为什么这样写、参数怎么调、结果不对时该看哪里。如果你正在准备西电、山大这类学校的机器学习期末或者刚入门机器学习想找个抓手这套路径会比单纯刷题更扎实。下面从环境准备开始一步步把习题变成能跑、能改、能验证的代码。2. 环境准备与西瓜书习题代码化的最小闭环2.1 为什么选 Python NumPy 而不是直接上框架西瓜书习题的核心是理解算法内部机制不是调包。用 scikit-learn 一行fit确实能出结果但你看不到梯度怎么更新、信息增益怎么算、核函数怎么映射。所以我的建议是核心算法用 NumPy 手写数据加载和评估用 pandas 和 sklearn 辅助。这样既不会陷入纯手写矩阵运算的泥潭又能保证每个关键步骤都在你掌控之中。环境配置上Python 3.9 以上即可核心依赖就四个NumPy 做矩阵运算pandas 做数据读取matplotlib 做可视化scikit-learn 只用来生成数据集和做最终对比。安装命令如下pip install numpy pandas matplotlib scikit-learn这里有个常见误区有人一上来就装 PyTorch 或 TensorFlow结果西瓜书第三章的线性回归还没写完先被 GPU 驱动和 CUDA 版本搞崩溃了。血泪经验是前六章习题用 NumPy 完全够用等到神经网络那一章再考虑要不要上框架。2.2 用 NumPy 复现线性回归从公式到可运行代码西瓜书第三章线性回归的习题通常要求推导最小二乘解并验证正则化效果。直接看代码实现import numpy as np def linear_regression(X, y, reg_lambda0.0): 最小二乘线性回归支持 L2 正则化 X: (n_samples, n_features) 特征矩阵 y: (n_samples, ) 标签向量 reg_lambda: L2 正则化系数0 表示无正则化 返回: (n_features, ) 权重向量 n_features X.shape[1] # 构造正则化项注意不惩罚偏置项假设 X 已拼接全 1 列 reg_matrix reg_lambda * np.eye(n_features) reg_matrix[0, 0] 0 # 偏置项不参与正则化 # 闭式解: w (X^T X lambda I)^(-1) X^T y w np.linalg.inv(X.T X reg_matrix) X.T y return w # 生成模拟数据验证 np.random.seed(42) X_raw np.random.randn(100, 2) X np.hstack([np.ones((100, 1)), X_raw]) # 拼接偏置列 true_w np.array([1.5, 2.0, -1.0]) y X true_w np.random.randn(100) * 0.1 w_est linear_regression(X, y, reg_lambda0.0) print(估计权重:, w_est) print(真实权重:, true_w)这段代码的逻辑说明reg_matrix是正则化矩阵关键点在于偏置项对应的对角线元素要置零否则会对偏置也施加惩罚导致模型欠拟合。参数reg_lambda控制正则化强度设为 0 时就是普通最小二乘。运行后估计权重应该接近真实权重如果差距大先检查 X 是否拼接了偏置列再检查reg_lambda是否过大。参数选择上reg_lambda一般从 0.001 到 10 之间调可以用交叉验证选。注意np.linalg.inv在矩阵接近奇异时会报错或数值不稳定实际项目中更推荐np.linalg.solve或np.linalg.pinv。2.3 数据集的加载与划分别在第一步就埋雷西瓜书习题常用西瓜数据集 3.0α但书里只给了表格没有现成 CSV。我一般会手动整理成 DataFrame或者用 sklearn 的make_classification生成类似结构的二分类数据。加载和划分的代码import pandas as pd from sklearn.model_selection import train_test_split # 西瓜数据集 3.0α 手动录入 data { 密度: [0.697, 0.774, 0.634, 0.608, 0.556, 0.403, 0.481, 0.437, 0.666, 0.243, 0.245, 0.343, 0.639, 0.657, 0.360, 0.593, 0.719], 含糖率: [0.460, 0.376, 0.264, 0.318, 0.215, 0.237, 0.149, 0.211, 0.091, 0.267, 0.057, 0.099, 0.161, 0.198, 0.370, 0.042, 0.103], 好瓜: [1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0] } df pd.DataFrame(data) X df[[密度, 含糖率]].values y df[好瓜].values # 划分训练集和测试集stratify 保证类别比例一致 X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.3, random_state42, stratifyy ) print(f训练集样本数: {len(X_train)}, 测试集样本数: {len(X_test)})这里的关键参数是stratifyy它保证划分后训练集和测试集的正负样本比例与原始数据一致。西瓜数据集只有 17 个样本如果不加这个参数很可能测试集里全是负样本评估结果就失去意义了。random_state固定随机种子保证每次运行划分结果一致方便调试。注意西瓜数据集样本量极小任何模型在这个数据上都很容易过拟合。习题里用它主要是为了验证算法逻辑不要指望在这个数据上刷出高准确率。3. 决策树与神经网络习题的代码实现与参数调优3.1 信息增益与基尼指数的代码实现西瓜书第四章决策树的核心是划分准则。信息增益对应 ID3增益率对应 C4.5基尼指数对应 CART。习题常要求手算这几个指标但手算容易出错用代码验证更可靠。以信息增益为例import numpy as np def entropy(y): 计算信息熵 _, counts np.unique(y, return_countsTrue) probs counts / len(y) return -np.sum(probs * np.log2(probs 1e-12)) def information_gain(X_column, y, threshold): 计算某个特征在给定阈值下的信息增益 X_column: 特征列 y: 标签 threshold: 划分阈值 parent_entropy entropy(y) left_mask X_column threshold right_mask ~left_mask if np.sum(left_mask) 0 or np.sum(right_mask) 0: return 0.0 n len(y) left_entropy entropy(y[left_mask]) right_entropy entropy(y[right_mask]) child_entropy (np.sum(left_mask) / n) * left_entropy (np.sum(right_mask) / n) * right_entropy return parent_entropy - child_entropy # 在西瓜数据集上找最佳划分点 best_gain 0 best_feature None best_threshold None for feature_idx in range(X_train.shape[1]): thresholds np.unique(X_train[:, feature_idx]) for t in thresholds: gain information_gain(X_train[:, feature_idx], y_train, t) if gain best_gain: best_gain gain best_feature feature_idx best_threshold t print(f最佳特征索引: {best_feature}, 阈值: {best_threshold:.3f}, 信息增益: {best_gain:.4f})逻辑说明entropy函数用np.unique统计各类别频次加1e-12防止 log(0)。information_gain先算父节点熵再按阈值划分后算子节点加权熵差值就是增益。参数threshold遍历所有唯一值这是离散化连续特征的标准做法。注意当某个划分导致子集为空时直接返回 0否则加权熵会出 NaN。这个边界条件在习题里经常被忽略但实际写代码时必须处理。3.2 用 NumPy 实现三层神经网络并调参西瓜书第五章神经网络习题通常要求推导反向传播并实现一个多层感知机。下面是一个简化的三层网络用于西瓜数据集二分类def sigmoid(z): return 1 / (1 np.exp(-np.clip(z, -500, 500))) def neural_network(X, y, hidden_size4, lr0.1, epochs5000): 三层神经网络输入层 - 隐藏层 - 输出层 hidden_size: 隐藏层神经元数量 lr: 学习率 epochs: 迭代次数 n_samples, n_features X.shape # 初始化权重用小随机数打破对称性 np.random.seed(42) W1 np.random.randn(n_features, hidden_size) * 0.5 b1 np.zeros((1, hidden_size)) W2 np.random.randn(hidden_size, 1) * 0.5 b2 np.zeros((1, 1)) losses [] for i in range(epochs): # 前向传播 Z1 X W1 b1 A1 sigmoid(Z1) Z2 A1 W2 b2 A2 sigmoid(Z2) # 交叉熵损失 loss -np.mean(y.reshape(-1, 1) * np.log(A2 1e-12) (1 - y.reshape(-1, 1)) * np.log(1 - A2 1e-12)) losses.append(loss) # 反向传播 dZ2 A2 - y.reshape(-1, 1) dW2 A1.T dZ2 / n_samples db2 np.sum(dZ2, axis0, keepdimsTrue) / n_samples dA1 dZ2 W2.T dZ1 dA1 * A1 * (1 - A1) dW1 X.T dZ1 / n_samples db1 np.sum(dZ1, axis0, keepdimsTrue) / n_samples # 更新权重 W1 - lr * dW1 b1 - lr * db1 W2 - lr * dW2 b2 - lr * db2 return W1, b1, W2, b2, losses W1, b1, W2, b2, losses neural_network(X_train, y_train, hidden_size4, lr0.5, epochs5000) print(f最终损失: {losses[-1]:.4f})参数说明hidden_size控制隐藏层容量西瓜数据集样本少设 4 到 8 就够了设太大直接过拟合。lr学习率是关键0.1 到 0.5 之间比较稳太大损失震荡太小收敛慢。epochs设 5000 是因为数据量小需要多轮迭代。np.clip防止 sigmoid 溢出这是数值稳定性的常规操作。训练完成后可以用测试集算准确率。如果准确率在 0.6 到 0.8 之间波动别慌西瓜数据集就 17 个样本这个结果正常。重点看损失是否单调下降如果损失不降先检查学习率是不是太大再检查标签形状是否对齐。3.3 支持向量机与核函数的简化实现西瓜书第六章支持向量机是难点完整实现 SMO 算法代码量不小。习题里常要求验证核函数效果我一般用简化版用 sklearn 的 SVC 做对比同时手写一个线性 SVM 的合页损失版本。def linear_svm(X, y, lr0.001, epochs1000, C1.0): 线性 SVM 合页损失 梯度下降 y 标签需转换为 -1 和 1 C: 正则化系数越大越不容忍误分类 y_signed np.where(y 1, 1, -1) n_samples, n_features X.shape w np.zeros(n_features) b 0 for i in range(epochs): for j in range(n_samples): margin y_signed[j] * (X[j] w b) if margin 1: w w - lr * (2 * (1 / C) * w - X[j] * y_signed[j]) b b lr * y_signed[j] else: w w - lr * 2 * (1 / C) * w return w, b这里C是惩罚系数C 越大对误分类容忍度越低容易过拟合C 越小正则化越强容易欠拟合。lr设 0.001 是因为合页损失对学习率敏感太大直接发散。这个简化版没有实现核技巧如果要验证高斯核建议直接用 sklearn 的SVC(kernelrbf)然后对比手写线性版本和核版本在西瓜数据集上的决策边界差异。4. 集成学习与模型评估的习题落地4.1 AdaBoost 的代码实现与弱分类器选择西瓜书第八章集成学习里AdaBoost 是习题高频考点。核心思想是串行训练弱分类器每轮调整样本权重。下面用决策树桩作为弱分类器def adaboost(X, y, n_estimators10): AdaBoost 二分类实现 n_estimators: 弱分类器数量 n_samples len(y) weights np.ones(n_samples) / n_samples models [] alphas [] y_signed np.where(y 1, 1, -1) for t in range(n_estimators): # 训练一个决策树桩选最佳特征和阈值 best_err 1.0 best_feat, best_thresh, best_polarity None, None, 1 for feat in range(X.shape[1]): thresholds np.unique(X[:, feat]) for thresh in thresholds: for polarity in [1, -1]: pred np.ones(n_samples) pred[polarity * X[:, feat] polarity * thresh] -1 err np.sum(weights[pred ! y_signed]) if err best_err: best_err err best_feat, best_thresh, best_polarity feat, thresh, polarity # 计算分类器权重 alpha 0.5 * np.log((1 - best_err) / (best_err 1e-12)) # 更新样本权重 pred np.ones(n_samples) pred[best_polarity * X[:, best_feat] best_polarity * best_thresh] -1 weights * np.exp(-alpha * y_signed * pred) weights / np.sum(weights) models.append((best_feat, best_thresh, best_polarity)) alphas.append(alpha) return models, alphas参数说明n_estimators控制弱分类器数量西瓜数据集上设 5 到 10 就够太多会过拟合。best_err是加权错误率注意权重在每轮更新后要归一化。alpha是弱分类器的投票权重错误率越低权重越大。如果某轮best_err大于 0.5说明弱分类器比随机猜还差实际实现中应该提前终止或重新初始化权重。4.2 交叉验证与留一法在西瓜数据集上的对比西瓜书第二章模型评估习题常要求比较留一法和交叉验证。西瓜数据集只有 17 个样本留一法LOO是天然选择。代码实现from sklearn.model_selection import LeaveOneOut, cross_val_score from sklearn.tree import DecisionTreeClassifier loo LeaveOneOut() clf DecisionTreeClassifier(max_depth3, random_state42) scores cross_val_score(clf, X, y, cvloo) print(f留一法准确率: {scores.mean():.4f}, 标准差: {scores.std():.4f}) # 对比 5 折交叉验证 from sklearn.model_selection import StratifiedKFold skf StratifiedKFold(n_splits5, shuffleTrue, random_state42) scores_5fold cross_val_score(clf, X, y, cvskf) print(f5折交叉验证准确率: {scores_5fold.mean():.4f}, 标准差: {scores_5fold.std():.4f})留一法每次只留一个样本测试训练集几乎等于全体数据偏差小但方差大且计算量大。5 折交叉验证在样本量极小时每折测试集只有 3 到 4 个样本评估结果波动很大。我的经验是样本量小于 50 时优先用留一法样本量大于 100 时用 5 折或 10 折。西瓜数据集上留一法准确率通常比 5 折高一点但不要因此认为留一法更好这只是样本量太小导致的统计波动。4.3 模型评估指标准确率之外的查准率与查全率西瓜书第二章还讲了查准率precision、查全率recall和 F1。在西瓜数据集上好瓜是正例如果模型把所有瓜都预测为好瓜准确率有 8/17 约 0.47但查全率是 1.0查准率只有 0.47。代码from sklearn.metrics import precision_score, recall_score, f1_score, confusion_matrix # 假设 y_pred 是模型预测结果 y_pred clf.fit(X_train, y_train).predict(X_test) print(混淆矩阵:\n, confusion_matrix(y_test, y_pred)) print(f查准率: {precision_score(y_test, y_pred):.4f}) print(f查全率: {recall_score(y_test, y_pred):.4f}) print(fF1: {f1_score(y_test, y_pred):.4f})注意zero_division参数当某个类别没有被预测出来时precision 会报除零警告。sklearn 新版本默认zero_division0老版本需要手动设置。西瓜数据集测试集只有 5 到 6 个样本混淆矩阵可能非常稀疏解读时要谨慎。5. 避坑与排查西瓜书习题代码化最容易翻车的五个地方5.1 矩阵维度不匹配导致广播错误现象ValueError: operands could not be broadcast together。原因NumPy 的广播规则在矩阵乘法中不适用X w要求 X 的列数等于 w 的行数。解决在每步矩阵运算前打印shape养成习惯。特别是拼接偏置列时np.hstack和np.c_行为不同前者要求维度一致后者会自动广播。5.2 学习率过大导致损失震荡或 NaN现象损失函数值在几轮后变成 NaN 或剧烈跳动。原因学习率太大梯度更新步长超过最优解范围甚至导致指数溢出。解决先把学习率调小一个数量级观察损失是否单调下降。如果仍然 NaN检查是否有 log(0) 或除零操作加1e-12平滑项。5.3 标签形状不对导致交叉熵计算错误现象损失值异常大或梯度方向相反。原因y的形状是(n_samples,)而预测值是(n_samples, 1)广播后变成(n_samples, n_samples)的矩阵。解决统一用y.reshape(-1, 1)或y.ravel()在关键运算前确认形状一致。5.4 正则化项惩罚了偏置导致欠拟合现象模型在训练集上准确率就很低增加迭代次数也没用。原因L2 正则化矩阵的对角线全设了lambda包括偏置项对应的位置。解决把正则化矩阵第一行第一列置零或者把偏置项单独拿出来不参与正则化。5.5 留一法评估结果波动大被误判为模型问题现象换一个随机种子留一法准确率从 0.8 掉到 0.5。原因西瓜数据集只有 17 个样本留一法每次测试集只有一个样本预测对错直接导致准确率跳变 1/17。解决不要用单次留一法结果下结论重复多次或用分层交叉验证同时报告标准差。6. 把习题代码变成可复用的验证习惯最后一章说一个我反复用的技巧给每个算法写一个“最小验证用例”。不是跑完西瓜数据集就完事而是构造一个已知答案的极简数据确认算法输出符合预期。比如线性回归我一般会生成y 2x 1的带噪声数据看估计权重是否接近[1, 2]。决策树就用一个特征明显可分的数据看树是否只用一个特征就完成划分。这个习惯的好处是当你后面调参调到怀疑人生时可以快速排除“算法实现本身有 bug”这个选项。具体做法是建一个test_cases.py每个算法配一个assert语句# 线性回归验证 X_test_case np.array([[1, 1], [1, 2], [1, 3], [1, 4]]) y_test_case np.array([3, 5, 7, 9]) # y 2x 1 w linear_regression(X_test_case, y_test_case) assert np.allclose(w, [1, 2], atol0.01), f线性回归验证失败: {w} # 信息熵验证 assert np.isclose(entropy(np.array([1, 1, 1, 1])), 0.0), 纯节点熵应为 0 assert np.isclose(entropy(np.array([1, 1, 0, 0])), 1.0), 等比例二分类熵应为 1参数atol控制数值容差浮点运算不要用比较。这些验证用例跑一遍不到一秒但能帮你省下大量排查时间。另一个习惯是记录每次实验的超参数和结果。不用搞复杂的实验管理工具就在代码旁边写注释# 实验记录 2024-01-15 # hidden_size4, lr0.5, epochs5000 - 测试准确率 0.67 # hidden_size8, lr0.5, epochs5000 - 测试准确率 0.67 # hidden_size4, lr0.1, epochs5000 - 测试准确率 0.83 # 结论西瓜数据集上隐藏层不宜过大学习率 0.1 比 0.5 更稳这些记录看起来琐碎但当你过两周回头改代码时能立刻知道上次调到哪一步。我踩过最大的坑就是改了一版参数效果变差想回退却发现没记之前的值只能从头再试。最后说一个心态上的教训西瓜书习题代码化的目的不是刷准确率而是建立“公式到代码”的映射直觉。同一个算法手写一遍再调包跑一遍你对它的理解会完全不同。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑