news 2026/10/22 6:18:18

决策树三大算法ID3、C4.5、CART原理详解与实战选型指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
决策树三大算法ID3、C4.5、CART原理详解与实战选型指南

决策树这个算法,我最早接触是在做一个用户流失预测的小项目上。当时手头有一批用户行为数据,领导要求给出一个"能解释清楚为什么判断这个用户会流失"的模型。试过逻辑回归,系数解释起来太绕;试过KNN,根本说不清哪些特征在起作用。最后选了决策树,原因很简单——它把判断逻辑直接摊开给你看,像一棵倒着长的树,每个分叉点都在回答"这个特征重不重要、重要到什么程度"。这就是决策树最核心的价值:可解释性。而ID3、C4.5、CART这三个经典算法,恰好代表了决策树从"能跑"到"跑得好"再到"什么都能跑"的三个阶段。这篇文章我会把这三个算法的构建原理拆开讲透,包括它们各自用什么指标选特征、为什么这么选、实际写代码时哪里容易踩坑,以及我这些年用下来的一些经验判断。

1. 决策树到底在解决一个什么问题

1.1 从"二十问"游戏理解决策树的本质

决策树的思路其实特别朴素,你小时候可能玩过一个游戏:我心里想一个东西,你问我问题,我只能回答"是"或"不是",看你能不能在二十个问题内猜出来。聪明的问法不是"它是苹果吗",而是"它是水果吗"——因为后者能把可能性空间砍掉一大半。

决策树干的就是这件事。它把特征空间一层层切分,每次切分都试图让切出来的子集"纯度"更高。所谓纯度,就是子集里的样本尽可能属于同一个类别。切到某个节点时,如果里面的样本已经全是同一类了,就不用再切了,这个节点变成叶子节点,它的类别就是预测结果。

这个过程的数学化表达就是:给定训练集D,特征集A,递归地选择一个最优划分特征,把D分成若干子集,对每个子集重复这个过程,直到满足停止条件。听起来简单,但"最优划分特征"怎么定义,就是ID3、C4.5、CART三个算法的分水岭。

1.2 为什么不用"准确率"直接选特征

有人可能会想,那我直接试每个特征,看哪个划分后子集准确率最高不就行了?这个想法有两个致命问题。

第一,准确率对多路划分有偏好。假设有个特征叫"用户ID",每个ID对应一个样本,按它划分后每个子集只有一个样本,纯度100%,准确率满分。但这棵树毫无泛化能力,纯粹是记住了训练数据。这就是过拟合的典型表现。

第二,准确率不是凸函数,优化起来不友好。我们需要一个能衡量"不确定性"的指标,不确定性越小说明纯度越高。信息熵和基尼指数就是干这个的。信息熵来自信息论,基尼指数来自概率论,两者本质都在度量"随机抽两个样本,类别不一致的概率"。

这里有个常见误解:很多人以为决策树选特征就是选"最重要的特征"。实际上它选的是"当前节点上最能降低不确定性的特征",这个特征在树的全局重要性排名里可能并不靠前,因为它的作用可能被上层节点"抢"走了。

1.3 三个算法的核心差异一览

在深入细节之前,先用一张表把ID3、C4.5、CART的核心区别摆出来,后面再逐个拆解。

维度ID3C4.5CART
特征选择指标信息增益信息增益率基尼指数
特征类型离散离散+连续离散+连续
树的形态多叉树多叉树二叉树
缺失值处理不支持支持支持
剪枝策略无(易过拟合)悲观剪枝代价复杂度剪枝
任务类型分类分类分类+回归

这张表建议先记住,后面每个算法我会展开讲为什么会有这些差异,以及这些差异在实际使用中意味着什么。

2. ID3:用信息增益选特征的"开山之作"

2.1 信息熵:不确定性的度量尺

ID3的核心是信息增益,而信息增益建立在信息熵之上。信息熵的公式是:

$$H(D) = -\sum_{k=1}^{K} p_k \log_2 p_k$$

其中$p_k$是第k类样本在数据集D中的比例。这个公式在干什么?它在算"把D中样本的类别当作随机变量,它的不确定性有多大"。

举个具体例子。假设D有10个样本,5个正类5个负类,那么$p_正=0.5$,$p_负=0.5$,$H(D) = -(0.5 \times \log_2 0.5 + 0.5 \times \log_2 0.5) = 1$比特。如果10个样本全是正类,$p_正=1$,$H(D) = -(1 \times \log_2 1) = 0$比特。熵为0意味着完全确定,熵为1意味着最不确定(二分类情况下)。

