查看全课大纲(第 32 / 82 节)
  1. 1.函数与数据泛化
  2. 2.向量:从几何直觉到高维空间
  3. 3.矩阵与张量
  4. 4.特征值与特征向量:矩阵变换的不动方向
  5. 5.凸函数:从向量求导的矩阵记号到好优化的函数形状
  6. 6.概率与统计:机器学习的数学基础
  7. 7.独立性与随机变量:贝叶斯公式与二项分布
  8. 8.期望、方差与协方差:随机变量的数字画像
  9. 9.联合概率:当一条轴不够用的时候
  10. 10.概率分布(一):从伯努利到伽马函数
  11. 11.概率分布(二):贝塔、泊松、正态与两个贝叶斯故事
  12. 12.机器学习的模型:找输入与输出的关系
  13. 13.机器学习的应用
  14. 14.过拟合
  15. 15.机器学习的分类
  16. 16.有监督学习:从房价预测说起
  17. 17.损失函数
  18. 18.梯度下降(上):更新规则与学习速率
  19. 19.梯度下降(2)
  20. 20.牛顿方法
  21. 21.KNN简介
  22. 22.距离测算
  23. 23.朴素贝叶斯
  24. 24.朴素贝叶斯的实际应用
  25. 25.信息熵:决策树构建的度量基础
  26. 26.信息增益与决策树的构建
  27. 27.ID3算法
  28. 28.决策树节点的划分
  29. 29.CART树
  30. 30.Bagging
  31. 31.Boosting:自适应提升算法
  32. 32.Gradient Boosting
  33. 33.线性模型:回归与分类的同一副骨架
  34. 34.最小二乘:把最优参数直接解出来
  35. 35.最小二乘法的几何与概率视角
  36. 36.极大似然:给最小二乘一个概率出身
  37. 37.逻辑回归(1):把线性模型接到分类上
  38. 38.逻辑回归(2):损失函数与多分类
  39. 39.---- 手造数据: 5 个样本, 2 个特征; x2 几乎 = 2*x1 (偏差手工指定, 无随机) ----
  40. 40.讲师例: 向量 "23014" -- 三把范数尺子量同一个向量
  41. 41.优化方法(1):坐标下降与前向选择
  42. 42.优化方法(2):前向策略与最小角回归
  43. 43.拉格朗日乘子法(1):从无约束到等式约束
  44. 44.拉格朗日乘子法(2):等式约束下的平行条件与拉格朗日函数
  45. 45.拉格朗日乘子法(3):不等式约束与 KKT 条件
  46. 46.最大间隔:把分类问题变成优化问题
  47. 47.拉格朗日对偶问题:把 w 和 b 全部换成 α
  48. 48.支持向量:边界上的少数派
  49. 49.SVM 计算过程:一笔到底算完,再走进核方法
  50. 50.贝叶斯公式:概率即信念
  51. 51.隐含变量和概率模型:给看不见的原因建模
  52. 52.EM 算法:期望与最大化的迭代
  53. 53.琴生不等式:EM 的数学钥匙
  54. 54.三个硬币问题:EM 的复盘与衔接
  55. 55.高斯混合模型:用隐变量解释双峰数据
  56. 56.隐马尔可夫模型
  57. 57.隐马尔可夫模型(2):前向算法与动态混合模型
  58. 58.隐马尔可夫模型在 NLP 中的运用
  59. 59.EM 算法:把「为什么收敛」推到底
  60. 60.HMM 在 NLP 中的运用:从分词到机器翻译
  61. 61.HMM 参数估计:Baum-Welch 的三块拉格朗日
  62. 62.高维下的高斯模型
  63. 63.主题模型:从词袋到隐含主题
  64. 64.Dirichlet 分布(1)
  65. 65.Dirichlet 分布(2)
  66. 66.统计学习内容回顾
  67. 67.K-means 算法
  68. 68.PCA 降维
  69. 69.SVD 降维
  70. 70.从聚类到流形
  71. 71.局部线性嵌入
  72. 72.随机邻域嵌入:SNE 与 t-SNE
  73. 73.无参模型:让数据自己说话
  74. 74.高斯过程:函数空间里的正态分布
  75. 75.高斯过程(2):噪声、正则化与核的线性组合
  76. 76.神经网络简介:从感知器到自编码器
  77. 77.反向传播:把误差一层层传回去
  78. 78.卷积神经网络(CNN):让特征自己学出来
  79. 79.循环神经网络(RNN):给网络装上记忆
  80. 80.深度学习回顾:静态一类,动态一类
  81. 81.增强学习(1):蒙特卡洛、树搜索与 Q-learning
  82. 82.强化学习(2):从乌鸦吃核桃到井字棋的 value 回传

