↓跳到主要内容

从一棵树到一片森林:树模型的数学原理、算法演进与工程实践

·36588 字·74 分钟
一幅插画,描绘了从一棵简单、独立的决策树演变为一片茂密、相互连接的树林的过程,象征着机器学习中集成树模型的演进和复杂性。
目录

一家门店想预测某款商品明天的销量。店主可以凭经验编写规则:“如果明天是周末且有促销,就将销量预估得高一些。”这种人工规则系统(rule-based system)易于理解和检查;但当价格、节假日和商品差异等多重因素共同影响销量时,店主难以穷尽所有条件组合。

如果销量与这些因素之间确实存在可重复的模式,却很难用一个现成公式准确描述,而门店又积累了足够且有代表性的历史记录,就可以尝试让模型从数据中学习。决策树(Decision Tree)是一个直观的起点:它会从过去的记录中找出有用的判断条件,把不同情况分开,再给出预测。沿着树走过的每条路径,仍能读成一条“如果……那么……”的规则。

从单棵树到多棵树,树模型逐渐发展出不同的学习方法。20 世纪 80 年代,ID3 和 CART 研究一棵树如何选择判断条件;90 年代,C4.5 改进了单棵树,Bagging 开始让多棵树合作。2000 年前后,随机森林与 GBDT 走出了两条不同的多树路线;到了 2010 年代,XGBoost、LightGBM 和 CatBoost 进一步面对训练效率、类别数据等难题。这里先给出一张路线图,具体做法后文再展开;这些年代依据代表性著作或论文,并非软件首次开发的时间。

这条路线也与计算机硬件的发展相互推动。更多内存和更强的并行计算能力,让研究者有机会处理更大的数据集;但样本和特征增多后,反复读取数据、比较切分仍会拖慢训练。于是,改进树的训练方式和数据处理方式,与增加硬件算力同样重要。

文章导览 #

本文沿着三个问题展开:单棵树如何学习,为什么要把多棵树集成起来(第 1—4 章);XGBoost、LightGBM 和 CatBoost 各自解决什么训练难题(第 5—7 章);面对异常值、类别不平衡等数据问题,如何评估模型并将预测用于门店补货(第 8—9 章)。下面先介绍两个固定的模拟案例。每个案例各有一份便于手算的八行小表,以及一份按照同一业务设定独立生成的完整实验集:前者帮助读懂公式,后者用于训练、验证和测试模型。

本文使用的合成数据、生成脚本和实验 Notebook 均可从公开 GitHub 仓库 from-tree-to-forest 下载。

树模型阅读路线图:从零售预测与单棵树出发,比较 Bagging 和 Boosting、XGBoost、LightGBM 与 CatBoost,再进入数据评估和补货应用。

教学案例:两个案例怎样贯穿全文? #

只看公式,很难知道切分条件、类别字段和评估指标在业务记录中对应什么。本文构造两个案例、共四份固定 CSV:案例 A 是银行交易欺诈识别,案例 B 是门店商品的次日销量预测。每个案例都把八行教学小表与完整实验集分开保存,但沿用相同的字段含义和业务背景。它们完全由教学规则和固定随机种子生成,不来自真实银行或零售商,也不代表行业平均欺诈率或销量水平。

案例 A:银行交易数据与欺诈识别任务 #

先看数据。每行是一笔银行交易,记录交易发生时可以取得的 4 个字段,以及事后获得的真实标签 y。最右列统一取教学行 S1,便于预览一条完整记录。

字段类型/单位含义示例
amount非负小数;模拟货币单位交易金额30
distance非负小数;公里距惯常交易地点的距离2
merchant_type字符串类别商户类型:retail、digital、travel 或 servicesretail
merchant_id字符串类别商户标识 M00—M79;编号不表示大小顺序M00
y整数 0 或 1目标标签:0 表示正常,1 表示欺诈0

这些字段为什么可能有用?在随机生成的 6,000 笔完整实验数据中,脚本先抽取金额 \(a_i\)、距离 \(d_i\)、商户类型 \(c_i\) 和商户 ID \(m_i\),再按下面的教学规则计算欺诈概率:

\[ \begin{aligned} z_i&=-5.1+\log(1+a_i/100)+u_i+b_{m_i},\\ u_i&=1.4\,\mathbb{1}(d_i>150) +0.9\,\mathbb{1}(c_i=\text{digital}),\\ p_i&=\frac{1}{1+\exp(-z_i)},\qquad y_i\sim\operatorname{Bernoulli}(p_i). \end{aligned} \]

其中 \(\mathbb{1}(\cdot)\) 在条件成立时取 1,否则取 0;\(b_{m_i}\) 是同一商户 ID 共享的固定随机偏移。因此,金额或距离较大只会改变生成概率,不能单凭一列认定欺诈。金额、距离分别从对数正态分布抽取,商户类型按预设比例抽取。

另一个教学 CSV 保存特意设定的八笔交易 S1—S8。它们不按上述概率公式抽样,只用于后文手算熵、梯度和增益;其中四笔正常、四笔欺诈。完整实验集则有 168 笔欺诈(2.8%),并按标签分层保存为 3,600 笔训练、1,200 笔验证和 1,200 笔测试数据。教学小表不参与这三个数据分区或软件指标计算。

现在假设银行希望设计一个交易记录欺诈识别模型:模型在交易发生时读取金额、距离、商户类型和商户 ID,输出该交易为欺诈的概率;事后标签 y 只用于训练与评估,不能作为预测输入。如果这些字段与欺诈之间存在可重复的模式,却很难用一个现成公式准确描述,而银行又积累了足够且有代表性的历史记录,就可以让模型从数据中学习。这是一个二分类任务。

案例 B:门店商品逐日销量数据与次日销量预测任务 #

先看数据。每行记录一款商品在一家门店某一天的销量,同时保存商品、门店、日期、促销、折扣和历史销量等字段。最右列统一取教学行 D1,便于预览一条完整记录。

字段类型/单位含义示例
sku_id字符串类别商品标识;编号不表示大小顺序SKU00
store_id字符串类别门店标识;编号不表示大小顺序ST0
business_dateYYYY-MM-DD预测日期2025-01-13
weekday整数 0—6星期一为 0、星期日为 60
promo_type字符串类别已计划的促销类型:none(无促销)、member(会员促销)、campaign(活动促销)none
discount整数;百分点已计划的折扣,如 10 表示 10%0
lag7小数;件同一商品—门店在预测日前七天的平均销量10
y非负整数;件目标值:预测日记录的销量10

CSV 覆盖 20 个商品、2 家门店、120 天。脚本为每个商品设置一个固定偏移 \(v_s\),为两家门店分别设置 \(-2\) 和 \(3\) 件的偏移 \(v_u\)。对普通生成行,商品 \(s\)、门店 \(u\) 在第 \(t\) 天的销量按下式产生:

\[ \begin{aligned} \mu_{s,u,t} &=23+v_s+v_u+4W_t\\ &\quad+0.65q_{s,u,t}+2\sin(2\pi t/30),\\ y_{s,u,t} &=\max\!\left(0,\operatorname{round} (\mu_{s,u,t}+\epsilon_{s,u,t})\right). \end{aligned} \]

这里 \(W_t\) 在周末取 1,其他日期取 0;\(q_{s,u,t}\) 是折扣百分点,所以 10% 折扣代入的是 10;\(\epsilon_{s,u,t}\) 是标准差为 4 件的随机扰动。式中的周末、折扣和周期项都是人为设定的教学机制,不是对真实门店的估计。促销类型按预设比例抽取,并决定普通行的折扣。

历史销量特征另外按预测前的数据计算,不能把当天的 \(y_{s,u,t}\) 放进去:

\[ \operatorname{lag7}_{s,u,t} =\frac{1}{7}\sum_{r=1}^{7}y_{s,u,t-r}, \qquad t\geq 7. \]

前七天(第 0—6 天)的 280 行仅供形成历史窗口;第 7—79 天的 2,920 行用于训练,第 80—99 天的 800 行用于验证,第 100—119 天的 800 行用于测试。完整实验集全部按照上述生成机制产生。D1—D8 单独保存在教学 CSV 中,lag7 是为手算显式指定的数值,不参与完整面板的时间划分或软件指标计算。

现在假设零售商希望设计一个门店商品次日销量预测模型:每天收盘后,模型使用届时已经知道的 sku_id、store_id、promo_type、discount、weekday 和 lag7,预测次日销量 y;business_date 只用于确定时间顺序。如果这些因素与次日销量之间存在可重复的模式,却很难用一个现成公式准确描述,而零售商又积累了足够且有代表性的历史记录,就可以让模型从数据中学习。这是一个回归任务。当前数据假定库存充足,因此 y 是可完整观察的销量;缺货时销量与潜在需求可能不同,第九章再讨论如何定义需求预测目标。两份数据都保留原始类别列,第八章将用相同字段和划分比较 XGBoost、LightGBM 与 CatBoost。

现在只需把字段与目标分清,模型接口留到后文。

一、一棵决策树如何完成分类与回归? #

1.1 从门店销量预测开始 #

一棵决策树要从哪些记录中学习?先看案例 B 的门店商品次日销量预测教学数据中专门留出的八条训练行。它们是八个商品—门店组合在同一天的销量;折扣和促销是事先计划,七日均值由前一天以前的记录计算。

记录计划折扣过去七日均销量(件)促销类型当天销量(件)
D10%10无10
D20%15无14
D310%12会员18
D410%20会员26
D520%15活动40
D630%35活动60
D740%40活动92
D850%60活动140

这八行是为手算而设定的,不是随机抽样的“典型门店”。先只按计划折扣画一棵示意回归树:

                 折扣 ≤ 25%?
                 /          \
              是              否
          预测 21.6 件     折扣 ≤ 35%?
                         /          \
                      是              否
                  预测 60 件       预测 116 件

例如,计划折扣为 40% 的 D7 会走到右下叶节点。这个叶节点包含 D7(销量 92 件)和 D8(销量 140 件),因此给 D7 的预测值是 \((92+140)/2=116\) 件。左叶的 \(21.6\) 件是 D1—D5 的销量均值。这棵树只演示如何沿路径预测,并不声称这些切分是模型在完整 CSV 上学出的最佳切分;即使它贴合这八行,也不代表能可靠预测未来日期。

这个例子让我们看到一条预测路径。后面要讨论树如何生长、何时停止,先得认清它的结构。

1.2 先看清一棵树的结构 #

把上一节的示意树只保留节点与连线,可以得到下面这张简图:

                 根节点            深度 0:1 个节点
                 /   \
             叶节点  判断        深度 1:2 个节点
                    /    \
                 叶节点  叶节点   深度 2:2 个叶节点

从图的最上方看起:

  • 根节点(Root Node)是第一处判断;
  • 内部节点(Internal Node)执行特征判断;
  • 分支(Branch)表示判断结果;
  • 叶节点(Leaf Node)输出预测;
  • 从根节点到叶节点的一条路径构成一组预测规则。

本文约定根节点的深度为 0:每向下经过一条分支,深度增加 1。因此,这棵树共有 3 层,最大深度为 2。各层分别有 1、2、2 个节点;树的宽度指节点最多的那一层有多少个节点,所以这里的宽度是 2。宽度只是结构描述,不是后文需要设置的训练参数。

图中每个内部节点都分出两个子节点,因此它是一棵二叉树;“二叉”说的是每次切分的分支数,并不是说它只能解决二分类问题。决策树也可以有多于两个分支。

决策树不需要预先假设销量与折扣之间存在线性关系。刚才的三条路径把输入分成三组条件,树在每组条件下给出一个预测值。这就是下一节“分段常数函数”的直观含义。

1.3 决策树本质上是分段常数函数 #

为把刚才的判断路径写成统一形式,我们先约定 \(i\) 表示样本,\(j\) 表示叶节点;\(N\)、\(J\) 分别是样本数和叶节点数,\(p\) 是输入特征数。分类时再用 \(k\) 表示类别,\(C\) 表示类别数。后文出现新的符号时,会在相应位置说明。

一条记录的输入特征可以表示为:

\[ x=(x_1,x_2,\ldots,x_p) \]

树上的每条完整路径对应着一块输入区域。若共有 \(J\) 个叶节点,就有 \(J\) 个互不重叠的区域:

\[ R_1,R_2,\ldots,R_J \]

对于回归问题,预测函数可以表示为:

\[ f(x)=\sum_{j=1}^{J}c_j\mathbb{I}(x\in R_j) \]

其中:

  • \(R_j\):第 \(j\) 个叶节点对应的特征区域;
  • \(c_j\):该叶节点的预测值;
  • \(\mathbb{I}(\cdot)\):指示函数,条件成立时为 1,否则为 0。

区域 \(R_j\) 是输入特征的取值范围;落入该区域的训练样本索引另记为 \(I_j=\{i:x_i\in R_j\}\)。因此,\(|I_j|\) 才是叶节点中的训练样本数。对于使用平方误差训练的普通回归树,叶节点的预测值是这些样本的目标均值:

\[ c_j=\frac{1}{|I_j|}\sum_{i\in I_j}y_i \]

回到示意树:折扣高于 35% 的 D7、D8 进入同一个叶节点;它们的销量分别是 92 和 140 件,因此叶节点输出为 \((92+140)/2=116\) 件。回归树正是用这样一组局部常数近似更复杂的预测关系。

如果换成案例 A 的银行交易欺诈识别任务,树仍沿路径划分记录,但叶节点不再仅报告一个销量数值,而是要汇总落入其中的正常与欺诈样本。

设类别索引 \(k=1,\ldots,C\)。对于类别 \(k\),叶节点的经验概率可以表示为:

\[ \hat p_{jk} = \frac{ \sum_{i\in I_j}\mathbb{I}(y_i=k) }{ |I_j| } \]

如果直接按叶节点的类别概率给出预测标签,通常选择概率最大的那一类:

\[ \hat y_j=\arg\max_{1\leq k\leq C}\hat p_{jk} \]

需要注意,预测类别(Label)与预测概率(Probability)并不是同一个输出。二分类任务也可以另设决策阈值,而不直接采用概率最大的类别。例如,案例 A 的银行交易欺诈识别模型若给某笔交易 0.72 的欺诈概率,阈值为 0.5 时会将其标为待核查;阈值为 0.8 时则不会。改变的是审核规则,而不是模型输出的概率。因此,分类模型的训练目标、概率估计和最终业务决策需要分别讨论。

1.4 分类与回归的根本区别 #

虽然分类树与回归树可以采用相似的结构,但两者优化的目标不同。

问题分类(Classification)回归(Regression)
预测对象离散类别连续数值
典型应用欺诈识别、客户流失、故障检测销量预测、价格预测、需求预测
输出类别分数、概率或标签数值预测
单棵树常见的切分依据基尼不纯度、信息熵平方误差、绝对误差
常见评估Precision、Recall、AP、ROC-AUCMAE、RMSE、WAPE、Forecast Bias

