简介这份资源是北京邮电大学自动化专业机器学习课程实验的配套代码面向正在学习决策树算法、需要完成课程作业或希望动手实践分类模型的高年级本科生与自学者。实验以Python实现决策树为核心覆盖数据加载与预处理、基于信息增益或基尼不纯度的特征选择、模型构建与训练、预测评估以及可视化与剪枝等完整流程帮助读者把课堂理论落到可运行的代码上。资源包为7z压缩格式内含1个py文件整体约1KB文件虽小但结构集中便于直接阅读与调试。目前已有1055人学习下载说明该实验在课程学习者中具有一定参考价值。通过这份代码读者可以对照理解决策树从特征划分到剪枝优化的关键步骤熟悉pandas与scikit-learn在分类任务中的配合使用并借助评估指标与可视化手段检验模型效果适合作为课程实验的起步模板或算法复习的实操素材。1. 北邮自动化机器学习实验里的决策树一份能直接跑通的 Python 代码包如果你正在北邮自动化大三下学期修机器学习这门课大概率会在某个周末对着 yhh 老师布置的实验要求发愁——决策树实验要求用 Python 实现还得跑出可解释的结果。这个 Ex2_DecTree 代码包就是为这个场景准备的一个 7z 压缩包里面是一个 Ex2_DecTree.py 脚本覆盖了从数据加载、特征选择、建树、剪枝到可视化的完整链路。它解决的不是“决策树是什么”这种教科书问题而是“实验报告怎么交、代码怎么跑、参数怎么调”的落地问题。适合两类人一是正在赶这个实验的自动化专业学生二是想拿一个结构清晰的决策树实现来对照自己代码的机器学习入门者。下面我按实际拆包和跑通的顺序把这份资源讲透。2. 拆开 Ex2_DecTree.py从数据加载到建树的完整链路2.1 先看代码结构别急着跑拿到 Ex2_DecTree.py 之后第一件事不是双击运行而是用编辑器打开通读一遍。常见的课程实验脚本结构大致是导入区、数据加载函数、预处理函数、特征选择函数、递归建树函数、预测函数、评估与可视化入口。这个脚本大概率也是这个骨架因为决策树实验的教学目标就是让你手写一遍 ID3 或 C4.5 的核心逻辑而不是直接调 sklearn 的 DecisionTreeClassifier 一行了事。我一般会先看导入区用了哪些库。如果看到 pandas、numpy、math 这几个说明数据处理和熵计算是手写的如果还看到 sklearn那可能是用来做对比实验或者交叉验证的。graphviz 或 matplotlib 出现在导入区说明有可视化环节。先把这些确认清楚后面调参和排错才有方向。提示如果脚本里同时有手写建树和 sklearn 调用注意区分哪部分是实验要求手写的哪部分是对照组。实验报告里通常要求你解释手写部分的每一个公式。2.2 数据加载与预处理pandas 读进来之后先做三件事课程实验的数据集一般是 CSV 格式特征列加一个标签列。加载逻辑通常长这样import pandas as pd import numpy as np def load_data(filepath): # 读取CSV注意编码课程数据集常见gbk或utf-8 df pd.read_csv(filepath, encodingutf-8) # 打印前5行确认列名和数据类型 print(df.head()) print(df.dtypes) # 检查缺失值 print(df.isnull().sum()) return df这段代码的逻辑很直白读文件、看结构、查缺失。参数上最容易被忽略的是 encoding课程数据集如果是老师从 Excel 导出的很可能是 gbk 编码用 utf-8 读会直接报 UnicodeDecodeError。另一个坑是列名里带空格后面按列名取数据时会 KeyError所以 head() 打印出来之后要肉眼确认一遍列名。预处理阶段常见做法是把分类变量做标签编码或独热编码把连续变量做分箱或者保留原值让树自己找分裂点。如果脚本里用了 pd.get_dummies注意它会把一列拆成多列特征维度变了之后后面特征选择的循环要能适配。如果脚本里是手写的标签编码映射检查映射字典是否覆盖了所有类别值漏一个就会在预测时 KeyError。2.3 特征选择信息增益、增益率还是基尼指数决策树的核心就在这一步。ID3 用信息增益C4.5 用信息增益率CART 用基尼指数。课程实验通常要求你至少实现一种有的老师会要求两种做对比。信息增益的计算逻辑是先算数据集本身的熵再算按某个特征划分后的条件熵两者相减。def calc_entropy(labels): # labels是标签列的值列表 from collections import Counter counts Counter(labels) total len(labels) entropy 0.0 for count in counts.values(): p count / total entropy - p * math.log2(p) return entropy def calc_info_gain(df, feature, label_col): # 划分前的熵 base_entropy calc_entropy(df[label_col]) # 按feature的每个取值分组算加权条件熵 values df[feature].unique() cond_entropy 0.0 for v in values: subset df[df[feature] v] weight len(subset) / len(df) cond_entropy weight * calc_entropy(subset[label_col]) return base_entropy - cond_entropy参数说明calc_entropy 里用 math.log2 而不是 log是因为信息论里熵的单位是比特。如果数据集某个特征取值特别多比如 ID 列信息增益会偏高这就是 ID3 偏向多值特征的缺陷C4.5 用增益率来修正。如果你的实验要求实现 C4.5增益率的公式是信息增益除以该特征本身的熵分母可能为零需要加一个极小值或者跳过该特征。常见翻车点Counter 统计标签时如果标签列有 NaN会被当成一个独立类别参与熵计算结果看起来正常但实际是错的。所以预处理阶段必须把缺失值处理干净要么删行要么填充。2.4 递归建树与剪枝递归终止条件写错就无限循环建树函数是递归的每个节点选当前最优特征按取值分裂子集然后对每个子集递归调用。终止条件一般有三个子集标签全相同、特征用完了、子集为空。少写一个就可能死循环或者报错。def build_tree(df, features, label_col): # 终止条件1标签全相同 if len(df[label_col].unique()) 1: return df[label_col].iloc[0] # 终止条件2特征用完了返回多数类 if len(features) 0: return df[label_col].mode()[0] # 选最优特征 gains {f: calc_info_gain(df, f, label_col) for f in features} best_feature max(gains, keygains.get) tree {best_feature: {}} remaining_features [f for f in features if f ! best_feature] # 按最优特征的每个取值分裂 for v in df[best_feature].unique(): subset df[df[best_feature] v] if subset.empty: tree[best_feature][v] df[label_col].mode()[0] else: tree[best_feature][v] build_tree(subset, remaining_features, label_col) return tree剪枝部分课程实验常见的是预剪枝限制最大深度或最小样本数和后剪枝用验证集自底向上裁剪。如果脚本里用的是预剪枝参数一般是 max_depth 和 min_samples_leaf直接在建树递归里加判断就行。后剪枝实现起来复杂一些需要先建完整棵树再遍历子树做替换判断。实验报告里如果要求对比剪枝前后准确率记得留出一部分数据做验证集别拿训练集自己评自己。注意递归建树时 remaining_features 的传递方式很关键。如果每层都传同一个列表的引用修改会互相影响。用列表推导式生成新列表是安全的做法。3. 跑通实验环境配置、训练评估与可视化3.1 Python 环境与依赖安装版本别乱选课程实验的代码通常是在 Python 3.8 到 3.10 之间写的太新的版本可能有些库不兼容。我一般会建议用 conda 建一个独立环境避免和系统 Python 打架。conda create -n ml_exp python3.9 conda activate ml_exp pip install pandas numpy scikit-learn matplotlib graphviz参数说明python3.9 是稳妥选择3.8 也行3.11 以上有些老库的 wheel 可能没有。graphviz 这个包分两部分pip 装的是 Python 接口系统层面还需要装 Graphviz 软件本身否则画图时会报 ExecutableNotFound。Windows 上要去官网下安装包装完把 bin 目录加到 PATHLinux 上直接 apt install graphviz 或 yum install graphviz。如果实验要求用 sklearn 做对比DecisionTreeClassifier 的 criterion 参数可以设 entropy 或 gini分别对应 ID3 和 CART 的划分标准。max_depth 控制树深min_samples_split 控制节点分裂的最小样本数这两个是预剪枝最常用的参数。3.2 训练、预测与评估指标手写建树完成后预测函数就是沿着树往下走到叶子节点返回类别。评估一般用准确率、精确率、召回率、F1 分数sklearn 的 classification_report 一行就能出。from sklearn.metrics import classification_report, accuracy_score def predict(tree, sample): # 沿树递归查找 if not isinstance(tree, dict): return tree feature list(tree.keys())[0] value sample[feature] subtree tree[feature].get(value) if subtree is None: # 遇到训练集没见过的取值返回一个默认类 return None return predict(subtree, sample) # 假设 X_test 是测试集特征y_test 是真实标签 preds [predict(tree, row) for _, row in X_test.iterrows()] print(classification_report(y_test, preds)) print(Accuracy:, accuracy_score(y_test, preds))参数说明predict 函数里对未见取值的处理是个关键点。训练集里某个特征没有取过某个值测试集出现了subtree 就是 None这时候要么返回多数类要么走一个默认分支。课程实验里如果没处理这种情况测试时可能直接报错或者返回 None 导致评估指标计算失败。评估阶段常见做法是留出法或 K 折交叉验证。留出法简单70% 训练 30% 测试交叉验证更稳但手写建树跑 5 折可能比较慢。如果数据量不大课程实验通常几百行交叉验证完全跑得动。3.3 决策树可视化graphviz 出图与中文乱码可视化是实验报告里加分项也是理解模型决策逻辑最直观的方式。手写树的可视化需要自己写递归函数导出 DOT 格式sklearn 的树可以用 export_graphviz 直接导出。from sklearn.tree import export_graphviz import graphviz # 假设 clf 是训练好的 DecisionTreeClassifier export_graphviz(clf, out_filetree.dot, feature_namesfeature_names, class_namesclass_names, filledTrue, roundedTrue) with open(tree.dot) as f: dot_graph f.read() graphviz.Source(dot_graph).render(tree, formatpng, cleanupTrue)参数说明filledTrue 让节点按类别填充颜色roundedTrue 圆角边框这两个参数让图更易读。feature_names 和 class_names 要按实际数据传入否则图上显示的是 feature_0、class_0 这种。中文乱码是高频问题graphviz 默认字体不支持中文需要在 DOT 文件里指定 fontnameSimHei 或 Microsoft YaHeiLinux 下可能要装中文字体并指定路径。提示如果 render 报错说找不到 dot 命令说明系统层面的 Graphviz 没装好或者 PATH 没配。先在命令行跑 dot -V 确认版本能打印出来。3.4 剪枝参数怎么调从过拟合到泛化决策树不剪枝几乎必然过拟合训练集准确率能到 100%测试集可能只有 60%。预剪枝的参数调整是个经验活参数作用典型取值范围调大后果max_depth限制树的最大深度3~10太浅欠拟合太深过拟合min_samples_split节点分裂所需最小样本数2~20太大树长不起来min_samples_leaf叶子节点最小样本数1~10太大叶子太少max_features每次分裂考虑的最大特征数sqrt/None限制后增加随机性我一般会先设 max_depth5 跑一遍看准确率然后逐步加到 8 或 10观察测试集准确率的变化曲线。如果训练集和测试集准确率差距在 5% 以内说明当前深度基本合适。后剪枝的话用验证集自底向上遍历如果剪掉某棵子树后验证集准确率不降反升就剪掉。血泪经验调参时不要盯着测试集反复调那样测试集就变成了验证集最终报告里的泛化性能是虚的。正确做法是训练集切一部分出来做验证集调参在验证集上做测试集只在最后评一次。4. 避坑与排查课程实验里最容易翻车的五个点4.1 报错 UnicodeDecodeError编码不对现象pd.read_csv 直接抛 UnicodeDecodeError提示某字节无法解码。 原因课程数据集从 Excel 导出时默认 gbk 编码而代码里写的是 utf-8。 解决先试 encodinggbk如果还不行就试 gb18030这个兼容性更好。或者用记事本打开 CSV 另存为 UTF-8 格式。4.2 建树递归深度超限RecursionError现象RecursionError: maximum recursion depth exceeded。 原因递归终止条件没写全或者特征取值太多导致树极深。 解决先检查终止条件是否覆盖了标签全同、特征用完、子集为空三种情况。如果确认逻辑没问题可以在脚本开头加 sys.setrecursionlimit(10000)但这只是治标根本原因可能是某个特征取值过多比如 ID 列需要在预处理阶段把这类列删掉。4.3 预测时 KeyError测试集出现训练集没见过的特征取值现象predict 函数里 tree[feature][value] 报 KeyError。 原因训练集某个特征没有取过某个值测试集出现了。 解决在 predict 里用 .get(value) 代替直接索引取不到时返回多数类或 None然后在评估前把 None 预测值替换成多数类。4.4 graphviz 画图中文变方块现象生成的决策树图里中文标签显示为方块或乱码。 原因graphviz 默认字体不含中文字形。 解决在 export_graphviz 里加 fontnameSimHeiWindows或 fontnameWenQuanYi Micro HeiLinux同时确认系统里确实装了这个字体。如果还不行把 feature_names 和 class_names 临时换成英文再画图。4.5 准确率异常高或异常低现象训练集准确率 100%测试集 50%或者两者都低得离谱。 原因前者是过拟合树太深或没剪枝后者可能是标签编码错了比如把类别标签当成了连续值或者数据划分时没打乱。 解决过拟合就加 max_depth 或 min_samples_leaf准确率整体低就先检查标签列的数据类型和取值分布用 df[label_col].value_counts() 看一眼确认没有异常值混入。5. 从实验代码到可复用的决策树模块我的封装习惯把 Ex2_DecTree.py 跑通之后如果只是交完实验就扔其实挺可惜的。我一般会做一步封装把数据加载、特征选择、建树、预测、评估拆成独立函数用一个 main 函数串起来这样下次遇到类似分类任务可以直接改数据集路径和参数就能跑。具体做法是定义一个 DecisionTree 类fit 方法里做建树predict 方法里做递归查找score 方法里算准确率。参数方面把 criterioninfo_gain 或 gini、max_depth、min_samples_leaf 作为构造参数传入。这样封装之后和 sklearn 的接口风格一致切换对比也方便。class MyDecisionTree: def __init__(self, criterioninfo_gain, max_depthNone, min_samples_leaf1): self.criterion criterion self.max_depth max_depth self.min_samples_leaf min_samples_leaf self.tree None def fit(self, df, label_col): features [c for c in df.columns if c ! label_col] self.tree self._build(df, features, label_col, depth0) def _build(self, df, features, label_col, depth): # 终止条件里加入max_depth和min_samples_leaf判断 if len(df[label_col].unique()) 1: return df[label_col].iloc[0] if not features or (self.max_depth and depth self.max_depth): return df[label_col].mode()[0] if len(df) self.min_samples_leaf: return df[label_col].mode()[0] # 后续选特征、分裂逻辑同上 ...验证封装是否正确我习惯用鸢尾花数据集跑一遍因为它的特征都是连续值需要先做分箱或者用阈值分裂能顺带测出代码对连续特征的处理能力。如果鸢尾花上准确率能到 90% 以上说明建树和预测逻辑没问题。然后再换回课程数据集对比手写实现和 sklearn 的结果如果准确率差距在 2% 以内基本可以确认实现是对的。从那以后我每次做完课程实验都会把核心代码抽成一个独立模块加一个 ifname main 的入口下次遇到同类问题直接 import 就能用。这个习惯帮我省了不少重复劳动也让实验报告里的代码看起来更工程化。希望帮到你。本文还有配套的精品资源点击获取