Gradient Boosting

约 34 分钟

📺 正在播放小象官方高清录播(支持倍速与清晰度调节)

小象实战讲义 · 机器学习算法精讲

在上一节我们学习了自适应提升(AdaBoost),它通过调整样本权重来让后续的弱分类器聚焦于之前分错的样本。本节我们将学习另一种更强大、更通用的集成方法——梯度提升(Gradient Boosting)。梯度提升的核心思想是,将提升过程视为在函数空间中进行梯度下降,通过不断拟合当前模型的负梯度(即残差)来构建一个新的弱学习器,从而逐步逼近最优解。学完本节,你将理解梯度提升的理论框架,掌握其核心算法流程,并能通过手写代码实现一个简单的梯度提升回归树(GBDT)模型。

💡 核心导读

  • 从残差到梯度:理解梯度提升如何将提升过程转化为在函数空间中沿损失函数的负梯度方向进行优化。
  • 通用算法框架:掌握梯度提升的通用算法步骤,包括初始化、计算负梯度、拟合弱学习器和更新模型。
  • GBDT 实例:通过一个具体的回归示例,学习如何使用决策树(CART)作为弱学习器来实现梯度提升,并观察模型如何逐步减小残差。
  • 进阶优化与 XGBoost:了解梯度提升的常用优化技巧(如收缩率)以及 XGBoost 等高效实现背后的核心思想。
  • 集成学习的意义:从偏差-方差分解的角度,理解集成学习(包括 Bagging 和 Boosting)为何能提升模型性能。

梯度提升的核心思想

在 AdaBoost 中,我们通过调整样本权重来让新分类器关注错误样本。梯度提升则从一个更数学化的视角来看待提升过程:将集成模型看作一个加法模型,其训练目标是最小化一个指定的损失函数。

假设我们有一个监督学习问题,目标是找到一个函数 F(x)F(x),使其预测值 F(xi)F(x_i) 尽可能接近真实标签 yiy_i。我们通过最小化一个损失函数 L(y,F(x))L(y, F(x)) 来达到这个目的,例如均方误差 L(y,F(x))=(y−F(x))2L(y, F(x)) = (y - F(x))^2 或交叉熵损失。

梯度提升将最终模型 F(x)F(x) 表示为一系列弱学习器(例如决策树)的加权和: FM(x)=∑m=1Mαmhm(x)F_M(x) = \sum_{m=1}^{M} \alpha_m h_m(x) 其中,hm(x)h_m(x) 是第 mm 个弱学习器,αm\alpha_m 是其权重。

训练过程是前向分步的:我们从初始模型(例如一个常数)F0(x)F_0(x) 开始,然后依次添加弱学习器。在第 mm 步,我们已经有了模型 Fm−1(x)F_{m-1}(x),目标是找到一个新弱学习器 hm(x)h_m(x) 及其权重 αm\alpha_m,使得新模型 Fm(x)=Fm−1(x)+αmhm(x)F_m(x) = F_{m-1}(x) + \alpha_m h_m(x) 的损失最小。

关键洞察:寻找最优的 hm(x)h_m(x) 和 αm\alpha_m 可以类比于在函数空间中进行梯度下降。损失函数 LL 关于当前模型 Fm−1F_{m-1} 的负梯度,正好指向损失下降最快的方向。对于第 ii 个样本,这个负梯度为: rmi=−[∂L(yi,F(xi))∂F(xi)]F(x)=Fm−1(x)r_{mi} = -\left[ \frac{\partial L(y_i, F(x_i))}{\partial F(x_i)} \right]_{F(x)=F_{m-1}(x)}

对于均方误差损失 L(y,F(x))=12(y−F(x))2L(y, F(x)) = \frac{1}{2}(y - F(x))^2,其负梯度恰好是残差 rmi=yi−Fm−1(xi)r_{mi} = y_i - F_{m-1}(x_i)。因此,拟合残差等价于拟合损失函数的负梯度方向。梯度提升的通用策略就是:让新加入的弱学习器 hm(x)h_m(x) 去拟合当前模型的负梯度 rmir_{mi}。这样,将 hm(x)h_m(x) 加到模型上,就相当于沿着损失函数下降的方向前进了一步。