在案例 A 的银行交易欺诈识别任务中,Precision 关心标为可疑的交易有多少真是欺诈,Recall 关心欺诈交易有多少被找出;误报占用人工审核时间,漏报可能带来损失。在案例 B 的门店商品次日销量预测任务中,MAE 关心销量预测平均差多少件,Forecast Bias 关心总体倾向于高估还是低估。第 8.2 节和第 9.5 节会用这两份数据讨论指标与业务取舍。

这种区别不仅影响模型评估,也会改变单棵树的切分标准。切分依据与最终评估指标各有用途,不能直接画等号;提升模型使用的训练损失将在第四章说明。

1.5 训练一棵树,究竟要确定什么? #

前面的两个案例都区分了预测时可用的输入特征 \(x_i\) 与要预测的目标 \(y_i\)。下面不依赖某份 CSV 的具体字段,将 \(N\) 条训练样本统一记为 \(D=\{(x_i,y_i)\}_{i=1}^{N}\)。

训练前后的决定可以分成三类:

类别在零售例子中是什么由谁决定
一:数据与任务输入特征、预测目标及其历史样本由业务问题、数据采集和建模者定义
二:训练设置如何评价切分,以及树的最大深度、叶节点最少样本数等限制由建模者预先设置,必要时用验证数据选择
三:训练得到的模型各节点使用的特征与切分位置、整棵树的结构,以及各叶节点的预测值由训练算法根据数据和训练设置确定

第二类通常称为超参数或算法配置,由建模者在训练前设定,必要时通过验证数据选择。第三类才是这次训练得到的模型:它既包括树的结构,也包括叶节点输出,不必把它想成线性回归那样的一组固定系数。具体软件怎样命名这些设置,等弄清切分原理后再对照。

现在可以把“训练一棵树”说得更具体:它既要决定在哪里切分,也要决定每个叶节点预测多少。以平方误差回归树为例,设 \(\mathcal T\) 表示树的结构,\(q_{\mathcal T}(x_i)\) 表示样本 \(x_i\) 落入的叶节点编号,\(c_j\) 是第 \(j\) 个叶节点的预测值。训练希望在允许的树中尽量减小:

\[ \mathcal E(\mathcal T,\boldsymbol c) =\sum_{i=1}^{N}\bigl(y_i-c_{q_{\mathcal T}(x_i)}\bigr)^2 \]

例如,若设置最大深度 \(d_{\max}\)、每个叶节点至少有 \(n_{\min}\) 条训练样本,那么允许的树还要满足 \(\operatorname{depth}(\mathcal T)\leq d_{\max}\) 且 \(|I_j|\geq n_{\min}\)。这里的 \(d_{\max}\) 和 \(n_{\min}\) 是训练前的限制;节点切分与 \(c_j\) 则由训练得到。给定叶节点划分后,第 1.3 节的均值公式就给出了使该叶节点平方误差最小的 \(c_j\)。

这个目标也解释了为何需要限制树的复杂度。D1—D8 的销量依次为 10、14、18、26、40、60、92 和 140 件。不切分时八行都预测均值 50 件,训练平方误差和为 \(14{,}560\) 件²。若只按“折扣不超过 25%”切分一次,D1—D5 进入左叶、D6—D8 进入右叶,分别预测 \(21.6\) 和约 \(97.33\) 件,误差和降至约 \(3{,}805.87\) 件²;继续细分甚至让每行独占一叶,可以使这八行的训练误差降至 0。但更贴合这八行不等于更能预测未来销量,仍须用完整实验 CSV 中靠后的验证和测试日期检验;第九章还会介绍多轮滚动起点回测。

上面的式子描述的是希望优化的目标,不表示普通 CART 会枚举所有可能的树,求出全局最优解。实际训练通常从根节点开始,在当前节点比较候选切分,选出局部最好的一个,再继续处理子节点;停止条件和剪枝设置会影响最终结构。分类树同样要学习结构和叶节点输出,只是节点通常改用基尼不纯度或信息熵评价切分,叶节点保存类别比例。现在,训练中“由谁决定什么”已经清楚,下一章就来回答每个节点怎样比较候选切分。

二、决策树如何选择最佳切分? #

一棵树最重要的学习过程不是执行判断,而是决定:

应该使用哪个特征,以及在哪个位置切分数据?

无论选择哪个特征,切分后都要回答同一个问题:子节点是否比父节点更适合做预测?不同算法对“更好”的定义不同。本章按切分准则便于比较的顺序介绍 ID3、C4.5 和 CART,而不是按发表年份排序。

2.1 ID3:使用信息熵衡量分类的不确定性 #

Quinlan 在 1986 年的论文中介绍了 ID3(Iterative Dichotomiser 3)[1],旨在解决如何从训练数据中逐步选出能区分不同类别的判断条件。

当节点内的样本几乎同属一类时,分类判断更为明确;若各类混杂,判断则犹豫不决。ID3 利用信息熵(Entropy)来量化这种类别不确定性。

对于包含 \(C\) 个类别的数据集 \(D\),信息熵定义为:

\[ H(D)=-\sum_{k=1}^{C}p_k\log_2p_k \]

其中 \(p_k\) 是类别 \(k\) 的样本比例。

先看案例 A 的银行交易欺诈识别教学数据 S1—S8。这里仅列计算所需的商户类别、商户 ID 和标签;八笔中四笔正常、四笔欺诈,商户 ID 各不相同。

记录商户类型商户 ID欺诈标签 \(y\)
S1retailM000
S2retailM010
S3retailM020
S4retailM030
S5digitalM041
S6digitalM051
S7digitalM061
S8retailM071

这张刻意平衡的小表在根节点的熵为:

\[ H(D)=-0.5\log_2 0.5-0.5\log_2 0.5=1 \]

若八笔都属同一类,熵为 0。这里的 1 bit 是教学小表的结果,不能拿来描述欺诈比例仅 2.8% 的完整实验集。

ID3 希望找到能够最大程度降低熵的特征。

对于候选特征 \(A\),信息增益(Information Gain)定义为:

\[ IG(D,A) = H(D) -\sum_{v\in Values(A)} \frac{|D_v|}{|D|}H(D_v) \]

其中 \(D_v\) 表示按照特征 \(A\) 的取值 \(v\) 划分出的子集。

例如按商户类型切分,八笔中五笔 retail 交易有一笔欺诈,三笔 digital 全为欺诈。切分后的加权熵为 \((5/8)H(1/5)\approx0.451\),信息增益约 \(1-0.451=0.549\) bit。增益越大,表示小表中的类别混杂程度下降越多;但“分得很细”不必然等于学到了可泛化的规律。

信息增益的局限 #

S1—S8 恰好来自八个不同的商户 ID。若按这八个 ID 各建一组,小表中每组只有一笔,训练熵可降为零,信息增益达到 1 bit;这个结果比商户类型的 0.549 更大,却只是在记住八行标签。完整实验集的商户 ID 会重复出现,是否真的有稳定差异,必须在验证集检验。这说明信息增益为何可能偏好取值很多的特征。

2.2 C4.5:使用增益率修正特征选择偏好 #

Quinlan 在 1993 年出版的《C4.5》中,延续并发展了分类树的研究路线[2]。鉴于 ID3 信息增益偏好取值多的特征,C4.5 引入增益率等设计来修正这一偏向。

它引入增益率(Gain Ratio):

\[ GainRatio(D,A)=\frac{IG(D,A)}{SplitInfo(D,A)} \]

其中:

\[ SplitInfo(D,A)=-\sum_v\frac{|D_v|}{|D|}\log_2\frac{|D_v|}{|D|} \]

SplitInfo 描述的是特征本身将样本划分为多少个部分,以及这些部分的规模分布。

回到 S1—S8,商户类型分出 5 笔和 3 笔,SplitInfo 约为 \(0.954\),增益率约为 \(0.549/0.954=0.575\)。八个唯一商户 ID 虽获得 1 bit 信息增益,却有 3 bit 的 SplitInfo,增益率仅约 \(0.333\)。这一比较说明分母怎样压低“把每行单独分组”的得分,但不保证增益率在所有数据上都选对特征。

然而,如果切分后的各组规模极不均衡,分母可能变得很小,使增益率被放大。因此,C4.5 在选择特征时,并非直接选取增益率最大的,而是结合信息增益进行筛选。

除了增益率,C4.5 还改进了连续特征、缺失值处理和剪枝机制。

2.3 CART:统一分类与回归 #

Breiman 等人在 1984 年系统提出了 CART(Classification and Regression Trees)[3],该研究路线与 ID3、C4.5 并行发展。本文将 CART 放在信息熵之后介绍,旨在便于读者比较其切分标准,而非按发表年份排序。

与早期主要面向分类的决策树方法不同,CART 同时支持分类和回归;它每次将节点切分为左、右两个子节点,因此构造的是第一章所见的二叉树。

分类:Gini Impurity #

对于分类任务,CART 常使用基尼不纯度(Gini Impurity):

\[ Gini(D)=1-\sum_{k=1}^{C}p_k^2 \]

在案例 A 的银行交易欺诈识别教学数据中,四笔交易正常、四笔为欺诈:

\[ Gini(D)=1-0.5^2-0.5^2=0.5 \]

对于一次候选切分,模型计算切分后两个子节点的加权不纯度:

\[ Gini_{split} = \frac{|D_L|}{|D|}Gini(D_L) +\frac{|D_R|}{|D|}Gini(D_R) \]

按商户类型切分后,retail 组的基尼不纯度为 \(1-(1/5)^2-(4/5)^2=0.32\),digital 组为 0;加权结果是 \((5/8)\times0.32=0.20\),比根节点低 \(0.30\)。模型选择能够最大程度降低不纯度的切分。

回归:Squared Error #

对于回归任务,CART 可以使用平方误差衡量节点内部的预测差异。这里的 \(D\) 表示当前节点中的样本记录集合,\(D_L\) 和 \(D_R\) 是切分后的两个记录子集:

\[ SSE(D) = \sum_{(x_i,y_i)\in D}(y_i-\bar y_D)^2 \]

其中 \(\bar y_D\) 为节点内目标值的均值。

一次切分带来的误差下降为:

\[ \Delta SSE = SSE(D) -SSE(D_L) -SSE(D_R) \]

回归树选择能够最大程度降低平方误差的切分:子节点内的目标值越接近,\(\Delta SSE\) 越大。第 5.8 节会就地重列案例 B 的门店商品次日销量预测教学数据 D1—D8,并完整计算一次候选切分;这里先保留切分准则,不重复同一组数值。

此处 \(SSE\) 指节点内平方误差之和。若将误差改为节点均方误差,则在比较同一父节点下的候选切分时,按子节点样本量加权后的下降量与 \(\Delta SSE\) 成比例,切分排序保持一致,但两者的数值未经归一化不能直接比较。

对照刚才两类公式,就能抓住 CART 的共同动作和不同目标:它们都在划分特征空间;分类树希望子节点的类别更加纯净,回归树希望子节点中的目标值更加接近。

2.4 决策树为什么容易过拟合? #

第一章的 D1—D8 已揭示过拟合的风险:若持续切分至每个叶节点仅含一条记录,树虽能极大降低训练误差,但也可能将随机波动、测量误差乃至错误标签误记为普遍规律。

案例 B 的门店商品次日销量预测教学数据中,D8 售出 140 件,是有计划的高折扣教学记录,不能只因数值高就当作错误。第 8.10 节会另在约 3% 的训练标签中注入记录错误;如果树过度拟合这类被污染的行,就可能专门构造一条路径来预测错误数值。

控制树复杂度的常见方法包括:

  • 限制最大深度(Maximum Depth);
  • 设置叶节点最小样本量;
  • 限制叶节点数量;
  • 要求切分带来足够大的误差下降;
  • 使用剪枝(Pruning)删除泛化收益不足的分支。

这些限制通常能减少对训练记录的记忆,但限制太强又会漏掉有用的差异。如何在训练拟合与新样本表现之间取得平衡,正是下一章讨论多棵树的起点。

接口对照。 第一章提及的训练设置,在 scikit-learn 的 DecisionTreeRegressor 中,对应 criterion、max_depth 和 min_samples_leaf 等参数;这些并非训练时逐节点学习的切分位置。上文平方误差示例对应 criterion="squared_error"。该接口也提供绝对误差、Poisson 等切分准则,叶节点输出与切分口径会随准则改变[4]。其文档把 squared_error 描述为节点均方误差,因而不能把接口报告的数值直接当作上文未经归一化的 \(\Delta SSE\)。

三、为什么一棵树不够?从 Bagging 到 Random Forest #

3.1 单棵决策树的高方差问题 #

单棵决策树的问题在于其判断路径可能不稳定,不仅可能记住异常值,还容易对训练数据敏感。假设训练数据略有差异,决策树在根节点选择的特征就可能不同,导致后续路径及对新数据的预测发生波动。统计学习把这种随训练样本变化而产生的波动称为方差(Variance)。

对于平方误差回归,先固定一个输入 \(x\),再设模型会因训练样本的变化而得到不同的预测。在新观测噪声与训练样本独立等常见假设下,该点的期望预测误差可以分解为:

\[ \mathbb E[(Y-\hat f(x))^2\mid X=x] = \operatorname{Bias}(x)^2+\operatorname{Var}(\hat f(x))+\sigma^2(x) \]

其中:

  • \(\operatorname{Bias}(x)\) 表示模型在 \(x\) 处的平均预测与真实条件均值 \(\mathbb E[Y\mid X=x]\) 之间的差异;
  • \(\operatorname{Var}(\hat f(x))\) 表示训练样本变化造成的预测方差;
  • \(\sigma^2(x)\) 表示该点目标值的不可约噪声方差。

若要讨论总体预测误差,还需对不同输入 \(x\) 的分布再取平均。这里的统计偏差也不同于第 9.5 节衡量预测整体高估或低估的预测偏差(Forecast Bias)。

尽管充分生长的树能紧贴训练数据,但其对样本变化的敏感性可能导致测试预测波动。这引出了一个朴素的问题:能否通过训练多棵略有差异的树并汇总预测,来稳定结果?

3.2 Bagging:训练多棵树并平均预测 #

Breiman 在 1996 年发表的 Bagging(Bootstrap Aggregating)论文中,提出通过重抽样和平均预测来改善不稳定学习器的表现[5]。对决策树而言,这提供了一个直接的思路:让多棵树独立学习,再汇总它们的判断。

Bagging 通过从原始训练数据中有放回地抽样,生成多个 Bootstrap 数据集。每个数据集用于训练一棵树,最后组合所有树的预测。有放回抽样确保了同一条记录可能在某个数据集中出现多次,也可能完全不出现,从而为各棵树引入了适度差异。