我习惯用一个类比来解释:熵就像一锅汤的"混乱程度"。所有料都均匀混在一起,混乱度最高;如果料分层了,每层都是同一种,混乱度就低。决策树要做的就是不断"分层",让每个子集尽量纯。

2.2 信息增益的计算过程与偏好陷阱

信息增益的定义是:划分前的熵减去划分后的条件熵。

$$Gain(D, a) = H(D) - \sum_{v=1}^{V} \frac{|D^v|}{|D|} H(D^v)$$

其中$a$是特征,$V$是$a$的取值个数,$D^v$是$a$取第v个值的样本子集。

我拿一个具体数据走一遍。假设有6个样本,特征"天气"取值为晴、阴、雨,标签是"是否出门"。

样本天气是否出门
1晴是
2晴是
3阴否
4雨否
5雨否
6阴是

划分前:3个"是",3个"否",$H(D) = 1$。

按天气划分:

  • 晴:2个样本,全是"是",$H = 0$
  • 阴:2个样本,1是1否,$H = 1$
  • 雨:2个样本,全是"否",$H = 0$

条件熵 = $\frac{2}{6} \times 0 + \frac{2}{6} \times 1 + \frac{2}{6} \times 0 = \frac{1}{3}$

信息增益 = $1 - \frac{1}{3} = \frac{2}{3} \approx 0.667$

现在问题来了。如果还有一个特征叫"样本编号",取值1到6,每个值对应一个样本。按它划分,每个子集只有一个样本,熵全是0,条件熵为0,信息增益 = 1。比天气的0.667大,ID3会选样本编号。但这显然是个垃圾特征。

这就是信息增益的偏好陷阱:它偏好取值多的特征。因为取值越多,划分越细,每个子集越纯,条件熵越低,增益越大。C4.5就是为了解决这个问题而生的。

2.3 ID3的代码实现与实操注意

用Python手写一个简化版ID3,核心逻辑如下:

import numpy as np from collections import Counter def entropy(y): counts = Counter(y) probs = [c / len(y) for c in counts.values()] return -sum(p * np.log2(p) for p in probs if p > 0) def information_gain(X, y, feature_idx): base_entropy = entropy(y) values = np.unique(X[:, feature_idx]) cond_entropy = 0 for v in values: mask = X[:, feature_idx] == v subset_y = y[mask] cond_entropy += (len(subset_y) / len(y)) * entropy(subset_y) return base_entropy - cond_entropy def id3(X, y, features): # 停止条件1:全同一类 if len(set(y)) == 1: return y[0] # 停止条件2:特征用完 if len(features) == 0: return Counter(y).most_common(1)[0][0] gains = [information_gain(X, y, f) for f in features] best_f = features[np.argmax(gains)] tree = {best_f: {}} remaining = [f for f in features if f != best_f] for v in np.unique(X[:, best_f]): mask = X[:, best_f] == v tree[best_f][v] = id3(X[mask], y[mask], remaining) return tree

这段代码有几个实操中容易忽略的点。第一,entropy函数里加了if p > 0的判断,因为$0 \times \log 0$在数学上定义为0,但代码里会报错。第二,停止条件2返回多数类,这是处理特征用完但样本还没分纯的情况,实际工程中还会加一个"节点样本数小于阈值"的停止条件。第三,递归时remaining去掉了已用特征,这隐含了一个假设:每个特征在一条路径上只用一次。这个假设在ID3里成立,但在某些变体中会被打破。

实操心得:ID3对连续特征无能为力,如果你的数据里有年龄、收入这类连续值,必须先手动分箱。分箱的粒度很讲究,太粗丢失信息,太细又容易过拟合。我一般先用等频分箱切成5到10箱,再根据业务含义微调。

3. C4.5:用信息增益率修正ID3的偏好

3.1 信息增益率怎么算,为什么能修正偏好

C4.5引入了一个叫"分裂信息"的量,也叫固有值:

$$SplitInfo(D, a) = -\sum_{v=1}^{V} \frac{|D^v|}{|D|} \log_2 \frac{|D^v|}{|D|}$$

这个量衡量的是特征$a$本身的取值分布有多分散。取值越多、越均匀,分裂信息越大。然后信息增益率就是:

$$GainRatio(D, a) = \frac{Gain(D, a)}{SplitInfo(D, a)}$$

回到刚才"样本编号"的例子。样本编号有6个取值,每个取值1个样本,分裂信息 = $-\sum_{v=1}^{6} \frac{1}{6} \log_2 \frac{1}{6} = \log_2 6 \approx 2.585$。信息增益是1,增益率 = $1 / 2.585 \approx 0.387$。

天气的分裂信息:晴2个、阴2个、雨2个,$-\left(\frac{2}{6}\log_2\frac{2}{6} \times 3\right) = \log_2 3 \approx 1.585$。增益率 = $0.667 / 1.585 \approx 0.421$。

现在天气的增益率反而比样本编号高了,偏好被修正了。

3.2 增益率也有坑:偏好取值少的特征

但增益率不是完美的。如果某个特征只有一个取值,分裂信息为0,增益率会变成无穷大(或者未定义)。所以C4.5的实际做法是:先从候选特征中选出信息增益高于平均值的特征,再从中选增益率最高的。这是一个两阶段筛选,避免了增益率对取值少特征的极端偏好。

这个细节很多教程一笔带过,但实际写代码时如果漏掉,会出现除零错误或者选出无意义的单值特征。我在早期实现时就踩过这个坑,当时数据里有个特征所有样本取值都一样,结果程序直接崩了。

3.3 C4.5对连续特征和缺失值的处理

C4.5相比ID3最大的工程改进是支持连续特征和缺失值。

连续特征处理用的是二分法。假设特征$a$在D上有m个不同取值,排序后得到${a^1, a^2, ..., a^m}$。取相邻值的中点$\frac{a^i + a^{i+1}}{2}$作为划分点,把D分成$\leq t$和$> t$两部分。对每个候选划分点算信息增益率,选最好的。这里有个优化:如果相邻两个样本类别相同,它们之间的划分点可以跳过,因为不会带来纯度提升。

缺失值处理分两个层面。一是在选划分特征时,只用在特征$a$上没有缺失的样本子集$\tilde{D}$来计算增益,最后乘以一个系数$\frac{|\tilde{D}|}{|D|}$来惩罚缺失多的特征。二是划分样本时,如果样本在特征$a$上缺失,就把它同时分配到所有子节点,但带一个权重,权重等于各子节点样本比例。

def gain_ratio_with_missing(X, y, feature_idx, missing_mask): # 只用非缺失样本计算增益 valid = ~missing_mask[:, feature_idx] X_valid, y_valid = X[valid], y[valid] if len(y_valid) == 0: return 0 gain = information_gain(X_valid, y_valid, feature_idx) # 惩罚系数 penalty = len(y_valid) / len(y) # 分裂信息 values, counts = np.unique(X_valid[:, feature_idx], return_counts=True) probs = counts / len(y_valid) split_info = -sum(p * np.log2(p) for p in probs if p > 0) if split_info == 0: return 0 return (gain / split_info) * penalty

注意:缺失值处理会让样本在树中"分裂"成多份,训练完后每个样本可能落在多个叶子节点上。预测时如果遇到缺失值,也是按权重加权各叶子的输出。这个机制在sklearn的决策树里没有完全复现,sklearn要求先自己填充缺失值。

3.4 悲观剪枝:C4.5的过拟合对策

决策树不剪枝几乎必然过拟合。C4.5用的是悲观剪枝(Pessimistic Error Pruning, PEP)。它的思路是:对每个子树,比较剪枝前后在训练集上的错误率,如果剪枝后错误率没有显著上升,就剪掉。

具体做法是给每个叶子节点的错误数加0.5的惩罚(连续修正),然后计算子树的错误率上界。如果子树根节点的错误率上界小于等于子树的加权错误率,就剪枝。这个"悲观"体现在它假设训练集上的错误率会比真实错误率低,所以加了惩罚项。

PEP的优点是只需要训练集,不需要单独的验证集,在数据量少的时候很实用。缺点是偏向于剪掉那些叶子节点多的子树,可能剪过头。我个人的经验是,如果数据量够,还是用验证集做剪枝更稳。

4. CART:基尼指数与二叉树的组合拳

4.1 基尼指数为什么比熵计算更快

CART用基尼指数替代信息熵:

$$Gini(D) = 1 - \sum_{k=1}^{K} p_k^2$$

