西瓜书习题代码实战:决策树与SVM从数据到实现
2026/9/23 10:32:52 网站建设 项目流程

简介:这套资源是《机器学习》(西瓜书)习题的代码实现合集,面向正在刷书、想把公式转化为可运行代码的学习者,也适合准备算法面试或课程设计的人群。作者将书中各章习题拆解为清晰的Python脚本,并搭配可视化图表、Markdown笔记与HTML页面,帮助读者从数据预处理到模型训练、结果分析全链路理解算法。压缩包共224个文件,以127张png图表、30个py源码、19份md笔记和18个html页面为主体,另含9个csv数据集与若干配置、说明文件,整体仅5.47MB,轻量紧凑,便于随时离线查阅;目录按章节组织,定位快捷。目前已有1472人学习下载,适合需要动手验证、对照公式调试的学习场景。通过这套代码,读者能拿到多个西瓜数据集及预处理脚本,并看到决策树、线性模型、聚类等习题的完整实现与可视化输出;结合笔记中记录的思路与排错要点,可以大幅降低从零实现算法的时间成本,尤其适合边读边练的初学阶段。

1. 西瓜书习题代码:这批数据和代码到底能帮你干什么

很多人学《机器学习》(周志华,也就是俗称的西瓜书)卡在同一个地方:公式推了一遍,课后题翻答案看懂了,但一合上书让你自己手写一个决策树,连数据长什么样都不知道。这份资源解决的就是这个问题——它把书上的西瓜数据集整理成了可以直接跑的 CSV(watermelon_2.0、3.0、3.0a、4.2、datatypes,外加 UCI 的 transfusion.data),并配上了对应习题的 Python 实现。换句话说,你不用再满网找"课后习题答案 PDF",而是直接拿到数据和可复现代码,从信息增益手算到 SVM 调参,边跑边对答案。适合正在啃西瓜书的学生、准备机器学习期末的人,以及想快速复习经典算法实现细节的从业者。这份资源值不值得下,关键看它对不对得上你要做的题。

2. 先认清数据集:六个 CSV 文件分别对应哪道习题

2.1 三套西瓜数据:2.0、3.0、3.0a 对应书上的哪些内容

西瓜书最常用的数据集就是那 17 条好瓜样本,但这个资源里给了三个版本,很多人第一次打开就懵:watermelon_2.0、watermelon_3.0、watermelon_3.0a 有什么区别?

先说结论:它们对应书中不同章节和不同用途。

  • watermelon_2.0.csv:7 个离散属性(色泽、根蒂、敲声、纹理、脐部、触感),没有连续属性。正好对应第 4 章决策树里表 4.1 的原始数据,属性值全是中文离散值,比如"青绿""蜷缩""浊响"这些。这一版最原始,适合手工推 ID3、C4.5 的信息增益和信息增益率。
  • watermelon_3.0.csv:在第 2.0 的基础上加了两个连续属性——密度和含糖率,一共 8 个属性加 1 列标签。这是书中表 4.3 用的版本,也是决策树处理连续属性(二分法找划分点)和朴素贝叶斯习题的默认数据。
  • watermelon_3.0a.csv:很多人在网上下载的版本命名混乱,这个 3.0a 实际上是 3.0 的变体,只是某些样本属性值做了微调,列结构相同。如果你跑代码发现和书上的树对不上,先检查自己用的是不是 3.0a——这个版本在不少公开习题解答中也被引用过,具体以你的习题编号为准。

实操中我的建议:做第 4 章习题,直接用 watermelon_3.0.csv,因为连续属性处理是必考点;如果需要单纯演示信息熵计算,用 watermelon_2.0.csv 更清晰,属性全离散,手算不容易乱。

2.2 watermelon_4.2 和 datatypes:缺失值处理与数据类型陷阱