设一共训练 \(B\) 棵树,第 \(b\) 棵树的预测记为 \(\hat f_b(x)\)。对于回归任务:

\[ \hat f_{bag}(x) = \frac{1}{B}\sum_{b=1}^{B}\hat f_b(x) \]

对于分类任务,可以平均类别概率:

\[ \hat p_k(x) = \frac{1}{B}\sum_{b=1}^{B}\hat p_{bk}(x) \]

然后根据概率选择预测类别。

如果多棵树的预测误差并不完全相关,平均预测通常可以降低方差。

在同一个输入 \(x\) 处,记上面的平均预测为 \(\bar f(x)=\hat f_{bag}(x)\)。设每棵树预测值的方差为 \(v\),任意两棵树预测值之间的相关系数为 \(\rho\),在简化的等方差、等相关假设下,平均预测的方差为:

\[ \operatorname{Var}(\bar f(x)) = \rho v +\frac{1-\rho}{B}v \]

例如,若 \(v=100\)、\(\rho=0.2\),平均 10 棵树的预测方差在这个简化模型下是 \(0.2\times100+0.8\times100/10=28\);即使继续增加树,第一项 20 也不会消失。这说明树之间越相似,单靠增加数量越难继续降低波动。

因此,仅仅增加树的数量还不够,降低树之间的相关性也很重要。

3.3 Random Forest:通过特征随机化降低相关性 #

Breiman 于 2001 年系统提出了 Random Forest(随机森林)[6]。在 Bagging 已经能通过平均降低方差的基础上,随机森林进一步指出:如果多棵树总是依赖同一批强特征,它们的预测误差仍可能高度相关。

随机森林在 Bagging 的基础上,增加了一个关键机制:每棵树在寻找节点切分时,只考虑随机选择的一部分特征。强特征有时不会进入候选集,其他特征便有机会参与切分,从而增加树的多样性。

以案例 B 的门店商品次日销量预测任务为例,若折扣总能解释较多训练误差,普通 Bagging 的多棵树就可能反复优先使用折扣。随机森林在节点切分时只抽取部分候选字段,过去七日销量或商品类别等便有机会参与切分。这里说的是训练机制,并非断言完整 CSV 上哪一列实际最重要。

Random Forest 的基本流程是:

训练数据
   |
   +-- Bootstrap Sample 1 --> Tree 1 --+
   |                                    |
   +-- Bootstrap Sample 2 --> Tree 2 --+--> 平均或投票
   |                                    |
   +-- Bootstrap Sample 3 --> Tree 3 --+
   |
   +-- ...

Random Forest 的不同树可以相对独立地训练,因此容易并行化。

这与后面讨论的 Boosting 存在根本区别。

3.4 Bagging 与 Boosting 的区别 #

在深入探讨 Boosting 如何逐轮优化损失之前,我们先概览这两种集成路线的整体区别。

比较Bagging / Random ForestBoosting / GBDT
训练方式多棵树相对独立训练后一轮依赖前一轮模型
主要机制Bootstrap、随机化、平均逐轮添加模型优化损失
典型作用降低方差通过加法模型逐步改善拟合
树的角色各自完成预测每棵树通常贡献一部分修正
并行方式树与树之间容易并行主要在单轮训练内部并行

因此,Random Forest 并不是 GBDT 的旧版本,GBDT 也不是 Random Forest 的简单升级。

接口对照。 算法中的特征随机化不等于某个软件的默认设置。本文实验所用 scikit-learn RandomForestRegressor 的默认 max_features=1.0,每个节点会考虑全部特征;若希望在节点切分时只看部分特征,应显式设置更小的 max_features。bootstrap=True 控制有放回抽样[7]。

下一章沿着表中“逐轮修正”的路线,看后一棵树如何确定修正方向。

四、从 Boosting 到 GBDT:后一棵树为什么能够修正前一棵树? #

4.1 Boosting:逐步构造更强的预测模型 #

与前一章中各树独立训练并平均结果不同,Boosting 让后续模型在前一轮的基础上进行修正。Boosting 同样组合多个简单模型,但其核心在于“下一轮该如何修正”当前预测。

Freund 与 Schapire 在 1997 年的代表性论文中系统研究了 AdaBoost;它是早期具有代表性的 Boosting 算法[8]。以二分类为例,AdaBoost 通过调整训练样本权重,使后续弱学习器更加关注此前被错误分类的样本。设一共训练 \(M\) 轮,第 \(m\) 轮得到的弱学习器记为 \(h_m\)。

最终模型可以表示为:

\[ F(x)=\sum_{m=1}^{M}\alpha_mh_m(x) \]

其中:

  • \(h_m(x)\) 是第 \(m\) 个弱学习器;
  • \(\alpha_m\) 是该学习器的组合权重。

AdaBoost 主要通过样本权重变化组织训练过程。接下来要看的 Gradient Boosting,则把逐轮修正与一般损失函数的优化联系起来。

4.2 GBDT:把训练过程看作函数优化 #

Friedman 在 2001 年发表的 Gradient Boosting Machine 论文中,把逐轮添加模型与损失函数的梯度联系起来,为构造加法模型提供了系统框架[9]。以决策树为基学习器时,通常称为梯度提升树(Gradient Boosted Decision Trees,GBDT)。随机森林主要通过对预测值取平均来减小方差;梯度提升则根据当前损失的下降方向,指导下一棵树如何修正预测。

假设模型由多棵树相加构成:

\[ F_M(x)=F_0(x)+\sum_{m=1}^{M}\eta f_m(x) \]

其中,\(M\) 是总轮数,\(m\) 是当前轮次;\(F_0(x)\) 是初始预测,\(f_m(x)\) 是第 \(m\) 棵树,\(\eta\) 是学习率(Learning Rate)。

GBDT 希望最小化训练损失:

\[ \mathcal L(F) = \sum_{i=1}^{N}l(y_i,F(x_i)) \]

这里 \(l\) 是单条样本的损失,\(\mathcal L\) 是对所有训练样本求和后的目标。第五章加入树的复杂度惩罚时,仍沿用这一区分。

它采用逐步加法的方式更新预测函数:

\[ F_m(x) = F_{m-1}(x)+\eta f_m(x) \]

于是,关键不再是多加一棵树,而是决定第 \(m\) 棵树应朝哪个方向修正当前预测。平方误差下,这个方向可以先从残差看出来;一般损失函数下,则要看梯度。

4.3 从残差到负梯度 #

考虑案例 B 的门店商品次日销量预测教学数据 D6:实际售出 60 件,先给八行都设初始预测 50 件。平方误差会怎样告诉下一棵树“往上修正”?令损失为:

\[ l(y,F(x)) = \frac12(y-F(x))^2 \]

对模型预测值求导:

\[ \frac{\partial l}{\partial F(x)} = F(x)-y \]

因此,负梯度为:

\[ -\frac{\partial l}{\partial F(x)} = y-F(x) \]

平方误差下,负梯度正是预测残差。代回 D6 的销量:

\[ r=60-50=10 \]

下一棵树旨在学习这个修正方向。它并非直接预测最终销量,而是在当前预测的基础上,补充模型未能解释的残余部分。

对于一般损失函数,GBDT 使用负梯度:

\[ r_{im} = -\left[ \frac{\partial l(y_i,F(x_i))} {\partial F(x_i)} \right]_{F=F_{m-1}} \]

因此,“GBDT 学习残差”只是在平方误差下特别直观的解释。更一般地,GBDT 学习的是损失函数在当前预测位置的下降方向。

4.4 分类任务中的 GBDT #

对于二分类问题,模型通常不直接累加类别标签,而是累加原始预测分数(Raw Score)。

设:

\[ \hat p_i=\sigma(F(x_i)) = \frac{1}{1+e^{-F(x_i)}} \]

其中 \(\sigma\) 是 Sigmoid 函数。

二分类 Log Loss 为:

\[ l(y_i,\hat p_i) = -y_i\log \hat p_i -(1-y_i)\log(1-\hat p_i) \]

对原始分数 \(F(x_i)\) 求导:

\[ g_i=\hat p_i-y_i \]

由此可见,分类任务与回归任务的梯度形式有所不同,但 GBDT 的总体训练框架没有改变:模型仍然通过逐轮添加树来优化损失。

4.5 与机器学习优化理论的关系 #

读到这里,也可以把 GBDT 与更熟悉的参数优化作个对照。此前的《机器学习中的优化:从梯度下降到 Boosting、神经网络与 MAP》对损失、梯度与正则化有更完整的讨论;这里只比较两种更新对象。

普通梯度下降直接更新参数向量:

\[ \theta_{t+1} = \theta_t-\eta\nabla_\theta\mathcal L \]

而 Gradient Boosting 在函数空间中逐步增加新的基学习器:

\[ F_m=F_{m-1}+\eta f_m \]

两者都利用损失函数的局部变化方向来优化模型,但更新的对象和方式有所区别;这一思想也为理解 XGBoost 的二阶优化奠定了基础。

五、XGBoost:如何利用二阶信息优化树模型? #

Chen 和 Guestrin 在 2016 年的论文中系统介绍了 XGBoost(Extreme Gradient Boosting)[10]。梯度提升的基本思路此时已臻成熟,但新的挑战在于,当数据量增大时如何高效训练模型:这要求树模型反复读取特征、计算候选切分,导致计算量和数据访问成本显著增加。

本章将探讨两个核心问题:如果下一棵树仍按梯度方向修正,每个叶节点应修正多少?面对海量数据,如何高效计算这些候选切分?XGBoost 通过结合正则化目标、二阶近似、稀疏特征处理和高效计算,系统地回应了这两个挑战。

5.1 XGBoost 的预测函数 #

假设模型逐轮加入 \(M\) 棵树。沿用第 4 章的 \(F_m\) 表示加法模型的原始预测分数,则经过 \(M\) 轮迭代后,模型的预测分数为:

\[ z_i^{(M)}=F_M(x_i) =F_0(x_i)+\sum_{m=1}^{M}\eta f_m(x_i) \]

其中每棵树属于函数空间:

\[ f_m\in\mathcal F \]

一棵树可以表示为:

\[ f(x)=w_{q(x)} \]

这里:

  • \(q(x)\) 将输入样本映射到叶节点;
  • \(w_j\) 是第 \(j\) 个叶节点的输出权重;
  • \(J\) 是当前这棵树的叶节点数量;\(q(x)=j\) 等价于 \(x\in R_j\),此处 \(w_j\) 对应第 1 章的 \(c_j\),但表示加法模型的增量。

这与第一章的分段常数函数形式一致,但叶节点的角色有所不同:第 1 章中,\(c_j\) 是单棵回归树直接给出的预测值;而这里的 \(w_j\) 通常作为增量,被加到当前预测分数上,而非直接的最终预测值。

在回归任务中,此增量可用于修正预测值,例如销量。

在分类任务中,该增量通常作用于 Logit 等原始分数,并通过 Sigmoid 或 Softmax 函数转换为概率。因此,下文统一用 \(z_i^{(m)}\) 表示模型训练时优化的原始分数;二分类概率写作 \(\hat p_i=\sigma(z_i^{(m)})\),回归预测写作 \(\hat y_i\)。对于平方误差回归,\(\hat y_i=z_i^{(m)}\)。

5.2 目标函数:数据损失与模型复杂度 #

为评估模型的修正效果,仅凭训练误差是不够的,还需防止模型为少数记录生成过多叶节点(即过拟合)。因此,XGBoost 的目标函数同时考虑了数据损失和模型复杂度:

\[ \mathcal L = \sum_{i=1}^{N}l(y_i,z_i^{(M)}) +\sum_{m=1}^{M}\Omega(f_m) \]

其中:

  • 第一项衡量模型预测误差;
  • 第二项约束树模型的复杂度。

各轮树的叶节点数可以不同。下面推导单棵候选树时,\(J\) 只表示该树的叶节点数;若展开上式中的第 \(m\) 项,其叶节点数应记为 \(J_m\)。

对于典型的树模型,其正则化项可表示为:

\[ \Omega(f) = \gamma J+\frac12\lambda\sum_{j=1}^{J}w_j^2 \]

其中:

  • \(\gamma\) 控制新增叶节点所需的复杂度成本;
  • \(\lambda\) 控制叶节点权重 L2 正则化的强度。

这里的 \(\gamma J\) 对叶节点数量进行惩罚,与对叶节点权重取绝对值的 L1 正则化方式不同。

若启用 L1 正则化,则目标函数中还需额外加入以下项:

\[ \alpha\sum_{j=1}^{J}|w_j| \]

通过这种方式,新增叶节点或设置过大的叶节点权重都会受到惩罚。下一个问题是:面对众多的候选树结构,如何快速评估其代价与收益?

5.3 为什么使用二阶 Taylor 展开? #

接下来的几节将推导最优叶节点权重和切分增益,并在第 5.8 节用案例 B 的门店商品次日销量预测教学数据 D1—D8 详细演算。阅读时,请重点关注两个核心问题:每个叶节点应修正多少,以及一次切分是否值得。

假设已训练 \(m-1\) 棵树,现在准备增加第 \(m\) 棵树。为推导候选树的最优叶节点权重,我们首先考察未乘学习率的试探性更新:

\[ \widetilde z_i^{(m)} = z_i^{(m-1)}+f_m(x_i) \]

我们对这次试探更新构建局部目标函数;在求得最优叶节点权重后,实际模型仍会按 \(z_i^{(m)}=z_i^{(m-1)}+\eta f_m(x_i)\) 进行更新:

\[ \mathcal L^{(m)} = \sum_i l(y_i,z_i^{(m-1)}+f_m(x_i)) +\Omega(f_m) \]

直接搜索所有可能的树结构在计算上通常不可行。XGBoost 转而估计“当对当前预测分数进行微小改动时,损失函数会如何变化”。为此,它对损失函数进行二阶 Taylor 近似:

\[ l(y_i,z_i^{(m-1)}+f_m(x_i)) \approx l(y_i,z_i^{(m-1)}) +g_if_m(x_i) +\frac12h_if_m(x_i)^2 \]

其中:

\[ g_i= \left.\frac{\partial l(y_i,z)}{\partial z}\right|_{z=z_i^{(m-1)}} \]\[ h_i= \left.\frac{\partial^2 l(y_i,z)}{\partial z^2}\right|_{z=z_i^{(m-1)}} \]

它们分别表示损失函数对预测值的一阶梯度与二阶导数。为建立直观理解,我们先考察平方误差损失函数:若 \(l(y,z)=\frac12(y-z)^2\),则一阶梯度 \(g_i=z_i^{(m-1)}-y_i\),二阶导数 \(h_i=1\)。梯度 \(g_i\) 描述了损失函数随预测值变化的方向(负梯度即为下降方向),而二阶导数 \(h_i\) 在此例中为常数。分类损失函数的情况将在第 5.7 节进行对比说明。

