跳至主要内容

梯度提升算法指南

在尽量不增加数学负担的前提下,深入理解梯度提升的内部机制,并学习如何调优算法的超参数。
· 15分钟 阅读

使用 AI 探索

本文主体内容较长但信息详尽,因此我们将尽量简短开场,直接从一个问题切入:为什么还要费心研究梯度提升?

理由有很多:

  1. 梯度提升效果一流:在结构化(表格)监督学习任务中,其精度与性能常常无出其右。
  2. 梯度提升用途广泛:可用于回归、分类、排序、存活分析等诸多关键任务。
  3. 梯度提升可解释:不同于神经网络等黑箱算法,梯度提升无需以可解释性换取性能。它像瑞士手表一样精密,而且只要有耐心,甚至可以向中小学生讲清楚其原理。
  4. 梯度提升有成熟实现:并非纸上谈兵。Python 生态中的 XGBoost、LightGBM 等库拥有数十万用户。
  5. 梯度提升常常夺冠:自 2015 年以来,专业人士在 Kaggle 等平台的结构化数据竞赛中屡屡依靠它夺冠。

如果这些点哪怕只触动了您的一点兴趣,都值得继续读下去。

那就开始吧!

本教程您将学到什么?

本文最重要的目标,是让您在不被繁琐数学困扰的情况下,牢牢掌握梯度提升的内部工作机制。毕竟,梯度提升更偏重实践应用,而非纯数学推导。

梯度提升到底是什么?

Boosting(提升)是机器学习中一种强大的集成方法。与彼此独立学习的数据驱动模型不同,提升通过组合多个弱学习器的预测,得到一个更准确的强学习器。

上面有几个新术语,先从“弱学习器”讲起。

弱学习器指性能仅略优于随机猜测的模型。举个例子:我们要把蘑菇分成可食与不可食。若随机猜测的准确率是 40%,那么弱学习器大概在 50%-60% 左右。

提升会将几十、上百个这样的弱学习器组合起来,构建出在同一问题上精度可超过 95% 的强学习器。

最常见的弱学习器是决策树,因为它几乎能胜任任何数据集。如果您对决策树还不熟悉,可以参考这篇 DataCamp 的决策树分类教程。

梯度提升的真实世界应用

梯度提升在机器学习领域已占据主导地位,其应用覆盖众多行业,从预测客户流失到小行星探测。下面简要看看它在 Kaggle 与真实业务中的成功案例:

称霸 Kaggle 竞赛:

  • Otto Group 产品分类挑战:前 10 名全部使用了 XGBoost 的梯度提升实现。
  • Santander 客户交易预测:基于 XGBoost 的方案再次包揽前排,用于预测客户行为和金融交易。
  • Netflix 影片推荐挑战:梯度提升在构建类似 Netflix 这类千亿美元公司的推荐系统中发挥关键作用。

赋能商业与产业:

  • 零售与电商:个性化推荐、库存管理、欺诈检测
  • 金融与保险:信用风险评估、流失预测、算法交易
  • 医疗与医药:疾病诊断、药物发现、个性化医疗
  • 搜索与在线广告:搜索排序、广告定向、点击率预测

现在,让我们一窥这个“传奇算法”的内部吧!

梯度提升算法:分步讲解

输入

梯度提升适用于表格数据,包含一组特征(X)和一个目标(y)。与其他机器学习算法一样,目标是在训练数据上学到足够的模式,从而对未见样本具有良好的泛化能力。

为理解其内部流程,我们用一个只有四行记录的简单销售数据集。基于三项特征——顾客年龄、购买品类、购买重量,我们要预测购买金额:

一个包含四行与三项特征、一个目标的示例表格

梯度提升中的损失函数

在机器学习中,损失函数是用来量化模型预测与真实值差异的关键组件。简言之,它衡量模型表现如何。

其作用可以分解为:

  • 计算误差:将模型的预测与真实值(观测值)比较。如何比较、如何“算差”,会因函数不同而不同。
  • 指导训练:模型训练的目标就是最小化损失函数。整个训练过程中,模型不断更新其内部结构与配置,以尽可能降低损失。
  • 评估指标:比较训练集、验证集与测试集上的损失,您可以评估模型的泛化能力并避免过拟合。