watermelon_4.2.csv 是第 4 章习题 4.2 的数据——处理缺失值的决策树。这个文件的特殊之处在于部分属性的值被替换成了空值(缺失值),用来练习 C4.5 的缺失值处理策略:带缺失值的样本在计算信息增益时要按权重打折,划分时要把缺失值样本分到所有分支并按权重计算。

datatypes.csv 是最容易被人忽略的文件。它里面混合了字符串、数值、日期格式的列,看起来不像能直接跑模型。我拆这个资源时发现它的真正用途是让你验证数据读取和类型转换——很多初学者直接把 CSV 读进来丢给 sklearn 报错,就是没意识到"看起来是数字的列其实被 pandas 读成了 object"。这个文件适合作为你写数据预处理脚本的沙盒数据。

2.3 transfusion.data:UCI 真实数据怎么进 SVM 课后题

transfusion.data 是台湾新竹输血中心公开的献血数据,来自 UCI 机器学习库,西瓜书第 6 章支持向量机的课后习题(6.2 题)明确指定用这个数据做 SVM 分类。它没有表头,共 748 条样本、5 列:

列名含义说明
Recency距上次献血月数数值型
Frequency总献血次数数值型
Monetary献血总量(毫升)数值型
Time距第一次献血月数数值型
Donated是否再次献血二分类标签(1/0)

注意第四列 Time 和第一列 Recency 的单位都是"月",但含义完全不同:Recency 是最近一次,Time 是最早一次。这个数据集正负样本比例大约 1:3,正例 178 条、反例 570 条,类别不平衡明显,直接跑准确率会虚高。这也是习题想让你注意的点。

拿到这个资源后第一件事,不是打开代码跑,而是先写一个脚本把每个 CSV 都读一遍、看形状、看缺失值、看列类型。常见做法是:

import pandas as pd files = [ "watermelon_2.0.csv", "watermelon_3.0.csv", "watermelon_3.0a.csv", "watermelon_4.2.csv", "datatypes.csv", ] for f in files: df = pd.read_csv(f, encoding="gbk") # 西瓜书数据通常是 GBK 编码 print(f) print("shape:", df.shape) print("columns:", list(df.columns)) print("dtypes:\n", df.dtypes) print("缺失值数量:\n", df.isnull().sum()) print("-" * 50)

这段代码的关键点是encoding="gbk"。西瓜书数据集在网上的流传版本绝大多数是 GBK 或 GB2312 编码,用 pandas 默认的 UTF-8 读会直接报UnicodeDecodeError。如果你用的编辑器保存时转成了 UTF-8,那就要反过来改成encoding="utf-8"。判断方法简单粗暴:报解码错误就换另一个编码试。

df.isnull().sum()这行是检查缺失值的,watermelon_4.2.csv 读进来这里不会全为 0,否则说明你下载的版本被处理过、缺失值被填掉了,跟习题要求不一致,得换源。

3. 决策树复现:手工算信息增益,再和 sklearn 对答案

3.1 手工走一遍信息熵计算:第一层分割到底怎么选

决策树第 4 章的重点是信息增益(ID3)和信息增益率(C4.5)。我建议你不管用哪套代码,都先用 watermelon_2.0.csv 的 17 条样本手工算一次根节点的划分。

计算逻辑是:先算总的熵 E(D),然后对每个属性 a 计算加权平均熵,两者相减就是信息增益 Gain(D, a)。举个具体数字,总样本 17 条,好瓜 8 个、坏瓜 9 个,所以:

E(D) = -(8/17 * log2(8/17) + 9/17 * log2(9/17)) ≈ 0.998

然后用 Python 验证:

import math def entropy(labels): from collections import Counter counter = Counter(labels) total = len(labels) return -sum((count / total) * math.log2(count / total) for count in counter.values()) # 好瓜 8 个,坏瓜 9 个 print(entropy(["是"] * 8 + ["否"] * 9)) # 0.9975025463691153

entropy函数接收标签列表,内部用Counter统计各类别数量,再按信息熵公式累加。注意math.log2是以 2 为底的对数,对应信息量的单位"比特"。

