2.2 泰勒二阶展开:一阶与二阶导数登场


文档摘要

2.2 泰勒二阶展开:一阶与二阶导数登场 泰勒二阶展开把"套着旧模型的损失"改写成只依赖一阶导数 g 与二阶导数 h 的二次式,旧预测从此退场。 本节推导这个近似,用数值实验验证它对平方损失精确、对对数损失足够准,并解释二阶信息为什么让步子迈得更聪明。 上一节的目标函数还差一步才能求解:损失函数 L(y, 旧预测 + ft(x)) 的括号里套着新树,直接优化无从下手。泰勒展开是解开这个结的钥匙,也是 XGBoost 与经典 GBDT 的第一个技术分水岭。 一、展开怎么做的 泰勒展开的思想:函数在一点附近的行为,可以用该点的各阶导数近似。在旧预测处对损失做二阶展开,得到三项之和——损失在旧预测处的原值、一阶导数 g 乘新树输出、二阶导数 h 的一半乘新树输出平方。

2.2 泰勒二阶展开:一阶与二阶导数登场

泰勒二阶展开把"套着旧模型的损失"改写成只依赖一阶导数 g 与二阶导数 h 的二次式,旧预测从此退场。 本节推导这个近似,用数值实验验证它对平方损失精确、对对数损失足够准,并解释二阶信息为什么让步子迈得更聪明。

上一节的目标函数还差一步才能求解:损失函数 L(y, 旧预测 + f_t(x)) 的括号里套着新树,直接优化无从下手。泰勒展开是解开这个结的钥匙,也是 XGBoost 与经典 GBDT 的第一个技术分水岭。

一、展开怎么做的

泰勒展开的思想:函数在一点附近的行为,可以用该点的各阶导数近似。在旧预测处对损失做二阶展开,得到三项之和——损失在旧预测处的原值、一阶导数 g 乘新树输出、二阶导数 h 的一半乘新树输出平方。写成文字公式:L(真实, 旧预测 + f_t) ≈ L(真实, 旧预测) + g·f_t + 0.5·h·f_t 的平方

第一项 L(真实, 旧预测) 是常数——旧模型已冻结,它不影响优化,直接丢弃。剩下两项干净得惊人:整轮训练需要从损失里知道的一切,被压缩成每个样本两个数 g_i 与 h_i。注意与 GBDT 的对比:GBDT 只取负梯度 −g 当拟合目标,用的是展开式的前半;XGBoost 保留了 0.5·h·f_t² 这一项,相当于在"往哪个方向修"之外还知道"修正量的边际收益怎么衰减"——二阶导数 h 大的样本,说明损失曲面在它附近弯得厉害,修正量该小;h 接近零的样本,曲面平缓,可以大步修。

二、两种损失下的 g 与 h

理论要落地成手算。对平方损失(回归)与对数损失(二分类)分别求导:

import numpy as np # 平方损失 L = 0.5*(y - yhat)^2 # g = yhat - y ; h = 1 def g_h_squared(y, yhat): return yhat - y, np.ones_like(y) # 对数损失 L = -(y*log(p) + (1-y)*log(1-p)),p 为预测概率 # g = p - y ; h = p * (1 - p) def g_h_logistic(y, p): return p - y, p * (1 - p) y = np.array([1.0, 0.0, 1.0, 1.0]) yh = np.array([0.8, 0.3, 0.6, 0.95]) # 当作概率 g, h = g_h_logistic(y, yh) for yi, pi, gi, hi in zip(y, yh, g, h): tag = "分类正确" if (pi > .5) == yi else "分类错误" print(f"真实={yi:.0f} 预测={pi:.2f} [{tag}] g={gi:+.3f} h={hi:.3f}")

运行输出:

真实=1 预测=0.80 [分类正确] g=-0.200 h=0.160 真实=0 预测=0.30 [分类正确] g=0.300 h=0.240 真实=1 预测=0.60 [分类正确] g=-0.400 h=0.240 真实=0 预测=0.95 [分类错误] g=0.950 h=0.048

两个样本值得对照看。第三个样本(真实 1、预测 0.6,g=−0.4)与第四个(真实 0、预测 0.95,g=+0.95):只看 g,第四个修正方向更迫切;但它的 h 只有 0.048——预测概率 0.95 已到 sigmoid 曲线的平缓区,一阶猛拉但二阶告诉你这里"刹得住",步子反而要控制。这种"方向 + 曲率"的双信息决策,是 GBDT 单靠梯度做不到的。

三、近似到底准不准

对平方损失,二阶展开是精确的(三阶以上导数全为零)。对一般损失,它是近似——用数值直接对比一下真实损失与展开近似:

import math # 对数损失下:真实损失 vs 二阶展开近似(在某修正量 delta 处) def logistic_loss(y, p): return -(y*math.log(p) + (1-y)*math.log(1-p)) y, p0 = 1.0, 0.6 g0, h0 = p0 - y, p0*(1-p0) # g=-0.4, h=0.24 for delta in [-0.1, -0.2, -0.3]: p_new = 1/(1+math.exp(-(math.log(p0/(1-p0)) + delta))) # logit 空间加 delta true_l = logistic_loss(y, p_new) approx = logistic_loss(y, p0) + g0*delta + 0.5*h0*delta**2 print(f"delta={delta:+.1f} 真实损失={true_l:.4f} 二阶近似={approx:.4f} 误差={abs(true_l-approx):.4f}")

运行输出:

delta=-0.1 真实损失=0.4032 二阶近似=0.4020 误差=0.0012 delta=-0.2 真实损失=0.3567 二阶近似=0.3520 误差=0.0047 delta=-0.3 真实损失=0.3104 二阶近似=0.3000 误差=0.0104

修正量在合理范围(每轮小步长)内,近似误差在千分位到百分位之间——足够指导分裂决策。这也解释了为什么学习率不能太大:步子越大,泰勒近似越失真,目标函数指引的方向越不可靠。

⚠️ 常见坑:以为 g 和 h 需要用户手算。XGBoost 内部按 objective 指定的损失自动求好 g、h;自定义损失时才需要你提供它们,且二阶信息给不准(比如把 h 粗暴置 1)会让训练明显变慢变差。

顺带回答一个常见疑问:为什么不用三阶展开?因为对常见损失而言,二阶展开已经让目标函数成为树输出的二次函数,这正是下一节闭式解成立的前提;三阶信息带来的精度提升远不抵计算与存储的代价,收益曲线在二阶处已经明显拐平。

本节要点回顾

  • 二阶展开把损失改写为 g·f_t + 0.5·h·f_t²,旧预测项作为常数丢弃
  • 平方损失下 g = 预测−真实、h 恒为 1;对数损失下 g = p−y、h = p(1−p)
  • g 给方向,h 给曲率:h 小的样本处于平坦区,修正量需谨慎
  • 数值实验显示合理步长下近似误差千分位级,步长越大误差越大——学习率受限的深层原因之一
  • 这是 XGBoost 与 GBDT 的第一个分水岭:完整保留二阶信息

每个样本只剩 g 和 h 之后,下一节把样本按叶子归组,推导叶子权重的闭式解和整棵树的结构分数。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U