最常用的两类损失函数:

  • 均方误差(MSE):回归中常用,计算预测值与真实值差的平方和。梯度提升常用其如下变体:

用于梯度提升的改写版均方误差损失函数。

平方项前的二分之一与求导有关。对该损失函数求导时,因幂次法则,二分之一会与平方相消。最终结果就是 -(Observed - Predicted),从而简化了计算,降低了计算开销。

  • 交叉熵:衡量两个概率分布之间的差异,常用于目标为离散类别的分类任务。

由于我们在做回归,所以采用 MSE。

步骤 1:做一个初始预测

梯度提升会逐步提升精度。要启动这个过程,我们先需要一个初始猜测或预测。初始猜测总是目标值的平均数。换言之,第一轮我们的模型认为所有购买金额都一样——156 美元:

计算回归目标的平均值。结果为 156

选择均值与损失函数及其导数有关。每一步我们都在寻找使损失函数达到最小的值,也就是让损失函数的导数(梯度)为 0 的值。

当我们对每个观测值对应的损失函数关于预测值求导并求和后,结果正好是目标的平均数。

因此,我们的初始预测就是平均数——156 美元。记住它,后面还会用到。

步骤 2:计算伪残差

接下来,计算每个观测值与初始预测之间的差:156 - Observed。为了演示,我们将这些差放在新的一列里:

同一数据集,新增一列残差

还记得在线性回归中,观测值与预测值之差称为残差。为与线性回归区分,这里称为伪残差(其命名还有其他原因,本文不展开)。

步骤 3:构建一个弱学习器

现在,用三项特征(年龄、品类、购买重量)训练一棵预测残差的决策树(弱学习器)。本文将树的叶子(终端节点)数量限定为 4;实际应用中通常在 8 到 32 之间。

同一数据集,上方叠加了一棵已拟合的决策树。该树为弱学习器

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

将第一行样本送入决策树,得到 123.45 的预测

下面配图里有个小错误:应当写“预测的购买金额”,而不是“预测的重量”

第一行样本的特征是:品类为电子产品(根节点左侧),顾客年龄小于 30(子节点左侧)。因此落到叶子上的值为 -32.55。做最终预测时,我们将 -32.55 加到初始预测上,结果恰好与观测值相同——123.45 美元!

既然已经完美预测,为什么还要继续建树?因为此时我们对训练数据严重过拟合。我们希望模型具备泛化能力。为缓解这个问题,梯度提升引入了名为学习率的参数。

学习率是 0 到 1 之间的一个倍乘因子,用来缩放每个弱学习器的预测(关于学习率的细节见下文)。若我们取一个任意的学习率 0.1,那么预测就会变成 152.75,而不是完美的 123.45。

在加入学习率后重新计算预测的购买金额。

再来预测第二行:

作为弱学习器的第二棵决策树。

将样本过一遍树后,得到 146.08 的预测。以此类推,我们为四行样本得到四个预测:152.75、146.08、174.945、150.2。先把它们加成新的一列:

同一数据集,新增伪残差与新预测两列

接下来,用购买金额减去新预测,得到新的伪残差。将其加入表格,并删除最后两列:

同一数据集,新增一列新的伪残差

如您所见,新的伪残差更小了,说明损失在下降。

步骤 4:迭代

接下来重复步骤 3,也就是继续构建更多弱学习器。需要记住的一点是:要不断把每棵树的残差按顺序叠加到初始预测上,生成下一次的预测。

例如,我们训练 10 棵树,并将每棵树的残差记为 r_i(1 <= i <= 10),那么第 10 轮的预测为 p_10 = 156 + eta * (r_1 + r_2 + ... + r_10),其中 p_10 表示第十轮的预测。

实际中,工程师常常从 100 棵树起步,而不止 10 棵。这时我们称算法训练了 100 轮提升轮次。

如果您不清楚具体需要多少棵树,可以使用早停(early stopping)。

早停的做法是:先把树的数量设得很大,比如 1000 或 10000。然后不必等所有树都训练完,实时监控损失;若损失在若干个提升轮次(如 50 轮)内不再改进,就提前停止训练,节省时间与算力。