删除与当前新增树无关的常数项 \(l(y_i,z_i^{(m-1)})\) 后,简化目标函数为:

\[ \widetilde{\mathcal L}^{(m)} = \sum_i \left[ g_if_m(x_i) +\frac12h_if_m(x_i)^2 \right] +\Omega(f_m) \]

需要注意的是,这里的二阶 Taylor 展开是一个局部近似,而非对任意损失函数都精确成立的等式。

5.4 从样本梯度到叶节点统计量 #

假设当前候选树共有 \(J\) 个叶节点。

沿用第 1.3 节的样本索引集合。现在用叶节点映射 \(q\) 表示同一件事:

\[ I_j=\{i:q(x_i)=j\} \]

由于同一叶节点中的样本具有相同的树输出:

\[ f_m(x_i)=w_j \]

因此,我们可以将所有样本的一阶梯度和二阶导数(Hessian)汇总到每个叶节点:

\[ G_j=\sum_{i\in I_j}g_i \]\[ H_j=\sum_{i\in I_j}h_i \]

目标函数随之变为:

\[ \widetilde{\mathcal L}^{(m)} = \sum_{j=1}^{J} \left[ G_jw_j +\frac12(H_j+\lambda)w_j^2 \right] +\gamma J \]

这一关键步骤将逐样本的损失变化(由 \(g_i\) 和 \(h_i\) 表示)汇总为每个叶节点的统计量 \(G_j\) 和 \(H_j\)。如此一来,原本需要逐条检查样本变化,现在只需合并同一叶节点中的信息;在给定候选切分后,通过分别汇总左右子节点的统计量,即可高效计算出修正幅度。

5.5 最优叶节点权重 #

在切分位置暂时固定不变的情况下,下一步是确定该叶节点应修正的幅度。固定树结构后,对目标函数关于某个叶节点权重 \(w_j\) 求导:

\[ \frac{\partial\widetilde{\mathcal L}}{\partial w_j} = G_j+(H_j+\lambda)w_j \]

令导数等于零,求解 \(w_j\):

\[ G_j+(H_j+\lambda)w_j=0 \]

得到:

\[ \boxed{ w_j^* = -\frac{G_j}{H_j+\lambda} } \]

这里假设未启用 L1 正则化,且分母 \(H_j+\lambda\) 为正。

这个公式说明:

  • 梯度总和 \(G_j\) 决定了更新方向;
  • Hessian 总和 \(H_j\) 反映了损失函数的局部曲率;
  • 正则化参数 \(\lambda\) 用于抑制过大的叶节点输出,防止过拟合。

若启用 L1 正则化,最优权重 \(w_j^*\) 则需经过软阈值(soft-thresholding)处理:

\[ w_j^* = -\frac{ \operatorname{sgn}(G_j) \max(|G_j|-\alpha,0) }{ H_j+\lambda } \]

这使得在梯度信号较弱时,叶节点权重可能被压缩为零。

5.6 XGBoost 如何判断一次切分是否值得? #

在确定了每个叶节点的最优修正幅度后,我们还需要判断“将一个节点切分为两个新叶节点,其收益是否值得额外付出复杂度成本”。以下仍采用未启用 L1 正则化的情形;将相应的最优叶节点权重代入目标函数,即可得到在当前树结构下的最优近似目标函数值:

\[ \widetilde{\mathcal L}^{(m)}(q) = -\frac12 \sum_{j=1}^{J} \frac{G_j^2}{H_j+\lambda} +\gamma J \]

对于某个候选切分点,设其父节点的统计量为 \(G\) 和 \(H\),左右子节点的统计量分别为:

\[ G_L,H_L \]\[ G_R,H_R \]

则此次切分的增益(Split Gain)为:

\[ \boxed{ Gain = \frac12 \left[ \frac{G_L^2}{H_L+\lambda} +\frac{G_R^2}{H_R+\lambda} -\frac{G^2}{H+\lambda} \right] -\gamma } \]

如果 Gain 大于零,则表明在当前近似目标函数和复杂度惩罚的考量下,通过多分出一个叶节点所带来的收益超过了其代价,即此次切分是有利的。

这也是 XGBoost 将数学优化问题高效转化为实际树结构搜索过程的关键所在。

第 5.7 节将进一步阐述不同任务下梯度的来源;第 5.8 节将通过同一张零售小表计算切分增益,并将其与 CART 的平方误差下降和 GBDT 的残差更新进行对比。

5.7 分类与回归的梯度为什么不同? #

XGBoost 的树结构搜索虽然采用了统一的 \(G,H\) 统计框架,但不同任务会产生不同的梯度和 Hessian 统计量。

回归:Squared Error #

在第 5.3 节中,我们已经推导出:对于平方误差 \(l(y,z)=\frac12(y-z)^2\),第 \(i\) 条样本在第 \(m\) 轮开始时的一阶梯度 \(g_i=z_i^{(m-1)}-y_i\),二阶导数 \(h_i=1\)。因此,在回归任务中,修正方向可以直观地理解为残差,且 Hessian 为常数。那么,当切换到分类损失函数时,这些统计量还会保持不变吗?

二分类:Logistic Loss #

对第 \(i\) 条样本,设:

\[ \hat p_i=\sigma(z_i^{(m-1)}) \]

则:

\[ g_i=\hat p_i-y_i \]\[ h_i=\hat p_i(1-\hat p_i) \]

在分类任务中,Hessian \(h_i\) 随预测概率 \(\hat p_i\) 的变化而变化。

案例 A 的银行交易欺诈识别教学数据 S1—S8 可以逐笔核对分类梯度。下面只重列这次手算会用到的金额、距离和真实标签;\(y=0\) 表示正常,\(y=1\) 表示欺诈。

记录金额距离(公里)欺诈标签 \(y\)
S13020
S28050
S3150100
S4250200
S545081
S61,200501
S73,0001201
S8803001

为便于手算,显式设定所有样本的初始概率 \(p_i^{(0)}=0.5\),对应原始分数 \(z_i^{(0)}=0\)。把 S1 的标签 \(y_1=0\) 代入上面的公式:

\[ \begin{aligned} g_1&=0.5-0=0.5,\\ h_1&=0.5(1-0.5)=0.25. \end{aligned} \]

对欺诈交易 S5,\(g_5=0.5-1=-0.5\),而 \(h_5=0.25\)。同一初始概率下,两类样本的梯度方向相反,Hessian 却相同。

接着看 S1 的下一轮概率从哪里来。指定一棵教学用候选树:先按金额 \(\leq350\) 切分,再在左侧按距离 \(\leq160\) 切分。表中 S1—S4 落入同一叶节点;S8 虽然金额低于 350,但距离为 300 公里,走向另一叶节点。因此,这个叶节点的 \(G=4(0.5)=2\)、\(H=4(0.25)=1\)。取 \(\lambda=1\)、学习率 \(\eta=0.3\),代入第 5.5 节的叶权重公式:

\[ \begin{aligned} w^*&=-\frac{2}{1+1}=-1,\\ z_1^{(1)}&=0+0.3(-1)=-0.3. \end{aligned} \]

因此,S1 的概率变为 \(p_1^{(1)}=\sigma(-0.3)\approx0.4256\)。第二轮再代入同一组梯度公式:

\[ \begin{aligned} g_1^{(2)}&=0.4256-0=0.4256,\\ h_1^{(2)}&=0.4256(1-0.4256)\approx0.2445. \end{aligned} \]

这个切分只用于展示从数据到更新的计算,不声称软件会在完整 CSV 上学到同一棵树。八笔教学交易恰有四笔欺诈,仅用于核对计算;第 8.9 节用同一案例中独立生成的完整不平衡数据评价模型。

这个例子也说明,即使两组样本具有相同的梯度总和 \(G_j\),它们的 Hessian 统计量 \(H_j\) 仍可能不同,进而影响叶节点权重 \(w_j^*\) 和切分增益。XGBoost 正是通过这套统一的树结构搜索框架支持不同目标函数。

5.8 同一组数据中的 CART、GBDT 与 XGBoost #

继续使用第一章的 D1—D8。为方便就地核对,下面只重列本节计算用到的计划折扣与销量;这些仍是案例 B 的门店商品次日销量预测教学 CSV 中的同八条记录,并非另一组数据。

记录计划折扣当天销量(件)
D10%10
D20%14
D310%18
D410%26
D520%40
D630%60
D740%92
D850%140

固定考察计划折扣 \(\leq25\%\) 这一候选切分:D1—D5 走左边,D6—D8 走右边。八行的销量均值是 50 件。CART 用它计算节点误差;下面的 GBDT 和 XGBoost 教学计算都显式设定从 50 件开始,学习率同为 \(\eta=0.3\)。三者使用同一组记录和切分,但计算动作不同;这不是三个软件在全量 CSV 上自动学出的首棵树。

CART:直接预测叶节点均值 #

CART 先问:按这个阈值切分,能使训练样本的平方误差和降低多少?切分前,父节点全部预测 50 件,因此:

\[ \begin{aligned} SSE(D) &=\sum_{i=1}^{8}(y_i-50)^2\\ &=14{,}560\ \text{件}^2 \end{aligned} \]

切分后,左叶预测 \((10+14+18+26+40)/5=21.6\) 件,右叶预测 \((60+92+140)/3\approx97.33\) 件。左右节点的 \(SSE\) 分别是 \(563.20\) 与约 \(3{,}242.67\) 件²;CART 的误差下降为:

\[ \Delta SSE\approx14{,}560-563.20-3{,}242.67=10{,}754.13\ \text{件}^2 \]

GBDT:拟合残差,再乘学习率 #

GBDT 此时已有初始预测 \(F_0(x_i)=50\) 件,要让第一棵树学习还需修正多少。平方误差下,第一轮负梯度就是各条样本的残差:

\[ (r_{i1})_{i=1}^{8}=(-40,-36,-32,-24,-10,10,42,90) \]

若第一棵树采用同一切分,左叶拟合的残差均值为 \(-142/5=-28.4\) 件,右叶为 \(142/3\approx47.33\) 件。它们是修正量,不是最终销量。取学习率 \(\eta=0.3\) 后:

\[ F_1(\text{左})=50+0.3(-28.4)=41.48 \]\[ F_1(\text{右})=50+0.3(142/3)=64.20 \]

将这两个预测分别代回 D1—D5 与 D6—D8,训练 \(SSE\) 降至约 \(9{,}075.39\) 件²。这是带学习率的第一轮更新,尚未达到 CART 直接给出的叶均值。

XGBoost:汇总梯度与 Hessian,判断切分是否值得 #

XGBoost 同样从人为设定的 \(z_i^{(0)}=50\) 件开始,但先用梯度和 Hessian 为候选切分打分。取单样本损失 \(l(y_i,z_i)=\frac12(y_i-z_i)^2\),可得:

\[ (g_i)_{i=1}^{8}=(40,36,32,24,10,-10,-42,-90) \]\[ h_i=1\quad(i=1,\ldots,8) \]

这里的梯度恰好与上一段 GBDT 的残差符号相反;平方误差的 Hessian 则都等于 \(1\)。将它们按同一切分汇总,父节点与左右叶的统计量分别为:

\[ (G,H)=(0,8),\quad (G_L,H_L)=(142,5),\quad (G_R,H_R)=(-142,3) \]

取叶节点权重的 L2 正则化强度 \(\lambda=1\)、叶节点数量惩罚 \(\gamma=100\),代入第 5.6 节的切分增益公式:

\[ \begin{aligned} Gain &=\frac12\left(\frac{142^2}{5+1}+\frac{(-142)^2}{3+1}\right)-100\\ &\approx4{,}100.83 \end{aligned} \]

正的净增益表示在这套教学目标与参数下切分有利;它已经扣除 \(\gamma\)。左右叶权重分别为 \(-142/6\) 和 \(142/4=35.5\),尚未乘学习率。如果令 \(\lambda=0\),左右叶权重将分别为 \(-142/5=-28.4\) 和 \(142/3\approx47.33\),正好对应上一段 GBDT 的残差修正量。当前设置取 \(\lambda=1\),分母由 \(H_j\) 变为 \(H_j+\lambda\),因此两个叶权重都向零收缩;这正是 XGBoost 与前述 GBDT 更新值不同的原因。乘 \(\eta=0.3\) 后,左右预测为 \(42.9\) 和 \(60.65\) 件,训练 \(SSE\) 为 \(10{,}111.32\) 件²。软件在完整 CSV 上自动寻找的切分和内部得分不必与这次指定切分一致。

把三种方法完成上述一步后的训练预测放在同一口径下,可以得到下表。GBDT 和 XGBoost 均只做指定切分后的第一轮更新,学习率为 \(0.3\):

方法左/右预测(件)训练 SSE(件²)
CART21.60/97.333,805.87
GBDT41.48/64.209,075.39
XGBoost42.90/60.6510,111.32

最后一列采用同一平方误差口径,便于核对预测值;它不是模型优劣排名。CART 的 \(10{,}754.13\) 是未取 \(\frac12\) 的误差下降,GBDT 的 \(9{,}075.39\) 是更新后的误差和,XGBoost 的 \(4{,}100.83\) 则是尚未乘学习率、包含正则化的净增益,不能把这些不同量直接比较大小。

这个算例值得记住的是三种不同的计算动作:CART 用叶节点均值直接预测,GBDT 在平方误差下让下一棵树拟合残差并逐轮修正,XGBoost 则用梯度与 Hessian 计算候选切分的增益和叶节点权重。

D1—D8 已经展示了指定切分。下节继续用教学 CSV 中的这八条记录比较其他候选阈值,并看左右分支为什么不必同时生长。

5.9 从一次手算切分到高效搜索 #

第 5.8 节预先指定了折扣 \(\leq25\%\)。真正训练时,模型并不知道该选哪个阈值;七日均销量等其他字段也可能成为候选特征。下面先只沿折扣列排序,并就地列出搜索所需的数据:

记录折扣率梯度 \(g_i\)Hessian \(h_i\)
D10%401
D20%361
D310%321
D410%241
D520%101
D630%-101
D740%-421
D850%-901

这张表按折扣率排列,便于从上到下累加梯度和 Hessian,观察搜索究竟在算什么。

依次把同一折扣值的样本移到左侧,累加 \(G_L,H_L\);右侧统计量则用父节点的 \((G,H)=(0,8)\) 减去左侧。仍取 \(\lambda=1,\gamma=100\),将五个候选的统计量代入第 5.6 节公式:

候选阈值左侧 \((G_L,H_L)\)切分增益 \(Gain\)
折扣 \(\leq5\%\)\((76,2)\)\(1{,}275.24\)
折扣 \(\leq15\%\)\((132,4)\)\(3{,}384.80\)
折扣 \(\leq25\%\)\((142,5)\)\(4{,}100.83\)
折扣 \(\leq35\%\)\((132,6)\)\(4{,}048.57\)
折扣 \(\leq45\%\)\((90,7)\)\(2{,}431.25\)