梯度提升通用算法

基于上述思想,梯度提升的通用算法可以概括为以下几步:

  1. 初始化:用一个常数值初始化模型,这个常数通常是使初始损失最小的值。对于均方误差,就是目标值 yy 的均值。 F0(x)=arg⁡min⁡γ∑i=1NL(yi,γ)F_0(x) = \arg\min_{\gamma} \sum_{i=1}^{N} L(y_i, \gamma)

  2. 对于 m = 1 到 M(迭代轮数): a. 计算负梯度(伪残差):对于每个样本 ii,计算损失函数关于当前模型预测值的负梯度。 rmi=−[∂L(yi,F(xi))∂F(xi)]F(x)=Fm−1(x)r_{mi} = -\left[ \frac{\partial L(y_i, F(x_i))}{\partial F(x_i)} \right]_{F(x)=F_{m-1}(x)} b. 拟合弱学习器:使用训练数据 (xi,rmi)i=1N{(x_i, r_{mi})}_{i=1}^{N} 训练一个弱学习器 hm(x)h_m(x),使其尽可能拟合这些负梯度值。 c. 计算步长(权重):通过线搜索确定弱学习器 hm(x)h_m(x) 的最优权重 αm\alpha_m,使得加上该弱学习器后总损失最小。 αm=arg⁡min⁡α∑i=1NL(yi,Fm−1(xi)+αhm(xi))\alpha_m = \arg\min_{\alpha} \sum_{i=1}^{N} L(y_i, F_{m-1}(x_i) + \alpha h_m(x_i)) d. 更新模型:将新弱学习器乘以其权重后加到现有模型中。 Fm(x)=Fm−1(x)+αmhm(x)F_m(x) = F_{m-1}(x) + \alpha_m h_m(x)

  3. 输出最终模型:FM(x)F_M(x)。

这个框架是通用的,适用于不同的损失函数和弱学习器。当弱学习器为决策树时,就得到了著名的梯度提升决策树(GBDT)。

梯度提升决策树(GBDT)示例

让我们用一个简单的回归示例,手动实现一个使用决策树桩(深度为1的决策树)作为弱学习器的GBDT模型。我们使用均方误差作为损失函数,此时负梯度就是残差。

假设我们有5个数据点 (x,y)(x, y):(1, 1.2), (2, 1.9), (3, 3.2), (4, 3.8), (5, 5.1)。我们的目标是学习一个回归模型 F(x)F(x)。

下面的代码将演示两轮提升的过程:

import math

# 训练数据
x_data = [1, 2, 3, 4, 5]
y_data = [1.2, 1.9, 3.2, 3.8, 5.1]
n = len(y_data)

# 1. 初始化:F0(x) = 目标值y的均值(对于MSE损失)
F0 = sum(y_data) / n
print(f"初始模型预测值 F0: {[F0]*n}")
print(f"初始模型常数项(均值): {F0:.4f}\n")

# 辅助函数:计算均方误差
def mse(y_true, y_pred):
    return sum((yt - yp) ** 2 for yt, yp in zip(y_true, y_pred)) / n

# 第一轮提升 (m=1)
# a. 计算负梯度(残差):r1i = yi - F0(xi)
residuals1 = [y - F0 for y in y_data]
print(f"第一轮负梯度(残差): {[round(r, 3) for r in residuals1]}")

# b. 拟合一个决策树桩(弱学习器h1(x))。这里我们手动选择一个分裂点。
#    为了简单,我们选择在 x < 3.5 和 x >= 3.5 处分裂。
split_point1 = 3.5
left_indices1 = [i for i, x in enumerate(x_data) if x < split_point1]
right_indices1 = [i for i, x in enumerate(x_data) if x >= split_point1]

#    每个区域的输出值γ是该区域内残差的均值(对于MSE损失,这是最优输出)
gamma_left1 = sum(residuals1[i] for i in left_indices1) / len(left_indices1) if left_indices1 else 0
gamma_right1 = sum(residuals1[i] for i in right_indices1) / len(right_indices1) if right_indices1 else 0
print(f"第一棵树区域划分: x < {split_point1} 和 x >= {split_point1}")
print(f"  左区域输出值 γ_left: {gamma_left1:.4f}")
print(f"  右区域输出值 γ_right: {gamma_right1:.4f}")

