鸢尾花分类实验:决策树原理、剪枝调参与可视化实战
简介鸢尾花分类是机器学习入门的经典案例此份资源以C实现决策树算法完成对Setosa、Versicolour、Virginica三个品种的自动判别适合正在学习数据挖掘、机器学习或C算法实现的学生与开发者。压缩包共1个文件为3KB的cpp源码不依赖额外工程文件可单独编译运行。代码结构清晰完整覆盖数据读取预处理、决策树类定义、基于熵或基尼系数的特征选择、递归建树及新样本预测等模块并附有主函数调用流程便于对照书本原理逐段理解。已有3413人学习下载。通过阅读这份代码读者可以直观掌握信息增益、递归分割、剪枝条件等核心概念也能熟悉C中结构化表示数据与模型的方法是一份适合课内实验、课程设计或自学参考的紧凑示例。1. 鸢尾花分类实验决策树是最适合入门解释黑箱的模型如果你刚接触机器学习第一个跑通的项目大概率就是鸢尾花分类实验。这个数据集只有150条样本、4个特征、3个类别小到几秒钟就能训练完却能把分类任务的核心流程完整走一遍。而决策树在这类实验里的价值不是精度——它通常打不过调好参的SVM或随机森林——而是它能把分类依据彻底暴露出来模型做出判断靠的是哪些特征、在什么阈值上切分、每一步损失多少纯度。这种可解释性对理解分类本质的帮助比任何精度指标都直观。鸢尾花实验用决策树常见做法是拿sklearn的DecisionTreeClassifier做一轮完整训练再看树的结构和剪枝参数如何影响泛化。整个过程涉及数据切分、特征量纲理解、过拟合控制和规则提取这些恰恰是工程里用树模型前必须想清楚的事。本文就按这条路径把鸢尾花分类实验中的决策树从理论到调参完整走一遍新手能照着复现老手可以直接跳到最后两章看边界行为和剪枝细节。2. 决策树分裂逻辑与鸢尾花特征量纲的对应关系2.1 为什么先看数据分布而不是直接训练大多数决策树教程上来就调fit但鸢尾花实验里有个容易被忽略的点数据集的4个特征量纲不同花萼长度和花瓣宽度不在一个尺度上。决策树虽然对量纲不敏感因为它的分裂只依赖排序和阈值比较但理解特征分布能帮助你解释树的结构而不是无条件相信feature_importances_。我一般会在训练前用pandas快速统计一下数据分布代码很简单import pandas as pd from sklearn.datasets import load_iris iris load_iris() df pd.DataFrame(iris.data, columnsiris.feature_names) df[target] iris.target print(df.describe().round(2))输出会显示花萼长度在4.3到7.9之间花瓣宽度只有0.1到2.5均值差异接近一个数量级。这里想说明的是树模型分裂时比较的是阈值与特征值的大小关系不需要归一化但如果你后续要对比逻辑回归或KNN的效果特征缩放就变成必要条件。这也是鸢尾花实验适合用来对比不同算法家族的原因——同一份数据不同模型预处理要求不同。2.2 决策树分裂时如何选择特征与阈值决策树的核心是递归地找最优分裂。对每个候选特征算法遍历该特征的所有取值作为阈值候选把样本分成左右两组再用不纯度指标衡量分组效果。分类任务里常用的是基尼不纯度Gini impurity或信息熵sklearn默认用基尼不纯度。基尼不纯度的计算公式如下Gini(D) 1 - sum(p_k^2)其中p_k是第k类样本在当前节点中的占比。分裂后的整体基尼不纯度是左右子节点的加权平均权重为子节点样本数占比。每次分裂都选择使加权不纯度下降最大的特征与阈值组合这就是决策树最大程度利用鸢尾花特征的方式。鸢尾花数据集里第一个分裂通常落在花瓣长度或花瓣宽度上原因是这两个特征的类间区分度远高于花萼特征。从数据分布上看setosa的花瓣长度普遍小于2厘米versicolor和virginica则多在3厘米以上所以一个切分点就能干净地分离出setosa这是决策树在鸢尾花实验里表现稳定的根本原因。2.3 不纯度下降量与特征重要性的关系特征重要性在决策树中不是独立评估的它来自每次分裂带来的不纯度减少量。sklearn对每个特征累加它参与分裂时的不纯度下降值再按样本量加权最后归一化成重要性分数。理解这一点就知道feature_importances_只反映该特征在树中的实际参与程度。如果树浅某特征即使区分度高但没有被选中重要性就是0。鸢尾花实验中常见的结果是petal length和petal width特征重要性接近1两个花萼特征接近0。这并不说明花萼特征毫无信息只是被花瓣特征的信息量盖过了。树模型用贪心策略选择每次的最优分裂不会考虑多个弱特征组合出强判别力的情况。这也是初学者容易误读特征重要性的地方——它描述的是这棵树用了什么而不是数据里有什么。3. 用scikit-learn在本地跑通鸢尾花决策树的最小代码3.1 数据切分手写train_test_split还是用sklearn鸢尾花实验的数据量只有150条训练集和测试集的划分方式直接影响结果评估。我一般用sklearn的train_test_split但有一个细节设置random_state并保持stratify让三个类别在训练集和测试集中的比例与原始分布一致。from sklearn.model_selection import train_test_split from sklearn.tree import DecisionTreeClassifier X iris.data y iris.target X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.3, random_state42, stratifyy ) print(X_train.shape, X_test.shape) print(y_train.shape, y_test.shape)test_size设为0.3意味着训练集105条、测试集45条。stratifyy保证了三类鸢尾花在训练集和测试集中各占三分之一避免因随机抽样导致某一类别在测试集中缺失。对150条样本的小数据集来说如果不做分层抽样一次糟糕的随机切分就可能让测试集里某个类别只有很少样本分类报告的可信度会显著下降。3.2 训练决策树并查看基础指标切分完成后可以直接训练一个不限制深度的决策树作为后续剪枝对比的基线。第一步不要急着配置参数先看模型在默认设置下的表现from sklearn.metrics import accuracy_score, classification_report clf DecisionTreeClassifier(random_state42) clf.fit(X_train, y_train) y_pred clf.predict(X_test) print(基线准确率:, accuracy_score(y_test, y_pred)) print(classification_report(y_test, y_pred, target_namesiris.target_names))这里random_state42只控制树内部的随机性。决策树在分裂时如果遇到多个特征产生相同的不纯度下降会随机选一个固定随机种子可以保证结果可复现。准确率通常落在0.93到1.0之间如果出现1.0也不用高兴太早因为默认决策树不限制深度时会生长到完全纯的叶子节点对训练数据学习得非常彻底需要在下一步用交叉验证或剪枝参数确认泛化能力。3.3 基线模型的过拟合迹象从准确率看不出的问题测试集准确率在鸢尾花这种小数据集上往往虚高因为样本少、类别可分性强。真正暴露过拟合的是训练集准确率——如果训练集准确率是1.0测试集只有0.93说明树把训练集里的噪声也学进去了。更可靠的做法是看交叉验证分数我在实际实验中通常用5折交叉验证来评估不同参数设置下的泛化表现from sklearn.model_selection import cross_val_score scores cross_val_score(DecisionTreeClassifier(random_state42), X, y, cv5) print(5折交叉验证准确率:, scores.mean().round(3), /-, scores.std().round(3))交叉验证把150条样本分成5份每次用4份训练、1份验证重复5次后取平均。这样每个样本都参与过验证评估结果不依赖某一次的随机切分对只有150条数据的鸢尾花实验比单次训练测试切分可靠得多。基线模型如果交叉验证均值和测试集准确率差距过大说明测试集划分恰好碰上了对模型友好的样本分布这种偶然性在数据量大时不明显在鸢尾花这种小数据集上却需要警惕。4. 决策树可视化与剪枝参数的实战调节4.1 用plot_tree画出鸢尾花分类的结构图决策树区别于其他模型的核心优势是可视化sklearn从1.0版本开始内置了plot_tree函数不再依赖graphviz外部库使用门槛大幅降低。鸢尾花实验画出来的树能直接看到每一步的切分条件这是排查模型行为最直观的手段。import matplotlib.pyplot as plt from sklearn.tree import plot_tree plt.figure(figsize(16, 8)) plot_tree( clf, feature_namesiris.feature_names, class_namesiris.target_names, filledTrue, roundedTrue, fontsize9 ) plt.show()filledTrue会按类别给节点上色颜色越纯说明该节点中某一类占比越高roundedTrue只影响节点边框样式。观察这棵未经剪枝的树典型的现象是深度达到5到6层接近底部的叶子节点样本数很少有的只有个位数还能看到某些分裂依据的是sepal width值——这种细碎的分支是针对个训训练样本的划分在测试集上意义不大是过拟合的直观证据。4.2 控制过拟合限制树的生长还是事后剪枝决策树的剪枝策略分两类预剪枝在树生长过程中提前停止后剪枝等树完全生长后再合并叶子节点。sklearn的DecisionTreeClassifier主要支持预剪枝参数包括max_depth、min_samples_split、min_samples_leaf和max_leaf_nodes真正的后剪枝可以通过cost_complexity_pruning路径实现也就是ccp_alpha参数底层复杂度代价剪枝算法。实际处理鸢尾花实验时我通常先试max_depth和min_samples_leaf因为它们语义明确、和业务解释直接相关。各参数对模型的影响对比如下参数作用鸢尾花实验中常用值设置过大或过小的风险max_depth限制树的最大深度3~5过大易过拟合过小欠拟合min_samples_split节点分裂所需最小样本数5~10过小分裂过多过大会导致树太粗min_samples_leaf叶子节点最少样本数3~5过小无法平滑噪声过大丢失细节max_leaf_nodes限制叶子节点总数5~10过小限制表达力ccp_alpha复杂度代价剪枝参数0.005~0.02过小剪枝不足过大整棵树被剪成一棵桩4.3 用学习曲线确认max_depth的合适取值手动调参容易陷入局部最优因为参数之间存在交互——max_depth4和min_samples_leaf3的组合效果不等于各自单独设置时效果的叠加。我一般先用循环扫描单个参数观察训练集和验证集分数随参数变化的情况再在最优附近做小范围联合搜索。import numpy as np from sklearn.model_selection import validation_curve param_range np.arange(1, 10) train_scores, test_scores validation_curve( DecisionTreeClassifier(random_state42), X, y, param_namemax_depth, param_rangeparam_range, cv5, scoringaccuracy ) train_mean train_scores.mean(axis1) test_mean test_scores.mean(axis1) for depth, tr, te in zip(param_range, train_mean, test_mean): print(fmax_depth{depth}, 训练准确率{tr:.3f}, 验证准确率{te:.3f})观察这条曲线典型的规律是深度从1增加到3时训练准确率和验证准确率同步上升深度到3或4时验证准确率见顶此后训练准确率继续上升而验证准确率走平或下降这就是过拟合信号。鸢尾花数据集通常max_depth3或4就足够继续加深只是让树去记忆训练样本的细微偏差。4.4 用GridSearchCV同时搜索多个剪枝参数实际工程里参数之间是联动的一个个试效率太低。用GridSearchCV一次性搜索多组参数组合更符合从业者的做法。网格搜索内部会对每一组参数做一次交叉验证最终返回验证分数最高的参数组合。from sklearn.model_selection import GridSearchCV param_grid { max_depth: [2, 3, 4, 5, 6], min_samples_split: [2, 5, 10], min_samples_leaf: [1, 2, 4] } grid GridSearchCV( DecisionTreeClassifier(random_state42), param_grid, cv5, scoringaccuracy, n_jobs-1 ) grid.fit(X, y) print(最优参数:, grid.best_params_) print(最优交叉验证准确率:, grid.best_score_.round(4))param_grid里有5乘3乘3共45组参数组合每组做5折交叉验证总共训练225次。对鸢尾花数据量来说整个搜索几秒就能完成但如果是大规模数据这种穷举方式会非常慢届时要改用RandomizedSearchCV做随机搜索或者按先深度后叶子样本数的顺序分阶段调参。这里有一个常见误区grid.best_score_来自交叉验证是经过多轮验证的平均结果实际测试集评分通常会略低读者不要期望两个数字完全一致。5. 从树到规则将鸢尾花分类逻辑转成可解释的判定条件5.1 用export_text把树转成纯文本规则可视化适合人眼观察但要把决策树的分裂规则转成可维护的代码或业务文档纯文本形式更实用。sklearn提供的export_text函数可以把决策树输出为缩进格式的if-else规则方便直接阅读和嵌入其他系统。from sklearn.tree import export_text clf_pruned DecisionTreeClassifier(max_depth3, random_state42) clf_pruned.fit(X_train, y_train) tree_rules export_text( clf_pruned, feature_nameslist(iris.feature_names), decimals2 ) print(tree_rules)这段代码在鸢尾花实验中通常输出的规则结构是根节点先判断petal length是否小于等于某个阈值左分支直接分离出setosa右分支继续根据petal width切分versicolor和virginica。输出的规则文本可以直接转成函数或SQL条件这就是决策树在风控、故障诊断和工业场景里受欢迎的原因。5.2 手动提取规则并验证人为划分与树划分的差异规则可视化之后值得做一步验证把树的阈值和散点图对比能看出树在哪些位置上做了不直观的划分。这个实验做透了后续学随机森林的思路会清楚很多——随机森林本质上是多个决策树在数据子集和特征子集上各自生长最后通过投票消除单棵树的方差。我通常会做这样一次手动验证用花瓣长度和花瓣宽度作二维散点图着色再对照树的切分阈值看判断边界的合理性。对于new data手动推理路径比直接调用predict更有体感sample np.array([[5.1, 3.5, 1.4, 0.2]]) path clf_pruned.decision_path(sample) print(path.toarray())sample是setosa的一个典型样本。decision_path返回样本经过的节点索引矩阵矩阵中值为1的位置表示该样本通过了对应节点。打印出来可以确认它是往左走了两步就到达叶子节点还是被更深的分支处理这一步在做模型解释时经常用到。5.3 与随机森林对比单棵树的特性与集成模型的差异树模型做鸢尾花分类实验到了这一步自然要对比一下随机森林。单棵决策树方差大——训练数据轻微扰动树结构可能完全不同这是max_depth不足时的特性。随机森林通过行采样和列采样制造多个略有差异的数据子集并行训练多棵树再投票方差显著降低。在鸢尾花实验里同一份数据切分决策树的准确率可能在0.93到1.0之间波动随机森林通常在0.97上下稳定输出。两种模型的核心差异不是准确率而是偏差方差特性的差异。如果看重可解释性且数据量小、特征少单棵树足够如果追求稳定性和对噪声的鲁棒性随机森林更可靠。用joblib保存模型并导入新数据做推理时这些差异会直接影响模型上线后的表现import joblib joblib.dump(clf_pruned, iris_decision_tree.joblib) loaded_model joblib.load(iris_decision_tree.joblib)保存模型时可以把特征名、类别名和阈值一并序列化方便后续追溯模型版本。如果未来接入到Flask或FastAPI服务里决策树分类器推理速度极快几百条样本的预测耗时在微秒级不需要显式优化这是树模型在实时推理场景里一直被选用的原因。本文还有配套的精品资源点击获取