例如第一行左侧只有 D1、D2,梯度和为 \(40+36=76\),右侧为 \(-76\);代入公式得到约 \(1{,}275.24\) 的净增益。五次比较后,才知道折扣 \(\leq25\%\) 是这个节点、这个特征上的最佳候选;还应与七日均销量等其他特征比较。长出子节点后,搜索要在各子节点的记录上继续。

继续看两个子节点。 根节点左侧有 D1—D5,右侧有 D6—D8。它们已经有不同的销量分布,下一刀未必都值得切。

先分别在两侧比较候选切分。表中的 \((G,H)\) 都汇总自第 5.8 节的八行梯度;左侧的最佳候选把 D1—D4 与 D5 分开。右侧的折扣率 \(\leq35\%\) 与七日均销量 \(\leq37.5\) 都把 D6 与 D7—D8 分开,因此得到相同的净增益。为延续前面的折扣示例,下表列出前者。

当前节点与候选切分子节点统计量 \((G,H)\)净增益
左侧 D1—D5:折扣 \(\leq15\%\)D1—D4:\((132,4)\);D5:\((10,1)\)\(-12.93\)
右侧 D6—D8:折扣 \(\leq35\%\)D6:\((-10,1)\);D7—D8:\((-132,2)\)\(308.50\)

左右两侧并不需要同时继续分裂:左侧最佳候选的净增益仍小于零,因此在这些设定下只继续切右侧。叶权重依次为 \(-142/6\)、\(5\)、\(44\),乘学习率 \(\eta=0.3\) 后,三组记录的新预测分别为 \(42.9\)、\(51.5\)、\(63.2\) 件。第二轮再以新预测计算梯度,例如最后一条 140 件的记录仍有 \(g_i^{(2)}=63.2-140=-76.8\)。这里的净增益已经扣除 \(\gamma\),判断是否值得切分时只与零比较。

这个逐一检查原始特征值的过程便于手算,但样本多时,候选位置和数据读取次数都会增加。XGBoost 可改用直方图方法:先把连续值映射到有限数量的桶,再按桶汇总 \(G,H\),只在桶边界比较切分。它减少了候选位置,却没有免去构造直方图时对样本的处理;第 6.1 节会沿用案例 B 的门店商品次日销量预测教学数据中的折扣率说明分桶统计,作为比较 LightGBM 的共同起点。

案例 B 的门店商品次日销量预测完整 CSV 在实际应用中还可能遇到缺失折扣:一条训练记录即使没有该特征,仍有销量标签、梯度和 Hessian。搜索阈值时,算法需要比较“缺失记录走左侧”和“走右侧”的增益,并为该切分保存较优的默认方向。固定教学 CSV 并未刻意加入缺失折扣,所以这只是说明部署时可能遇到的边界,不混入上表的八行手算。

多线程、缓存布局、数据压缩和分片也不是完成切分后才执行的下一步,而是让上述搜索在更多特征、样本和计算设备上运行的系统手段。XGBoost 的 2016 年论文讨论了这些设计[10]:若反复读取特征的内存或磁盘成本压过了增益计算,再快的公式也无法单独保证训练速度。实际收益仍取决于数据与硬件,不能只凭算法名称判断。

5.10 XGBoost 接口:参数映射与版本边界 #

论文解释了算法设计,工程代码则必须服从所安装版本的接口。以下说明按 XGBoost 3.4.2 的 XGBRegressor、XGBClassifier Python API 核对,访问日期为 2026 年 10 月 14 日[11]。第 8.9 节的对照实验固定安装 3.4.1;这里的 3.4.2 是文档核对版本,复现时仍应核对所装版本的接口。类别特征、树参数和多输出限制另以官方专题文档、参数文档和多输出文档为准。

正则化参数。 第 5.2 节的 L1 与 L2 叶权重惩罚,在 XGBoost 中分别对应 reg_alpha 与 reg_lambda。理论目标中的 \(\gamma J\) 对新增叶节点施加结构成本;接口参数 gamma(别名 min_split_loss)则表现为继续切分所需的最小损失下降。这些都是训练前设置的约束,不是训练后学出的叶节点权重。

直方图方法。 第 5.9 节介绍的分桶构建,在接口中可通过 tree_method="hist" 选择;它不同于逐一比较原始取值的切分方法。

类别特征需要正确的数据类型和树方法。 XGBoost 从 1.5 版开始实验性支持原生类别特征;3.3 版发布说明将类别支持改为默认启用,同时保留 enable_categorical 开关。使用 scikit-learn 接口和 Pandas DataFrame 时,仍需把相应列转换为 category;较早版本通常还要显式设置 enable_categorical=True。类别切分不支持 exact 树方法,应使用 hist 或 approx。包含类别信息的模型应使用当前稳定的 JSON 或 UBJSON 模型格式保存,以保留类别元数据。

树生长策略可以配置。 grow_policy="depthwise" 优先扩展靠近根节点的节点,grow_policy="lossguide" 优先扩展损失下降最大的节点;后者只在 hist 或 approx 下可用。因此,把 XGBoost 固定概括为 Level-wise 会掩盖实际配置。

多输出能力仍有实验边界。 Python 包从 1.6 版开始实验性支持多输出回归和多标签分类。默认的 multi_strategy="one_output_per_tree" 为每个目标建立模型;2.0 版加入的 multi_output_tree 使用向量叶节点,并要求 tree_method="hist"。截至 3.4.2,官方多输出文档仍将整体能力标为实验性且功能受限,并明确说明向量叶策略仍在开发中。生产使用前应针对目标数量、可用目标函数与指标、序列化和解释工具单独验证,不能只确认 fit() 能够运行。

六、LightGBM:如何让梯度提升树训练得更快? #

第五章用 \(G,H\) 与切分增益回答了“哪一次切分值得做”,又看到搜索候选位置和读取数据的成本。微软研究团队在 2017 年的代表性论文中介绍了 LightGBM[12],继续面对同一个问题:当样本和特征增多时,如何降低寻找切分的开销?

LightGBM 同样通过梯度提升构造加法模型,也用梯度与 Hessian 统计量评价切分。两者并不是一阶优化与二阶优化的区别,直方图方法也并非某一方独有。本章先用分桶例子补全这一共同基础,再比较树优先扩展哪个叶节点、处理哪些样本和特征,以及这些选择的适用边界。

6.1 Histogram:减少候选切分位置 #

第 5.9 节只交代了分桶为何能减少候选位置。这里看桶内统计量怎样形成:LightGBM 将连续特征值映射到有限数量的桶,候选切分只需在桶边界比较,而非逐一测试所有原始值;XGBoost 的直方图方法也遵循这一思路。

仍用案例 B 的门店商品次日销量预测教学数据中的八条记录。为展示计算,先人为指定三个折扣率桶;这不是声称 LightGBM 在实际训练中必定选用这些边界:

教学桶:折扣率样本桶内梯度和 \(G_\ell\)桶内 Hessian 和 \(H_\ell\)
0%—20%D1—D51425
30%D6-101
40%—50%D7—D8-1322

第一个桶的梯度和来自 D1—D5:\(40+36+32+24+10=142\),Hessian 和为 \(1+1+1+1+1=5\)。前两个桶合并后,累计统计量为 \((G,H)=(132,6)\);只需比较桶边界,而不必在每个原始折扣率之间都试切。第一个桶之后的边界对应第五章的 \(25\%\) 候选切分;若要保留其他原始阈值,就必须使用更细的桶。

记 \(\mathcal B_\ell\) 为落入第 \(\ell\) 个桶的样本索引集合。模型累积桶统计量 \(G_\ell=\sum_{i\in\mathcal B_\ell}g_i\) 和 \(H_\ell=\sum_{i\in\mathcal B_\ell}h_i\),再比较桶边界的切分增益。第五章的 \(G_j,H_j\) 汇总的是叶节点样本,这里的下标 \(\ell\) 则指桶;\(\ell\) 也与随机森林的树编号 \(b\) 无关。分桶会牺牲一部分切分位置的精细度,以换取更低的候选扫描和存储开销。

直方图是两者的共同基础,而非 LightGBM 独有的速度来源。接下来比较:有了这些桶统计量,树先扩展哪个叶节点?LightGBM 又如何减少参与计算的样本和特征?

6.2 Level-wise 与 Leaf-wise #

完成分桶后,树仍需决定下一步扩展哪个节点。案例 B 的门店商品次日销量预测教学树在根节点已分出左、右两侧:沿用第五章的教学目标函数与参数,左侧候选净增益为 \(-12.93\),右侧为 \(308.50\)。在这组条件下,只有右侧值得继续切。这个比较用来解释 Leaf-wise 的选择,不表示 LightGBM 必然输出与 XGBoost 相同的内部增益。下图改用抽象节点 A、B,说明更多节点出现时的生长顺序。

Level-wise #

典型的 Level-wise 策略按照树的层次扩展节点:

          Root
         /    \
        A      B
       / \    / \
      C   D  E   F

按层生长会遍历并考虑同一层所有可扩展节点,整体深度更容易控制。

Leaf-wise #

LightGBM 的 Leaf-wise 策略则会优先扩展当前收益最大的叶节点。图中的 A 可以看作当前更值得继续切分的叶节点,B 则停止生长;如果 A 的收益高于 B,模型会优先切分 A。下一轮若 A 的某个子节点仍然最有价值,则会继续沿此路径切分:

          Root
         /    \
        A      B
       / \
      C   D
         / \
        E   F

如果某个局部区域仍然具有较大的损失下降空间,模型会优先继续划分这个区域。这可能在相同叶节点数量下实现更大的局部训练目标改善,但并不保证最终测试误差更低。

Leaf-wise 也有风险:当训练数据较少、噪声较大或者叶节点样本量过低时,模型可能过度细分局部区域。因此,使用按叶生长的树时,需要同时控制叶节点总数、树的深度、叶节点中的样本量与叶节点权重修正幅度;仅限制叶节点总数,可能导致少数路径切分过深。

按层与按叶是两种树生长策略,不应将其视为两个框架的永久标签。具体框架支持哪些策略,取决于其接口配置。

6.3 GOSS:优先保留梯度较大的样本 #

Leaf-wise 解决了“优先扩展哪个节点”的问题。另一个性能瓶颈来自计算候选切分时需要扫描的样本量。GOSS(Gradient-based One-Side Sampling)是 LightGBM 论文提出的样本层面加速方法之一[12]。

GOSS 基于以下观察:对于当前模型预测误差较大的样本,其梯度绝对值往往较大,对下一次切分决策具有更显著的影响。

GOSS 的策略因此是:

  1. 保留梯度绝对值较大的样本;
  2. 从梯度较小的样本中随机抽取一部分;
  3. 对被抽取的小梯度样本进行权重修正;
  4. 使用这些样本估计切分增益。

GOSS 的目的并非单纯删除易于预测的样本,而是在减少计算量的同时,尽可能地保留对切分决策重要的信息。配套实验另有显式启用 GOSS 的训练;少量样本和单次运行不足以证明训练速度优势。

然而,大梯度并不等同于高质量样本:错误标签也可能产生很大的梯度。如果训练数据中存在少量错误记录,模型可能反复关注这些错误记录。因此,GOSS 减少样本扫描的思路,不能替代对数据标签质量的检查;一般 Boosting 模型也面临类似问题。

6.4 EFB:减少稀疏特征的计算成本 #

GOSS 减少了参与统计的样本量;如果特征列数众多,是否还能减少需要独立扫描的列?EFB(Exclusive Feature Bundling)是 LightGBM 提出的另一项技术[12]。

它利用稀疏特征之间可能存在的互斥性,将多个特征组合为更紧凑的表示。

例如,某些 One-hot 编码特征不会同时取非零值:

Category_A  Category_B  Category_C
    1           0           0
    0           1           0
    0           0           1

如果多个特征足够互斥,便可通过特征捆绑减少需要处理的有效特征数量。案例 A 的银行交易欺诈识别数据中,商户类型在 One-hot 展开后逐行只有一列为 1;这解释了 EFB 可利用的结构,并不等于测出了软件实际节省的时间或内存。

但真实数据中的稀疏特征未必严格互斥,实际实现中仍需处理潜在冲突。EFB 能节省多少计算开销,取决于特征稀疏程度和数据结构。

GOSS 减少了参与切分统计的样本量,EFB 减少了需要独立扫描的特征数;两者共同回应了 2017 年论文指出的大样本、高维特征带来的开销[12]。随着内存容量和并行处理能力提高,模型可以处理更多数据;减少无效扫描,使得这些资源能更有效地用于真正影响切分的统计。

6.5 类别特征:不一定需要 One-hot Encoding #

前文借由 One-hot 特征解释了 EFB。然而,两份 CSV 的商户类型、商户 ID、商品 ID、门店 ID 和促销类型本身就是类别字段;是否都要展开为 One-hot 编码?案例 A 的银行交易欺诈识别完整数据包含 80 个商户 ID,尤其能说明:类别一多,展开后的列数也会增加。

对于高基数类别特征,直接使用 One-hot Encoding 可能显著增加特征维度。LightGBM 提供了原生类别特征处理机制,可以根据类别相关的梯度统计量寻找有效划分点,而非始终展开为大量 One-hot 列。

但这不意味着可以无条件将 sku_id、store_id 等高基数字段直接输入模型。这些字段可能包含真实的业务差异,也可能导致模型记忆训练集中反复出现的实体;能否泛化,取决于验证集是否正确模拟未来部署场景。例如:

  • 如果未来仍然预测同一批门店和商品,历史实体信息可能有价值;
  • 如果未来需要预测新门店或新商品,模型对未见类别的处理能力就更加重要。

6.6 LightGBM 为什么不一定比 XGBoost 快? #

LightGBM 在许多大型表格数据任务中展现出出色的训练效率,但其速度优势并非无条件成立的。

实际表现取决于多方面因素:

  • 样本数量与特征数量;
  • 连续特征和类别特征比例;
  • 特征稀疏程度;
  • CPU、GPU 与内存配置;
  • 树的深度、叶节点数量和分桶数量;
  • 是否启用合适的训练算法;
  • 使用的软件版本。

因此,比较 XGBoost 和 LightGBM 时,应尽可能控制相同的验证方案、计算预算和合理的参数搜索范围。

否则,所谓的“算法性能差异”可能仅仅是默认参数或实验条件不同造成的。

6.7 LightGBM 接口与跨框架工程边界 #

前文介绍了算法层面的设计,而实际运行效果还取决于其接口与默认设置。以下说明按 LightGBM 4.7.0 的 LGBMRegressor、LGBMClassifier Python API 核对(访问日期:2026 年 10 月 14 日)[13]。GOSS 的默认值及类别处理机制,另以官方参数文档和高级主题为准。

