简介这份资源是周志华《机器学习》西瓜书课后习题的代码实现合集面向正在系统学习机器学习理论、希望用代码验证公式推导的本科生、研究生及自学者。内容围绕西瓜数据集展开覆盖线性模型、决策树、神经网络、支持向量机、贝叶斯分类、聚类与降维等章节的习题求解思路适合边读书边动手复现的读者对照使用。压缩包共224个文件约5.47MB其中30个py脚本承载各章习题的核心实现19个md与18个html用于记录推导过程和结果说明127个png与8个jpg保存实验图表另有9个csv及data、names等数据文件提供西瓜数据集与输血数据集便于直接运行验证。目前已有1472人学习下载可作为课程作业参考、期末复习或面试前巩固基础的实战材料帮助读者把抽象公式落到可运行代码上理解算法细节与调参过程。1. 从一份 CSV 清单说起西瓜书习题代码到底能跑出什么如果你正在啃周志华《机器学习》这本被大家叫作“西瓜书”的教材大概率经历过这种尴尬公式推导看懂了课后习题也知道要证什么但真让你把算法写成能跑通的代码对着那一堆西瓜数据集就是下不去手。这份“机器学习西瓜书习题代码部分实现”的资源核心价值就在这儿——它把书里第 2 到第 4 章涉及决策树、线性模型、神经网络等章节的习题配上了可直接运行的 Python 实现并且把用到的数据集单独抽了出来。你拿到的不是一份空泛的笔记而是一组能对着watermelon_3.csv、watermelon_3a.csv、transfusion.data这些文件跑出结果的脚本。适合谁适合正在做机器学习期末复习、想拿西瓜书当入门教材但卡在“理论到代码”这一步的人也适合已经工作、想快速回顾 ID3、CART、BP 神经网络底层实现细节的从业者。它解决的不是“教你什么是机器学习”而是“让你亲手把书上的公式变成能打印出准确率的那几行代码”。2. 数据集与代码结构拆解先搞清楚每个 CSV 对应哪道题2.1 西瓜数据集 2.0、3.0 与 3.0a 的区别拿到资源第一件事不是急着跑代码而是先分清那几个长得像双胞胎的 CSV 文件。watermelon_2.csv对应的是西瓜书里最经典的西瓜数据集 2.0包含编号、色泽、根蒂、敲声、纹理、脐部、触感、密度、含糖率、好瓜这些字段一共 17 个样本。watermelon_3.csv和watermelon_3a.csv则是第 3 章线性模型用的数据集 3.0a它把离散属性做了数值化处理密度和含糖率保留为连续值好瓜用 0/1 表示。很多人在这一步翻车是因为直接拿watermelon_3.csv去跑决策树结果发现特征全是数字信息增益算出来跟书上对不上。常见做法是决策树章节用watermelon_2.csv线性模型和 LDA 用watermelon_3a.csvBP 神经网络用watermelon_3.csv或watermelon_3a.csv都行但要注意标签列的位置。文件名对应章节样本数关键字段典型用途watermelon_2.csv第 4 章17色泽、根蒂、敲声、纹理、脐部、触感、密度、含糖率、好瓜ID3、C4.5、CART 决策树watermelon_3.csv第 3 章17密度、含糖率、好瓜线性回归、对数几率回归watermelon_3a.csv第 3 章17密度、含糖率、好瓜数值化LDA、线性分类watermelon_4_2.csv第 4 章17与 2.0 类似用于剪枝预剪枝、后剪枝对比transfusion.data第 3 章扩展748R、F、M、T、是否献血线性模型实战扩展2.2 代码文件组织与依赖安装资源里的代码通常按章节分目录比如chapter3/下放线性回归和对数几率回归chapter4/下放决策树chapter5/下放神经网络。每个脚本开头会写明依赖一般就是numpy、pandas、matplotlib三件套。我一般会先建一个干净的虚拟环境避免跟系统里的包版本打架。python -m venv venv_watermelon source venv_watermelon/bin/activate # Windows 用 venv_watermelon\Scripts\activate pip install numpy pandas matplotlib scikit-learn这里scikit-learn不是必须的但有些实现会拿它做交叉验证或对比实验。装完之后先跑一个最简单的脚本验证环境比如chapter3/logistic_regression.py如果它能打印出迭代次数和最终准确率说明环境没问题。注意不要一上来就装最新版numpy有些老代码用了np.float这类已弃用的别名在 1.24 以上版本会直接报错。遇到这种情况要么降级到numpy1.23要么手动把np.float改成float。2.3 数据加载的通用写法与标签列处理不管哪个章节读 CSV 的套路基本一致但标签列的处理方式决定了你后面会不会白跑。下面这段代码是我从资源里提炼出来的通用加载函数你可以直接抄到自己的脚本里。import pandas as pd import numpy as np def load_watermelon(path, label_col好瓜, drop_colsNone): 加载西瓜数据集返回特征矩阵 X 和标签向量 y :param path: CSV 文件路径 :param label_col: 标签列名默认 好瓜 :param drop_cols: 需要丢弃的列如 [编号] :return: X (numpy array), y (numpy array) df pd.read_csv(path) if drop_cols: df df.drop(columnsdrop_cols) # 把好瓜映射为 1/0兼容 是/否 和 1/0 两种写法 if df[label_col].dtype object: df[label_col] df[label_col].map({是: 1, 否: 0}) y df[label_col].values X df.drop(columns[label_col]).values return X, y # 示例加载西瓜数据集 3.0a X, y load_watermelon(watermelon_3a.csv, drop_cols[编号]) print(X.shape, y.shape)逻辑说明drop_cols用来去掉编号这种对建模无意义的列标签映射那一步是为了兼容不同版本 CSV 里“好瓜”列可能是中文“是/否”也可能是数字 1/0。参数label_col默认是“好瓜”如果你的 CSV 里列名是英文label改一下就行。跑完打印(17, 2) (17,)说明加载正确。如果打印出来维度不对先检查 CSV 是不是有多余的索引列。3. 从 ID3 到 CART决策树习题的代码落地与参数调优3.1 信息增益、增益率与基尼指数的代码实现西瓜书第 4 章的核心习题就是手算信息增益、增益率和基尼指数然后写决策树。资源里的chapter4/decision_tree.py通常会把这三个指标拆成独立函数。以信息增益为例关键就两步先算数据集的信息熵再算某个特征划分后的条件熵两者相减。import numpy as np from collections import Counter def entropy(y): 计算信息熵 counter Counter(y) probs [c / len(y) for c in counter.values()] return -sum(p * np.log2(p) for p in probs if p 0) def info_gain(X, y, feature_idx): 计算某个特征的信息增益 base_ent entropy(y) values np.unique(X[:, feature_idx]) cond_ent 0.0 for v in values: mask X[:, feature_idx] v cond_ent (mask.sum() / len(y)) * entropy(y[mask]) return base_ent - cond_ent逻辑说明entropy里用Counter统计标签分布p 0是为了避免log2(0)报错。info_gain遍历特征的所有取值按比例加权求条件熵。参数feature_idx是特征在X中的列索引。跑的时候注意如果某个特征的取值全是连续值直接这样算会过拟合需要先做离散化常见做法是按中位数二分。增益率就是在信息增益基础上除以特征本身的固有值基尼指数则是把熵换成基尼不纯度代码结构几乎一样改一下公式就行。3.2 递归建树与预剪枝、后剪枝的代码差异建树函数一般用递归写终止条件有三个当前节点样本全属于同一类、特征用完了、或者达到某个剪枝阈值。资源里watermelon_4_2.csv就是专门用来演示剪枝的。预剪枝在划分前判断验证集精度是否提升不提升就不分后剪枝是先建完整棵树再自底向上检查。两者代码差异主要在递归的入口和出口。def build_tree(X, y, features, depth0, max_depth5): 递归建树max_depth 控制预剪枝 # 终止条件标签纯了、没特征了、或达到深度上限 if len(np.unique(y)) 1 or len(features) 0 or depth max_depth: return Counter(y).most_common(1)[0][0] best_feat, best_gain None, -1 for i in features: gain info_gain(X, y, i) if gain best_gain: best_gain, best_feat gain, i if best_gain 0: return Counter(y).most_common(1)[0][0] tree {best_feat: {}} remaining [f for f in features if f ! best_feat] for v in np.unique(X[:, best_feat]): mask X[:, best_feat] v tree[best_feat][v] build_tree(X[mask], y[mask], remaining, depth 1, max_depth) return tree逻辑说明max_depth就是最简单的预剪枝参数设成 3 或 4 能明显抑制过拟合。best_gain 0表示划分后信息增益没有提升直接返回多数类。后剪枝需要额外写一个prune函数用验证集递归判断是否把子树替换成叶节点。参数上预剪枝快但容易欠拟合后剪枝慢但泛化通常更好。我一般会先用max_depth5跑一遍看准确率再决定要不要上后剪枝。3.3 连续值处理与缺失值分支西瓜数据集 3.0a 里密度和含糖率是连续值直接按每个取值划分会导致每个样本一个分支树深得没法看。常见做法是二分法对某个连续特征按从小到大排序取相邻值中点作为划分点算信息增益选最大的那个中点。缺失值处理更麻烦西瓜书里给了加权划分的思路代码实现时要给每个样本加权重划分时按权重比例分配到子节点。资源里如果没实现缺失值你可以自己补一个weight数组初始全为 1遇到缺失特征时把权重按子节点样本比例拆分。这一步是很多期末考题的拉分点建议对着书上的公式自己推一遍再写代码。4. 线性模型与 BP 神经网络从对数几率回归到误差逆传播4.1 对数几率回归的梯度下降与牛顿法第 3 章的对数几率回归本质就是在算极大似然估计代码上可以用梯度下降也可以用牛顿法。资源里chapter3/logistic_regression.py一般会给出梯度下降版本因为好理解。关键公式是sigmoid(wxb)损失函数是对数似然取负。def sigmoid(z): return 1.0 / (1.0 np.exp(-z)) def train_logistic(X, y, lr0.1, epochs1000): 梯度下降训练对数几率回归 m, n X.shape w np.zeros(n) b 0.0 for epoch in range(epochs): z X.dot(w) b y_pred sigmoid(z) # 梯度计算 dw X.T.dot(y_pred - y) / m db np.mean(y_pred - y) w - lr * dw b - lr * db if epoch % 200 0: loss -np.mean(y * np.log(y_pred 1e-9) (1 - y) * np.log(1 - y_pred 1e-9)) print(fepoch {epoch}, loss {loss:.4f}) return w, b逻辑说明lr是学习率设太大容易震荡设太小收敛慢西瓜数据集样本少lr0.1通常几百轮就收敛。1e-9是防止log(0)的后悔药。牛顿法需要算 Hessian 矩阵代码复杂但迭代次数少适合特征维度低的情况。参数上如果你发现 loss 变成 nan先检查学习率是不是超过 1再检查数据有没有做归一化。密度和含糖率量纲差很多不归一化的话梯度下降会走得很歪。4.2 BP 神经网络的误差逆传播推导与代码映射第 5 章的 BP 神经网络是很多人的血泪经验重灾区。书上的公式一堆下标代码里其实就是链式法则。资源里chapter5/bp_network.py通常实现一个单隐层网络输入层 2 个节点密度、含糖率隐层 4 到 8 个节点输出层 1 个节点。核心就四步前向算输出、算输出层误差、把误差逆传播到隐层、更新权重和阈值。def bp_train(X, y, hidden_size4, lr0.5, epochs2000): 单隐层 BP 神经网络 m, n X.shape # 初始化权重和阈值 np.random.seed(42) V np.random.randn(n, hidden_size) * 0.1 # 输入到隐层 gamma np.random.randn(hidden_size) * 0.1 # 隐层阈值 W np.random.randn(hidden_size, 1) * 0.1 # 隐层到输出 theta np.random.randn(1) * 0.1 # 输出层阈值 for epoch in range(epochs): # 前向 alpha X.dot(V) gamma b_h sigmoid(alpha) beta b_h.dot(W) theta y_pred sigmoid(beta).ravel() # 反向 g y_pred * (1 - y_pred) * (y_pred - y) # 输出层梯度 e b_h * (1 - b_h) * g.dot(W.T) # 隐层梯度 W - lr * b_h.T.dot(g.reshape(-1, 1)) / m theta - lr * np.mean(g) V - lr * X.T.dot(e) / m gamma - lr * np.mean(e, axis0) return V, gamma, W, theta逻辑说明hidden_size是隐层节点数设太小欠拟合设太大过拟合西瓜数据集上 4 到 8 比较合适。lr设 0.5 是因为样本少需要快一点收敛。np.random.seed(42)是为了结果可复现不然每次跑出来的准确率都在跳。注意输出层梯度g的符号取决于你损失函数怎么定义如果发现 loss 不降反升把(y_pred - y)改成(y - y_pred)试试。这个符号问题坑过无数人包括我。4.3 标准 BP 与累积 BP 的差异及 early stopping标准 BP 每处理一个样本就更新一次权重累积 BP 是遍历完整个数据集才更新一次。资源里如果只给了一种你可以自己改。标准 BP 收敛快但容易震荡累积 BP 更稳但每轮计算量大。西瓜数据集只有 17 个样本两者差异不明显但换成transfusion.data的 748 个样本就能看出区别。early stopping 的做法是留出一部分验证集每次 epoch 结束算验证集误差连续几轮不下降就停。参数上我一般设patience50也就是连续 50 轮验证误差没改善就停同时保存验证误差最小的那组权重。5. 避坑与排查跑西瓜书代码时最容易翻车的五个地方5.1 现象准确率只有 50% 左右跟瞎猜一样原因标签列没映射对或者特征矩阵里混进了编号列。watermelon_3a.csv里“好瓜”如果是“是/否”很多实现直接拿字符串去算sigmoid输出全是 0.5。解决用第 2 章那个load_watermelon函数先打印y[:5]确认是 0/1 还是字符串是字符串就映射。5.2 现象决策树递归报RecursionError原因连续值特征没做二分每个样本一个取值树无限深。或者终止条件里忘了判断特征是否用完。解决对连续特征先排序取中点限制max_depth并且在递归前检查len(features) 0。5.3 现象BP 网络 loss 变成 nan原因学习率太大或者sigmoid输出接近 0 或 1 时log溢出。解决把学习率降到 0.1 以下损失函数里加1e-9并且对输入做归一化。密度和含糖率归一化到 [0,1] 之后收敛会稳很多。5.4 现象np.float或np.int报 AttributeError原因numpy1.24 以上版本移除了这些别名。解决全局搜索替换成float和int或者pip install numpy1.23.5。这个坑在新环境里几乎必踩建议装完环境先跑一遍import numpy as np; np.float看看报不报错。5.5 现象后剪枝之后准确率反而降了原因验证集太小或者剪枝时把有用的分支剪掉了。西瓜数据集只有 17 个样本留一法验证波动很大。解决换用交叉验证或者把watermelon_4_2.csv和watermelon_2.csv合并起来增加样本量。后剪枝不是万能的样本少的时候预剪枝反而更稳。6. 进阶技巧用 transfusion.data 验证线性模型泛化能力西瓜数据集只有 17 个样本跑出来的准确率波动大很难判断模型是真的学到了东西还是碰巧。资源里附带的transfusion.data是一个 748 样本的献血数据集字段是 R、F、M、T 和是否献血正好拿来验证第 3 章线性模型的泛化能力。我一般会先用watermelon_3a.csv调通对数几率回归再把同样的代码套到transfusion.data上看准确率能不能稳定在 75% 以上。下面这段代码演示了如何做 5 折交叉验证比单次划分靠谱得多。from sklearn.model_selection import StratifiedKFold from sklearn.preprocessing import StandardScaler def cross_validate(X, y, k5): 5 折交叉验证返回平均准确率 skf StratifiedKFold(n_splitsk, shuffleTrue, random_state42) accs [] for train_idx, val_idx in skf.split(X, y): X_train, X_val X[train_idx], X[val_idx] y_train, y_val y[train_idx], y[val_idx] # 归一化注意 scaler 只能在训练集上 fit scaler StandardScaler() X_train scaler.fit_transform(X_train) X_val scaler.transform(X_val) w, b train_logistic(X_train, y_train, lr0.1, epochs500) y_pred (sigmoid(X_val.dot(w) b) 0.5).astype(int) acc np.mean(y_pred y_val) accs.append(acc) return np.mean(accs), np.std(accs) # 加载 transfusion.data注意这个文件没有表头 df pd.read_csv(transfusion.data, headerNone, names[R, F, M, T, label]) X df[[R, F, M, T]].values y df[label].values mean_acc, std_acc cross_validate(X, y) print(f5 折交叉验证准确率: {mean_acc:.4f} ± {std_acc:.4f})逻辑说明StratifiedKFold保证每折里正负样本比例一致避免某一折全是一类。StandardScaler的fit只能在训练集上做验证集用transform这是很多人交叉验证时容易搞错的地方——如果在全量数据上fit验证集的信息就泄漏了准确率会虚高。transfusion.data没有表头所以read_csv要加headerNone和names。跑完如果标准差超过 0.05说明模型不稳定可以试试增加正则项或者换用牛顿法。从那以后我每次跑线性模型都强制走一遍交叉验证再也不信单次划分的准确率了。希望帮到你。本文还有配套的精品资源点击获取