跳至内容
决策树与集成学习

决策树与集成学习

决策树把复杂的分类决策变成一串「是/否」问题;集成学习则把多棵树组合起来,效果远超单棵树。XGBoost / LightGBM 正是基于此,成为结构化数据竞赛的默认武器。

决策树的直觉

假设要判断「这个邮件是不是垃圾邮件」,决策树的工作方式就像问卷:

是否包含「免费领取」?
├─ 是 → 是否来自陌生发件人?
│        ├─ 是 → 垃圾邮件 ✓
│        └─ 否 → 正常邮件 ✗
└─ 否 → 是否有附件?
         ├─ 是 → 正常邮件 ✗
         └─ 否 → 正常邮件 ✗

每个节点选一个特征做判断,叶节点给出预测。关键问题:每步选哪个特征来切分?

信息增益与基尼系数

好的切分应该让每个子集尽可能「纯」(同一类别集中)。衡量纯度的两个常用指标:

基尼系数(CART 算法默认)

Gini(D) = 1 - Σpₖ²

pₖ 是类别 k 在样本集中的比例。全部是同一类时 Gini = 0(最纯),各类均等时 Gini = 0.5(最乱)。

信息熵(ID3/C4.5 算法)

Entropy(D) = -Σpₖ × log₂(pₖ)

每次切分选让信息增益(熵的减少量)最大的特征。

from sklearn.tree import DecisionTreeClassifier, export_text
from sklearn.datasets import load_iris

X, y = load_iris(return_X_y=True)

# max_depth 控制树深度,防止过拟合
tree = DecisionTreeClassifier(max_depth=3, criterion="gini", random_state=42)
tree.fit(X, y)

# 可视化树结构(文字版)
print(export_text(tree, feature_names=load_iris().feature_names))

决策树的优缺点

优点缺点
可视化,业务可解释极易过拟合(深树几乎记住训练集)
不需要特征缩放对训练数据的小变动很敏感
自动处理非线性单棵树预测能力上限低

随机森林:集成的第一步

核心思想:训练多棵独立的决策树,预测时投票(分类)或取平均(回归)。

为了让每棵树"不一样"(增加多样性),随机森林做了两个随机化:

  1. Bootstrap 采样:每棵树从训练集中有放回地随机抽取等量样本(大约 63% 不重复的样本)。
  2. 特征随机选取:每次切分时,只从随机选取的一个特征子集中找最优切分(而非全部特征)。

多棵「有差异的树」集成,随机误差相互抵消,偏差基本不变但方差大幅降低,泛化能力远超单棵树。

from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import cross_val_score
from sklearn.datasets import load_breast_cancer

X, y = load_breast_cancer(return_X_y=True)

rf = RandomForestClassifier(
    n_estimators=200,    # 树的数量,越多越稳定,但边际效益递减
    max_depth=None,      # None 表示不限深度(每棵树会过拟合,但集成后不怕)
    max_features="sqrt", # 每次切分考虑 √特征数 个候选特征
    n_jobs=-1,           # 并行训练
    random_state=42,
)

scores = cross_val_score(rf, X, y, cv=5, scoring="f1")
print(f"5-Fold F1: {scores.mean():.4f} ± {scores.std():.4f}")

# 特征重要性
rf.fit(X, y)
importances = pd.Series(rf.feature_importances_,
                        index=load_breast_cancer().feature_names)
print(importances.nlargest(10))

梯度提升树(GBDT):序列集成

随机森林是并行集成(同时训练多棵树),梯度提升是串行集成:每棵新树专门去修正前面所有树的残差。

第 1 棵树:粗略拟合目标
第 2 棵树:拟合第 1 棵树的残差(没学对的部分)
第 3 棵树:拟合前两棵树累计残差
...
最终预测 = 所有树预测值的加权求和

这里的「梯度」是:每棵新树拟合的是损失函数关于前一轮预测的负梯度,从而让集成的损失函数沿梯度方向减小。这是 GBDT 名称的来源。


XGBoost 与 LightGBM:工程级实现

XGBoost(2016)和 LightGBM(2017)是对 GBDT 的工程优化,也是结构化数据的首选算法:

特性XGBoostLightGBM
生长策略按层(level-wise)按叶(leaf-wise,精度更高)
大数据速度较快更快(直方图算法)
内存占用较高更低
类别特征需手动编码原生支持
适用规模中小数据集大数据集首选

LightGBM 完整示例

import lightgbm as lgb
import pandas as pd
import numpy as np
from sklearn.model_selection import train_test_split
from sklearn.metrics import roc_auc_score