基尼指数可以理解为"从D中随机抽两个样本,类别不一致的概率"。它的计算比熵简单,因为不需要算对数。在特征选择时,CART算的是划分后的加权基尼指数:

$$Gini_index(D, a) = \sum_{v=1}^{V} \frac{|D^v|}{|D|} Gini(D^v)$$

选使这个值最小的特征。

为什么基尼指数比熵快?熵要算$\log_2$,基尼只要算平方。在大规模数据上,这个差异会累积。有研究表明,基尼指数和熵在大多数情况下选出的特征是一致的,只在少数边界情况下有差异。所以sklearn默认用基尼指数,不是没有道理的。

4.2 CART为什么坚持二叉树

ID3和C4.5生成的是多叉树,一个特征有几个取值就分几叉。CART不管特征有几个取值,只做二分。对离散特征,它会把取值分成两组,穷举所有二分组合选最优的。对连续特征,它选一个切分点$t$,分成$\leq t$和$> t$。

二叉树的好处有三个。第一,避免多叉树带来的碎片化。多叉树在取值多的特征上会把数据切得很碎,每个子节点样本少,统计不可靠。第二,二叉树等价于递归地做二分类,结构更规整,剪枝和后续处理更方便。第三,对离散特征的有序性不敏感。多叉树把离散取值当作无序的,但CART的二分可以捕捉到"取值A和B一组,C和D一组"这种组合信息。

代价是计算量。对V个取值的离散特征,CART要穷举$2^{V-1} - 1$种二分组合。V大了会爆炸。所以实际实现里,对离散特征通常限制取值数,或者用启发式搜索代替穷举。

4.3 CART回归树:用方差代替基尼

CART不仅能分类,还能回归。回归树的叶子输出不是类别,而是该节点样本的均值。特征选择的指标从基尼指数变成均方误差(MSE):

$$MSE(D, a) = \sum_{v=1}^{2} \frac{|D^v|}{|D|} \text{Var}(D^v)$$

选使MSE最小的划分。预测时,新样本落到哪个叶子,就输出那个叶子的均值。

这里有个实操细节:回归树对异常值敏感,因为均值会被极端值拉偏。如果数据里有异常值,可以考虑用中位数代替均值,或者先做异常值处理。我在一个房价预测项目里就遇到过,几套豪宅把整个区域的预测值拉高了,后来把目标变量做了对数变换才缓解。

def cart_regression_split(X, y): best_mse = float('inf') best_feature, best_threshold = None, None for f in range(X.shape[1]): thresholds = np.unique(X[:, f]) for t in thresholds: left = y[X[:, f] <= t] right = y[X[:, f] > t] if len(left) == 0 or len(right) == 0: continue mse = (len(left) * np.var(left) + len(right) * np.var(right)) / len(y) if mse < best_mse: best_mse = mse best_feature, best_threshold = f, t return best_feature, best_threshold

4.4 代价复杂度剪枝:CART的剪枝逻辑

CART用的是代价复杂度剪枝(Cost Complexity Pruning, CCP),也叫 weakest link pruning。它的目标函数是:

$$R_\alpha(T) = R(T) + \alpha |T|$$

其中$R(T)$是树T的训练误差,$|T|$是叶子节点数,$\alpha$是惩罚系数。$\alpha$越大,对树的大小惩罚越重,树越简单。

算法分两步。第一步,从完整的树开始,自下而上计算每个内部节点的"剪枝收益"。对每个内部节点,算它被剪掉后$R_\alpha$的变化,找到使$R_\alpha$最小的那个节点剪掉,得到一个子树序列。第二步,用交叉验证在子树序列里选最优的$\alpha$。

sklearn里的ccp_alpha参数就是控制这个的。默认是0,不剪枝。实际用的时候,我一般先用cost_complexity_pruning_path拿到一系列$\alpha$值,然后对每个$\alpha$训练一棵树,用验证集选最好的。

实操心得:ccp_alpha的调参不要凭感觉。先用path = clf.cost_complexity_pruning_path(X_train, y_train)拿到path.ccp_alphas,然后对每个alpha训练模型看验证集准确率,画一条曲线,选拐点。这个流程比手动试快得多,而且有据可循。

5. 三个算法在实际项目中的选型与踩坑记录

5.1 什么场景选哪个算法

虽然现在sklearn的DecisionTreeClassifier默认就是CART的变体,但理解三个算法的差异对选型和调参仍然重要。