树复杂度参数。 第 6.2 节提及的叶节点数量和树深度,可分别通过 num_leaves、max_depth 限制;min_data_in_leaf 与 min_sum_hessian_in_leaf 约束叶节点继续划分的条件,lambda_l1、lambda_l2 则用于约束叶节点权重。不能仅凭 num_leaves 判断模型复杂度,还需结合其余参数限制。

生长策略边界。 XGBoost 也支持 grow_policy="lossguide" 等配置,因此,“XGBoost 始终 Level-wise,LightGBM 始终 Leaf-wise”的说法并不准确。比较时应核对实际采用的策略,而非仅凭框架名称判断。

GOSS 在论文中是核心算法之一,但当前默认的 data_sample_strategy 是 bagging,并不会自动启用 GOSS 策略。LightGBM 4.0.0 起可以设置 data_sample_strategy="goss";此时 top_rate 控制保留大梯度样本的比例,other_rate 控制抽取小梯度样本的比例。旧代码中也可能看到 boosting_type="goss",因此,迁移版本时应以当前版本的参数文档和实际运行日志为准。

对于 Pandas DataFrame,categorical_feature="auto" 仅会自动识别无序的 category 列;有序类别默认按照数值顺序切分。LightGBM 会将类别编码转换为 int32,负值被视为缺失值处理,未在训练集中出现的类别,在预测时也按缺失值处理。高基数特征还需要结合 min_data_per_group、cat_smooth 与面向部署场景的验证,而非将“原生类别支持”等同于“无需数据设计”。

这两个框架都在持续演进。正文引用了具体的文档版本和访问日期;读者在复现实验时,应先打印实际安装版本,再核对相应的版本文档。

七、CatBoost:如何在梯度提升树中处理类别特征与 Prediction Shift? #

第五章解释了 XGBoost 如何评价切分,第六章比较了 LightGBM 如何降低切分搜索的成本。面对案例 A 银行交易欺诈识别数据中的商户字段,以及案例 B 门店商品次日销量预测数据中的商品、门店和促销字段,还有另一个问题:训练时能使用哪些信息,才不会让模型提前“看见”待预测样本的标签?

XGBoost 和 LightGBM 都支持原生类别切分,并非只有 CatBoost 能处理类别特征[11][13]。区别在于处理路径:前两者可在树节点上依据统计量寻找类别划分;CatBoost 则在建树时构造类别统计特征,并用样本排列限制其中可使用的目标信息。若自行把类别编号当作连续数值输入,树可能把任意编号误当成有意义的顺序;若在训练前用全部训练样本的目标均值编码,每条训练记录的特征又会掺入自己的标签。这两种问题不能归咎于 XGBoost 或 LightGBM 的原生类别功能。

Prokhorenkova 等人在 2018 年的论文中系统介绍了 CatBoost 的 Ordered Target Statistics(有序目标统计,下文简称有序统计)与 Ordered Boosting(有序提升)[14]。前者处理类别编码中的样本自身标签泄露;后者应对 Prediction Shift,即训练样本与未见样本的预测分布可能不同的问题。两种方法都关心训练时的信息如何形成,而不只是切分增益算得有多快。

7.1 Ordered Target Statistics:为什么不能直接用全体目标均值? #

案例 B 的门店商品次日销量预测教学数据中,D5—D8 均为“活动”促销。若把四行销量的目标均值 83 件回填给每一行作为输入,则 D6 的特征值也包含它自己的 60 件目标。即使编码表只用训练集计算,这种训练样本内部的自我泄露仍会让训练特征比预测新记录时多含一些自身目标的信息。

换一种方法:人为固定 D5、D6、D7、D8 的排列,并设先验均值为 50 件、平滑强度为 1。每行只参考排列中更早出现的同类记录:

当前记录当天销量(件)此前可用的“活动”记录简化有序编码(件)
D540无50
D660D545
D792D5、D650
D8140D5—D760.5

D6 的编码为 \((40+50)/2=45\) 件,不含自己的 60 件目标;D8 的编码为 \((40+60+92+50)/4=60.5\) 件,也不含自己的 140 件目标。下面的公式将“只使用排列中同类前例”的原则写成一般形式。

CatBoost 的有序统计使用训练样本的随机排列:为排列中某条样本构造类别统计时,只使用排在它前面的样本及平滑先验,不使用这条样本自身的标签。用 \(\pi(i)\) 表示样本 \(i\) 在排列中的位置、\(a_i\) 表示它的类别值,一个简化的目标均值编码可写成:

