第四章:决策树与集成学习

决策树这东西,说白了就是「如果-那么」的规则集合。我刚开始做机器学习时,觉得它太简单了,不就是一堆if else嘛。后来在工业界做风控模型时才发现,决策树的可解释性简直是救命稻草——业务方要你解释为什么拒绝贷款申请,你总不能甩给他一个神经网络权重矩阵吧?

4.1 信息增益与基尼系数

决策树的核心问题只有一个:如何选择最优划分特征。这里有两个主流指标,我分别说说我的理解。

4.1.1 信息增益(ID3算法)

信息增益基于信息论中的「熵」概念。熵衡量的是数据的不确定性——熵越大,数据越混乱。

# 计算熵的公式
import numpy as np

def entropy(y):
    classes, counts = np.unique(y, return_counts=True)
    probs = counts / len(y)
    return -np.sum(probs * np.log2(probs))

# 信息增益 = 父节点熵 - 子节点加权熵
def information_gain(X, y, feature_idx, threshold):
    parent_entropy = entropy(y)
    # 按阈值划分左右子节点
    left_mask = X[:, feature_idx] <= threshold
    right_mask = ~left_mask
    # 加权子节点熵
    n = len(y)
    child_entropy = (np.sum(left_mask) / n) * entropy(y[left_mask]) + \
                    (np.sum(right_mask) / n) * entropy(y[right_mask])
    return parent_entropy - child_entropy

我在项目中遇到过一个问题:ID3算法偏好取值多的特征。比如「用户ID」这种特征,每个样本一个值,信息增益会特别大,但显然不适合用来做决策。这就是信息增益的天然缺陷。

避坑指南:我曾经用ID3做文本分类,特征维度很高,结果树长得又深又乱,过拟合得一塌糊涂。后来改用C4.5(信息增益率)才解决问题。

4.1.2 基尼系数(CART算法)

基尼系数是另一种分裂标准,它衡量的是从数据集中随机抽取两个样本,其类别不一致的概率。基尼系数越小,纯度越高。

def gini(y):
    classes, counts = np.unique(y, return_counts=True)
    probs = counts / len(y)
    return 1 - np.sum(probs ** 2)

# 基尼增益
def gini_gain(X, y, feature_idx, threshold):
    parent_gini = gini(y)
    left_mask = X[:, feature_idx] <= threshold
    right_mask = ~left_mask
    n = len(y)
    child_gini = (np.sum(left_mask) / n) * gini(y[left_mask]) + \
                 (np.sum(right_mask) / n) * gini(y[right_mask])
    return parent_gini - child_gini

你想想看,基尼系数计算起来比熵快——没有对数运算。CART算法默认用基尼系数,而且生成的是二叉树,结构更简洁。我个人习惯用CART做基线模型,速度快,效果也不差。

指标计算公式特点适用算法
信息增益Entropy(parent) - Σ(weight * Entropy(child))偏好取值多的特征ID3
信息增益率信息增益 / 特征熵惩罚多取值特征C4.5
基尼系数1 - Σ(p_i²)计算快,二叉树CART

4.2 决策树剪枝

决策树如果不剪枝,很容易长成「参天大树」——训练集准确率100%,测试集一塌糊涂。这就是典型的过拟合。

剪枝分两种:预剪枝和后剪枝。我个人的经验是:预剪枝更实用,因为后剪枝需要先生成完整树,计算成本高。

4.2.1 预剪枝

在树生长过程中提前停止。常用策略包括:

  • 限制最大深度(max_depth)
  • 限制叶子节点最少样本数(min_samples_leaf)
  • 限制分裂所需的最小样本数(min_samples_split)
  • 限制信息增益/基尼增益的最小阈值
调参技巧:我一般先设max_depth=3跑一遍,看训练集和验证集的准确率差距。如果差距大,说明过拟合,继续减小深度或增大min_samples_leaf。如果差距小但准确率低,说明欠拟合,适当增加深度。

4.2.2 后剪枝

先生成完整树,再自底向上剪掉对泛化能力贡献不大的分支。CART的后剪枝用「代价复杂度剪枝」,引入一个参数α来控制树的复杂度。

# 代价复杂度剪枝的损失函数
# Loss = 训练误差 + α * 叶子节点数
# α越大,树越简单

嗯,这里要注意:后剪枝虽然理论上更优,但实际项目中我很少用。为什么?因为数据量大时,生成完整树太慢了。预剪枝配合交叉验证,效果已经足够好。

