1. 决策树算法家族的整体设计思路拆解
1.1 为什么决策树值得反复拿出来讲
干了这么多年数据挖掘,如果让我选一个最能体现“机器学习直觉”的算法,决策树绝对排前三。它不像神经网络那样是个黑盒,也不像支持向量机那样需要核函数技巧,决策树的每一个分支、每一次分裂,你都能用自然语言解释清楚——“因为收入大于某个阈值,所以判断这个人会买”。这种可解释性在实际业务中太重要了,风控、医疗辅助诊断、客户流失预警,这些场景里你没法跟业务方说“模型说不行就是不行”,你得给出理由。
CART、ID3、C4.5这三个算法,基本代表了决策树从诞生到成熟的三代演进。ID3是祖师爷,1986年由Quinlan提出,核心是用信息增益做特征选择;C4.5是Quinlan自己对ID3的改进,1993年发表,引入了信息增益率来解决ID3偏向取值多的特征的毛病;CART是Breiman等人在1984年提出的,全称Classification And Regression Trees,用基尼指数做分裂准则,而且天然支持回归任务。这三个算法串起来学,你能清楚地看到一条算法演进的脉络:从“能用”到“好用”再到“通用”。
我见过不少同学一上来就调sklearn的DecisionTreeClassifier,参数一设,fit完事。这样用没问题,但遇到特征类型复杂、缺失值多、需要剪枝调优的场景,不了解底层原理就会很被动。比如为什么C4.5要单独处理连续特征?为什么CART用二叉树而不是多叉树?这些问题不搞清楚,调参就是盲人摸象。
1.2 三个算法的核心差异与选型逻辑
先上一张对比表,把三个算法的关键差异摆清楚:
| 维度 | ID3 | C4.5 | CART |
|---|---|---|---|
| 分裂准则 | 信息增益 | 信息增益率 | 基尼指数 |
| 树结构 | 多叉树 | 多叉树 | 二叉树 |
| 特征类型 | 离散 | 离散+连续 | 离散+连续 |
| 缺失值处理 | 不支持 | 支持 | 支持 |
| 剪枝策略 | 无 | 悲观剪枝 | 代价复杂度剪枝 |
| 任务类型 | 分类 | 分类 | 分类+回归 |
| 特征复用 | 不可复用 | 不可复用 | 可复用 |
这张表里的每一行背后都有故事。ID3用信息增益,问题是信息增益偏向于取值多的特征。举个例子,如果有个“身份证号”特征,每个样本取值都不同,那它的信息增益会非常高,因为按它分裂后每个叶子节点纯度都是100%。但这显然没有泛化意义。C4.5引入信息增益率,用分裂信息量做分母来惩罚取值多的特征,就是为了解决这个问题。
CART选择基尼指数而不是熵,主要是因为基尼指数的计算不涉及对数运算,速度更快。而且CART强制二叉树,每次分裂只分成两份,这让树的结构更简洁,也方便处理连续特征——连续特征只需要找一个最优切分点,一分为二即可。另外CART的树可以重复使用同一个特征,这在ID3和C4.5里是不允许的。
选型上我的经验是:如果数据量不大、特征全是离散的、想快速出个可解释的模型,ID3够用;如果特征里有连续值、有缺失值,C4.5更合适;如果要做回归任务,或者想要更灵活的剪枝控制,直接上CART。实际工程中CART用得最多,sklearn的DecisionTreeClassifier底层就是CART的优化实现。
1.3 从信息论到基尼指数的数学直觉
这部分我想用最直白的方式讲清楚,不堆公式。
信息熵衡量的是不确定性。一个数据集里如果只有一类样本,熵为0,因为结果完全确定;如果两类样本各占一半,熵最大为1(二分类情况下)。熵的公式是:
$$H(D) = -\sum_{k=1}^{K} p_k \log_2 p_k$$
其中$p_k$是第k类样本的比例。log以2为底,单位是比特。你可以把熵理解为“要把这个数据集的所有样本类别都确定下来,最少需要问多少个是/否问题”。
信息增益是分裂前的熵减去分裂后的加权熵。增益越大,说明用这个特征分裂后不确定性下降得越多。但前面说了,取值多的特征天然占便宜。
信息增益率在信息增益的基础上除以分裂信息量:
$$GainRatio(D, A) = \frac{Gain(D, A)}{IV(A)}$$
其中$IV(A) = -\sum_{v=1}^{V} \frac{|D^v|}{|D|} \log_2 \frac{|D^v|}{|D|}$,V是特征A的取值个数。取值越多,IV越大,增益率就被压得越低。
基尼指数则是另一种衡量纯度的方式:
$$Gini(D) = 1 - \sum_{k=1}^{K} p_k^2$$
二分类时,如果两类各占一半,基尼指数为0.5;如果只有一类,基尼指数为0。基尼指数越小,纯度越高。CART选择让分裂后基尼指数最小的特征和切分点。
用生活类比:熵和基尼指数都在衡量“一筐水果里混了多少种”。熵像是用“需要问几个问题才能猜出随机拿的是哪种水果”来衡量混乱程度,基尼指数则是“随机拿两个水果,它们不是同一种的概率”。两者单调性一致,但基尼指数算起来不用查对数表,手算都快。
2. 核心细节解析与实操要点
2.1 ID3的信息增益计算与递归分裂
ID3的整个训练过程就是一个递归分裂的过程。伪代码逻辑是这样的:
- 计算当前数据集D的信息熵
- 对每个特征A,计算用A分裂后的信息增益
- 选择信息增益最大的特征作为当前节点的分裂特征
- 按该特征的每个取值把D划分成子集
- 对每个子集递归执行1-4步
- 直到子集为空、或所有样本同属一类、或没有特征可用了
这里有几个实操细节值得注意。第一,ID3不能处理连续特征,遇到连续值必须先离散化,比如等频分箱或等宽分箱。第二,ID3没有剪枝,生成的树容易过拟合,尤其在特征多、样本少的时候。第三,ID3对缺失值无能为力,数据里有缺失就得先填充或删除。
我早期用ID3做客户分类时踩过一个坑:有个特征叫“客户编号”,取值几百个,信息增益排第一,结果树的第一层就按客户编号分裂,每个叶子就一两个样本,完全没有泛化能力。这就是信息增益偏向多值特征的典型表现。后来换成C4.5才解决。
2.2 C4.5的连续特征处理与缺失值策略
C4.5对连续特征的处理是二分法。假设特征A有n个不同取值,排序后得到$a_1 < a_2 < ... < a_n$,C4.5会考虑n-1个切分点,每个切分点取相邻两值的中点。对每个切分点,把样本分成“小于等于切分点”和“大于切分点”两组,计算信息增益率,选最大的那个切分点。
这个过程的时间复杂度是O(n log n)(排序)+ O(n)(遍历切分点),比离散特征的处理要慢,但效果通常更好。注意,C4.5对连续特征只能二分,不能多分,这是它的一个限制。
缺失值处理是C4.5的一大亮点。它的策略是:在计算信息增益率时,只考虑该特征非缺失的样本,然后按非缺失样本的比例对增益进行加权。在分裂时,缺失样本会被同时分配到所有子节点,但分配时带一个权重,权重等于该子节点样本数占非缺失样本总数的比例。这样既利用了缺失样本的信息,又不会引入偏差。
注意:C4.5的缺失值处理在预测阶段也生效。如果预测样本在某个分裂特征上缺失,它会走所有分支,然后把各分支的结果加权汇总。这个逻辑在实现时容易漏掉。
2.3 CART的基尼指数与二叉树构建
CART和ID3/C4.5最大的不同在于它构建的是二叉树。对离散特征,CART会把特征取值分成两组,每组是一个子节点。如果有V个取值,理论上有个$2^{V-1}-1$种二分方式(去掉空集和全集),CART会遍历所有二分方式找基尼指数最小的。当V很大时这个遍历开销很大,实际实现中通常会限制或采用启发式搜索。
对连续特征,CART和C4.5类似,也是找最优切分点二分。但CART的切分点选择是直接优化基尼指数,而不是信息增益率。
CART的另一个特点是支持回归任务。回归树的叶子节点输出的是该节点样本目标值的均值,分裂准则是最小化均方误差(MSE)。分类树和回归树在结构上完全一样,只是分裂准则和叶子输出不同。
CART的剪枝用的是代价复杂度剪枝(Cost-Complexity Pruning),也叫 weakest link pruning。它通过一个复杂度参数$\alpha$来平衡树的复杂度和拟合程度,最终选一个最优的$\alpha$对应的子树。sklearn里的ccp_alpha参数就是干这个的。
2.4 三种剪枝策略的对比与选择
剪枝是决策树防止过拟合的关键手段。三个算法对应的剪枝策略差异很大:
| 算法 | 剪枝策略 | 核心思想 | 优缺点 |
|---|---|---|---|
| ID3 | 无 | 不剪枝 | 容易过拟合 |
| C4.5 | 悲观剪枝 | 用训练集误差加上惩罚项估计泛化误差 | 不需要额外验证集,但偏向于过度剪枝 |
| CART | 代价复杂度剪枝 | 用α控制树复杂度,交叉验证选最优 | 理论完备,但计算量较大 |
悲观剪枝的思路是:把叶子节点的训练误差加上一个基于二项分布的惩罚项,如果父节点的估计误差小于子节点加权估计误差,就剪掉子树。这个方法的优点是只用训练集,不需要单独划验证集,适合小数据。缺点是惩罚项是启发式的,可能剪得过多。
代价复杂度剪枝则是自底向上生成一系列子树,每个子树对应一个$\alpha$值,然后用交叉验证选最优。这个方法更严谨,但计算量更大。实际用sklearn时,我通常先让树完全生长,然后通过ccp_alpha参数调剪枝强度,配合交叉验证找最优值。
3. 实操过程与核心环节实现
3.1 从零实现ID3的完整代码
下面这段代码是我自己写的ID3实现,没有调sklearn,纯numpy和pandas,方便你理解每一步在干什么。
import numpy as np import pandas as pd from collections import Counter def entropy(y): """计算信息熵""" counts = Counter(y) total = len(y) ent = 0.0 for count in counts.values(): p = count / total ent -= p * np.log2(p) return ent def information_gain(X, y, feature): """计算某个特征的信息增益""" base_ent = entropy(y) values = X[feature].unique() weighted_ent = 0.0 for v in values: subset_y = y[X[feature] == v] weighted_ent += (len(subset_y) / len(y)) * entropy(subset_y) return base_ent - weighted_ent def id3(X, y, features, depth=0, max_depth=10): """递归构建ID3树""" # 终止条件 if len(set(y)) == 1: return y.iloc[0] if len(features) == 0 or depth >= max_depth: return Counter(y).most_common(1)[0][0] # 选择信息增益最大的特征 gains = {f: information_gain(X, y, f) for f in features} best_feature = max(gains, key=gains.get) # 构建树 tree = {best_feature: {}} remaining_features = [f for f in features if f != best_feature] for v in X[best_feature].unique(): subset_X = X[X[best_feature] == v] subset_y = y[X[best_feature] == v] if len(subset_X) == 0: tree[best_feature][v] = Counter(y).most_common(1)[0][0] else: tree[best_feature][v] = id3(subset_X, subset_y, remaining_features, depth+1, max_depth) return tree这段代码里有个细节:max_depth参数是我自己加的,ID3原始版本没有深度限制,但实际用的时候不加限制很容易过拟合。另外,当某个特征取值对应的子集为空时,我用父节点样本的多数类作为叶子输出,这是常见的处理方式。
3.2 C4.5的增益率计算与连续特征二分
C4.5的实现比ID3复杂一些,主要是多了增益率和连续特征处理。增益率的计算:
def split_info(X, feature): """计算分裂信息量""" values = X[feature].unique() total = len(X) si = 0.0 for v in values: p = len(X[X[feature] == v]) / total si -= p * np.log2(p) return si def gain_ratio(X, y, feature): """计算信息增益率""" gain = information_gain(X, y, feature) si = split_info(X, feature) if si == 0: return 0 return gain / si连续特征的处理需要先找最优切分点:
def best_split_point(X, y, feature): """对连续特征找最优二分点""" sorted_values = sorted(X[feature].unique()) best_gain = -1 best_point = None for i in range(len(sorted_values) - 1): point = (sorted_values[i] + sorted_values[i+1]) / 2 # 二分 left_mask = X[feature] <= point right_mask = X[feature] > point if left_mask.sum() == 0 or right_mask.sum() == 0: continue # 计算二分后的信息增益 base_ent = entropy(y) left_ent = entropy(y[left_mask]) right_ent = entropy(y[right_mask]) weighted_ent = (left_mask.sum()/len(y))*left_ent + (right_mask.sum()/len(y))*right_ent gain = base_ent - weighted_ent if gain > best_gain: best_gain = gain best_point = point return best_point, best_gain注意:连续特征的最优切分点是在当前节点上找的,不是全局找的。每次递归到新节点,都要重新找切分点。这是很多人实现时容易搞错的地方。
3.3 CART的基尼指数与递归二分实现
CART的核心是基尼指数和二分。基尼指数的计算:
def gini(y): """计算基尼指数""" counts = Counter(y) total = len(y) gini_val = 1.0 for count in counts.values(): p = count / total gini_val -= p ** 2 return gini_val def cart_split(X, y, feature): """CART对某个特征找最优二分""" values = sorted(X[feature].unique()) best_gini = float('inf') best_point = None # 对离散特征,遍历所有二分组合;对连续特征,遍历相邻中点 if len(values) == 2: points = [values[0]] else: points = [(values[i] + values[i+1]) / 2 for i in range(len(values)-1)] for point in points: left_mask = X[feature] <= point right_mask = X[feature] > point if left_mask.sum() == 0 or right_mask.sum() == 0: continue left_gini = gini(y[left_mask]) right_gini = gini(y[right_mask]) weighted_gini = (left_mask.sum()/len(y))*left_gini + (right_mask.sum()/len(y))*right_gini if weighted_gini < best_gini: best_gini = weighted_gini best_point = point return best_point, best_giniCART的递归构建和ID3类似,但每次分裂是二分,而且特征可以重复使用。这意味着在递归时不需要从特征列表中移除已用特征。
3.4 用sklearn快速验证与参数调优
自己实现一遍是为了理解原理,实际项目里直接用sklearn。下面是一个完整的调参示例:
from sklearn.tree import DecisionTreeClassifier from sklearn.model_selection import GridSearchCV, train_test_split from sklearn.metrics import classification_report from sklearn.datasets import load_iris # 加载数据 data = load_iris() X_train, X_test, y_train, y_test = train_test_split( data.data, data.target, test_size=0.3, random_state=42 ) # 参数网格 param_grid = { 'criterion': ['gini', 'entropy'], 'max_depth': [3, 5, 7, 10, None], 'min_samples_split': [2, 5, 10], 'min_samples_leaf': [1, 2, 4], 'ccp_alpha': [0.0, 0.01, 0.05] } # 网格搜索 clf = DecisionTreeClassifier(random_state=42) grid_search = GridSearchCV(clf, param_grid, cv=5, scoring='accuracy', n_jobs=-1) grid_search.fit(X_train, y_train) print("最优参数:", grid_search.best_params_) print("最优交叉验证得分:", grid_search.best_score_) # 用最优模型预测 best_clf = grid_search.best_estimator_ y_pred = best_clf.predict(X_test) print(classification_report(y_test, y_pred))这里有几个参数值得展开说。criterion选gini还是entropy,实际差异不大,gini计算快一点,entropy在某些数据集上效果略好。max_depth是最重要的正则化参数,树越深越容易过拟合。min_samples_split和min_samples_leaf控制节点分裂的最小样本数,样本少的时候调大这两个值能有效防止过拟合。ccp_alpha是代价复杂度剪枝参数,从0开始逐步增大,配合交叉验证找最优。
我一般调参的顺序是:先定max_depth,再调min_samples_leaf,最后微调ccp_alpha。min_samples_split和min_samples_leaf有重叠,通常调一个就行。
4. 常见问题与排查技巧实录
4.1 信息增益偏向多值特征的诊断与解决
这个问题我在实际项目中遇到过好几次。症状是:树的第一层分裂特征是一个取值特别多的特征,比如用户ID、订单号、时间戳。诊断方法很简单,打印每个特征的信息增益和取值个数,如果某个特征取值数远大于其他特征,且信息增益排第一,基本就是这个问题。
解决方案有三个。第一,直接删掉这类无意义的ID类特征,这是最省事的。第二,改用C4.5的增益率,用分裂信息量做惩罚。第三,对多值特征先做降维或聚类,把取值数压下来。我通常优先选第一种,因为ID类特征本来就不该进模型。
实操心得:在特征工程阶段就把ID类、唯一值过多的特征标记出来,训练前直接排除。不要指望算法自动处理,信息增益的偏好是数学性质决定的,不是bug。
4.2 连续特征切分点选择的效率优化
C4.5和CART对连续特征都要遍历切分点,当特征取值很多时(比如几万个不同值),遍历开销很大。优化思路有两个。一是先对特征取值做等频分箱,把连续值离散化成几十个桶,再在桶边界上找切分点。二是用近似算法,比如只考虑分位数点作为候选切分点。
sklearn的DecisionTreeClassifier有个max_features参数,可以限制每次分裂时考虑的特征数,间接加速。另外,如果数据量特别大,可以考虑用直方图-based的决策树实现,比如LightGBM,它把连续特征离散化成直方图桶,分裂时只在桶边界找切分点,速度快很多。
4.3 过拟合与欠拟合的快速判断
决策树的过拟合判断很直观:训练集准确率很高(比如99%),测试集准确率低很多(比如70%),就是过拟合。欠拟合则是训练集和测试集准确率都低。
过拟合的解决手段按优先级排:第一,限制max_depth;第二,增大min_samples_leaf;第三,开启ccp_alpha剪枝;第四,减少特征数。欠拟合则相反:放宽深度限制、减小min_samples_leaf、增加特征。
我一般会画一条学习曲线,横轴是训练集大小,纵轴是准确率。如果训练集准确率远高于交叉验证准确率,且两者不收敛,就是过拟合。如果两者都低且收敛,就是欠拟合。
4.4 缺失值处理的常见坑
C4.5和CART都支持缺失值,但实现细节容易出错。第一个坑是:在计算分裂准则时,缺失样本的权重处理。C4.5的做法是只对非缺失样本计算增益,然后乘以非缺失样本比例。如果忘了乘这个比例,增益会被高估。
第二个坑是预测阶段的缺失值处理。训练时缺失样本被分配到所有子节点,预测时也要走所有分支,然后加权汇总。如果实现时只走了一个分支,结果会偏。
第三个坑是缺失值比例过高。如果某个特征缺失超过50%,即使算法支持,效果也会很差。我的经验是缺失超过30%的特征直接考虑删除或做特殊标记,不要硬塞给模型。
4.5 常见问题速查表
| 问题现象 | 可能原因 | 排查方法 | 解决方案 |
|---|---|---|---|
| 树第一层用ID类特征分裂 | 信息增益偏向多值特征 | 打印各特征取值数和增益 | 删除ID类特征或改用增益率 |
| 训练集准确率远高于测试集 | 过拟合 | 画学习曲线 | 限制深度、增大min_samples_leaf、剪枝 |
| 训练和测试准确率都低 | 欠拟合 | 检查特征是否太少 | 增加特征、放宽深度限制 |
| 连续特征分裂效果差 | 切分点选择不当 | 检查切分点分布 | 先做等频分箱再分裂 |
| 缺失值处理结果异常 | 权重计算错误 | 检查缺失样本权重 | 按非缺失比例加权 |
| 树太深训练太慢 | 无深度限制 | 查看树深度 | 设max_depth或min_samples_leaf |
4.6 决策树的可解释性输出技巧
决策树最大的优势是可解释性,但sklearn默认的export_text输出格式比较简陋。我通常用plot_tree配合matplotlib画图,或者用export_graphviz导出dot文件再用Graphviz渲染。如果要在报告里展示,我会手动提取关键路径,把从根到叶子的决策规则写成if-else形式。
from sklearn.tree import export_text tree_rules = export_text(best_clf, feature_names=data.feature_names) print(tree_rules)这段代码输出的规则很直观,比如“如果花瓣长度<=2.45,则是setosa”。实际业务中,我会把规则翻译成业务语言,比如“如果客户最近30天消费金额低于100元且活跃天数少于5天,则流失风险高”。这种规则业务方一看就懂,比给个概率值有用得多。
5. 三个算法的Python工程化封装建议
5.1 统一接口设计
如果你要把这三个算法封装成一个库,建议设计统一的接口。输入是DataFrame和标签列名,输出是树结构和预测结果。内部根据算法类型选择不同的分裂准则和树结构。这样调用方不需要关心底层是ID3还是CART,只需要传一个algorithm参数。
接口可以这样设计:
class DecisionTree: def __init__(self, algorithm='cart', max_depth=None, min_samples_leaf=1, ccp_alpha=0.0): self.algorithm = algorithm self.max_depth = max_depth self.min_samples_leaf = min_samples_leaf self.ccp_alpha = ccp_alpha self.tree = None def fit(self, X, y): if self.algorithm == 'id3': self.tree = self._fit_id3(X, y) elif self.algorithm == 'c4.5': self.tree = self._fit_c45(X, y) elif self.algorithm == 'cart': self.tree = self._fit_cart(X, y) return self def predict(self, X): return [self._predict_one(row, self.tree) for _, row in X.iterrows()]这种封装方式的好处是调用方代码简洁,切换算法只需要改一个参数。缺点是内部实现要处理三种算法的差异,代码量不小。
5.2 性能优化与并行化
决策树的训练瓶颈主要在特征分裂点的搜索上。对每个节点,都要遍历所有特征的所有切分点。优化手段有:第一,对特征预排序,这样找切分点时不用每次重新排序;第二,用直方图近似,把连续值离散化;第三,多线程并行搜索不同特征的切分点。
Python里可以用joblib做并行,但要注意GIL的限制。如果数据量真的很大,建议直接用sklearn或LightGBM,它们的底层是C实现的,比纯Python快几个数量级。自己实现主要是为了学习原理,生产环境还是用成熟库。
5.3 模型持久化与版本管理
训练好的树模型要保存下来供后续使用。最简单的方式是用pickle或joblib序列化整个模型对象。但要注意,如果代码更新了类的定义,旧模型可能反序列化失败。更稳妥的方式是把树结构导出成JSON或字典,预测时用独立的解析函数加载。
import json def save_tree(tree, filepath): with open(filepath, 'w') as f: json.dump(tree, f) def load_tree(filepath): with open(filepath, 'r') as f: return json.load(f)这种方式的另一个好处是树结构可读,方便人工审查。我通常会把树结构存成JSON,同时存一份特征列表和类别映射,这样即使代码重构了,旧模型也能正常加载。
6. 从决策树到集成学习的延伸思考
6.1 决策树作为基学习器的优势
决策树虽然单棵效果一般,但它是集成学习的绝佳基学习器。随机森林、GBDT、XGBoost、LightGBM,这些工业界最常用的模型,底层都是决策树。原因有三:第一,决策树对特征缩放不敏感,不需要归一化;第二,决策树能自动处理特征交互;第三,决策树的训练可以并行化,适合大规模数据。
随机森林用bagging思想,训练多棵决策树然后投票。GBDT用boosting思想,每棵树拟合前面模型的残差。XGBoost和LightGBM在GBDT基础上做了工程优化,速度和精度都大幅提升。理解了单棵决策树的原理,这些集成模型就不难理解了。
6.2 特征重要性评估的两种方式
决策树天然能输出特征重要性。sklearn的feature_importances_属性用的是基尼重要性,即某个特征在所有节点分裂中带来的基尼指数下降的加权和。这个指标计算简单,但有个问题:它偏向于取值多的特征,和ID3的信息增益偏好类似。
另一种方式是排列重要性(Permutation Importance),把某个特征的值随机打乱,看模型性能下降多少。下降越多,特征越重要。这种方式更可靠,但计算量更大。我通常两种都算,如果差异很大,就以排列重要性为准。
6.3 决策树在业务规则提取中的应用
决策树最被低估的用途是业务规则提取。训练一棵浅树(深度3-5),把从根到叶的路径翻译成if-else规则,直接给业务方用。这些规则不需要部署模型,业务系统里写几个if-else就能跑,维护成本极低。
我做过一个客户流失预警的项目,用CART训练了一棵深度4的树,提取出8条规则。业务方把这8条规则配到CRM系统里,对命中的客户自动发优惠券,流失率降了十几个百分点。整个过程没有用任何复杂的模型部署,就是一棵浅树加几条规则。这种落地方式在传统行业特别受欢迎,因为IT部门不用维护模型服务,业务部门也能看懂规则。
实操心得:给业务方交付规则时,不要直接给树结构图,他们看不懂。把规则翻译成“如果...那么...”的自然语言,每条规则配一个业务解释。比如“如果最近一次消费距今超过90天且历史消费频次低于3次,则流失风险高”。这样业务方才能用起来。
6.4 决策树的局限性与替代方案
决策树不是万能的。它的主要局限有:第一,对异或问题处理不好,因为异或需要多层分裂才能表达;第二,对平滑的决策边界拟合能力差,决策边界是轴平行的;第三,单棵树方差大,数据微小变化可能导致树结构剧变。
替代方案取决于场景。如果追求精度,用随机森林或GBDT;如果追求可解释性且数据线性可分,用逻辑回归;如果决策边界复杂且数据量大,用神经网络。但决策树作为基线模型永远值得先跑一遍,因为它快、可解释、能帮你快速理解数据。
我个人在实际操作中的体会是,决策树这三个算法值得每个做数据的人亲手实现一遍。不是为了在生产中用自己写的版本,而是为了在调sklearn参数时知道每个参数在干什么,在模型效果不好时知道往哪个方向调。信息增益、增益率、基尼指数这三个概念,理解了之后再看随机森林和GBDT,很多设计选择就豁然开朗了。最后再分享一个小技巧:如果你不确定该用gini还是entropy,先用gini跑一遍,再用entropy跑一遍,对比交叉验证得分,差异通常在小数点后两位,选哪个都行,别在这个上面纠结太久。