简介:这份资源是周志华《机器学习》西瓜书课后习题的代码实现合集,面向正在系统学习机器学习理论、希望用代码验证公式推导的本科生、研究生及自学者。内容围绕书中各章节习题展开,覆盖数据预处理、模型训练与评估等环节,适合边读书边动手复现的读者对照练习。压缩包共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_lambda=0.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_lambda=0.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_size=0.3, random_state=42, stratify=y ) print(f"训练集样本数: {len(X_train)}, 测试集样本数: {len(X_test)}")这里的关键参数是stratify=y,它保证划分后训练集和测试集的正负样本比例与原始数据一致。西瓜数据集只有 17 个样本,如果不加这个参数,很可能测试集里全是负样本,评估结果就失去意义了。random_state固定随机种子,保证每次运行划分结果一致,方便调试。
注意:西瓜数据集样本量极小,任何模型在这个数据上都很容易过拟合。习题里用它主要是为了验证算法逻辑,不要指望在这个数据上刷出高准确率。
3. 决策树与神经网络习题的代码实现与参数调优
3.1 信息增益与基尼指数的代码实现
西瓜书第四章决策树的核心是划分准则。信息增益对应 ID3,增益率对应 C4.5,基尼指数对应 CART。习题常要求手算这几个指标,但手算容易出错,用代码验证更可靠。以信息增益为例:
import numpy as np def entropy(y): """计算信息熵""" _, counts = np.unique(y, return_counts=True) 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_size=4, lr=0.1, epochs=5000): """ 三层神经网络:输入层 -> 隐藏层 -> 输出层 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, axis=0, keepdims=True) / n_samples dA1 = dZ2 @ W2.T dZ1 = dA1 * A1 * (1 - A1) dW1 = X.T @ dZ1 / n_samples db1 = np.sum(dZ1, axis=0, keepdims=True) / 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_size=4, lr=0.5, epochs=5000) 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, lr=0.001, epochs=1000, C=1.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(kernel='rbf'),然后对比手写线性版本和核版本在西瓜数据集上的决策边界差异。
4. 集成学习与模型评估的习题落地
4.1 AdaBoost 的代码实现与弱分类器选择
西瓜书第八章集成学习里,AdaBoost 是习题高频考点。核心思想是串行训练弱分类器,每轮调整样本权重。下面用决策树桩作为弱分类器:
def adaboost(X, y, n_estimators=10): """ 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_depth=3, random_state=42) scores = cross_val_score(clf, X, y, cv=loo) print(f"留一法准确率: {scores.mean():.4f}, 标准差: {scores.std():.4f}") # 对比 5 折交叉验证 from sklearn.model_selection import StratifiedKFold skf = StratifiedKFold(n_splits=5, shuffle=True, random_state=42) scores_5fold = cross_val_score(clf, X, y, cv=skf) print(f"5折交叉验证准确率: {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(f"F1: {f1_score(y_test, y_pred):.4f}")注意zero_division参数:当某个类别没有被预测出来时,precision 会报除零警告。sklearn 新版本默认zero_division=0,老版本需要手动设置。西瓜数据集测试集只有 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], atol=0.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_size=4, lr=0.5, epochs=5000 -> 测试准确率 0.67 # hidden_size=8, lr=0.5, epochs=5000 -> 测试准确率 0.67 # hidden_size=4, lr=0.1, epochs=5000 -> 测试准确率 0.83 # 结论:西瓜数据集上隐藏层不宜过大,学习率 0.1 比 0.5 更稳这些记录看起来琐碎,但当你过两周回头改代码时,能立刻知道上次调到哪一步。我踩过最大的坑就是改了一版参数,效果变差,想回退却发现没记之前的值,只能从头再试。
最后说一个心态上的教训:西瓜书习题代码化的目的不是刷准确率,而是建立“公式到代码”的映射直觉。同一个算法,手写一遍再调包跑一遍,你对它的理解会完全不同。希望帮到你。
本文还有配套的精品资源,点击获取