决策树与集成学习
决策树把复杂的分类决策变成一串「是/否」问题;集成学习则把多棵树组合起来,效果远超单棵树。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))决策树的优缺点
| 优点 | 缺点 |
|---|---|
| 可视化,业务可解释 | 极易过拟合(深树几乎记住训练集) |
| 不需要特征缩放 | 对训练数据的小变动很敏感 |
| 自动处理非线性 | 单棵树预测能力上限低 |
随机森林:集成的第一步
核心思想:训练多棵独立的决策树,预测时投票(分类)或取平均(回归)。
为了让每棵树"不一样"(增加多样性),随机森林做了两个随机化:
- Bootstrap 采样:每棵树从训练集中有放回地随机抽取等量样本(大约 63% 不重复的样本)。
- 特征随机选取:每次切分时,只从随机选取的一个特征子集中找最优切分(而非全部特征)。
多棵「有差异的树」集成,随机误差相互抵消,偏差基本不变但方差大幅降低,泛化能力远超单棵树。
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 的工程优化,也是结构化数据的首选算法:
| 特性 | XGBoost | LightGBM |
|---|---|---|
| 生长策略 | 按层(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 还能解释单条预测。