场景推荐算法理由
纯离散特征,数据量小ID3实现简单,信息增益直观
混合特征,有缺失值C4.5原生支持连续值和缺失值
需要回归任务CART唯一支持回归的
需要特征重要性排序CART基尼重要性稳定,sklearn原生支持
需要概率输出CART叶子节点类别比例即概率

我个人的经验是,实际项目里90%的情况直接用CART。ID3和C4.5更多是理解原理的阶梯,工程上CART的二叉树结构、剪枝机制、回归能力都更完善。但如果你在面试或者教学场景,能把三个算法的演进逻辑讲清楚,是很加分的。

5.2 特征重要性怎么算才靠谱

sklearn的feature_importances_用的是基尼重要性:一个特征在所有节点上带来的基尼指数下降的加权和。但这个指标有两个坑。

第一,它偏向于高基数特征(取值多的特征),和ID3的信息增益偏好类似。第二,它是在训练集上算的,如果过拟合了,重要性排序可能不可信。

更稳的做法是用排列重要性(Permutation Importance):把某个特征的值随机打乱,看模型性能下降多少。下降越多说明该特征越重要。这个方法是模型无关的,而且可以在验证集上算。

from sklearn.inspection import permutation_importance result = permutation_importance(clf, X_val, y_val, n_repeats=10, random_state=42) for i in result.importances_mean.argsort()[::-1]: print(f"Feature {i}: {result.importances_mean[i]:.4f} +/- {result.importances_std[i]:.4f}")

5.3 决策树过拟合的几种表现和应对

决策树过拟合的表现很直观:训练集准确率接近100%,验证集准确率差一大截。我总结了几种典型情况和应对手段。

情况一:树太深,叶子节点样本太少。应对:限制max_depth,或者设min_samples_leaf。我一般先设min_samples_leaf=5起步,根据数据量调整。

情况二:特征太多,噪声特征被选中。应对:先做特征筛选,或者用max_features限制每次分裂考虑的特征数。后者会让树更随机,类似随机森林的思路。

情况三:连续特征切分点太细。应对:对连续特征先分箱,或者设min_samples_split防止在样本少的节点上继续切。

情况四:类别不平衡,树偏向多数类。应对:设class_weight='balanced',或者对少数类过采样。

from sklearn.tree import DecisionTreeClassifier clf = DecisionTreeClassifier( criterion='gini', max_depth=8, min_samples_leaf=5, min_samples_split=10, class_weight='balanced', ccp_alpha=0.01, random_state=42 )

5.4 一个完整的调参流程

我把实际项目里的调参流程整理成步骤,可以直接抄。

  1. 先不剪枝训练一棵完整的树,看训练集和验证集的准确率差距。差距大说明过拟合严重,需要强剪枝。
  2. 用cost_complexity_pruning_path拿到alpha序列,对每个alpha训练模型,画验证集准确率曲线,选拐点附近的alpha。
  3. 固定alpha,调max_depth和min_samples_leaf。这两个参数控制树的复杂度,先用网格搜索粗调,再细调。
  4. 如果还有提升空间,试max_features。设成sqrt(n_features)或log2(n_features),引入随机性。
  5. 最后用排列重要性检查特征,把重要性接近0的特征删掉,重新训练。

这个流程走下来,一般能比默认参数提升5到15个百分点。具体提升多少取决于数据质量,如果特征本身噪声大,提升有限。

5.5 决策树的几个隐藏坑

坑一:random_state不设,结果不可复现。决策树在特征重要性相同时会随机选一个,不设随机种子每次跑的结果可能不一样。这个坑我在复现实验时踩过,找了半天原因。

坑二:类别标签必须是数值。sklearn不接受字符串标签,要先LabelEncoder。但注意,LabelEncoder会把标签映射成0,1,2...,如果标签本身有序(比如"低中高"),这个映射是合理的;如果无序(比如"红绿蓝"),映射后的数值大小没有意义,但决策树不会受影响,因为它只关心相等与否。

坑三:连续特征的量纲不影响决策树。决策树不需要归一化,因为它只关心大小关系,不关心绝对距离。这点和SVM、KNN不同。但如果你用排列重要性,量纲会影响打乱后的效果,不过影响不大。

坑四:决策树对训练集的小扰动敏感。数据里改几个样本,树的结构可能大变。这是决策树方差大的表现,也是随机森林、GBDT这些集成方法存在的原因。如果单棵树效果不稳定,考虑上集成。