接下来按属性划分数据集。以"色泽"为例,青绿 6 个(好瓜 3 个、坏瓜 3 个),乌黑 6 个(好瓜 4 个、坏瓜 2 个),浅白 5 个(好瓜 1 个、坏瓜 4 个)。三个子集的熵分别算出来,加权后约 0.889,所以色泽的信息增益约 0.109。同理可以算出纹理、触感的信息增益,你会发现纹理最高,根节点应该选纹理。这一步强烈建议手算一遍,因为后面调试代码时你会需要"第一层分裂属性"这个预期值来判断程序对不对。

3.2 从零写一个最简 ID3 决策树

网上成熟的 sklearn 实现当然能用,但习题解答的"代码部分实现"讲究的是能看出算法过程。我自己拆这个资源时,习惯把树节点打印逻辑写进去,每一步分裂了什么属性、按什么值分、样本数量多少,一清二楚。下面这个精简版 ID3 足够跑通西瓜书数据:

import pandas as pd import numpy as np from collections import Counter import math def entropy(labels): counter = Counter(labels) total = len(labels) if total == 0: return 0 return -sum((cnt / total) * math.log2(cnt / total) for cnt in counter.values()) def info_gain(data, labels, attr): """计算属性 attr 的信息增益""" total_entropy = entropy(labels) values = data[attr].unique() weighted_entropy = 0.0 for v in values: subset_labels = labels[data[attr] == v] weighted_entropy += (len(subset_labels) / len(labels)) * entropy(subset_labels) return total_entropy - weighted_entropy def id3_fit(data, labels, attrs, depth=0): """递归构建 ID3 树,返回嵌套字典""" if len(set(labels)) == 1: return labels.iloc[0] if not attrs: return Counter(labels).most_common(1)[0][0] # 选信息增益最大的属性 gains = {a: info_gain(data, labels, a) for a in attrs} best_attr = max(gains, key=gains.get) print(" " * depth + f"第 {depth} 层分裂属性: {best_attr}, 增益: {gains[best_attr]:.4f}") tree = {best_attr: {}} for v in data[best_attr].unique(): subset_idx = data[best_attr] == v tree[best_attr][v] = id3_fit( data[subset_idx], labels[subset_idx], [a for a in attrs if a != best_attr], depth + 1, ) return tree df = pd.read_csv("watermelon_2.0.csv", encoding="gbk") labels = df["好瓜"] attrs = [c for c in df.columns if c != "好瓜"] tree = id3_fit(df, labels, attrs) print(tree)

这段代码是递归结构的,注意几个参数的设计:datalabels每次递归都传入子集,attrs是尚未使用的属性列表,depth只用于打印缩进方便观察树结构。递归出口有两个——labels全同类直接返回类别,属性用完返回多数类。

max(gains, key=gains.get)是取字典中值最大的键,即信息增益最大的属性。这个写法比sorted再取第一个更高效。运行后你会在控制台看到第一层分裂属性是"纹理",和书上一致,那代码基本没问题。

这个实现是纯粹的 ID3,只能处理离散属性。如果你换成 watermelon_3.0.csv,里面密度和含糖率是连续值,需要先做二分法离散化(对属性值排序后依次取相邻均值作为候选划分点,选择使信息增益最大的那个切分点)。这是第 4 章另一个常考步骤,代码可以在上述框架上加一个"连续属性候选点搜索"函数,常见做法是:

def best_split_for_continuous(data, labels, attr): """连续属性二分法:返回最佳切分点和对应增益""" sorted_vals = sorted(data[attr].unique()) best_gain = -float("inf") best_point = None for i in range(len(sorted_vals) - 1): point = (sorted_vals[i] + sorted_vals[i + 1]) / 2 left = labels[data[attr] <= point] right = labels[data[attr] > point] # 加权熵 gain = entropy(labels) - ( (len(left) / len(labels)) * entropy(left) + (len(right) / len(labels)) * entropy(right) ) if gain > best_gain: best_gain = gain best_point = point return best_point, best_gain