# c. & d. 更新模型:F1(x) = F0 + h1(x)。这里我们使用学习率 ν=1(无收缩)。
learning_rate = 1.0
h1_predictions = [gamma_left1 if x < split_point1 else gamma_right1 for x in x_data]
F1_predictions = [F0 + learning_rate * h1 for h1 in h1_predictions]
print(f"\n第一棵树预测值 h1(x): {[round(h, 4) for h in h1_predictions]}")
print(f"更新后模型预测值 F1: {[round(f, 4) for f in F1_predictions]}")
print(f"真实目标值 y: {y_data}")
print(f"F1的均方误差(MSE): {mse(y_data, F1_predictions):.6f}\n")

# 第二轮提升 (m=2),基于更新后的模型F1
# a. 计算新的残差:r2i = yi - F1(xi)
residuals2 = [y - f1 for y, f1 in zip(y_data, F1_predictions)]
print(f"第二轮负梯度(残差): {[round(r, 3) for r in residuals2]}")

# b. 拟合第二个决策树桩h2(x)。选择另一个分裂点,例如 x < 2.5。
split_point2 = 2.5
left_indices2 = [i for i, x in enumerate(x_data) if x < split_point2]
right_indices2 = [i for i, x in enumerate(x_data) if x >= split_point2]

gamma_left2 = sum(residuals2[i] for i in left_indices2) / len(left_indices2) if left_indices2 else 0
gamma_right2 = sum(residuals2[i] for i in right_indices2) / len(right_indices2) if right_indices2 else 0
print(f"第二棵树区域划分: x < {split_point2} 和 x >= {split_point2}")
print(f"  左区域输出值 γ_left2: {gamma_left2:.4f}")
print(f"  右区域输出值 γ_right2: {gamma_right2:.4f}")

# c. & d. 更新模型:F2(x) = F1(x) + h2(x)
h2_predictions = [gamma_left2 if x < split_point2 else gamma_right2 for x in x_data]
F2_predictions = [f1 + learning_rate * h2 for f1, h2 in zip(F1_predictions, h2_predictions)]
print(f"\n第二棵树预测值 h2(x): {[round(h, 4) for h in h2_predictions]}")
print(f"更新后模型预测值 F2: {[round(f, 4) for f in F2_predictions]}")
print(f"真实目标值 y: {y_data}")
print(f"F2的均方误差(MSE): {mse(y_data, F2_predictions):.6f}")
初始模型预测值 F0: [3.04, 3.04, 3.04, 3.04, 3.04]
初始模型常数项(均值): 3.0400

第一轮负梯度(残差): [-1.84, -1.14, 0.16, 0.76, 2.06]
第一棵树区域划分: x < 3.5 和 x >= 3.5
  左区域输出值 γ_left: -0.9400
  右区域输出值 γ_right: 1.4100

第一棵树预测值 h1(x): [-0.94, -0.94, -0.94, 1.41, 1.41]
更新后模型预测值 F1: [2.1, 2.1, 2.1, 4.45, 4.45]
真实目标值 y: [1.2, 1.9, 3.2, 3.8, 5.1]
F1的均方误差(MSE): 0.581000

第二轮负梯度(残差): [-0.9, -0.2, 1.1, -0.65, 0.65]
第二棵树区域划分: x < 2.5 和 x >= 2.5
  左区域输出值 γ_left2: -0.5500
  右区域输出值 γ_right2: 0.3667

第二棵树预测值 h2(x): [-0.55, -0.55, 0.3667, 0.3667, 0.3667]
更新后模型预测值 F2: [1.55, 1.55, 2.4667, 4.8167, 4.8167]
真实目标值 y: [1.2, 1.9, 3.2, 3.8, 5.1]
F2的均方误差(MSE): 0.379333

过程解读:

  1. 初始化:F0(x)F_0(x) 是所有 yy 的均值 3.04,一个很粗糙的起点(初始 MSE 为 1.9064)。
  2. 第一轮:残差显示模型普遍高估了左边、低估了右边。树桩以 x=3.5x=3.5 为界拟合残差(左 −0.94-0.94、右 1.411.41),加上去之后 MSE 从 1.906 直落到 0.581。
  3. 第二轮:基于 F1F_1 算新残差,换 x=2.5x=2.5 再切一刀,MSE 进一步降到 0.379。