配置梯度提升模型

在机器学习中,为模型选择设置的过程称为“超参数调优”。这些设置即“超参数”,需要工程师自行选择。与模型参数不同,超参数无法通过训练自动学得最优值。

梯度提升模型有不少超参数,下面列出其中一些。

Objective(目标)

该参数决定算法的任务方向与损失函数。若任务是回归,则损失函数选 MSE;若是分类,则使用交叉熵。Python 库(如 XGBoost)还提供了其他任务类型(如排序)对应的目标与损失函数。

学习率

学习率也许是梯度提升中最重要的超参数。它通过调节收缩因子来控制每个弱学习器的贡献。更小的取值(趋近 0)意味着每个弱学习器在集成中的“话语权”更低,需要构建更多的树、花更长的训练时间。但最终得到的强学习器更强大,也更不易过拟合。

树的数量

也称提升轮次或 n_es timators,控制要构建多少棵树。树越多,集成越强、性能越好,同时复杂度也更高,因更多的树能捕捉更多数据模式。然而,树更多也显著增加过拟合风险。为缓解此问题,结合早停与较低学习率使用。

最大深度

控制每棵弱学习器(决策树)的层数。最大深度为 3 表示有三层(包含叶子层)。树越深,模型越复杂、计算开销越大。为防止过拟合,建议取值靠近 3;最大不应超过 10。

叶节点最小样本数

控制决策树分裂的约束。若叶节点的最小样本数设得太小,算法会对噪声更敏感。较大的最小样本数能抑制过拟合,避免基于过少样本做出分裂。

子样本率(行采样)

控制训练每棵树时使用的数据比例。上面的示例中只有四行,我们用了 100% 的样本。真实数据往往更大,通常需要采样。若将子样本率设为小于 1 的值,如 0.7,则每个弱学习器在随机抽取的 70% 行上训练。更小的子样本率可能加快训练,但也可能导致过拟合。

特征采样率(列采样)

此参数与子样本率类似,但作用于列(特征)而非行。对于拥有数百特征的数据集,建议在 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 还为各种监督/无监督任务提供了海量模型。

此外,使用 Scikit-learn 构建的梯度提升模型还能无缝融入其丰富生态,如管道、交叉验证估计器、数据预处理器等。

下面是使用 GradientBoostingClassifier 进行分类的分步指南。我们将基于价格与其他物理测量指标来预测钻石的切工质量。该数据集内置于 Seaborn 库。

专业提示:使用 DataCamp 代码片段编辑器的“Explain code”按钮,可获得逐行的详细解释。

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. 加载数据

# Load the diamonds dataset from Seaborn
diamonds = sns.load_dataset("diamonds")

# Split data into features and target
X = diamonds.drop("cut", axis=1)
y = diamonds["cut"]

3. 划分数据集

# Split data into train and test sets
X_train, X_test, y_train, y_test = train_test_split(
   X, y, test_size=0.2, random_state=42
)

4. 指定类别与数值特征

# Define categorical and numerical features
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. 交叉验证与训练

# Perform 5-fold cross-validation
cv_scores = cross_val_score(pipeline, X_train, y_train, cv=5)

# Fit the model on the training data
pipeline.fit(X_train, y_train)

# Predict on the test set
y_pred = pipeline.predict(X_test)

# Generate classification report
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 分类器文档)。

结语与进阶学习

尽管我们已经学到了很多,但本文的重点仍在于梯度提升算法的内部机制。理解其工作原理并不等于能在实践中用得好,不过直观理解始终是巨大的助力。

若要继续深入,推荐如下资源:

感谢您的阅读!

Bexruz (Bex) Tuychiev

作者

Bexruz (Bex) Tuychiev

我是一名数据科学内容创作者,拥有超过 2 年的经验,并在 Medium 上拥有较大的读者群。我喜欢用略带讽刺的笔调撰写有关 AI 和机器学习的深度文章——毕竟总得想点办法让这些内容不那么枯燥。我已发表 130 多篇文章,并制作了一门 DataCamp 课程,另有一门正在筹备中。我的内容已被超过 500 万人次阅读,其中有 2 万人在 Medium 和 LinkedIn 上成为关注者。 

主题