候选切分点只取相邻值的中点,这是西瓜书上的标准做法。注意entropy函数在子集为空时会返回 0,所以leftright为空也不会报错,但说明候选点在边界上,实际中应该跳过——我在代码里没加这个判断,你复现时如果看到增益为 0 的切分点,大概率就是边界问题。

3.3 和 sklearn 对答案:树结构不一致时先查这两处

手写树跑通后,用 sklearn 验证一下整体准确率是靠谱的收尾方式:

from sklearn.tree import DecisionTreeClassifier from sklearn.preprocessing import LabelEncoder df = pd.read_csv("watermelon_3.0.csv", encoding="gbk") # 对离散属性做编码,连续属性保持原值 le_dict = {} X = df.drop(columns=["好瓜"]).copy() for col in X.columns: if X[col].dtype == object: le = LabelEncoder() X[col] = le.fit_transform(X[col]) le_dict[col] = le y = (df["好瓜"] == "是").astype(int) clf = DecisionTreeClassifier(criterion="entropy", random_state=42) clf.fit(X, y) print("训练集准确率:", clf.score(X, y)) print("特征重要性:", dict(zip(X.columns, clf.feature_importances_)))

LabelEncoder会把"青绿/乌黑/浅白"这类中文枚举值映射成 0、1、2 整数,sklearn 的决策树不接受字符串特征。连续属性列直接保留浮点值不用编码,因为决策树对数值型特征天然做二分。criterion="entropy"是 ID3 的准则,sklearn 默认是"gini"(CART),两个结果可能不同,对答案时注意别混。

人为制造一棵训练集准确率 100% 的树没有任何实际意义,这棵树的真正用途是检查分裂属性顺序和你的手写树是否一致。sklearn 用tree_.feature可以查看每个节点的分裂特征索引,如果第一层分裂出的特征和手算的纹理不一致,优先怀疑数据列顺序问题——很多下载版本把"好瓜"放在了中间而不是最后一列,导致索引对不上。

4. SVM 与 transfusion.data:从 UCI 数据到课本习题全流程

4.1 数据归一化:RBF 核的必修课

transfusion.data 四列特征的量纲差异很大:Monetary(献血总量)通常是几百到几千,而 Recency 只有 0 到 70 多。如果直接丢进 SVM,数值大的特征会在核函数计算中完全主导距离度量,模型的决策边界会被带偏。这在西瓜书第 6 章也多次强调过——特征缩放是 SVM 使用的前置条件。

常见做法是用StandardScaler做 Z-score 标准化:

import pandas as pd import numpy as np from sklearn.model_selection import train_test_split from sklearn.preprocessing import StandardScaler from sklearn.svm import SVC df = pd.read_csv("transfusion.data", header=None) df.columns = ["Recency", "Frequency", "Monetary", "Time", "Donated"] X = df.iloc[:, :-1].values y = df.iloc[:, -1].values print("原始值范围:") for i, col in enumerate(df.columns[:-1]): print(f" {col}: min={X[:, i].min()}, max={X[:, i].max()}") scaler = StandardScaler() X_scaled = scaler.fit_transform(X) print("标准化后均值:", np.round(X_scaled.mean(axis=0), 8)) print("标准化后标准差:", np.round(X_scaled.std(axis=0), 6))

StandardScaler对每列独立计算均值和标准差,变换后每个特征均值为 0、方差为 1。fit_transform在训练集上同时完成拟合和转换,后续测试集只能用同一个 scaler 的transform,不能重新fit_transform——这是新手最常犯的错误之一,会导致数据泄漏。严格来说应该在划分训练测试集之后再 fit 标准化器,先全局标准化再划分同样存在轻微泄漏风险,但在这个数据集上影响不大。

print 输出这里就是给你确认数据结构用的。如果Monetary列的最大值明显异常(比如超过 5000),不用慌,那是总献血量累计值,不是脏数据。