通过不断添加拟合残差的树,模型 Fm(x)F_m(x) 逐步逼近真实值。实际的 GBDT 会用更深的树、更多轮迭代,直到残差足够小或触发停止条件。

讲师的经典例子:用两棵树预测年龄

课末讲师用一个人人都能看懂的例子收束了 GBDT。任务:预测四个人 A、B、C、D 的年龄——14、16、24、26 岁。

如果是传统决策树:一棵树学到底。比如先按”月购物金额 < 1000 / ≥ 1000”分成两组(14、16 在左,24、26 在右),再在每组内按”上网时长”细分,一次建出四个区域,每个区域一个精确年龄。

GBDT 的做法是”先粗后补”:

  • 第一棵树(弱分类器):只用一个特征——购物金额。< 1000 的一组是 {14, 16},均值 15;≥ 1000 的一组是 {24, 26},均值 25。每个区域都用均值代替真值。
  • 算残差:14 − 15 = −1,16 − 15 = +1,24 − 25 = −1,26 − 25 = +1。四个人的残差正是 −1、+1、−1、+1(组内均值恰好为 0)。
  • 第二棵树:不再学原始年龄,改学残差。这次换个特征——“是否常在百度上提问”:爱提问的是 14 岁的初中生和 24 岁刚入职场的新人(残差 −1 那两个),爱回答的是 16 岁的高中生和 26 岁的职场老手(残差 +1)。按这个特征一刀切下去,两边的残差均值正好是 −1 和 +1——切完之后残差清零,问题完美解决。
  • 预测一个 24 岁的人:第一棵树按购物金额给出 25;第二棵树按”爱提问”给出 −1;两棵树相加:25 + (−1) = 24。分毫不差。

这就是 GBDT 与原始决策树的本质区别:不建一棵完美的大树,而是用多棵”只管一件事”的弱树,每棵拟合上一棵剩下的残差,最后把结果累加起来。

优化技巧与 XGBoost

基本的梯度提升算法已经很强大了,但仍有改进空间。常见的优化技巧包括:

  • 收缩(Shrinkage):在更新模型时不直接加上 hm(x)h_m(x),而是加上 θ⋅hm(x)\theta \cdot h_m(x),其中 θ\theta 是一个很小的正数——讲师给的范围是 0.001~0.01。 Fm(x)=Fm−1(x)+θ⋅αmhm(x)F_m(x) = F_{m-1}(x) + \theta \cdot \alpha_m h_m(x) 收缩让每一步只朝目标方向挪一小步,每轮都留下一部分残差交给后面的树继续消化。它减慢了拟合速度,但”小步慢走”不容易冲过头、错过最优解,通常能取得更好的最终效果。

  • 子采样(Subsampling):类似于随机森林,在每一轮训练弱学习器时,不是使用全部训练数据,而是随机抽取一部分样本(有放回或无放回)。这增加了模型的随机性,有助于降低方差,也是防止过拟合的有效手段。

  • 正则化:在决策树作为弱学习器时,可以通过限制树的最大深度、最小叶子节点样本数等来进行正则化;也可以直接在损失函数上加正则项。

XGBoost(X 代表 eXtreme——“极致”的梯度提升)是梯度提升算法的高效工程实现,也是各类机器学习竞赛的常胜工具。它背后的核心道理正是本节的 Gradient Boosting,在此之上叠加了多重强化:二阶泰勒展开(不只利用一阶梯度,还利用损失函数的二阶导数信息,让每步更新方向更准)、正则化、Shrinkage,以及大量工程层面的速度优化。正是这些强化让它在很多问题上能不断逼近最好的结果。讲师课后提供了 XGBoost 与随机森林等算法对比的代码,建议亲手跑一跑。

集成学习的意义:偏差与方差的权衡

为什么集成学习(包括 Bagging 和 Boosting)通常比单一模型更好?我们可以从偏差(Bias)和方差(Variance)的角度来理解。

模型的泛化误差可以分解为偏差、方差和不可避免的噪声之和:

  • 偏差:模型预测值的期望与真实值之间的差异。高偏差意味着模型拟合能力不足,可能欠拟合。
  • 方差:模型预测值的变化范围。高方差意味着模型对训练数据中的随机波动过于敏感,可能过拟合。

