Bex Tuychiev 2023-12-27
本文正文较长但内容详实,因此我们将引言尽可能简短,直接从问题开始:“为什么要使用梯度提升?”
原因有很多,而且都非常充分:
- 梯度提升是最优选择:在表格型监督学习任务中,其准确性和性能无与伦比。
- 梯度提升高度通用:可用于众多重要任务,如回归、分类、排序和生存分析。
- 梯度提升可解释性强:不像神经网络这类“黑箱”算法,梯度提升不会为了性能而牺牲可解释性。它像一块瑞士手表一样精密,只要有耐心,甚至可以向小学生讲清楚它的工作原理。
- 梯度提升实现成熟:它不是那种只有理论价值而缺乏实际用途的算法。Python 中的 XGBoost 和 LightGBM 等多种梯度提升库已被数十万用户广泛使用。
- 梯度提升屡获佳绩:自 2015 年以来,专业人士一直在 Kaggle 等平台的表格数据竞赛中凭借梯度提升持续夺冠。
如果你对以上任何一点哪怕只有一丝兴趣,那么继续阅读本文就非常值得。
那么,让我们开始吧!
本教程你将学到什么?
本文最重要的目标是:让你在不经历太多数学痛苦的前提下,牢固掌握梯度提升算法的内部工作机制。毕竟,梯度提升是为了实践应用,而不是为了数学分析。
梯度提升究竟是什么?
提升(Boosting)是机器学习中一种强大的集成技术。与独立从数据中学习的传统模型不同,提升通过组合多个弱学习器(weak learners)的预测结果,构建出一个更准确的强学习器(strong learner)。
我刚刚用了一堆新术语,让我逐一解释,先从“弱学习器”开始。
弱学习器是一种机器学习模型,其性能仅略优于随机猜测模型。例如,假设我们要将蘑菇分为可食用和不可食用两类。如果一个随机猜测模型的准确率为 40%,那么弱学习器的准确率可能只是略高一些,比如 50%–60%。
提升算法会组合几十个甚至上百个这样的弱学习器,构建出一个强学习器,在同一个问题上准确率可能超过 95%。
最流行的弱学习器是决策树,因其几乎能适用于任何数据集而被广泛采用。
梯度提升的实际应用
梯度提升已成为机器学习领域的一股主导力量,其应用现已遍及各行各业,从预测客户流失到探测小行星。以下是它在 Kaggle 竞赛和现实世界中的成功案例:
称霸 Kaggle 竞赛:
- Otto Group 商品分类挑战赛:前十名全部使用了梯度提升的 XGBoost 实现。
- Santander 客户交易预测竞赛:基于 XGBoost 的解决方案再次包揽预测客户行为和金融交易的榜首位置。
- Netflix 电影推荐挑战赛:梯度提升在为 Netflix 等千亿级公司构建推荐系统中发挥了关键作用。
转型商业与产业:
- 零售与电商:个性化推荐、库存管理、欺诈检测
- 金融与保险:信用风险评估、客户流失预测、算法交易
- 医疗与医药:疾病诊断、药物发现、个性化医疗
- 搜索与在线广告:搜索排序、广告定向、点击率预测
现在,终于让我们揭开这个传奇算法的神秘面纱!
梯度提升算法:分步详解
输入
梯度提升算法适用于具有特征(X)和目标变量(y)的表格型数据。和其他机器学习算法一样,其目标是从训练数据中充分学习,以便对未见过的数据点具有良好泛化能力。
为了理解梯度提升的底层过程,我们将使用一个仅有四行记录的简单销售数据集。利用三个特征——客户年龄、购买类别和购买重量,我们希望预测购买金额:
| 客户年龄 | 购买类别 | 购买重量 | 购买金额(目标) |
|---|---|---|---|
| 25 | 电子产品 | 2.5 | 123.45 |
| 34 | 服装 | 1.3 | 56.78 |
| 42 | 电子产品 | 5.0 | 345.67 |
| 19 | 家居用品 | 3.2 | 98.01 |
梯度提升中的损失函数
在机器学习中,损失函数是一个关键组成部分,用于量化模型预测值与真实值之间的差异。本质上,它衡量模型的表现。
其作用可分解如下:
- 计算误差:将模型的预测输出与真实值(实际观测值)进行比较。具体如何比较(即如何计算差异)因函数而异。
- 指导模型训练:模型的目标是最小化损失函数。在训练过程中,模型不断更新其内部结构和参数配置,以使损失尽可能小。
- 评估指标:通过比较训练集、验证集和测试集上的损失,可以评估模型的泛化能力并避免过拟合。
最常见的两种损失函数是:
- 均方误差(MSE):这是回归任务中常用的损失函数,衡量预测值与真实值之间差值的平方和。梯度提升通常使用以下变体:
之所以乘以 1/2,是为了求导方便。根据幂法则,对该损失函数求导时,1/2 会与平方项抵消,最终导数简化为 ,大大简化了数学运算并降低了计算开销。
- 交叉熵(Cross-entropy):该函数衡量两个概率分布之间的差异,常用于目标为离散类别的分类任务。
由于我们这里做的是回归任务,因此将使用 MSE。
步骤 1:做出初始预测
梯度提升是一种逐步提高准确率的算法。要启动这一过程,我们需要一个初始猜测或预测。初始预测始终是目标变量的平均值。换句话说,在第一轮中,我们的模型预测所有购买金额都相同——156 美元:
之所以选择平均值,与我们选用的损失函数及其导数有关。在整个过程中,我们都在寻找使损失函数最小化的值。换句话说,我们在寻找使损失函数导数(梯度)为零的值。
当我们对每个观测值的损失函数关于预测值求导并求和时,结果恰好是目标变量的平均值。
因此,我们的初始预测是 156 美元。请记住这一点,我们继续往下走。
步骤 2:计算伪残差(Pseudo-residuals)
下一步是计算每个观测值与初始预测之间的差值:观测值 - 156。为便于说明,我们将这些差值放入一个新列:
| 客户年龄 | 购买类别 | 购买重量 | 购买金额(目标) | 伪残差(观测值 - 预测值) |
|---|---|---|---|---|
| 25 | 电子产品 | 2.5 | 123.45 | -32.55 |
| 34 | 服装 | 1.3 | 56.78 | -99.22 |
| 42 | 电子产品 | 5.0 | 345.67 | 189.45 |
| 19 | 家居用品 | 3.2 | 98.01 | -58.01 |
注意,在线性回归中,观测值与预测值之间的差称为“残差”。为了区别于线性回归,我们称其为“伪残差”(还有其他原因,但本文不展开)。
步骤 3:构建弱学习器
接下来,我们将构建一棵决策树(弱学习器),用三个特征(年龄、类别、购买重量)来预测这些伪残差。为简化问题,我们将决策树限制为仅四个叶节点(终端节点),但在实践中,人们通常选择 8 到 32 个叶节点。