4.2 网格搜索 C 和 gamma:RBF 核的两个关键旋钮

SVM 用 RBF 核时主要调两个参数:C(正则化系数,误分类惩罚程度)和 gamma(RBF 核的宽度倒数)。C 越大越容易过拟合训练集,gamma 越大决策边界越复杂。西瓜书课后题的答案通常不会告诉你具体参数,所以你得自己搜。

这里我直接给出一个可复现的网格搜索流程:

from sklearn.model_selection import GridSearchCV from sklearn.svm import SVC from sklearn.metrics import classification_report, confusion_matrix # 数据集划分,stratify 保证正负比例一致 X_train, X_test, y_train, y_test = train_test_split( X_scaled, y, test_size=0.3, random_state=42, stratify=y ) # 常见调参范围:C 在对数尺度上试,gamma 同理 param_grid = { "C": [0.1, 1, 10, 100], "gamma": [0.001, 0.01, 0.1, 1], "kernel": ["rbf"], } svm = SVC(probability=True, random_state=42) grid = GridSearchCV(svm, param_grid, cv=5, scoring="f1", n_jobs=-1) grid.fit(X_train, y_train) print("最佳参数:", grid.best_params_) print("交叉验证 F1:", grid.best_score_)

stratify=y保证训练集和测试集的正负样本比例与原始数据一致。原始数据正例约 23.8%,如果随机划分,小概率会出现测试集正例过少的问题。scoring="f1"而不是默认的accuracy,是因为这个数据集类别不平衡,准确率容易虚高——你用 accuracy 搜出来的参数大概率会把所有样本都预测成"不献血",然后准确率还有 76%,看起来不错实际毫无意义。

cv=5指五折交叉验证,n_jobs=-1让所有 CPU 核心并行。组合总数是 4×4=16 组参数,每组做 5 折,总共 80 次拟合,在普通笔记本上十几秒内能跑完。如果这个数据集跑得很慢,检查一下是不是probability=True导致的——这个参数会额外计算 Platt 缩放的概率估计,代价不小,不需要predict_proba时可以去掉。

4.3 看报告而不是看准确率:SVM 输出怎么解读

训练完成后,用测试集评估:

y_pred = grid.predict(X_test) print(classification_report(y_test, y_pred, target_names=["不献血", "献血"])) print("混淆矩阵:") print(confusion_matrix(y_test, y_pred)) # 抽查几条边界样本的置信度 proba = grid.predict_proba(X_test[:5]) print("前 5 条测试样本预测概率:") print(np.round(proba, 3))

classification_report会输出每类别的精确率、召回率和 F1。这个数据集上你会发现"献血"类的召回率通常偏低,因为正例本来就少,SVM 的决策边界偏向多数类。这恰恰是课后题想让你看到的——类别不平衡不是靠调参能完全解决的,过采样(SMOTE)、欠采样、调整 class_weight 才是后续手段。

predict_proba输出的是每个样本属于各类别的估计概率。注意概率本身不是真实概率,而是 Platt 缩放的结果,只能用于比较置信度高低,不能直接解释为"有 80% 概率献血"。抽查边界样本的预测概率,能帮你发现模型对哪些样本没把握——如果很多样本的概率都集中在 0.5 附近,说明特征区分度不够,或者 C、gamma 选得不合适。

5. 避坑指南:编码、类型、类别不平衡是最常翻车的三个点

5.1 现象:pd.read_csv直接报UnicodeDecodeError

原因:西瓜书数据集的流传版本以 GBK 编码为主,而 pandas 默认按 UTF-8 解码。macOS 和 Linux 系统更容易踩这个坑,Windows 上某些文本编辑器会自动转码保存,反而绕过去了。解决:读文件时显式指定编码,先试gbk,报错再试gb18030(GBK 的超集,容错更强)。实在不行的,用二进制模式打开看文件头再判断。

5.2 现象:模型能跑,但输出全是同一个类别