讲师用打靶把四种组合讲成了一幅图:

  • 低偏差、低方差:枪法准、瞄得也准——子弹都聚在靶心,最理想;
  • 高偏差、低方差:枪法不错但瞄歪了——弹着点很集中,却整体偏在一边,是系统性错误;
  • 低偏差、高方差:瞄准的是中心,但手抖——弹着点散布在靶心四周;
  • 高偏差、高方差:瞄得又不准、枪法又差——满靶乱飞,最糟糕。

不同的集成方法对偏差和方差的影响不同:

  • Bagging(如随机森林):通过自助采样构建多个差异化的模型,然后取平均(回归)或投票(分类)。其主要作用是降低方差。因为平均操作可以平滑掉各个模型因数据随机性而产生的波动。对于本身高方差、低偏差的模型(如深度决策树),Bagging 效果显著。
  • Boosting(如 AdaBoost, GBDT):通过顺序训练,让后续模型纠正前序模型的错误。其主要作用是降低偏差。因为每一步都在努力减少当前模型在训练集上的误差(损失),从而不断提升模型的拟合能力。同时,由于集成了多个模型,它也能在一定程度上降低方差。

理想模型是低偏差且低方差的。集成学习通过组合多个”弱”或”不稳定”的学习器,往往能在偏差和方差之间取得更好的平衡,从而获得更优的泛化性能——多个臭皮匠投票,把个体的误差与噪声”投”出去,留下集体的智慧。

最后留一个更高的视角:集成学习本身并不是某个单独的算法,它更像一种 meta learning(元学习)——在已有的一批算法之上,研究”用什么方式把它们组合起来”才能得到更好的最终结果。这正是本章从头到尾在做的事。

📝 动手练一练

  1. 概念辨析:请简述梯度提升(Gradient Boosting)与自适应提升(AdaBoost)的核心区别。它们分别主要降低的是模型的偏差还是方差?
👉 点击查看参考答案

梯度提升 vs. AdaBoost:

  • 优化目标:AdaBoost 通过调整样本权重,最小化指数损失函数,让新分类器聚焦于被前序分类器分错的样本。梯度提升则提供了一个通用框架,通过拟合损失函数的负梯度(对于不同损失函数形式不同)来最小化任意可微的损失函数。
  • 样本权重:AdaBoost 显式地维护和更新每个样本的权重。梯度提升不直接操作样本权重,而是通过计算负梯度来隐式地确定每个样本在当前轮次的重要性(梯度绝对值大的样本,当前模型对其拟合差,重要性高)。
  • 模型组合:AdaBoost 中每个弱分类器的话语权(权重)由其分类准确率决定。梯度提升中每个弱学习器的权重通过线搜索最小化总体损失来确定。

偏差与方差:

  • AdaBoost 和 梯度提升 都属于 Boosting 家族,它们通过顺序叠加模型来不断修正错误,因此主要作用是降低模型的偏差,提升模型的拟合能力。同时,由于集成了多个模型,它们也能在一定程度上降低方差,但主要优势在于偏差缩减。
  • 相比之下,Bagging(如随机森林) 的主要作用是降低模型的方差。
  1. 代码实践:在上面提供的 GBDT 示例代码中,我们使用了固定的分裂点(3.5 和 2.5)。请修改代码,实现一个简单的函数,让它自动寻找使左右区域残差平方和最小的分裂点(只考虑数据点之间的中点作为候选),并完成第三轮提升(m=3)。观察 MSE 是否继续下降。
👉 点击查看参考答案
import math

# 训练数据
x_data = [1, 2, 3, 4, 5]
y_data = [1.2, 1.9, 3.2, 3.8, 5.1]
n = len(y_data)

# 初始化
F_current = [sum(y_data) / n] * n
learning_rate = 1.0

def mse(y_true, y_pred):
    return sum((yt - yp) ** 2 for yt, yp in zip(y_true, y_pred)) / n

def find_best_split(x_vals, residuals):
    """在x_vals中寻找最佳分裂点(候选点为相邻值的中点),使左右区域残差平方和最小。"""
    best_split = None
    best_loss = float('inf')
    # 对x排序并获取唯一值
    sorted_pairs = sorted(zip(x_vals, residuals))
    unique_x = sorted(set(x_vals))
    
    for i in range(len(unique_x) - 1):
        # 候选分裂点为相邻x值的中点
        candidate = (unique_x[i] + unique_x[i+1]) / 2.0
        left_res = [r for x, r in sorted_pairs if x < candidate]
        right_res = [r for x, r in sorted_pairs if x >= candidate]
        
        # 计算左右区域残差平方和
        loss = sum(r*r for r in left_res) + sum(r*r for r in right_res)
        if loss < best_loss:
            best_loss = loss
            best_split = candidate
    return best_split