拟合完成后,我们对每行数据进行预测。以第一行为例:

- 类别为“电子产品”,年龄 25 < 30 → 落入左子树的叶节点,预测残差为 -32.55
- 最终预测 = 初始预测 + 残差 = 156 + (-32.55) = 123.45
哇!我们得到了完美预测!那为什么还要构建其他树呢?
因为此时模型严重过拟合了训练数据。我们希望模型具备泛化能力。为此,梯度提升引入了一个名为**学习率(learning rate)**的参数。
学习率是介于 0 和 1 之间的乘数,用于缩放每个弱学习器的预测值(详见下文)。如果我们设定学习率为 0.1,那么最终预测变为:
不再是完美的 123.45。
同样地,对第二行进行预测:

- 类别为“服装”,年龄 35 → 落入某叶节点,预测残差为 -99.22
- 最终预测 = 156 + 0.1 × (-99.22) ≈ 146.08
依此类推,我们得到四行的预测值:152.75、146.08、174.945、150.2。将它们加入表格:

接着,我们计算新的伪残差:购买金额 - 新预测值,并更新表格(去掉旧列):

可以看到,新的伪残差更小了,说明损失正在下降。
步骤 4:迭代
接下来,我们重复步骤 3,即构建更多弱学习器。唯一需要注意的是:每次都要将当前树的残差加到初始预测上,生成下一轮的预测。
例如,如果我们构建了 10 棵树,每棵树的残差记为 ( ),那么第 10 轮的预测为:
其中 是学习率,$p_{10}$ 是第 10 轮的预测值。
在实践中,专业人士通常从 100 棵树开始,而非仅 10 棵。此时,算法被称为进行了 100 轮提升(boosting rounds)。
如果你不确定具体需要多少棵树,可以使用一种简单技术:早停(early stopping)。
在早停中,我们先设定一个较大的树数量(如 1000 或 10000),然后在训练过程中监控损失。如果损失在连续若干轮(例如 50 轮)内不再改善,就提前停止训练,从而节省时间和计算资源。
配置梯度提升模型
在机器学习中,为模型选择设置称为“超参数调优”。这些设置称为“超参数”,是由机器学习工程师自行选择的选项。与模型参数不同,超参数无法通过训练数据自动学习得到。
梯度提升模型有许多超参数,下面列出几个关键的:
目标(Objective)
该参数设定算法的方向和损失函数。若目标是回归,则使用 MSE;若是分类,则使用交叉熵。XGBoost 等 Python 库还提供其他目标,如排序任务对应的损失函数。
学习率(Learning rate)
这可能是梯度提升最重要的超参数。它通过调整收缩因子(shrinkage factor)来控制每个弱学习器的贡献。较小的值(接近 0)会削弱每个弱学习器的影响力,因此需要构建更多树,训练时间更长,但最终的强学习器更稳健,不易过拟合。
树的数量(Number of trees)
也称为提升轮数(n_estimators),控制要构建的树的数量。树越多,集成模型越强、性能越好,也能捕捉更多数据模式。但树过多会显著增加过拟合风险。建议结合早停和低学习率来缓解。
最大深度(Max depth)
控制每棵弱学习器(决策树)的层数。最大深度为 3 表示树有三层(包含叶节点层)。树越深,模型越复杂、计算越昂贵。为防止过拟合,建议选择接近 3 的值,最大不超过 10。
叶节点最小样本数(Minimum number of samples per leaf)
控制决策树分支的分裂方式。若叶节点的最小样本数设得太低,模型会对噪声敏感。较大的最小样本数有助于防止过拟合,因为它使得树难以基于极少数据点进行分裂。
子采样率(Subsampling rate)
控制用于训练每棵树的数据比例。前面的例子中我们用了 100% 的数据(因为只有四行)。但在真实数据集中,通常需要采样。例如,若设为 0.7,则每棵弱学习器仅在随机抽取的 70% 行上训练。较低的采样率可加速训练,但也可能增加过拟合风险。
特征采样率(Feature sampling rate)
与子采样类似,但针对特征而非样本。对于拥有数百个特征的数据集,建议将特征采样率设在 0.5 到 1 之间,以降低过拟合概率。
梯度提升是我们处理表格型监督学习任务最强大的模型。因此,大多数时候你不必担心它“不够好”,而应专注于如何正则化它——驾驭其强大能力,防止它“吞噬”你的训练数据,导致在新数据上失效。
上述所有超参数都包含在 Python 的各类梯度提升实现中,请善加利用。
Python 中的梯度提升实现
如前所述,梯度提升在 Python 生态中已有成熟实现。以下是四大主流库:
- XGBoost:eXtreme Gradient Boosting
- LightGBM:Light Gradient Boosting Machine
- CatBoost:Categorical Boosting
- Scikit-learn:提供回归和分类两个梯度提升估计器
前三者非常相似:
- 性能卓越
- 支持 GPU
- 超参数丰富(配置友好)
- 社区支持强大
- 在工业界广泛应用
Scikit-learn 是一个流行的替代方案,但缺点是仅支持 CPU。由于梯度提升计算密集,对于大型数据集(数十万行以上),仅用 CPU 可能不可行。
不过,我们必须承认:Scikit-learn 本身的流行度远超上述三个库之和。除了两个梯度提升估计器,它还提供了数十种其他模型,覆盖各类监督与无监督学习任务。
此外,Scikit-learn 构建的梯度提升模型可无缝集成到其丰富的生态系统中,如管道(pipelines)、交叉验证估计器、数据预处理器等。
下面是一个使用 GradientBoostingClassifier 进行分类的分步指南。我们将根据钻石的价格和其他物理测量值预测其切工质量(cut quality)。该数据集内置于 Seaborn 库中。
1. 导入库
import pandas as pd
import seaborn as sns
from sklearn.compose import ColumnTransformer
from sklearn.ensemble import GradientBoostingClassifier
from sklearn.metrics import classification_report
from sklearn.model_selection import cross_val_score, train_test_split
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import OneHotEncoder, StandardScaler
2. 加载数据
# 从 Seaborn 加载 diamonds 数据集
diamonds = sns.load_dataset("diamonds")
# 分离特征和目标变量
X = diamonds.drop("cut", axis=1)
y = diamonds["cut"]
3. 划分数据
# 划分训练集和测试集
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42
)
4. 定义分类和数值特征
# 定义分类特征
categorical_features = X.select_dtypes(
include=["object"]
).columns.tolist()
# 定义数值特征
numerical_features = X.select_dtypes(
include=["float64", "int64"]
).columns.tolist()
5. 定义预处理步骤
preprocessor = ColumnTransformer(
transformers=[
("cat", OneHotEncoder(), categorical_features),
("num", StandardScaler(), numerical_features),
]
)
6. 创建梯度提升分类器管道
pipeline = Pipeline(
[
("preprocessor", preprocessor),
("classifier", GradientBoostingClassifier(random_state=42)),
]
)
7. 交叉验证与训练
# 执行 5 折交叉验证
cv_scores = cross_val_score(pipeline, X_train, y_train, cv=5)
# 在训练集上拟合模型
pipeline.fit(X_train, y_train)
# 在测试集上预测
y_pred = pipeline.predict(X_test)
# 生成分类报告
report = classification_report(y_test, y_pred)
8. 输出最终结果
print(f"Mean Cross-Validation Accuracy: {cv_scores.mean():.4f}")
print("\nClassification Report:")
print(report)
输出结果:
Mean Cross-Validation Accuracy: 0.7621
Classification Report:
precision recall f1-score support
Fair 0.90 0.91 0.91 335
Good 0.81 0.63 0.71 1004
Ideal 0.82 0.91 0.86 4292
Premium 0.70 0.86 0.77 2775
Very Good 0.66 0.41 0.51 2382
accuracy 0.76 10788
macro avg 0.78 0.74 0.75 10788
weighted avg 0.75 0.76 0.75 10788
加权准确率为 75%,对于使用默认参数的基线模型来说并不差。因此,作为挑战,我留给你一个任务:调整 GradientBoostingClassifier 的超参数,使其性能超过 95%。是的,这是可能的!(提示:仔细阅读上一节,并查阅 Scikit-learn 官方文档。)
结语与进一步学习
尽管我们已经学到了很多,但本文的重点始终是梯度提升算法的内部工作机制。理解其原理并不等于能在实践中用好它,但直观的理解永远是巨大的助力。