# 模拟结构化数据(二分类)
np.random.seed(42)
n = 5000
X = pd.DataFrame({
    "age":      np.random.randint(18, 70, n),
    "income":   np.random.exponential(50000, n),
    "score":    np.random.normal(600, 100, n),
    "category": np.random.choice(["A", "B", "C", "D"], n),
    "region":   np.random.choice(["东", "南", "西", "北"], n),
})
y = ((X["income"] > 60000) & (X["score"] > 620)).astype(int)

X_train, X_val, y_train, y_val = train_test_split(
    X, y, test_size=0.2, random_state=42, stratify=y
)

train_data = lgb.Dataset(X_train, label=y_train,
                         categorical_feature=["category", "region"])
val_data   = lgb.Dataset(X_val,   label=y_val, reference=train_data)

params = {
    "objective":       "binary",
    "metric":          "auc",
    "num_leaves":      63,       # 叶节点数,控制模型复杂度(越大越复杂)
    "learning_rate":   0.05,     # 学习率,配合 early_stopping
    "feature_fraction": 0.8,    # 每棵树随机选 80% 特征(防过拟合)
    "bagging_fraction": 0.8,    # 每棵树随机选 80% 样本
    "bagging_freq":    5,
    "min_child_samples": 20,    # 叶节点最小样本数(防过拟合)
    "verbose":         -1,
}

model = lgb.train(
    params,
    train_data,
    num_boost_round=1000,
    valid_sets=[val_data],
    callbacks=[
        lgb.early_stopping(stopping_rounds=50),  # 50 轮无改善自动停止
        lgb.log_evaluation(100),
    ]
)

y_pred = model.predict(X_val)
print(f"验证集 AUC: {roc_auc_score(y_val, y_pred):.4f}")
print(f"最优迭代轮次: {model.best_iteration}")

XGBoost 示例

import xgboost as xgb

dtrain = xgb.DMatrix(X_train, label=y_train, enable_categorical=True)
dval   = xgb.DMatrix(X_val,   label=y_val,   enable_categorical=True)

params = {
    "objective":   "binary:logistic",
    "eval_metric": "auc",
    "max_depth":   6,
    "eta":         0.05,      # 学习率
    "subsample":   0.8,
    "colsample_bytree": 0.8,
    "tree_method": "hist",    # 直方图加速(大数据集必用)
}

model = xgb.train(
    params, dtrain,
    num_boost_round=1000,
    evals=[(dval, "val")],
    early_stopping_rounds=50,
    verbose_eval=False,
)

超参数调优实战(Optuna)

import optuna

def objective(trial):
    params = {
        "objective":    "binary",
        "metric":       "auc",
        "num_leaves":   trial.suggest_int("num_leaves", 20, 200),
        "learning_rate":trial.suggest_float("lr", 0.01, 0.3, log=True),
        "feature_fraction": trial.suggest_float("ff", 0.5, 1.0),
        "min_child_samples": trial.suggest_int("min_child", 5, 100),
        "verbose": -1,
    }
    cv_result = lgb.cv(
        params, train_data, num_boost_round=300,
        nfold=5, stratified=True,
        callbacks=[lgb.early_stopping(20)],
        return_cvbooster=False,
    )
    return max(cv_result["valid auc-mean"])

study = optuna.create_study(direction="maximize")
study.optimize(objective, n_trials=50, show_progress_bar=True)
print(f"最佳参数: {study.best_params}")
print(f"最佳 AUC: {study.best_value:.4f}")

用 SHAP 解释模型预测

梯度提升树很强,但「黑盒」让业务方不放心。SHAP(SHapley Additive exPlanations)能解释每个特征对每条预测的贡献:

import shap

explainer = shap.TreeExplainer(model)
shap_values = explainer.shap_values(X_val)

# 全局特征重要性(Beeswarm 图)
shap.summary_plot(shap_values, X_val, plot_type="beeswarm")

# 单条样本解释(Waterfall 图)
shap.waterfall_plot(shap.Explanation(
    values=shap_values[0],
    base_values=explainer.expected_value,
    data=X_val.iloc[0],
    feature_names=X_val.columns.tolist()
))

一句话小结

决策树好理解但爱过拟合;随机森林并行集成降方差;GBDT / XGBoost / LightGBM 串行集成降偏差,是结构化数据的黄金标准。三者都不需要特征缩放,原生处理混合类型特征,配合 SHAP 还能解释单条预测。

最后更新于