Kaggle 表格赛里,XGBoost 为什么总有竞争力?
最后更新: 2026-04-08
Kaggle 表格赛里,XGBoost 为什么总有竞争力?¶
Kaggle 的表格赛里,能把 leaderboard 分数做上去的模型不少,但到了后面,大家最常用的还是 XGBoost 这类树模型。
很多人会把原因归到一句俗话上:表格数据适合树。这句话本身没有问题,但它只能解释树为什么常见,还不能解释 XGBoost 为什么经常更稳。
XGBoost 真正特别的地方,是它每补一棵树之前,都会先把这一步更新怎么算清楚。落到训练过程里,其实就是三个问题:
- 这批样本当前该往哪边修
- 这一轮应该修多少
- 修到这里以后,这个节点还要不要继续分
下面就顺着这三步往下看:先看单个样本怎么更新,再看这一步怎么变成叶子值,最后看什么时候值得继续分裂。
先放到因子表里看¶
如果把 Kaggle 里最典型的 tabular 比赛翻成量化语言,其实很像横截面因子表。
每一行是一只股票,每一列是一个特征,目标可能是下周收益、超额收益,或者某种风险状态。
先拿一类样本举例:低估值、低波动的股票。假设模型总把它们下周的收益预测得偏低。
后面就围绕这批股票看三件事:
- 既然老被低估,这一轮该怎么往回修
- 这一步修正,最后会怎么落成叶子值
- 修到这里以后,要不要再继续拆
先看误差怎么动¶
先别管树,只看单个样本的损失怎么变化。既然“低估值 + 低波动”这类股票老被低估,模型第一步就得回答:当前预测到底该怎么调。
如果预测目标是“下周收益”,那当前预测 \(\hat y\) 变成 \(\hat y + \Delta\) 以后,损失会怎么变。
训练时,每次更新其实都在回答两件事:该往哪边修,以及这一步该修多大。
一阶信息管方向,二阶信息管幅度。
单个样本怎么更新¶
要把这一步写出来,最直接的办法就是在当前点附近做一个局部近似,看看一小步 \(\Delta\) 会让损失怎么变:
这里的 \(g\) 是一阶导数,\(h\) 是二阶导数。式子里的 \(g\Delta\) 决定方向,\(\frac{1}{2} h \Delta^2\) 决定这一步不会走得太大。
把它再往下推一步,最低点就在
这个式子已经把核心说得很清楚了:\(g\) 决定往哪边修,\(h\) 决定这一步能修多大。\(h\) 越大,更新幅度通常越小。

- 黑色曲线:当前点附近的局部损失形状。
- 橙色虚线:红点处的切线,对应一阶导数,只负责告诉你哪边是下坡。
- 蓝色箭头:这一轮真正采用的更新量,从 \(\Delta=0\) 走到 \(\Delta^*=-g/h\)。
- 左图更弯,\(h\) 更大,所以步长更小;右图更平,所以步长可以更大。
到这里讨论的还是单个样本。但树模型不是给每只股票单独配一个参数,它的做法是先把样本分到不同叶子里,再让同一叶子的样本共用一个输出值。
叶子值怎么算¶
回到 XGBoost 的训练过程,这个问题就会落到树结构上。它不是一次长成一棵完整的大树,而是一轮一轮往现有模型上加新树。
前面那个“这批股票该修多少”的问题,到了这里就会变成:新加的这棵树,应该把哪些样本分到同一个叶子里,以及这个叶子该输出多少。
第 \(t\) 轮时,模型已经有了当前预测 \(\hat y_i^{(t-1)}\),现在它准备再加一棵树 \(f_t(x_i)\),于是新预测变成:

- 分裂节点:样本按条件往下走,不同条件对应不同分支。
- 叶子节点:样本最后落到一个叶子里,拿到一个输出值 \(w_j\)。这个值表示这一轮要修多少。
- 最下面那行 \(\hat y = \sum f_k(x)\):最终预测来自多棵树的输出累加,不是一棵树单独决定。
这张图对应的,其实就是从“单个样本怎么修”到“一组样本怎么修”的过渡。前面算的是单个样本的更新量,到了树里,就变成哪些样本分到一组,这一组统一给多少修正。
真正要算的不是“要不要加一棵树”,而是这棵树加进去以后,目标函数会下降多少。
原始损失函数通常不好直接和树结构一起处理,所以 XGBoost 就在当前预测点附近做二阶泰勒展开:
这样一来,“加一棵树以后损失怎么变”就被改写成了一个局部二次目标。
接下来就可以按叶子来处理样本了。既然不能给每只股票单独放一个 \(\Delta\),那就先把表现相近的样本分到同一个叶子里,再给这一组样本一个统一修正。
如果前面那批“低估值 + 低波动”的股票被分到同一个叶子里,模型就不会一只一只地修,而是直接给这一组一个共同的值。
如果第 \(j\) 个叶子的样本集合是 \(I_j\),记
那么这个叶子的最优输出就是:
这条式子读起来也很直接:
- \(G_j\) 决定修正方向
- \(H_j\) 限制修正幅度
- \(\lambda\) 用来压住过大的叶子输出
到这里,前面单个样本上的 \(\Delta\),就落成了树里的叶子值。单个样本该怎么修,到了这里,变成的是这一组样本一起修多少。
分裂值不值得¶
叶子值解决的是一个问题:如果先不继续分,这个叶子该输出多少。接下来还剩下另一个问题:这个叶子值已经够了吗,还是还值得再分一次。
还是用前面的例子来看。这批“低估值 + 低波动”的股票虽然已经能共用一个叶子值,但它们内部可能还有差异。比如再按“高换手 / 低换手”分开,误差也许还会继续下降。
这时候需要比较两件事:
- 不分裂,所有样本共用一个叶子值
- 分裂,左叶子和右叶子分别计算自己的叶子值
如果分裂后目标函数下降得更多,那这次分裂就值得保留。上面那套局部二次目标,刚好可以把这个下降量直接算出来,这就是 XGBoost 的 gain 公式:

- 父节点的 \(G, H\):表示不分裂时,把这批样本放在一起算。
- 左右叶子的 \(G_L, H_L\) 和 \(G_R, H_R\):表示分裂后,左右两边分开算。
- 最下面的 gain:就是“分开算”比“放在一起算”多带来的那部分下降量,再减去复杂度代价 \(\gamma\)。
到这里,叶子值和 gain 的分工就很清楚了:
- 叶子值回答:这一轮先怎么修
- gain 回答:这个节点还有没有继续分裂的必要
参数在控制什么¶
到了参数层面,核心也没变,还是在管同一件事:不要让模型太轻易相信局部模式。对应到前面的主线,就是不要修得太猛,也不要太容易继续往下分。
eta:控制每一轮修正的幅度lambda、alpha:压住过大的叶子输出min_child_weight、gamma:要求证据够多、收益够大,才允许继续分裂
再回到 XGBoost¶
很多 Kaggle 表格赛里,XGBoost 当然有树模型适合表格数据这层优势。但更关键的是,它不是一路往下拟合,而是每一步都会先把更新量和分裂收益算出来。
它每补一棵树,实际上都在做两层判断:这一轮该修多少,以及这个节点还有没有必要继续分裂。前一个问题靠一阶、二阶和叶子值,后一个问题靠 gain。
放到量化里看,这一点尤其重要。真正危险的往往不是模型不够复杂,而是模型太容易把阶段性行情、极端样本和偶然条件当成稳定规律。XGBoost 用一阶、二阶和正则化把每一步都卡得更严,本质上是在尽量延后这种失控。
XGBoost 总有竞争力,不只是因为它会长树,而是因为它每补一步之前,都会先把该怎么修、修多少、还要不要继续分清楚。