ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

决策树算法详解:从原理到随机森林的完整指南

决策树算法详解:从原理到随机森林的完整指南 机器学习里有一类模型特别适合入门因为它的原理几乎不需要数学基础画出来就是一棵树的样子从根节点开始一层一层往下问问题每个分支对应一个判断条件最后落到叶子节点上就是预测结果。这就是决策树Decision Tree。我最早接触机器学习的时候最先能完整讲给别人听、而且能讲明白的算法就是决策树。它不像神经网络那样是个黑箱每一步决策都可以回溯、可以解释这种“看得见摸得着”的特性让它在很多强调可解释性的场景里至今仍然占着重要位置。这篇我就围绕决策树展开从它解决什么问题开始讲到核心算法原理、手写实现、常见坑点再延伸到随机森林这类集成模型。无论你是正在准备期末考试的学生还是刚入门机器学习想做点实际项目的开发者希望这篇能帮你把决策树彻底吃透。1. 决策树到底解决什么问题1.1 从生活中的“猜人游戏”说起你有没有玩过那种猜人游戏心里想一个人别人问“是男是女”“是不是戴眼镜”“是不是程序员”通过一连串是/否问题就能锁定目标。决策树的思路一模一样。它本质上是一套嵌套的 if-else 规则集合只不过这套规则不是人拍脑袋想出来的而是算法自动从数据里学出来的。拿经典的“是否适合打网球”数据集来说天气、温度、湿度、风力是特征标签是“去”或“不去”。决策树会自己判断先用哪个特征分再在分支上选下一个特征最终形成一棵能对新样本做预测的树。整个过程不需要你告诉它“湿度超过80%就别去”它自己能从历史数据里总结出来。这种思路吸引人的地方在于模型本身就是一个规则列表你把它打印出来人人都能看懂。银行信贷审批、医疗辅助诊断、设备故障诊断这类场景不是只看预测准不准还得回答“为什么这么判”决策树在这类场景里有天然优势。1.2 和神经网络、SVM类型的模型对比那个“机器学习是玄学”的阶段基本都是从神经网络的黑箱特性开始的。模型效果是好但问它“为什么认为这张图是猫”它给不出人能理解的解释。决策树完全相反它的决策路径就是解释本身。对比一下三个常见模型的差异模型可解释性训练速度数据要求典型场景线性回归/逻辑回归较高快需特征与目标近似线性关系风控评分、营销响应预测决策树高很快能容忍非线性、缺失值、量纲差异规则提取、可解释性要求高的场景神经网络/深度学习低慢需GPU数据量大、特征抽象图像、语音、自然语言我再强调一点决策树训练时不需要对特征做标准化。神经网络那种“特征之间量纲差异大收敛慢”的问题在决策树这里根本不存在。因为它每次分裂只看特征取值和阈值比较不受量纲影响这在实际项目中省掉了很多预处理步骤。1.3 决策树的适用范围和边界决策树适合处理带明确决策边界的问题适合特征和标签之间有非线性关系的场景也适合需要输出规则、需要给业务方讲清楚逻辑的场景。但它有几个明显的软肋需要先跟你交底单棵决策树容易过拟合训练集上表现很好测试集上就拉胯。后面我会专门讲剪枝怎么应对。对数据中的噪声敏感个别异常点可能让树的结构产生比较大的变化。特征空间是横向/纵向切分对某些斜向边界的问题表达能力有限。类别不平衡时树容易偏向多数类。所以工程实践中很少直接用一棵裸树去做最终模型更多是拿它当基线模型或者作为随机森林、GBDT这些集成模型的基础组件。但是理解单棵树的原理是理解整个集成学习体系的必经之路。2. 建树的核心特征选择准则决策树学习的关键是每一步都选一个“最优特征”来分裂。这个“最优”怎么定义核心思想是让分裂之后子节点的数据尽可能“纯”——也就是同一个节点里的样本标签尽量一致。衡量“纯度变化”的指标主要有三个信息增益、增益率、基尼指数。2.1 信息熵和信息增益信息熵Entropy是信息论里的概念度量的是不确定性。一个集合的熵越大说明里面的样本越混乱。公式长这样[ Ent(D) -\sum_{k1}^{K} p_k \log_2 p_k ]其中 ( p_k ) 是第 k 类样本在集合 D 中占的比例。如果集合里只有一类样本熵就是 0纯度最高。如果两类各占一半熵就是 1最混乱。信息增益 分裂前的熵 – 分裂后各子节点熵的加权平均。公式是[ Gain(D, a) Ent(D) - \sum_{v1}^{V} \frac{|D^v|}{|D|} Ent(D^v) ]这个值的含义就是用了特征 a 做分裂之后不确定性减少了多少减少得越多说明这个特征越关键。我用一个非常小的例子来走一遍计算过程。假设有 14 条记录标签是“是否外出活动”其中 9 个去、5 个不去。根节点的熵是[ Ent(D) -\frac{9}{14}\log_2\frac{9}{14} - \frac{5}{14}\log_2\frac{5}{14} \approx 0.940 ]现在考虑用“天气”特征分裂有三个取值晴、阴、雨。晴有 5 条其中去 2 不去 3阴有 4 条全去雨有 5 条其中去 3 不去 2。[ Ent(D^{晴}) -\frac{2}{5}\log_2\frac{2}{5} - \frac{3}{5}\log_2\frac{3}{5} \approx 0.971 ] [ Ent(D^{阴}) -\frac{4}{4}\log_2\frac{4}{4} 0 ] [ Ent(D^{雨}) -\frac{3}{5}\log_2\frac{3}{5} - \frac{2}{5}\log_2\frac{2}{5} \approx 0.971 ]加权平均条件熵[ \sum_{v} \frac{|D^v|}{|D|} Ent(D^v) \frac{5}{14}\times0.971 \frac{4}{14}\times0 \frac{5}{14}\times0.971 \approx 0.694 ]信息增益[ Gain(D, 天气) 0.940 - 0.694 0.246 ]同理可以算出“湿度”“风力”等特征的信息增益然后选最大的那个作为根节点的分裂特征。这个计算过程就是决策树学习中最核心的“每一次分裂都让数据更有序”的量化表达。2.2 信息增益的偏好问题与增益率信息增益有一个毛病它偏好取值数目多的特征。极端情况下如果有一个特征叫“编号”每条样本都有一个唯一值用这个特征分裂每个子节点只有一个样本条件熵降到 0信息增益直接拉满。但这样一个模型完全没有泛化能力纯属过拟合。为了纠正这个偏好C4.5 算法使用增益率Gain Ratio它把信息增益除以特征本身的固有值Intrinsic Value[ Gain_ratio(D, a) \frac{Gain(D, a)}{IV(a)} ]其中 ( IV(a) ) 是特征 a 的取值分布熵。取值越多、分布越均匀IV 越大增益率就被惩罚得越厉害。但增益率也有反向问题它可能对取值少的特征有偏好。所以 C4.5 不是直接选增益率最大的而是先挑信息增益高于平均水平的特征再从中选增益率最高的。这个细节在《机器学习》周志华西瓜书里有详细说明期末复习的时候经常考这个点。2.3 CART 的基尼指数CARTClassification And Regression Tree用的是基尼指数Gini Index它衡量的是从集合中随机抽两个样本其标签不一致的概率。基尼值越小纯度越高。[ Gini(D) 1 - \sum_{k1}^{K} p_k^2 ]对于二分类如果两类各占一半基尼值是 ( 1 - 0.5^2 - 0.5^2 0.5 )。如果全是同一类基尼值是 0。特征 a 的基尼指数定义为分裂后各子节点基尼值的加权平均[ Gini_index(D, a) \sum_{v1}^{V} \frac{|D^v|}{|D|} Gini(D^v) ]选择基尼指数最小的特征作为分裂特征。基尼指数和信息增益最大的差别是它不需要算对数计算速度更快而且天然不偏向多取值特征相对信息增益来说偏好小很多所以 sklearn 里的 DecisionTreeClassifier 默认用的是 CART也就是基尼指数。很多人在面试的时候被问到“sklearn 决策树默认参数为什么是 gini”其实就是这个原因。2.4 三者对比与选型建议指标核心公式要点优点缺点对应算法信息增益熵减直观、易解释偏好取值多的特征ID3增益率信息增益/固有值缓解多取值偏好可能偏好取值少的特征C4.5基尼指数1 – 各类别概率平方和计算快、无对数对多分类问题区分度略弱CART我在实际项目里如果只是快速验证用 sklearn 默认的基尼指数就够了如果对泛化能力要求高、特征里又有不少类别型变量会试试 C4.5 的思路。但要记住特征选择准则只是建树的一部分真正决定模型好坏的是剪枝策略和数据质量。3. 实战中的关键细节与难点处理3.1 连续特征的离散化处理现实中的数据大多是连续值比如温度 25.3 度、身高 175cm。决策树不能直接拿连续值做分叉条件除非用 CART 的回归模式需要把连续特征离散化。最常用的是二分法把特征 a 的 n 个取值排序然后在相邻取值的中点候选切分点。对每个候选切分点把数据分成“≤ 阈值”和“ 阈值”两部分计算信息增益或基尼指数选最优的那个。比如温度特征有 22、25、28 三个值候选切分点是 23.5、26.5分别计算分裂效果选最好的。注意一个连续特征可以在树的不同层级被重复使用比如第一层用“温度 ≤ 25”某个分支下的子节点还能继续用“温度 ≤ 22”。这跟离散特征不同离散特征每个取值只在一个节点用一次因为分支已经分了。在 sklearn 里这个逻辑是自动处理的你不需要手动离散化。但如果你自己实现决策树这块是必须考虑的重点否则算法直接报错。3.2 缺失值的处理思路真实业务数据里缺失值太常见了。决策树处理缺失值有两个问题要解决一是怎么选分裂特征二是选定了特征后缺失该特征的样本该往哪个子节点走。C4.5 的做法是计算信息增益时只用那些在该特征上无缺失的样本然后按比例加权。对于缺失特征值的样本分裂时同时进入所有子节点但每个子节点里它的权重按该子节点的样本比例分配。听起来抽象实际实现也不算简单。sklearn 的决策树不支持缺失值所以你需要预处理比如填充均值/中位数/众数或者把缺失值单独做为一个类别。我个人习惯是先用“缺失指示器”标记缺失情况再结合填充值一起送入模型这样模型能学到“缺失本身可能携带信息”的模式。3.3 剪枝决策树防止过拟合的核心手段决策树如果不加限制会一直长到每个叶子节点只有一个样本或者所有样本都被完美分类。这种树在训练集上可能拿到 100% 准确率但换个数据集就废了。原因很简单树把训练数据里的噪声也学进去了。剪枝有两种预剪枝和后剪枝。预剪枝是在树生长过程中提前停止。常见的方式有限制树的最大深度max_depth这是最直接有效的参数。限制内部节点最少样本数min_samples_split样本太少就不再分裂了。限制叶子节点的最少样本数min_samples_leaf。限制分裂带来的收益阈值如果信息增益提升小于某个值就不分裂。后剪枝是先让树充分生长再自底向上判断把某个内部节点换成叶子节点如果验证集上的精度不下降甚至提升就剪掉它的子树。这种方法比预剪枝更乐观通常效果更好但计算开销大。我在调参的时候默认经验是这样的max_depth 先设 3~5 跑一版看训练集/测试集精度差距。如果差距大减少深度。min_samples_leaf 设 5~10对抑制过拟合效果明显而且能避免叶子节点样本太少导致预测不稳定。如果用了交叉验证就一起搜参如果只想快速出一个稳定模型优先调这两个参数。注意剪枝的本质是“用偏差换方差”。树浅了、叶子样本多了模型更简单、更稳健但可能欠拟合树深了更贴合训练集但泛化差。目标是在验证集上找到那个平衡点。3.4 决策树的可解释性和可视化决策树一个很大的卖点就是可以导出成规则。训练完一棵树用 sklearn 的 tree.export_graphviz 或 plot_tree 可以画出树结构。我自己在项目里经常做一件事把决策路径打印出来给业务方看比如如果 年龄 35 且 收入 20000 且 历史逾期次数 0则 放款这种规则对业务方来说是天然的“白盒模型”比告诉对方“神经网络给出评分 0.87”有说服力得多。如果树的深度是 3~4 层完全可以人工检查每条路径是否符合直觉如果不合理大概率是数据里有问题这本身也是数据质量验证的好方法。4. 手把手用 Python 从零实现一棵决策树4.1 环境准备与数据集说明为了让你能完全掌握决策树的实现细节我不用 sklearn而是用纯 Python 手写一个 ID3 决策树模型。虽然生产环境没必要这么做sklearn 的 CART 又稳又快但自己写一遍你对算法逻辑的记忆会深很多遇到面试手撕算法也心里有底。我用模拟的“是否外出活动”数据数据规模小方便展示每一步的决策逻辑。import numpy as np import pandas as pd from collections import Counter data { 天气: [晴, 晴, 阴, 雨, 雨, 雨, 阴, 晴, 晴, 雨, 晴, 阴, 阴, 雨], 温度: [高, 高, 高, 中, 低, 低, 低, 中, 低, 中, 中, 中, 高, 中], 湿度: [高, 高, 高, 高, 中, 中, 中, 高, 中, 中, 中, 高, 中, 高], 风力: [弱, 强, 弱, 弱, 弱, 强, 强, 弱, 弱, 弱, 强, 强, 弱, 强], 外出: [否, 否, 是, 是, 是, 否, 是, 否, 是, 是, 是, 是, 是, 否] } df pd.DataFrame(data) features [天气, 温度, 湿度, 风力] X df[features] y df[外出]这段数据就是经典的“打球数据集”14 条样本每个样本有 4 个离散特征标签是二分类。4.2 实现信息熵和信息增益计算先写信息熵函数。对一个标签序列统计每个类别的概率然后套公式算熵def entropy(labels): counter Counter(labels) total len(labels) ent 0.0 for count in counter.values(): p count / total ent - p * np.log2(p) return ent再写信息增益的计算函数。给定数据集、特征名、标签列先把数据按特征取值分组然后算分裂后的加权熵def info_gain(data, labels, feature): total_ent entropy(labels) values set(data[feature]) cond_ent 0.0 for v in values: subset_labels labels[data[feature] v] cond_ent (len(subset_labels) / len(labels)) * entropy(subset_labels) return total_ent - cond_ent比如用“天气”算信息增益调用info_gain(df, y, 天气)得到 0.246和我们手算的结果一致。这个函数就是 ID3 算法里“选特征”的核心。4.3 构建决策树的完整代码构建树是一个递归过程如果当前集合里样本标签全相同返回叶子节点类别就是该标签。如果特征用完了或所有样本特征取值都一样返回叶子节点类别是多数类。否则计算各特征信息增益选最大的按该特征的每个取值划分子集递归建子树。def build_tree(data, labels, features): # 停止条件1所有样本同一类别 if len(set(labels)) 1: return labels.iloc[0] # 停止条件2特征用完返回多数类 if len(features) 0: counter Counter(labels) return counter.most_common(1)[0][0] # 选择信息增益最大的特征 gains {feat: info_gain(data, labels, feat) for feat in features} best_feat max(gains, keygains.get) tree {best_feat: {}} remaining_features [f for f in features if f ! best_feat] for value in set(data[best_feat]): subset_data data[data[best_feat] value] subset_labels labels[data[best_feat] value] if len(subset_labels) 0: counter Counter(labels) tree[best_feat][value] counter.most_common(1)[0][0] else: tree[best_feat][value] build_tree( subset_data, subset_labels, remaining_features ) return tree tree build_tree(df, y, features) print(tree)输出是一层嵌套的字典结构比如{天气: {晴: {湿度: ...}, 阴: 是, 雨: ...}}。你可以用pprint打印它会发现树的结构一目了然。4.4 用模型对新样本做预测建完树之后要能用来预测。预测过程就是照着树的路径走从根节点开始看特征取值进入对应子节点如果是字典就继续如果是字符串就返回结果。def predict(tree, sample): if not isinstance(tree, dict): return tree feature list(tree.keys())[0] value sample[feature] subtree tree[feature].get(value, None) if subtree is None: counter Counter(y) return counter.most_common(1)[0][0] return predict(subtree, sample) sample {天气: 晴, 温度: 高, 湿度: 高, 风力: 强} print(predict(tree, sample))注意代码里的兜底逻辑如果样本在某个特征上取了树里没见过的值就返回训练集里的多数类。真实场景中这种情况不少预先处理好可以让模型更健壮。4.5 为什么还需要 sklearn 版本自己实现的这个 ID3 有几个缺点没有处理连续特征。没有剪枝逻辑。每个离散特征在每个分支只用一次特征取值多时会迅速“碎片化”树可能非常宽。叶子节点样本极少时预测结果很脆弱。所以在实际项目中我建议直接用 sklearnfrom sklearn.tree import DecisionTreeClassifier from sklearn.model_selection import train_test_split from sklearn.preprocessing import OrdinalEncoder # 将字符串特征转为数值编码 encoder OrdinalEncoder() X_encoded encoder.fit_transform(X) X_train, X_test, y_train, y_test train_test_split( X_encoded, y, test_size0.3, random_state42 ) clf DecisionTreeClassifier( criteriongini, max_depth4, min_samples_leaf2, random_state42 ) clf.fit(X_train, y_train) print(clf.score(X_test, y_test))如果你感觉模型效果不理想先别急着换模型先调参加大min_samples_leaf、降低max_depth、或者用GridSearchCV做交叉验证搜索。决策树的调参空间其实不小而且调参思路对理解偏差-方差权衡很有帮助。5. 常见问题与排查技巧实录5.1 问题速查表现象可能原因排查思路与解决方案训练集准确率 100%测试集很差过拟合树太深降低 max_depth提高 min_samples_leaf或启用后剪枝训练集和测试集都很差欠拟合特征不够/树太浅增大 max_depth检查特征工程是否遗漏重要变量特征重要性集中在少数几个特征上特征共线性或有效信息集中做特征相关性分析考虑特征组合对某个类别的预测明显偏差类别不平衡用 class_weightbalanced或对少数类过采样树结构大得离谱层级超过 20 层没限制深度设置 max_depth3~8重新训练特征里有高基数类别变量分裂后碎片化类别值过多树变宽对类别变量做目标编码或频次编码后再训练5.2 实战中容易踩的坑决策树看似简单但实际用起来有几个地方特别容易翻车。第一个坑是特征编码。不要把有序类别特征比如温度“高/中/低”直接用 OrdinalEncoder 编码成 0/1/2然后无脑丢给 CART。CART 是二分裂它会把阈值一分为二硬编码后的 0/1/2 会被强行解释成“≤ 1”和“ 1”如果“高/中/低”不是线性关系就会丢失结构信息。更稳妥的做法是对无序类别用 One-Hot 编码对有序类别保留顺序但注意解释边界。如果特征特别多也可以考虑先用树模型跑一版再看特征重要性做筛选。第二个坑是样本权重。决策树对样本权重很敏感如果业务上某些样本更重要比如欺诈检测里被标记的欺诈样本要利用 class_weight 或 sample_weight 显式告诉模型而不是简单复制样本来“伪造”权重。复制样本会改变数据分布容易导致过拟合。第三个坑是随机性。单棵决策树的方差很大换一批训练数据树结构可能完全不一样。这不是 bug是算法特性。所以集成方法才这么重要——随机森林通过在数据和特征上引入随机性再平均多棵树的结果能显著降低方差。5.3 如何判断树是否合理训练完一棵树我习惯做三件事第一看树的深度和叶子节点数。深度超过 10 且叶子节点样本数只有 1 的基本都是过拟合除非你有非常充分的理由。第二把树的决策路径拉出来跟业务常识对照。如果出现明显违背常识的规则比如“收入越低越放贷”大概率是数据里有标签泄漏或者样本选择偏差先查数据再查模型。第三用特征重要性判断哪些字段真正在起作用。决策树的 feature_importances_ 是按节点纯度减少量累加的它能帮你快速定位核心特征也可以用来做特征筛选的粗筛。6. 从单棵树走向随机森林6.1 为什么单棵树不够我在前面的内容里反复强调单棵树的缺陷方差大、容易过拟合、对噪声敏感。解决思路在统计学习里很经典组合多个弱学习器用集体决策替代单个模型的判断。这就是集成学习Ensemble Learning。随机森林 Bagging 决策树 特征随机选择。具体做法用 Bootstrap 采样从原始数据中有放回地抽出 n 个样本子集每个子集训练一棵树。每棵树分裂时不把所有特征都拿来候选而是随机选一个特征子集通常 sqrt(特征数)。多棵树投票或平均得到最终结果。6.2 随机森林的优势在哪里第一能有效降低方差。单棵树可能被局部噪声带偏但几十棵树的平均能把噪声抵消掉大部分。第二每棵树只用部分特征做分叉树之间的相关性更低集成的多样性更高效果也更好。如果所有树都只用同样的最强特征那集成出来的结果和单棵树差别不大。第三它可以给出 OOBOut-of-Bag得分。因为每棵树只用了约 63.2% 的样本做训练剩下约 36.8% 的样本没参与训练正好可以做天然的验证集。sklearn 里设置 oob_scoreTrue 就能拿到OOB得分不需要额外划分验证集这在样本量少时特别有用。from sklearn.ensemble import RandomForestClassifier rf RandomForestClassifier( n_estimators500, max_depthNone, min_samples_leaf2, max_featuressqrt, oob_scoreTrue, random_state42, n_jobs-1 ) rf.fit(X_train, y_train) print(rf.oob_score_) print(rf.feature_importances_)6.3 随机森林的局限和适用场景随机森林不擅长外推。如果训练数据里某个特征的范围是 0~100测试时出现 120 的取值树模型基本是靠最近邻区间的近似值来预测可能不太准。另外随机森林对高维稀疏特征比如大规模文本 One-Hot效果不如线性模型。但它在表格数据上依然是非常强的基线。Kaggle 上很多比赛随机森林跑出来的分数都是不错的起点尤其在特征量不大、且特征与目标关系复杂时它比线性模型泛化能力强得多。我的建议是拿到一张表格数据先跑逻辑回归和决策树做两个基线再跑随机森林看提升幅度。如果随机森林比决策树提升显著说明数据里非线性关系丰富值得继续往 GBDT、XGBoost 方向深挖。最后再分享一个我自己的实操习惯无论数据问题多么复杂我都不会跳过决策树/随机森林这层基线模型。它给你的是一个可解释、可验证、可快速迭代的起点。先用它把数据理解透再决定要不要上更重的模型。很多项目里数据清理和特征工程做到位之后随机森林的效果就已经足够业务使用了。这个套路我用了很多年几乎没失手过。
返回列表