ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

西瓜书习题代码实战:决策树与SVM从数据到实现

西瓜书习题代码实战:决策树与SVM从数据到实现 简介这套资源是《机器学习》西瓜书习题的代码实现合集面向正在刷书、想把公式转化为可运行代码的学习者也适合准备算法面试或课程设计的人群。作者将书中各章习题拆解为清晰的Python脚本并搭配可视化图表、Markdown笔记与HTML页面帮助读者从数据预处理到模型训练、结果分析全链路理解算法。压缩包共224个文件以127张png图表、30个py源码、19份md笔记和18个html页面为主体另含9个csv数据集与若干配置、说明文件整体仅5.47MB轻量紧凑便于随时离线查阅目录按章节组织定位快捷。目前已有1472人学习下载适合需要动手验证、对照公式调试的学习场景。通过这套代码读者能拿到多个西瓜数据集及预处理脚本并看到决策树、线性模型、聚类等习题的完整实现与可视化输出结合笔记中记录的思路与排错要点可以大幅降低从零实现算法的时间成本尤其适合边读边练的初学阶段。1. 西瓜书习题代码这批数据和代码到底能帮你干什么很多人学《机器学习》周志华也就是俗称的西瓜书卡在同一个地方公式推了一遍课后题翻答案看懂了但一合上书让你自己手写一个决策树连数据长什么样都不知道。这份资源解决的就是这个问题——它把书上的西瓜数据集整理成了可以直接跑的 CSVwatermelon_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.csv7 个离散属性色泽、根蒂、敲声、纹理、脐部、触感没有连续属性。正好对应第 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.dataUCI 真实数据怎么进 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, encodinggbk) # 西瓜书数据通常是 GBK 编码 print(f) print(shape:, df.shape) print(columns:, list(df.columns)) print(dtypes:\n, df.dtypes) print(缺失值数量:\n, df.isnull().sum()) print(- * 50)这段代码的关键点是encodinggbk。西瓜书数据集在网上的流传版本绝大多数是 GBK 或 GB2312 编码用 pandas 默认的 UTF-8 读会直接报UnicodeDecodeError。如果你用的编辑器保存时转成了 UTF-8那就要反过来改成encodingutf-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.9975025463691153entropy函数接收标签列表内部用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, depth0): 递归构建 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, keygains.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, encodinggbk) labels df[好瓜] attrs [c for c in df.columns if c ! 好瓜] tree id3_fit(df, labels, attrs) print(tree)这段代码是递归结构的注意几个参数的设计data和labels每次递归都传入子集attrs是尚未使用的属性列表depth只用于打印缩进方便观察树结构。递归出口有两个——labels全同类直接返回类别属性用完返回多数类。max(gains, keygains.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所以left或right为空也不会报错但说明候选点在边界上实际中应该跳过——我在代码里没加这个判断你复现时如果看到增益为 0 的切分点大概率就是边界问题。3.3 和 sklearn 对答案树结构不一致时先查这两处手写树跑通后用 sklearn 验证一下整体准确率是靠谱的收尾方式from sklearn.tree import DecisionTreeClassifier from sklearn.preprocessing import LabelEncoder df pd.read_csv(watermelon_3.0.csv, encodinggbk) # 对离散属性做编码连续属性保持原值 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(criterionentropy, random_state42) clf.fit(X, y) print(训练集准确率:, clf.score(X, y)) print(特征重要性:, dict(zip(X.columns, clf.feature_importances_)))LabelEncoder会把青绿/乌黑/浅白这类中文枚举值映射成 0、1、2 整数sklearn 的决策树不接受字符串特征。连续属性列直接保留浮点值不用编码因为决策树对数值型特征天然做二分。criterionentropy是 ID3 的准则sklearn 默认是giniCART两个结果可能不同对答案时注意别混。人为制造一棵训练集准确率 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, headerNone) 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(axis0), 8)) print(标准化后标准差:, np.round(X_scaled.std(axis0), 6))StandardScaler对每列独立计算均值和标准差变换后每个特征均值为 0、方差为 1。fit_transform在训练集上同时完成拟合和转换后续测试集只能用同一个 scaler 的transform不能重新fit_transform——这是新手最常犯的错误之一会导致数据泄漏。严格来说应该在划分训练测试集之后再 fit 标准化器先全局标准化再划分同样存在轻微泄漏风险但在这个数据集上影响不大。print 输出这里就是给你确认数据结构用的。如果Monetary列的最大值明显异常比如超过 5000不用慌那是总献血量累计值不是脏数据。4.2 网格搜索 C 和 gammaRBF 核的两个关键旋钮SVM 用 RBF 核时主要调两个参数C正则化系数误分类惩罚程度和 gammaRBF 核的宽度倒数。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_size0.3, random_state42, stratifyy ) # 常见调参范围C 在对数尺度上试gamma 同理 param_grid { C: [0.1, 1, 10, 100], gamma: [0.001, 0.01, 0.1, 1], kernel: [rbf], } svm SVC(probabilityTrue, random_state42) grid GridSearchCV(svm, param_grid, cv5, scoringf1, n_jobs-1) grid.fit(X_train, y_train) print(最佳参数:, grid.best_params_) print(交叉验证 F1:, grid.best_score_)stratifyy保证训练集和测试集的正负样本比例与原始数据一致。原始数据正例约 23.8%如果随机划分小概率会出现测试集正例过少的问题。scoringf1而不是默认的accuracy是因为这个数据集类别不平衡准确率容易虚高——你用 accuracy 搜出来的参数大概率会把所有样本都预测成不献血然后准确率还有 76%看起来不错实际毫无意义。cv5指五折交叉验证n_jobs-1让所有 CPU 核心并行。组合总数是 4×416 组参数每组做 5 折总共 80 次拟合在普通笔记本上十几秒内能跑完。如果这个数据集跑得很慢检查一下是不是probabilityTrue导致的——这个参数会额外计算 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报错再试gb18030GBK 的超集容错更强。实在不行的用二进制模式打开看文件头再判断。5.2 现象模型能跑但输出全是同一个类别原因transfusion.data 正例占比不到 24%如果用了accuracy作为评估指标模型学到的策略就是全部预测成负例准确率依然有 76%。这是典型的类别不平衡从评估指标选择上就错了。解决改用 F1、召回率、ROC-AUC 等对不平衡更敏感的指标在 SVM 里可以设置class_weightbalanced让少数类获得更高的惩罚权重。这是西瓜书第 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很容易出现维度不匹配或索引错乱。解决对特征编码使用OrdinalEncodersklearn 1.0 可用它支持同时编码多列并保持 DataFrame 形状或者像我 3.3 节那样手动循环每一列并单独保存编码器。另外LabelEncoder编码后的数值大小是有含义的0、1、2但决策树对这类编码后的离散值只是做等号比较数值大小不影响分裂。6. 验证代码对没对三个不用等老师批改的自检方法写完习题代码最大的心理负担是输出看起来合理但不确定对不对。我通常用三个方法自检都不用额外装库。第一个手工验证根节点。拿 watermelon_3.0.csv 跑出来的树第一层分裂属性如果是纹理或密度基本没问题如果不是先别继续调参回头检查数据读入是否正确、标签列是否被当作特征喂进去了。这个验证方法成本最低但能拦住一大半低级错误。第二个对比手写代码和 sklearn 在同一数据上的分类准确率。两者在离散属性上应该一致前提是手写代码实现了同样的分裂准则连续属性上可能有微小差异因为候选切分点和并列处理的策略不同。差异超过 5% 就说明手写代码有 bug优先检查递归出口条件有没有覆盖空子集的情况。第三个用 predict_proba 抽查置信度。像我前面写的取测试集前几条样本看看模型对它们的预测概率分布。如果某些样本的概率几乎平均比如 0.51 对 0.49说明特征对这些样本没有区分能力要么是数据量太少要么是特征工程没做好要么是模型容量不够。这比只看准确率虚标要可靠得多。我自己的习惯是每次跑完课后题都强制把这三个验证走一遍——手工算一层熵、对比 sklearn 准确率、抽查三条样本的预测概率。这套流程看起来慢但省掉了大量代码跑通但答案错了的返工。从那以后我交作业和期末复习都没再因为数据或代码细节翻过车希望这套验证思路对你有用。本文还有配套的精品资源点击获取
返回列表