6. 从单棵树到集成:决策树的延伸价值

6.1 为什么决策树是集成学习的基石

决策树虽然单棵效果一般,但它是随机森林、GBDT、XGBoost、LightGBM这些强算法的基学习器。原因有三个。

第一,决策树是"弱学习器"的典型代表,容易过拟合但偏差低。集成方法通过组合多棵低偏差、高方差的树,降低方差,提升泛化能力。第二,决策树的训练可以并行,随机森林里每棵树独立训练,天然适合分布式。第三,决策树对特征缩放不敏感,预处理简单,适合作为集成的组件。

理解了单棵树的构建原理,再看随机森林的"随机选特征"、GBDT的"拟合残差",就很容易明白它们在解决什么问题。随机森林是在降低方差,GBDT是在降低偏差。

6.2 决策树的可解释性在业务中的价值

我做过一个信贷风控的项目,业务方最关心的不是AUC多高,而是"为什么拒绝这个客户"。决策树能直接给出一条规则路径:收入低于X且负债率高于Y且近半年逾期次数大于Z,拒绝。这条规则业务方能看懂,能拿去和客户解释,也能拿去和监管沟通。

这是神经网络做不到的。神经网络可能效果更好,但它是黑盒。在需要解释性的场景,决策树和它的集成变体(尤其是GBDT配合SHAP值)是首选。

6.3 决策树规则提取的实操方法

从训练好的树里提取规则,sklearn提供了tree_.threshold和tree_.children_left/right,可以遍历。但更简单的方法是用export_text:

from sklearn.tree import export_text rules = export_text(clf, feature_names=feature_names) print(rules)

输出是缩进的文本,每个if-else分支就是一条规则。如果要转成业务规则表,可以自己写个递归函数遍历树结构,把路径上的条件收集起来。

注意:提取出来的规则在训练集上100%准确,但不代表在真实数据上有效。规则要配合覆盖率一起看,覆盖率太低的规则(比如只覆盖几个样本)不要用,那是过拟合的产物。

6.4 决策树在特征工程中的反向应用

决策树不仅能做预测,还能用来做特征工程。具体做法是:用决策树对特征做离散化,把连续特征切成若干区间,每个区间作为一个新特征。这个方法叫基于树的离散化,在逻辑回归等线性模型前处理中很有用。

另一个用法是用决策树的叶子节点编号作为新特征,喂给其他模型。这就是GBDT+LR的经典套路:GBDT负责特征组合和离散化,LR负责最终分类。这个方案在点击率预测里用了很多年,效果稳定。

# 用决策树叶子编号做特征 leaf_ids = clf.apply(X_train) # 返回每个样本落在哪个叶子 # leaf_ids 可以作为新特征喂给其他模型

这个套路的原理是:决策树的每条路径代表一种特征组合,叶子编号就是组合的编码。相比手动做交叉特征,树自动找到了有区分度的组合。

7. 手写实现与sklearn对照:几个关键差异

7.1 分裂标准的实现差异

手写ID3时,我用的是信息增益。sklearn的criterion='entropy'也是信息增益,但它的实现做了优化:用直方图加速连续特征的阈值搜索,而不是穷举所有唯一值。这个优化在大数据上差异明显,手写版在10万样本上可能跑几分钟,sklearn几秒就完了。

CART的基尼指数在sklearn里也是直方图实现。如果你要自己实现,建议先对特征排序,然后用累积和算左右子集的基尼,避免每次重新遍历。

7.2 停止条件的差异

手写版的停止条件通常只有"全同一类"和"特征用完"。sklearn多了几个:max_depth、min_samples_split、min_samples_leaf、min_impurity_decrease。其中min_impurity_decrease最容易被忽略,它要求分裂带来的不纯度下降至少达到这个值,否则不分裂。这个参数在噪声大的数据上很有用,能防止树在无意义的特征上浪费深度。

7.3 缺失值处理的差异

前面说过,C4.5原生支持缺失值,但sklearn的决策树不支持。sklearn要求输入没有NaN。如果你有缺失值,要么填充,要么用支持缺失值的梯度提升库(比如某些版本的XGBoost和LightGBM)。这是个实际使用中必须注意的差异,别以为C4.5支持就以为sklearn也支持。

7.4 多输出任务的差异