# 进行三轮提升
for round_num in range(1, 4):
    # 计算当前残差
    residuals = [y - f for y, f in zip(y_data, F_current)]
    
    # 寻找最佳分裂点
    best_split = find_best_split(x_data, residuals)
    
    # 划分区域并计算输出值
    left_indices = [i for i, x in enumerate(x_data) if x < best_split]
    right_indices = [i for i, x in enumerate(x_data) if x >= best_split]
    
    gamma_left = sum(residuals[i] for i in left_indices) / len(left_indices) if left_indices else 0
    gamma_right = sum(residuals[i] for i in right_indices) / len(right_indices) if right_indices else 0
    
    # 更新模型预测
    h_predictions = [gamma_left if x < best_split else gamma_right for x in x_data]
    F_new = [f + learning_rate * h for f, h in zip(F_current, h_predictions)]
    
    print(f"第{round_num}轮:")
    print(f"  最佳分裂点: {best_split:.2f}")
    print(f"  左区域输出γ: {gamma_left:.4f}, 右区域输出γ: {gamma_right:.4f}")
    print(f"  更新后预测值 F{round_num}: {[round(f, 4) for f in F_new]}")
    print(f"  当前MSE: {mse(y_data, F_new):.6f}\n")
    
    F_current = F_new
第1轮:
  最佳分裂点: 2.50
  左区域输出γ: -1.4900, 右区域输出γ: 0.9933
  更新后预测值 F1: [1.55, 1.55, 4.0333, 4.0333, 4.0333]
  当前MSE: 0.426333

第2轮:
  最佳分裂点: 1.50
  左区域输出γ: -0.3500, 右区域输出γ: 0.0875
  更新后预测值 F2: [1.2, 1.6375, 4.1208, 4.1208, 4.1208]
  当前MSE: 0.395708

第3轮:
  最佳分裂点: 1.50
  左区域输出γ: 0.0000, 右区域输出γ: -0.0000
  更新后预测值 F3: [1.2, 1.6375, 4.1208, 4.1208, 4.1208]
  当前MSE: 0.395708

真实运行的意外结局:前两轮 MSE 确实持续下降(0.426 → 0.396),但第三轮停滞了——算法找到的最佳分裂点上,左右区域的残差均值都已约等于 0,新树加进去等于什么都没加。这暴露了贪心树桩的极限:步长取满(learning rate = 1.0)、弱学习器太浅时,它在这个小数据上已”学到头”。实际的 GBDT 正是为此准备了三件武器——更深的树(一次能表达更复杂的残差结构)、Shrinkage(小步慢走,把残差留给后续轮次)、更多轮迭代——三者合力才能持续压低残差。这也正好呼应了本节前文的优化技巧。

本章小结

本节我们深入探讨了梯度提升(Gradient Boosting)这一强大的集成学习框架。我们从其核心思想——在函数空间中进行梯度下降以最小化损失函数——出发,推导出了通用的算法流程。通过一个具体的 GBDT 回归示例,我们亲手实践了如何用决策树拟合残差来逐步提升模型性能。此外,我们还了解了包括收缩、子采样在内的优化技巧,以及 XGBoost 如何在此基础上进行扩展和优化。最后,我们从偏差-方差分解的角度,理解了集成学习为何能有效提升模型的泛化能力。

📋 行动清单

  • 理解流程:复述梯度提升通用算法的四个关键步骤(初始化、计算负梯度、拟合弱学习器、更新模型)。
  • 手动推算:针对一个只有3个数据点的极小数据集,用纸笔模拟一轮 GBDT 的计算过程,验证你对残差计算和树桩拟合的理解。
  • 代码扩展:尝试修改动手练习中的代码,将损失函数从均方误差改为绝对误差(L1损失),并推导其负梯度的形式,观察算法如何工作。

—— 小象教研组

配套资料 · 直接下载

课件、代码、数据集等配套资料全部免费,点击即可直接下载,无需分享或解锁。

  • 第7章课件:经典机器学习模型 III(CART/集成)
    下载
  • 第7章笔记:CART 树
    下载
  • 第7章笔记:集成学习
    下载
  • 第7章笔记:GBDT
    下载
  • 第7章代码包(RandomForest)
    下载