原因:transfusion.data 正例占比不到 24%,如果用了accuracy作为评估指标,模型学到的策略就是"全部预测成负例",准确率依然有 76%。这是典型的类别不平衡,从评估指标选择上就错了。解决:改用 F1、召回率、ROC-AUC 等对不平衡更敏感的指标;在 SVM 里可以设置class_weight="balanced",让少数类获得更高的惩罚权重。这是西瓜书第 6 章习题里最容易忽略的隐含考点。

5.3 现象:watermelon_4.2.csv 读进来没有缺失值,习题做不了

原因:网上流传的很多版本提前把缺失值用"?"或者空字符串填回成了正常值,甚至直接删掉了带缺失值的行。你拿到的可能不是原始版本。解决:检查文件字节数和行数,原始 watermelon_4.2 应该有 17 行且部分单元格为空;如果行数不足 17,说明被清洗过了,换一个下载源。真正做缺失值处理时,注意 C4.5 是按权重计算带缺失值样本的贡献,不是简单 dropna 删行——删行等于回避考点。

5.4 现象:sklearn 版本不同,决策树跑出来的树不一样

原因:DecisionTreeClassifier在选择最优分裂特征时,如果多个特征的信息增益完全相等(比如 2.0 数据集的某些分支),sklearn 会按特征索引顺序取第一个,不同版本对并列情况的处理逻辑有差异。这在西瓜书数据上确实会出现,因为样本量只有 17 条,分支很容易走到纯度 100%。解决:对比树结构时不要追求逐节点完全一致,只要第一层分裂属性和整体准确率一致就够了。如果需要可复现,设置random_state,并固定 sklearn 版本(在 requirements.txt 里写死版本号)。

5.5 现象:LabelEncoder报错

原因:LabelEncoder的设计初衷是编码标签而不是特征,对二维 DataFrame 的某一列直接fit_transform会返回一维数组,但如果你在循环里对多列做编码并试图赋回原 DataFrame,很容易出现维度不匹配或索引错乱。解决:对特征编码使用OrdinalEncoder(sklearn 1.0+ 可用),它支持同时编码多列并保持 DataFrame 形状;或者像我 3.3 节那样,手动循环每一列并单独保存编码器。另外LabelEncoder编码后的数值大小是有含义的(0、1、2),但决策树对这类编码后的离散值只是做等号比较,数值大小不影响分裂。

6. 验证代码对没对:三个不用等老师批改的自检方法

写完习题代码,最大的心理负担是"输出看起来合理,但不确定对不对"。我通常用三个方法自检,都不用额外装库。

第一个,手工验证根节点。拿 watermelon_3.0.csv 跑出来的树,第一层分裂属性如果是纹理或密度,基本没问题;如果不是,先别继续调参,回头检查数据读入是否正确、标签列是否被当作特征喂进去了。这个验证方法成本最低,但能拦住一大半低级错误。

第二个,对比手写代码和 sklearn 在同一数据上的分类准确率。两者在离散属性上应该一致(前提是手写代码实现了同样的分裂准则),连续属性上可能有微小差异,因为候选切分点和并列处理的策略不同。差异超过 5% 就说明手写代码有 bug,优先检查递归出口条件有没有覆盖空子集的情况。

第三个,用 predict_proba 抽查置信度。像我前面写的,取测试集前几条样本,看看模型对它们的预测概率分布。如果某些样本的概率几乎平均(比如 0.51 对 0.49),说明特征对这些样本没有区分能力,要么是数据量太少,要么是特征工程没做好,要么是模型容量不够。这比只看准确率虚标要可靠得多。

我自己的习惯是:每次跑完课后题,都强制把这三个验证走一遍——手工算一层熵、对比 sklearn 准确率、抽查三条样本的预测概率。这套流程看起来慢,但省掉了大量"代码跑通但答案错了"的返工。从那以后我交作业和期末复习都没再因为数据或代码细节翻过车,希望这套验证思路对你有用。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询