sklearn的决策树支持多输出:一个样本可以同时预测多个标签。比如预测一个人的多个属性(性别、年龄段、收入档)。实现上,它把多输出当作多个单输出任务,在每个节点上算所有输出的平均不纯度下降。这个功能在手写版里通常不实现,因为用得少,但知道有这回事,遇到多标签任务时能想到。

8. 写在最后:我这些年用决策树的几点体会

决策树是我工具箱里最常用的算法之一,不是因为它效果最好,而是因为它最"透明"。每次模型效果不好,我第一件事就是打印一棵决策树看看,往往能发现数据里的问题:某个特征的分裂点很奇怪,说明数据有异常值;某个分支特别深,说明这类样本特别难分;某个特征从来没被用到,说明它可能是冗余的。

ID3、C4.5、CART这三个算法,我建议每个学机器学习的人都亲手实现一遍。不是为了造轮子,而是为了理解"特征选择"这件事的本质。信息增益、增益率、基尼指数,这三个指标背后的思想——用不确定性度量纯度——会贯穿你后续学随机森林、GBDT、甚至神经网络的很多设计。

最后分享一个我常用的调试技巧:训练完一棵树后,用export_text打印出来,然后拿几条验证集样本,手动沿着树走一遍,看预测路径是否符合直觉。如果不符合,要么是数据有问题,要么是树过拟合了。这个手动验证的过程,比看准确率数字更能发现问题。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/22 6:18:08

虚拟电厂投标三层MILP框架:联合投标、动态滚动与避坑实践

先别急着上三层框架。我一开始也是单层优化的支持者&#xff0c;原因很直接&#xff1a;VPP内部资源就那些&#xff0c;把批发能源市场和监管服务市场的所有约束一起塞进一个混合整数线性规划&#xff08;MILP&#xff09;模型&#xff0c;理论上可以同时给出投标量和运行计划&…

作者头像 李华
网站建设 2026/10/22 6:18:00

sherpa-onnx安卓端语音识别实战:离线ASR/TTS轻量化部署指南

1. 项目概述&#xff1a;为什么“把语音模型塞进手机”这件事突然变得关键又可行最近在几个技术社区里&#xff0c;几乎每天都能看到类似“sherpa-onnx Android ASR”“离线语音识别 手机端部署”这样的关键词被高频提起。不是因为大家突然对语音识别产生了新鲜感&#xff0c;而…

作者头像 李华
网站建设 2026/10/22 6:15:57

手推反向传播:从链式法则到梯度更新的完整推导

1. 为什么值得把反向传播的公式亲手推一遍很多人第一次接触神经网络&#xff0c;是从一行model.fit()或者一个loss.backward()开始的。框架把梯度算好了&#xff0c;参数也更新了&#xff0c;代码跑通的那一刻确实很爽&#xff0c;但心里总有点虚——那个backward()到底在背后干…

作者头像 李华
网站建设 2026/10/22 6:15:49

Agent工程化核心:全插件化架构与可回放会话日志设计

前段时间给一个 Agent 开源框架做工程化改造&#xff0c;我最大的体会是&#xff1a;让 Agent 跑起来不难&#xff0c;难的是让它在一个清晰的工程结构里稳定地跑、可插拔地扩展、出问题时还能把整个过程像放录像一样重放一遍。这篇想拆的&#xff0c;就是这个框架里最值得借鉴…

作者头像 李华
网站建设 2026/10/22 6:15:00

开源AI基础设施议程拆解:算力调度与数据编排成核心焦点

每年的开源年会我都会格外盯着基础设施方向的议程&#xff0c;今年的AI基础设施开源论坛议程放出之后&#xff0c;我第一时间把几个关键topic圈了出来——算力调度、存储编排、训练加速、推理服务化&#xff0c;基本覆盖了大模型落地时最让人头疼的那几个环节。作为一个长期在集…

作者头像 李华
网站建设 2026/10/22 6:14:27

概率统计真题PDF高效复习:结构化标签、考频分析与三轮模拟法

简介&#xff1a;川大《概率统计&#xff08;理工&#xff09;》14-17年历年期末试卷真题&#xff08;含答案&#xff09;PDF&#xff0c;是面向四川大学理工科本科生的期末复习资料。试卷覆盖概率论与数理统计核心考点&#xff0c;包含填空题与解答题&#xff0c;涉及全概率公…

作者头像 李华