Skip to content

元数据卡

  • 前置知识:ch04 ML 基础、ch05 线性模型
  • 预计时间:50 分钟
  • 核心难度:进阶
  • 阅读模式:高度专注
  • 完成标志:能实现决策树训练、理解 Bagging 和 Boosting 的区别、使用 XGBoost

你的进度

线性模型在工坊里跑了几天,表现不错——但数据越来越复杂。面积和价格之间不是简单的直线关系,有些拐点、有些分层。

你看了看旁边的工具架,拿出一把更粗糙但更灵活的工具:决策树。它不用算公式,它只是不停地问问题——每一步分裂数据,直到每个分组足够纯净。

你的任务

决策树是一种完全不同的学习范式:通过一系列"是/否"问题分割输入空间。单棵树容易过拟合,但通过集成——Bagging(随机森林)和 Boosting(XGBoost)——它成为过去十年最实战的模型家族之一。

本章分层

  • 必读:决策树分裂准则(Gini/信息增益)、随机森林、XGBoost
  • 选读:梯度提升的数学推导
  • 进阶:直方图加速、分位数近似

破局 · 溯源

想象你在给一个怪物分类。你拿了一把尺子,开始问:身高超过 2 米吗?体重超过 100kg 吗?有翅膀吗?

每个问题砍掉一部分可能性,分裂越来越细——最终每个叶子节点只剩下一类。这棵"问题树"不需要任何数学变换,不需要特征标准化,就是你用直觉做分类的方式。

决策树

决策树的训练核心是:在每个节点选择一个特征和一个切分点,使得分裂后的数据"纯度"最大。

常用纯度指标:

  • 信息增益 = 父节点熵 - 加权子节点熵
  • Gini 不纯度 = 1 - sum(p_k^2)
python
# 决策树从零实现(简化版)
import numpy as np

class DecisionTreeNode:
    def __init__(self, depth=0, max_depth=5, min_samples=2):
        self.depth = depth
        self.max_depth = max_depth
        self.min_samples = min_samples
        self.feature = None
        self.threshold = None
        self.left = None
        self.right = None
        self.value = None

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

    def build(self, X, y):
        n_samples, n_features = X.shape
        n_classes = len(np.unique(y))

        # 停止条件
        if (n_samples < self.min_samples
            or n_classes == 1
            or self.depth >= self.max_depth):
            self.value = np.bincount(y).argmax()
            return

        best_gain = -1
        for f in range(n_features):
            thresholds = np.unique(X[:, f])
            for t in thresholds:
                mask = X[:, f] <= t
                if sum(mask) == 0 or sum(~mask) == 0:
                    continue
                gain = self.gini(y) - (
                    sum(mask)/n_samples * self.gini(y[mask]) +
                    sum(~mask)/n_samples * self.gini(y[~mask]))
                if gain > best_gain:
                    best_gain = gain
                    self.feature = f
                    self.threshold = t

        if best_gain < 0:
            self.value = np.bincount(y).argmax()
            return

        mask = X[:, self.feature] <= self.threshold
        self.left = DecisionTreeNode(self.depth+1, self.max_depth, self.min_samples)
        self.right = DecisionTreeNode(self.depth+1, self.max_depth, self.min_samples)
        self.left.build(X[mask], y[mask])
        self.right.build(X[~mask], y[~mask])

单棵决策树极容易过拟合(除非剪枝或限制深度)。但它有几个先天优势:可解释性极强(你可以把树画出来直接看),对缺失值鲁棒,不需要特征缩放。

随机森林

随机森林用 Bagging(Bootstrap Aggregating)解决决策树的过拟合问题:训练多棵树,每棵树用不同的数据子集(有放回采样)和不同的特征子集,对结果取平均或投票。

python
from sklearn.ensemble import RandomForestClassifier

rf = RandomForestClassifier(
    n_estimators=100,
    max_depth=10,
    min_samples_split=5,
    random_state=42
)
rf.fit(X_train, y_train)
print(f"RF 准确率: {rf.score(X_test, y_test):.3f}")

随机森林的两个随机源(数据随机 + 特征随机)让各棵树的相关性降低,投票结果更稳定。在表格数据上,随机森林通常是应该最先尝试的模型。

特征重要性可以从随机森林中直接读到:遍历所有树,统计每个特征在节点分裂时带来的不纯度降低总量。

python
importances = rf.feature_importances_
for i, imp in enumerate(importances):
    print(f"特征 {i}: {imp:.3f}")

XGBoost

Boosting(提升)和 Bagging 思路相反:顺序训练一系列弱学习器,每个新学习器重点关注前一个的预测错误。

XGBoost 是梯度提升决策树(GBDT)的工程化实现,在 Kaggle 竞赛中统治了多年。

python
import xgboost as xgb

dtrain = xgb.DMatrix(X_train, label=y_train)
dtest = xgb.DMatrix(X_test, label=y_test)

params = {
    'objective': 'binary:logistic',
    'max_depth': 6,
    'eta': 0.3,      # 学习率
    'subsample': 0.8,
    'colsample_bytree': 0.8,
    'eval_metric': 'logloss'
}

model = xgb.train(
    params, dtrain,
    num_boost_round=100,
    evals=[(dtest, 'eval')],
    early_stopping_rounds=10
)

XGBoost 的几个关键工程优化:

  • 二阶泰勒展开近似损失函数(比一阶梯度信息更准)
  • 正则化项直接加入目标函数(控制模型复杂度)
  • 列采样和行采样(类似随机森林)
  • 分位数近似加速特征搜索
  • 稀疏感知算法(对缺失值自动学习分支方向)

Bagging 减少方差,Boosting 减少偏差。随机森林可以在满深度树上工作,XGBoost 需要用浅树(max_depth=3~6)配合学习率逐步逼近。

常见陷阱

  • 决策树对数据中的细微变化非常敏感——删掉一个训练样本可能导致整棵树重建。这就是为什么单棵树的稳定性差。
  • 随机森林的 n_estimators 不是越大越容易过拟合(它与 Boosting 不同),但更多树需要更多内存和推理时间。
  • XGBoost 的 early_stopping_rounds 依赖验证集——验证集不能太大也不能太小。
  • 调参顺序:先调树的深度/学习率,再调正则化参数,最后调采样比例。
  • 树模型对特征尺度的天然鲁棒是优势,但并不意味着特征工程不重要——特征交叉对树同样有帮助。

通关挑战

  • 热身(10 分钟):用 sklearn.datasets.load_iris,分别训练决策树(max_depth=3)、随机森林(100 棵树)、XGBoost(50 轮),比较三个模型的准确率。
  • 挑战(30 分钟):用随机森林的 feature_importances_ 做特征选择——只保留最重要的 5 个特征再用 XGBoost 训练,看性能变化。
  • 观察:在 XGBoost 中记录每轮迭代的训练和验证 logloss 曲线,观察 Boosting 如何逐步降低误差。

旅人笔记

集成学习是一种"三个臭皮匠顶一个诸葛亮"的工程思维:把多个不完美的模型组合起来,利用它们的多样性降低整体误差。随机森林和 XGBoost 各胜擅场,至今仍是表格数据上的王者。

-> 下一站预告

有监督学习需要标签。但现实中大量数据没有标签——无监督学习登场。

Built with VitePress | Software Systems Atlas