4.3 随机森林

随机森林是Bagging思想 + 决策树的组合。它做了两件事来降低方差:

  1. 样本随机:每棵树用Bootstrap采样训练
  2. 特征随机:每个分裂点只考虑随机子集的特征

我记得有一次做用户流失预测,单棵决策树的AUC只有0.72,随机森林直接干到0.85。这就是集成的力量——弱学习器组合成强学习器。

from sklearn.ensemble import RandomForestClassifier

rf = RandomForestClassifier(
    n_estimators=100,      # 树的数量
    max_depth=10,          # 最大深度
    min_samples_leaf=5,    # 叶子节点最少样本数
    max_features='sqrt',   # 特征采样比例
    random_state=42
)
rf.fit(X_train, y_train)
核心参数调优:
  • n_estimators:越大越好,但收益递减。我一般设100-500
  • max_depth:防止过拟合,通常5-20
  • max_features:分类问题用sqrt,回归问题用1/3
  • min_samples_leaf:防止叶子节点过小,通常1-10

4.4 XGBoost原理与调参

XGBoost是梯度提升决策树(GBDT)的工程化实现。它比随机森林更「聪明」——每棵新树都在拟合前一棵树的残差。

4.4.1 核心原理

XGBoost的目标函数包含两部分:

Obj = Σ L(y_i, ŷ_i) + Σ Ω(f_k)
其中:
- L是损失函数(如平方误差、对数损失)
- Ω是正则项,控制树的复杂度
- Ω(f) = γT + ½λ||w||²
  - T是叶子节点数
  - w是叶子权重
  - γ和λ是正则化参数

说白了,XGBoost在追求「准」的同时,也在追求「简」。这个正则项是它比传统GBDT强的重要原因。

4.4.2 调参实战

我调XGBoost有个固定套路,分享给你:

import xgboost as xgb

# 第一步:设定初始参数
params = {
    'objective': 'binary:logistic',
    'learning_rate': 0.1,      # 学习率,默认0.3
    'max_depth': 6,            # 树深度,默认6
    'subsample': 0.8,          # 样本采样比例
    'colsample_bytree': 0.8,   # 特征采样比例
    'gamma': 0,                # 分裂所需的最小损失减少
    'reg_lambda': 1,           # L2正则化
    'reg_alpha': 0,            # L1正则化
    'eval_metric': 'auc'
}

# 第二步:用早停法找最优迭代次数
dtrain = xgb.DMatrix(X_train, label=y_train)
dval = xgb.DMatrix(X_val, label=y_val)

model = xgb.train(
    params,
    dtrain,
    num_boost_round=1000,
    evals=[(dtrain, 'train'), (dval, 'val')],
    early_stopping_rounds=50,
    verbose_eval=50
)
我曾经踩过的坑:有一次调参,learning_rate设了0.01,num_boost_round设了5000,结果训练了整整两个小时还没收敛。后来发现learning_rate太小,迭代次数又不够。建议learning_rate从0.1开始调,配合早停法,效率高很多。

4.4.3 调参顺序

我建议按这个顺序调:

  1. 先调max_depth和learning_rate:深度3-10,学习率0.01-0.3
  2. 再调subsample和colsample_bytree:防止过拟合,通常0.6-0.9
  3. 最后调正则化参数:gamma、reg_lambda、reg_alpha

你想想看,为什么先调深度和学习率?因为这两个参数对模型性能影响最大,先确定它们的合理范围,后面调参才有意义。

知识体系总览

下面这张图展示了本章的核心逻辑:

决策树与集成学习知识体系 决策树 信息增益 / 基尼系数 剪枝策略 CART算法 集成学习 Bagging → 随机森林 Boosting → XGBoost 样本随机 + 特征随机 降低方差,防止过拟合 梯度提升 + 正则化 调参:深度→学习率→正则化 核心思想:弱学习器 → 强学习器

从决策树到随机森林,再到XGBoost,本质上是一个从「简单规则」到「集成智慧」的演进过程。我个人觉得,理解决策树是基础,掌握集成方法是进阶,而调参则是把理论落地到工程的关键一步。

最后说一句:别被调参吓到。先跑通一个基线模型,再逐步优化。我在实际项目中,80%的情况用默认参数就能拿到不错的结果。调参只是锦上添花,不是雪中送炭。

可可精品资料库