\[ \operatorname{Enc}_{\pi}(a_i) =\frac{\sum_{r:\,\pi(r)<\pi(i),\,a_r=a_i}y_r+\kappa\mu} {\#\{r:\pi(r)<\pi(i),\,a_r=a_i\}+\kappa} \]

其中 \(\mu\) 是先验均值,\(\kappa>0\) 控制平滑强度。这个公式只解释有序统计的原则,并非 CatBoost 在所有任务中的完整类别处理实现;少量类别值也可能走 One-hot 路径。预测新样本时可使用训练数据形成的统计量,却不能使用该样本尚未知晓的目标。实际训练还会使用更多样本和不同排列;上面的 50 与 45 是教学编码,并非模型最终生成的特征值。

案例 A 的银行交易欺诈识别教学数据也能用 S1—S8 的欺诈标签说明相同原则。这些数值是简化教学编码,并非 CatBoost 内部 CTR 或 Ordered Boosting 的逐步精确复现。

这里要区分两件事:原生类别切分不等于上述“全体目标均值编码”。XGBoost 和 LightGBM 能在节点上寻找类别划分,而不必先把全体目标均值回填给每条训练记录;CatBoost 的有序统计则是另一条将类别转换为可用特征的路径。若为任何框架另行制作目标编码,都必须在训练内部避免样本自身标签进入其编码,并在验证时避免跨集合或跨时间泄露。

7.2 Ordered Boosting:训练样本与新样本的预测为何不同? #

即使所有输入都是数值特征,没有进行目标均值编码,提升过程仍可能出现另一种偏移。回到第五章:第 \(m\) 轮的梯度 \(g_i\) 取决于当前预测 \(z_i^{(m-1)}\)。普通提升中,前几轮的树已经用样本 \(i\) 的标签训练过;现在又用这些树对同一样本的预测计算它的梯度。真正未见样本的标签没有参加前几轮训练,因此两类预测的统计性质可能不同。这正是 CatBoost 论文讨论的 Prediction Shift[14]。

Ordered Boosting 沿用“先前样本”的思路:为排列中的某条训练记录计算当前预测和梯度时,尽量只依靠在它之前的记录训练出的模型。这样,计算修正方向时,该记录不会先凭自身标签改善当前预测,再把这个预测当成未见样本的情况。实际实现为控制计算成本做了近似和复用,不是为每条记录独立训练一套完整模型;而且 CatBoost 还提供 Plain 提升方式,不能把 Ordered Boosting 当作所有运行配置的既定事实。

类别统计中的“自身标签泄露”发生在构造输入特征时;Prediction Shift 还可能发生在计算下一轮梯度时。这也不是训练集与验证集之间的泄露:有序方法不会自动阻止把未来销量误作当前特征,不能代替按预测时点进行的验证。这里的“有序”通常指训练内部的样本排列,不等同于零售记录的日期顺序;第九章将讨论滚动回测。

7.3 对称树:结构约束与预测成本 #

前两节解决的是训练信号怎样形成;CatBoost 的默认树形还会改变模型怎样使用这些信号。回想第 6.2 节根节点分出 A、B,且 A 更值得继续切分的例子:LightGBM 典型的按叶生长会优先扩展 A;XGBoost 可配置按深度或按增益选择扩展顺序。CatBoost 默认的对称树(Symmetric Tree)则在同一深度对所有当前叶节点使用同一个切分条件。它不是仅仅“按层生长”:按层生长的各节点仍可选择不同条件。

深度为 \(d\) 的完整对称树有 \(2^d\) 个叶节点。统一的切分规则使预测路径规整,便于快速定位叶节点;代价是 A、B 无法各自选择不同的下一次切分,也不能只沿 A 的高收益路径继续生长。若业务需要低延迟推理,这种约束可能有价值;若信号集中在少数局部区域,它也可能浪费部分树容量。CatBoost 仍支持其他 grow_policy,因此应比较实际配置下的验证误差、模型大小和推理时间,而不是把对称树当成不可更改的框架标签。

7.4 使用 CatBoost 时仍要核对什么? #

CatBoost 的 Python 接口通过 CatBoostRegressor 和 CatBoostClassifier 训练回归与分类模型;类别特征列可通过 cat_features 指定。iterations、depth、learning_rate 和 loss_function 是训练前的配置,不是模型学出的切分阈值。本节所述的有序统计、提升方式与对称树,依据官方算法说明和 Python API 文档核对,访问日期为 2026 年 10 月 14 日[15]。

CatBoost 提供 Plain 和 Ordered 两种提升方式;实际采用哪种方式取决于设备、数据规模和具体配置,不能认为每次调用都默认运行 Ordered Boosting。它也可通过 grow_policy 选择其他树生长策略,因此对称树并非不可更改的结构限制[15]。

类别值较少或训练配置不同时,CatBoost 也可能使用 One-hot 编码,而非对每个类别都计算第 7.1 节所示的有序统计量[15]。

尽管原生类别处理减少了手动编码的负担,但它并不能自动解决所有泛化问题。门店 ID 或商品 ID 可能有助于预测已见实体,但也可能导致模型过度记忆历史样本;在预测新门店、新商品时,仍然需要仔细检查未见类别、缺失值以及验证集的构建方式。模型是否更准确、运行更快,必须在相同的数据可用性和计算预算约束下进行客观测量,而不能仅仅依据框架的设计动机来判断。

八、XGBoost、LightGBM 和 CatBoost:如何面对真实数据问题并选择模型? #

三种框架都能训练分类和回归模型,也各有处理数据与组织树的办法。真正开始建模时,问题却不是“哪一个名字更强”,而是:标签可靠么?分类的少数类能否被识别?回归中的极端值是业务事实还是记录错误?在回答这些问题后,比较模型才有意义。竞赛中的高排名并不意味着某个算法在所有工业场景中都更优;模型选择至少要考虑:

  1. 预测质量;
  2. 训练和推理成本;
  3. 数据质量与分布;
  4. 部署、维护和监控成本。

8.1 三种提升树各有什么侧重? #

维度XGBoostLightGBMCatBoost
代表性设计正则化目标、高效树构建、丰富优化接口Histogram、Leaf-wise、GOSS、EFBOrdered Boosting、类别特征处理
树生长多种策略可选典型为 Leaf-wise典型为对称树
类别特征支持,需匹配数据类型、树方法与参数原生支持,但编码和未见类别有约束类别特征处理是核心设计之一
值得优先核对的条件正则化、缺失值与多输出需求大样本和高维稀疏特征的训练成本高基数类别、未见类别与推理成本

表格列的是各框架最值得先检查的设计,不是固定能力边界。比如 XGBoost 也支持类别特征,CatBoost 也不是每次都默认采用 Ordered Boosting。下面转向训练数据和评估:若正类极少或标签有误,哪怕模型实现完全正确,指标也可能误导选择。

这也解释了开篇为何这样设计两份 CSV:案例 A 的银行交易欺诈识别数据包含少数欺诈、重复商户 ID 和商户类型,使我们能检查类别处理与审核取舍;案例 B 的门店商品次日销量预测数据包含多组商品—门店、促销类别、历史销量和时间划分,使我们能检查分桶、局部切分、异常标签以及预测时点。数据包含某种结构,不等于某框架一定因该结构获胜;第 8.9、8.10 节只报告指定配置下的实际结果。

8.2 分类任务:Accuracy 为什么可能误导模型评估? #

案例 A 的银行交易欺诈识别任务使用一个包含 1,200 笔交易的固定测试集,其中 33 笔欺诈、1,167 笔正常。如果模型始终预测正常:

\[ Accuracy=\frac{1167}{1200}=97.25\% \]

这个数字看起来很高,但模型没有识别出任何欺诈交易。对负责审核的团队来说,更直接的问题是:被模型标成“欺诈”的交易里有多少是真的?实际欺诈又抓到了多少?

对于类别不平衡(Class Imbalance)问题,需要结合混淆矩阵。以第 8.9 节的加权 CatBoost 为例:在同一测试集上使用固定的 0.5 阈值,四种预测结果分别是:

实际欺诈实际正常
预测欺诈TP = 12FP = 245
预测正常FN = 21TN = 922

这两个问题分别对应 Precision 与 Recall;若要用一个数兼顾两者,可以看 F1:

\[ Precision=\frac{TP}{TP+FP} \]\[ Recall=\frac{TP}{TP+FN} \]\[ F_1= 2\frac{Precision\cdot Recall}{Precision+Recall} \]

从表中可见,模型标出 257 笔可疑交易,其中 12 笔确实欺诈、245 笔是正常交易;另外 21 笔欺诈未报出。Precision 为 \(12/257\approx4.7\%\),Recall 为 \(12/33\approx36.4\%\),F1 约为 \(8.3\%\)。它的 Accuracy 为 \((12+922)/1200\approx77.8\%\),但抓到了始终预测正常所遗漏的一部分欺诈。

对审核团队来说,这意味着要核查 257 笔预警,其中 245 笔是误报,同时仍有 21 笔欺诈漏过。指标把审核工作量与漏报规模说清楚了,但还不能直接算出净收益:那需要知道每笔审核的成本、漏报损失,以及拦截一笔欺诈实际能避免多少损失。这组结果也提醒我们:改变训练权重不等于已选好业务阈值。

此外,还应考虑:

  • ROC-AUC:衡量不同阈值下的排序表现;
  • PR 曲线及其汇总指标:在少数正类任务中比 Accuracy 更能展示查准率与召回率的取舍;
  • Log Loss:衡量概率预测的质量;
  • Calibration:检查预测概率是否与实际发生频率一致。

平均精度(Average Precision,AP)是对 PR 曲线的一种汇总方式,不应未经说明就等同于其他积分口径的“PR-AUC”。AP 也会受到正类比例的影响,因此跨数据集比较时需要注意样本分布。

上面的混淆矩阵只对应一个决策阈值,不能据此计算 AP 或 ROC-AUC;这两个指标需要案例 A 的银行交易欺诈识别模型输出的连续预测概率,以比较不同阈值下的排序表现。排序指标有助于选择候选模型,但仍需结合审核能力和误报、漏报成本确定实际使用的阈值。

8.3 类别不平衡应该如何处理? #

先确定业务更怕漏报还是误报,再决定如何训练和使用分数。常见做法有三类:

  • 调整样本权重。 让少数类样本在损失函数中具有更高权重。不过,权重改变的是训练目标,并不自动选出合适的业务阈值。
  • 调整决策阈值。 如果模型输出欺诈概率,不一定要使用 0.5 作为最终阈值。例如,欺诈漏报成本可能远高于人工复核成本,业务可以选择 Recall 更高的决策阈值。
  • 调整训练样本分布。 可以考虑欠采样、过采样等方法,但应只在训练数据内部执行,避免影响验证集的真实分布。

类别加权和重采样可能改变概率校准表现。因此,如果业务需要使用预测概率直接估计风险或成本,还应单独检查 Calibration。

8.4 回归任务:异常值应该删除吗? #

分类要辨认少数正例;回归遇到少数极端销量时,先不要急着删除。相同的“大数值”可能有三种来源,对应的处理方式也不同。

第一类:真实的极端业务事件 #

案例 B 的门店商品次日销量预测教学数据中,D8 售出 140 件,同时有事先计划的 50% 折扣。这是教学数据中明确设定的促销高销量,而不是记录错误;若因数值大就删除,模型可能学不到高折扣期间的销量变化。实际业务也应核对当时可知的促销、节日等解释变量,而不是按固定阈值一律删除。

第二类:采集或处理错误 #

例如:

  • 订单重复写入;
  • 销量单位转换错误;
  • 错误的商品映射;
  • 负销量与退货记录处理不一致。

这类记录应先核对业务规则、记录来源和字段加工过程;确认是记录错误后再修正或排除,而不是直接交给模型拟合。

第三类:观测值不等于真实目标 #

案例 B 的门店商品次日销量预测数据假定库存充足,因此 CSV 中的销量没有被缺货截断。但若把同一门店案例用于真实补货,商品缺货时,实际销量可能远低于顾客原本的需求。

这时:

\[ Observed\ Sales \neq True\ Demand \]

模型如果直接把缺货期间的销量当作真实需求,可能系统性低估需求。

这类记录不能仅靠“清洗异常值”处理。它涉及目标定义和观测机制:若补货决策需要真实需求,就要识别缺货,并考虑 Censored Demand(因库存不足而无法从销量中完整观察的需求);第九章将继续讨论。

8.5 不同损失函数如何影响异常值? #

即使确认 D8 的高销量是真实业务事件,训练目标仍会决定模型有多重视它。若沿用案例 B 的门店商品次日销量预测教学算例所设的 50 件初始预测,D6 的误差为 10 件,D8 为 90 件:平方误差项分别为 100 与 8,100 件²,绝对误差项分别为 10 与 90 件。两者单位不同,不能直接比较数值大小;这组记录只说明误差扩大时惩罚如何增长。下面分别写出三种常见损失。

Mean Squared Error #

\[ L_{MSE}(y,\hat y)=(y-\hat y)^2 \]

较大的预测误差会受到平方惩罚,因此 MSE 对极端误差较敏感。

Mean Absolute Error #

\[ L_{MAE}(y,\hat y)=|y-\hat y| \]

MAE 对极端误差的增长是线性的,不会像平方误差那样迅速放大大误差;但它在误差为零处不可微,训练算法需要采用相应的处理方法。

Huber Loss #

设残差:

\[ r=y-\hat y \]

Huber Loss 定义为:

\[ L_\delta(r)= \begin{cases} \frac12r^2,&|r|\leq\delta\\ \delta(|r|-\frac12\delta),&|r|>\delta \end{cases} \]

它在较小误差范围内使用平方损失,在较大误差范围内采用线性增长;\(\delta\) 决定从前者转向后者的位置。

因此,Huber Loss 可以降低极端误差对目标函数的影响,但稳健损失函数不能自动识别错误数据,也不能解决缺货时需求未被完整观测的问题。

8.6 少量错误标签为什么值得重视? #

对于分类问题,错误标签可能来自:

  • 人工标注错误;
  • 业务规则变化;
  • 事件发生后标签尚未更新;
  • 不同系统之间的记录关联错误。

例如,一笔欺诈交易在交易发生当天可能被标记为正常,几周后才被确认存在欺诈。如果训练集使用过早生成的标签,模型可能学习到错误监督信号;对于 Boosting 模型,这些样本可能长期产生较大的训练梯度。

但不能因此得出“Boosting 一定比 Random Forest 更容易受标签错误影响”的普遍结论,影响程度还取决于损失函数、树复杂度、样本权重、正则化和数据分布。工程上更可靠的方法是:

  • 检查标签生成逻辑;
  • 区分真实少数样本与错误标签;
  • 对高损失样本进行抽样审计;
  • 进行数据质量敏感性实验;
  • 使用独立测试集评估清洗策略是否改善泛化。

8.7 工程上如何比较三种提升树? #

在运行对比实验前,需要先确定比较条件。若要选择 XGBoost、LightGBM 或 CatBoost,至少应建立简单业务规则或统计基线,并保持相同的训练、验证与测试划分、特征可用时间、评价指标和可比较的计算预算。参数搜索也应给予相近的机会,而不是只比较三个未经调整的配置。

除预测指标外,还应记录:

指标为什么重要
训练时间影响实验迭代与周期性重训练
峰值内存决定训练基础设施需求
推理延迟影响在线服务或批量预测
模型大小影响部署和存储成本
分组误差避免总体指标掩盖少数业务群体
多次训练稳定性检查模型对随机种子和数据变化的敏感程度
数据漂移表现评估生产环境中的持续有效性

如果数据以数值特征为主,三者都值得在相同预算下试验;如果高基数类别很多,应特别检查编码方式、新类别和 CatBoost 的有序统计是否带来实际收益。不能把某个合成实验或框架默认值当作普遍排名。真正的模型选择是在业务约束下比较整体成本与收益。

8.8 模型融合一定更好吗? #

在机器学习竞赛中,XGBoost、LightGBM 和 CatBoost 经常被组合使用。常见方法包括加权平均、Blending 和 Stacking。对于回归问题,若非负权重 \(\omega_X,\omega_L,\omega_C\) 之和为 1,可以采用:

\[ \hat y =\omega_X\hat y_{XGB} +\omega_L\hat y_{LGB} +\omega_C\hat y_{CAT} \]

对于分类问题,也可以融合概率预测,但应注意各模型的概率校准情况。模型融合能否改善结果,取决于不同模型的预测误差是否具有足够的互补性;如果错误高度相似,收益可能很小。Stacking 还需要通过 Out-of-Fold 预测构造第二层训练数据,避免直接使用训练集内预测导致数据泄露。因此,竞赛中常见的融合策略不应成为工业应用的默认选择。

8.9 案例 A:银行交易欺诈识别模型评估 #

第五至第七章使用两份八行教学 CSV 解释公式;模型训练和测试则使用另外两份完整实验 CSV 中固定的训练、验证和测试划分。两种规模共享业务背景和字段语义,但教学行不参与完整数据的训练或评估。手算净增益是指定公式、初始预测和参数下的教学结果;下列指标来自固定版本软件在完整训练集上拟合后,对固定测试集的预测。两者不能直接互换。

案例 A 的银行交易欺诈识别完整实验集包含 6,000 笔合成交易,其中 168 笔为欺诈,整体欺诈率为 2.8%。数据按标签分层后固定划分如下;三组的欺诈率接近,八行教学小表 S1—S8 不在其中:

数据分区用途交易数正常/欺诈
训练集拟合模型3,6003,499/101(2.81%)
验证集选择业务阈值1,2001,166/34(2.83%)
测试集只做最终评估1,2001,167/33(2.75%)

训练集让模型学习金额、距离、商户类型和商户 ID 与欺诈标签之间的关系。验证集不参与模型拟合,只用于后文选择决策阈值;测试集既不训练模型,也不选择阈值,只回答冻结方案面对未参与决策的数据时表现如何。这里对每个框架只改变训练时是否按训练集的负正样本比例设置 scale_pos_weight,即是否让少数的欺诈样本在损失函数中获得更高权重。

固定 0.5 阈值下的分类结果如下。Accuracy 一并列出,正好说明不平衡任务中“高准确率”不等于识别出了少数类:

模型与设置AccuracyPrecisionRecallF1
XGBoost 基准0.9720.0000.0000.000
XGBoost 加权0.8510.0380.1820.063
LightGBM 基准0.9690.0000.0000.000
LightGBM 加权0.9230.0960.2120.132
CatBoost 基准0.9730.0000.0000.000
CatBoost 加权0.7780.0470.3640.083

使用连续预测概率计算的排序指标:

模型与设置APROC-AUC
XGBoost 基准0.0550.597
XGBoost 加权0.0470.577
LightGBM 基准0.0540.612
LightGBM 加权0.0590.627
CatBoost 基准0.0630.674
CatBoost 加权0.0640.643

三个基准配置在 0.5 阈值下都没有找出欺诈,但这不表示其排序分数没有信息,AP 和 ROC-AUC 仍由连续概率计算。加权后召回率上升,但 Precision 仍较低:加权 CatBoost 找到 12 笔欺诈,同时带来 245 笔误报。第一张表固定阈值,是为了隔离“只改变权重”的影响,而不是把 0.5 当作业务上的最佳选择。

固定 0.5 便于比较模型设置,却没有回答银行每天能审核多少笔交易。为此,假设这 1,200 笔验证交易代表审核团队一个工作周期内面对的业务量,而团队最多只能调查其中 10%,即 120 笔。超过 120 笔会形成积压;明显少于 120 笔则没有充分使用既定审核能力。这个 10% 是用于解释方法的容量假设,不是银行业的通用比例。

阈值选择按以下顺序进行。模型先为 1,200 笔验证交易输出欺诈概率,再从高到低排列;随后在预警数不超过 120 的候选阈值中,选择能够送审最多交易的阈值。在本组数据中,这相当于把验证集得分最高的 120 笔送审。阈值确定后便被冻结,不再查看测试标签或根据测试结果调整。最后将同一个阈值应用于 1,200 笔测试交易,并计算下表中的 Accuracy、Precision、Recall 和 F1。这样,验证集负责把审核能力转化为决策规则,测试集负责检验这条规则在新数据上的结果:

模型与验证集阈值AccuracyPrecisionRecallF1
XGBoost 基准(0.0586)0.8780.0550.2120.087
XGBoost 加权(0.5530)0.8810.0490.1820.077
LightGBM 基准(0.0558)0.8940.0650.2120.099
LightGBM 加权(0.4080)0.8980.0750.2420.115
CatBoost 基准(0.0441)0.9050.0740.2120.109
CatBoost 加权(0.5756)0.8930.0710.2420.110

冻结阈值后,六组模型在 1,200 笔测试交易中分别产生 128、122、108、106、95 和 112 笔预警,即测试集预警率约为 7.9%—10.7%。新数据不必恰好达到验证集的 10%,这种波动本身也应进入容量规划。不同模型的分数尺度与校准状态不同,所以阈值数值不能横向比较;应比较同一业务规则下形成的工作量、漏报和误报。调整阈值也没有让所有指标同时变好:它只是把技术分数转化为一项可执行的业务选择。

这组按概率随机生成标签的合成数据,以及这里采用的单次配置,不能用于给框架排位。真实系统还应根据实际审核容量、误报成本和漏报损失重新确定规则,并检查概率校准。

8.10 案例 B:训练数据污染如何影响门店商品次日销量预测? #

案例 B 是门店商品次日销量预测回归任务,完整面板覆盖 20 个商品、2 家门店和连续 120 天,因此每天有 40 条商品—门店记录。模型使用预测时已经知道的商品、门店、促销类型、折扣和星期信息,以及此前七天的平均销量 lag7,预测当天销量 \(y\)。独立的八行教学小表 D1—D8 不参与下面的训练或评估。

这份数据不能随机打散,因为那会让未来记录进入过去的训练过程。前 7 天只为 lag7 提供历史;其余记录按时间顺序固定划分如下。销量分布同时给出,便于把后面的 MAE 和 RMSE 放回“件”的尺度理解:

数据分区(日期序号)用途记录数日销量均值(范围)
历史期(0—6)构造首批 lag728028.11(5—52)
训练集(7—79)拟合模型2,92027.13(2—57)
验证集(80—99)预留验证(本次未调参)80026.92(3—56)
测试集(100—119)比较干净与污染训练80026.57(3—59)

实验为每种模型建立两组训练条件。干净组保留 2,920 条训练目标值原样;污染组使用固定随机种子从中抽取 88 条、约占 3%,再分别给这些记录的销量目标加上或减去训练集目标标准差的 5 倍,以模拟少量但幅度较大的记录错误。验证集和测试集在两组条件下完全相同,测试销量也不作清洗,因此两组测试指标的差异来自训练标签污染,而不是换了一批测试记录。D8 等真实高促销销量同样不作为错误删除。

下表每格按“干净训练 → 污染训练”列出同一测试集指标。WAPE 和 Forecast Bias 为比例;预测偏差采用第 9.5 节的“预测减实际”符号约定。

模型MAE(件)RMSE(件)
Decision Tree3.746 → 4.2944.761 → 6.084
Random Forest3.608 → 4.0224.587 → 5.291
XGBoost3.471 → 3.7064.341 → 4.754
LightGBM3.463 → 3.6794.339 → 4.635
CatBoost3.505 → 3.5314.385 → 4.408
模型WAPEForecast Bias
Decision Tree0.141 → 0.1620.021 → 0.027
Random Forest0.136 → 0.1510.027 → 0.024
XGBoost0.131 → 0.1390.030 → 0.028
LightGBM0.130 → 0.1380.030 → 0.026
CatBoost0.132 → 0.1330.026 → 0.021

这次运行中,五种模型在污染训练后的 MAE 和 RMSE 都升高;CatBoost 的变化在这组设定下相对较小,但不能据此推断它普遍更抗异常值。本实验没有比较训练时间、峰值内存或推理延迟,也没有让三个框架经过等预算调参。选生产模型仍需回到第 8.7 节的业务约束与第九章的时间验证。

九、从实验到生产:树模型如何用于零售需求预测? #

9.1 预测目标:销量还是需求? #

回到案例 B 的门店商品次日销量预测场景。固定 CSV 假设库存充足,未模拟缺货;现在仅为说明生产中的目标选择,额外假设某商品当天有 100 件潜在需求,但货架只剩 30 件,销售记录最多只能显示约 30 件。那么模型应该学习“卖出了多少”,还是“顾客本来想买多少”?这取决于预测要支持的业务决定。

假设 \(S_t\) 是日期 \(t\) 的记录销量,\(D_t\) 是同一天的真实顾客需求(这里的 \(D_t\) 与前文表示训练数据集的 \(D\) 不同)。若不考虑退货、预售等特殊情形,库存充足时 \(S_t\) 与 \(D_t\) 可能接近;缺货时通常有 \(S_t \lt D_t\)。销售记录只告诉我们 \(S_t\),无法直接给出两者的差额。

如果直接用这些记录训练补货模型,模型学到的可能是受库存约束的销量,而非补货决策所需的需求。建模时,应识别缺货区间、核对预测时可获得的库存信息,再通过回测比较保留、排除或估计缺货期间未观测需求等方案。

9.2 特征工程:让模型看到预测时真正可用的信息 #

确定目标后,不能把所有字段直接交给模型。更重要的问题是:做出预测的那一刻,我们已经知道什么?用 \(x_{s,u,t}\) 表示预测商品 \(s\) 在门店 \(u\)、日期 \(t\) 的需求时所用的一组特征。它不必固定包含哪些字段,可以从以下几类中选择:

  • 日历信息:星期、周末、月份、公共假日及距节假日的天数。
  • 商品与门店信息:商品类别、品牌、价格带、生命周期和门店区域。
  • 历史销售记录:前 1、7、28 天的销量(Lag 1、Lag 7、Lag 28)、滚动均值与标准差,以及历史零销量比例。
  • 业务计划与状态:已确定的促销、价格和活动安排,以及预测时已知的库存状态与门店营业时间。

这四类信息的共同门槛不是“数据表里有这一列”,而是预测时确实能取得这个值。例如,今天营业结束后预测明天的需求,“过去 7 天平均销量”只能用截至今天的记录;若把明天结束后才知道的销量算进去,就发生时间泄露(Temporal Leakage)。程序构造滚动(Rolling)特征时,应先按预测时点偏移,再计算历史窗口。价格、促销和库存字段也要区分事先已知的计划或状态,与事后记录的实际结果。

上一节还提醒我们:缺货日的低销量不一定代表低需求。因此,即使历史销量在时间上可用,也应标记或妥善处理缺货区间,再用回测检验这些特征是否有帮助。

9.3 分类与回归可以共同服务零售预测 #

零售预测不一定只训练一个回归模型。对于需求很少发生的商品,直接预测数量可能会混淆两个问题:明天是否有需求,以及有需求时大约需要多少。这里继续用 \(D_t\) 表示随机需求量,用 \(x_t\) 表示预测时已知的特征。

案例 B 的门店商品次日销量预测数据主要模拟持续有销量的商品,并未为下面的间歇性需求方法生成单独实验。若今后把同一零售案例扩展到经常零销量的商品,可以考虑两阶段建模。第一阶段用分类模型估计有需求的概率 \(p_t\):

\[ p_t=P(D_t>0\mid x_t) \]

第二阶段用回归模型估计有需求时的平均数量 \(m_t\):

\[ m_t=\mathbb E[D_t\mid D_t>0,x_t] \]

若需求量非负,真实的条件期望满足:

\[ \mathbb E[D_t\mid x_t]=p_t m_t \]

实际模型只能分别估计 \(p_t\) 和 \(m_t\),两者相乘也不保证比直接回归更准确。高频商品可能只需一个回归模型;低频商品是否适合两阶段方法,应在相同回测窗口下比较。如果训练标签只是受缺货影响的销量,还需先处理第 9.1 节的目标问题。

9.4 滚动起点回测:用过去预测未来 #

特征只用预测时已知的信息,验证也应遵守时间顺序。如果随机打散样本,模型可能用未来记录训练,再去预测更早的日期;这样的分数难以代表上线后的表现。时间序列预测因此不宜默认使用随机 K-Fold。滚动起点回测(Rolling-origin Backtesting)让训练期始终早于验证期,并把预测起点逐轮向前推进。

案例 B 的门店商品次日销量预测数据已固定一次时间留出:第 7—79 天训练,第 80—99 天验证,第 100—119 天测试。这不是滚动回测。若要进一步检查稳定性,可在仍保持日期先后的前提下,另外设计多轮起点,例如:

  • 第 1 轮:训练至第 59 天,检验第 60—79 天。
  • 第 2 轮:训练至第 79 天,检验第 80—99 天。
  • 第 3 轮:训练至第 99 天,检验第 100—119 天。

这只是基于同一 CSV 的后续验证方案;第 8.10 节报告的数字并非这三轮回测结果。若第 3 轮把原测试期用于模型选择,就必须另外留出最终测试集。

每一轮都要模拟当时真正能做出的预测:在预测起点,只使用那时已可获得的数据。如果业务要求提前 14 天制定补货计划,就应按这个提前期核对促销、价格、库存和历史销量特征,而不能沿用“明天才预测”时可用的字段。

9.5 如何评价需求预测模型? #

完成时间留出或滚动回测后,业务团队通常会问:平均偏离多少件?少数大误差是否格外严重?总体预测倾向于高估还是低估?MAE、RMSE 和预测偏差(Forecast Bias)分别回答这些问题。若还想知道总体绝对误差占实际总量的比例,可以补充 WAPE。下面给出的案例 B 门店商品次日销量预测结果来自一次时间留出,不是多轮滚动回测。

直接取案例 B 的门店商品次日销量预测结果:下表是干净训练的 XGBoost 对固定测试集前三行的预测,按“预测减实际”计算误差。表中预测值四舍五入到两位;下面的整体指标使用全部 800 行的未四舍五入预测值。

记录实际销量(件)预测销量(件)误差(件)
R100-00-02122.441.44
R100-00-13627.93-8.07
R100-01-02730.803.80

这三行帮助我们读懂误差方向,不应只拿三行重新计算下述测试集指标。完整测试集的实际销量合计 21,259 件,绝对误差合计约 2,776.95 件,平方误差合计约 15,077.56 件²,带符号误差合计约 641.61 件。

MAE #

\[ MAE=\frac1N\sum_i|y_i-\hat y_i| \]

在 800 行测试记录上,\(MAE\approx2{,}776.95/800=3.471\) 件:每次预测平均相差约 3.471 件,但看不出偏差的方向。

RMSE #

\[ RMSE= \sqrt{ \frac1N\sum_i(y_i-\hat y_i)^2 } \]

同一测试集的 \(RMSE\approx\sqrt{15{,}077.56/800}=4.341\) 件,高于 MAE,因为较大的逐行误差在平方后权重更高。上表第二行低估约 8.07 件,就是值得进一步检查的一行;不能仅凭三行判断它在全体误差中的重要程度。

WAPE #

\[ WAPE= \frac{\sum_i|y_i-\hat y_i|} {\sum_i|y_i|} \]

测试集的 \(WAPE\approx2{,}776.95/21{,}259=13.1\%\)。它衡量总体绝对误差相对于实际总量的比例,但在目标总量接近零时可能不稳定,也容易受高销量商品的影响。

预测偏差(Forecast Bias) #

\[ \operatorname{ForecastBias}= \frac{\sum_i(\hat y_i-y_i)} {\sum_i y_i} \]

测试集的预测偏差约为 \(641.61/21{,}259=3.0\%\)。按此处的定义,正值表示整体高估,负值表示整体低估;正值并不表示每天都高估,上表第二行就低估了 8.07 件。实际报告时应写清符号约定,以免不同团队对“正偏差”产生相反的理解。

这些指标描述预测误差,不能直接换算为补货收益。如果预测用于制定订货量,还要结合库存、提前期以及积压和缺货的不同成本来评估决策。对于零售业务,仅有总体 WAPE 也不够,还应从以下维度检查:

  • 高频与低频商品;
  • 促销与非促销日期;
  • 普通工作日与节假日;
  • 大型门店与小型门店;
  • 新商品与成熟商品;
  • 高库存与缺货风险商品。

如果案例 B 的门店商品次日销量预测模型总体误差不大,却系统性低估高折扣记录,实际补货仍可能受影响。分组检查应服务于业务风险,而不只是让总体指标更好看。

9.6 与 Prophet 教程的关系 #

我之前的《Prophet 零售需求预测教程》重点探讨了趋势、季节性、节假日和时间序列回测。

Prophet 和 GBDT 的建模侧重点不同。Prophet 显式构造趋势、季节性和节假日等时间序列组件;XGBoost、LightGBM 和 CatBoost 等 GBDT 框架通常依赖特征工程,把历史销量、日历、价格、促销和门店信息转换为结构化输入,三者对类别特征的处理方式也不同。

还应注意,回归树在叶节点中给出分段常数预测,并不会自然延续训练数据之外的增长或下降趋势。如果销量长期增长、价格超出历史范围,或者新门店仍处于爬坡阶段,应专门进行时间外推测试;必要时可以显式加入趋势特征、结合时间序列模型,或采用能够表达相应结构的其他方法。

两者没有脱离数据和评估条件的普遍胜负。季节性较稳定、外生变量较少时,可以先测试 Prophet;若有丰富的业务特征,且要同时预测大量门店与商品,也值得测试 GBDT。还可以把统计模型的预测值作为树模型特征,或融合两类模型;训练这类组合时,应使用按时间顺序生成的样本外预测,避免泄露。无论选择哪种方法,都应使用一致的预测窗口、特征可用时间和回测方案比较。

9.7 从离线模型到生产系统 #

数据科学家在研发阶段训练出模型,即使离线回测误差较低,也不代表它已能稳定用于生产。上线前,数据和模型团队应按第 9.2 节的时间口径生成训练及在线特征,并检查预测所需字段是否缺失、重复或延迟到账。出现问题时,要记录影响范围并启用约定的备用预测方案,不能悄悄用事后数据补齐。对于新门店、新商品,还应标记训练时未见过的类别,并检查模型输出。

模型上线后,团队应定期比较预测与实际结果,尤其要分别关注促销日、低频商品和有缺货风险的商品。若分组误差、缺失率或预测分布持续偏离回测时的范围,则需进一步排查数据管道和业务变化,判断是否需要重训或回退模型。观察窗口、触发阈值和回退责任人应在上线前明确约定;仅凭一次总体误差波动,不足以作为模型重训的理由。

预测还要进入补货决策。补货人员应核对高估带来的库存积压成本和低估带来的缺货损失,并约定哪些高风险预测需要人工复核。这两类成本通常不对称,因此最小化 MSE 未必能得到最合适的订货量。若回测和实际库存结果持续显示这种差异,可以测试分位数预测(Quantile Forecasting)或专门的库存优化模型,并记录离线误差如何影响补货结果。

结语:从树结构到工业级机器学习系统 #

从单棵树到多棵树集成,研究者逐步解决了数据切分、预测波动降低、逐轮误差修正等问题,并进一步处理了大规模训练的计算成本、类别特征与 Prediction Shift。硬件进步扩大了可处理的数据规模,而算法改进则优化了数据表示与计算路径,使这些能力得以充分发挥。XGBoost、LightGBM 和 CatBoost 各有侧重,不能脱离具体数据、评价指标和工程约束判断其优劣。

回到开头那款商品案例,在选择模型之前,我们仍需自问:预测的是实际记录的销量,还是缺货时未能观察到的潜在需求?价格与促销信息在预测时是否已知?应采用何种回测窗口来衡量漏估和高估的成本?若这些核心问题悬而未决,即便引入更复杂的树模型,也可能只是更精细地拟合了错误的目标。

树模型的发展远不止于增加树的数量,它更是统计学习、数值优化与工程实现的深度融合;而在业务中有效运用这些模型,则始于对目标、数据和验证条件清晰明确的定义。

参考文献 #

[1] Quinlan, J. R. (1986). Induction of Decision Trees. Machine Learning, 1, 81–106. https://doi.org/10.1007/BF00116251

[2] Quinlan, J. R. (1993). C4.5: Programs for Machine Learning. Morgan Kaufmann.

[3] Breiman, L., Friedman, J. H., Olshen, R. A., & Stone, C. J. (1984). Classification and Regression Trees. Wadsworth.

[4] scikit-learn Developers. DecisionTreeRegressor API. scikit-learn 1.9.1 文档,访问日期:2026-10-14. https://scikit-learn.org/1.9/modules/generated/sklearn.tree.DecisionTreeRegressor.html

[5] Breiman, L. (1996). Bagging Predictors. Machine Learning, 24, 123–140. https://doi.org/10.1007/BF00058655

[6] Breiman, L. (2001). Random Forests. Machine Learning, 45, 5–32. https://doi.org/10.1023/A:1010933404324

[7] scikit-learn Developers. RandomForestRegressor API. scikit-learn 1.9.1 文档,访问日期:2026-10-14. https://scikit-learn.org/1.9/modules/generated/sklearn.ensemble.RandomForestRegressor.html

[8] Freund, Y., & Schapire, R. E. (1997). A Decision-Theoretic Generalization of On-Line Learning and an Application to Boosting. Journal of Computer and System Sciences, 55(1), 119–139. https://doi.org/10.1006/jcss.1997.1504

[9] Friedman, J. H. (2001). Greedy Function Approximation: A Gradient Boosting Machine. The Annals of Statistics, 29(5), 1189–1232. https://doi.org/10.1214/aos/1013203451

[10] Chen, T., & Guestrin, C. (2016). XGBoost: A Scalable Tree Boosting System. Proceedings of KDD 2016, 785–794. https://doi.org/10.1145/2939672.2939785

[11] XGBoost Developers. XGBRegressor and XGBClassifier Python API. XGBoost 3.4.2 文档,访问日期:2026-10-14. https://xgboost.readthedocs.io/en/stable/python/python_api.html ; XGBoost 3.4 发布说明. https://xgboost.readthedocs.io/en/stable/changes/v3.4.0.html

[12] Ke, G., et al. (2017). LightGBM: A Highly Efficient Gradient Boosting Decision Tree. Advances in Neural Information Processing Systems 30. https://proceedings.neurips.cc/paper/2017/hash/6449f44a102fde848669bdd9eb6b76fa-Abstract.html

[13] LightGBM Developers. LGBMRegressor and LGBMClassifier Python API. LightGBM 4.7.0 文档,访问日期:2026-10-14. https://lightgbm.readthedocs.io/en/v4.7.0/pythonapi/lightgbm.LGBMRegressor.html ; https://lightgbm.readthedocs.io/en/v4.7.0/pythonapi/lightgbm.LGBMClassifier.html

[14] Prokhorenkova, L., et al. (2018). CatBoost: Unbiased Boosting with Categorical Features. Advances in Neural Information Processing Systems 31. https://proceedings.neurips.cc/paper/2018/hash/14491b756b3a51daac41c24863285549-Abstract.html

[15] CatBoost Developers. Categorical Features, Training Parameters, CatBoostRegressor and CatBoostClassifier. CatBoost 在线文档(未固定版本),访问日期:2026-10-14. https://catboost.ai/docs/en/features/categorical-features ; https://catboost.ai/docs/en/references/training-parameters/common ; https://catboost.ai/docs/en/concepts/python-reference_catboostregressor ; https://catboost.ai/docs/en/concepts/python-reference_catboostclassifier ; CatBoost 1.2.10 发布页. https://github.com/catboost/catboost/releases/tag/v1.2.10