Kaggle 比赛为什么 XGBoost 能赢
最后更新: 2026-03-30
Table of Content
Kaggle 比赛为什么 XGBoost 能赢¶
Kaggle 的表格赛有个很有意思的现象。
很多模型都能把分数做上去,但真到后半程,最常留在牌桌上的,往往还是 XGBoost 这一类树模型。你当然可以说,这是因为表格数据天然适合树去切条件、拆人群。这个判断没有问题,但还没说到它真正难缠的地方。
XGBoost 的厉害,不只是“会切”,而是每往前补一步,都尽量先把账算清楚。方向错了,它不该修;方向对了,它也不会默认往死里修。它先看这一步该往哪边走,再看这一步应该走多大,最后才决定值不值得把这次修正真正写进模型里。
问题也恰好出在这里。很多人知道 XGBoost 会用一阶和二阶信息,但一旦离开公式,脑子里就容易只剩几个标签:梯度、Hessian、二阶优化。标签是记住了,训练时到底在算什么,却没真正连起来。
我想换一个讲法。
不要先把它当成一组名词,而是把它当成模型每一步都要回答的两个问题:
这次修正,应该往哪个方向动。
如果方向没错,这次应该修得多猛。
把这两个问题看清楚以后,再回头看泰勒展开和 XGBoost,很多原来看着抽象的东西就会突然变得很具体。
Kaggle 的表格赛,放到量化里其实就是因子表¶
如果把 Kaggle 里最典型的 tabular 比赛翻译成量化语言,其实很像你手里的横截面因子表。
每一行是一只股票,每一列是一个特征,比如过去 20 日收益率、波动率、换手率、市值、PB、PE、行业变量,目标可能是下周收益、超额收益,或者某种风险状态。
这类问题难的地方,不是你找不到特征,而是同一个特征在不同组合下,含义会变。
小市值配高换手,可能是一种交易拥挤下的短线博弈;大市值配低波动,可能又是另一套逻辑。单看波动率可能没什么,但一旦叠上“最近 20 日已经涨了很多”,味道就可能完全变掉。
这本来就是树模型擅长的活。它不是拟合一条很光滑的大曲线,而是不断问条件:市值是不是低于某个阈值,换手率是不是高于某个阈值,最近收益是不是已经偏高。它做的事,说白了就是把混在一起的人群慢慢拆开。
所以 XGBoost 能在 Kaggle 这种比赛里常赢,第一层原因确实是树模型适合表格数据。
但真正把差距拉开的,不是它会拆人群,而是它知道每拆一步到底有没有赚到。这就回到了梯度、曲率和泰勒展开。
先别看树,先看误差是怎么动的¶
先不要急着回到树模型。把损失函数单独拎出来看,会更容易懂。你可以把它想成一张局部地形图。横轴不是时间,也不是特征,而是当前模型给出的预测值;纵轴是这个预测值对应的损失。
模型当前的预测,对应着这张图上的一个位置。训练做的事情其实很单纯,就是让这个位置一点点往更低的区域靠。
这时候,一阶和二阶信息就不是抽象名词了,而是当前位置附近的两种局部信息。
第一种是坡往哪边斜。
如果当前位置往右一点,损失会下降,那说明更新应该往右走。
如果当前位置往右一点,损失反而上升,那更新就该往左走。
这就是一阶信息真正干的事。它不是神秘地“给你答案”,而是在当前位置告诉你,朝哪边挪更可能把误差压下去。
第二种信息是这块地方到底陡不陡、弯不弯。
如果局部地形变化很快,你这一步就不能迈太大,不然很容易一下跨过去。反过来,如果这一带本来就很平,你每次都只蹭一点,收敛就会特别慢。
所以训练时真正要决定的,从来都不是一个问题,而是两个问题一起决定:
- 往哪边修
- 修多少
所谓“更稳定”,说的其实就是第二件事没有失控。模型不会一脚油门冲过头,也不会永远只肯挪半步。
用最短的泰勒展开,把这件事钉住¶
现在再回到数学,但只看最短的一段。
把损失函数记成 \(l(\hat y)\),我们只关心当前预测值 \(\hat y\) 附近,往旁边挪一小步 \(\Delta\) 会发生什么。那它在局部可以写成:
这里的 \(g\) 是一阶导数,\(h\) 是二阶导数。
这个式子真正有用的地方,不在于它漂亮,而在于它刚好把上面那两个问题写进去了。
\(l(\hat y)\) 是当前位置本来的损失。
\(g \Delta\) 这一项负责方向感。它决定了你往左还是往右更合理。
\(\frac{1}{2} h \Delta^2\) 这一项负责约束更新幅度。它反映的是局部曲率,也就是这一步到底适不适合走得太猛。
如果你把二阶项拿掉,局部近似其实只剩一条直线。直线可以给方向,但它自己不会给出一个自然的“合适步长”。
二阶项一进来,局部近似就不再只是线性外推,而是变成了一个有弯度的局部二次问题。到这一步,更新幅度才开始变得可算,而不是纯靠拍学习率。
如果把这个局部二次式继续往下推一步,它的最低点其实就在
这个式子很短,但信息已经够用了。\(g\) 的符号告诉你修正方向,\(h\) 的大小决定你敢不敢走大步。\(h\) 越大,说明这一带曲率越强,更新就该更保守。
所以更准确的说法不是“二阶更高级”,而是:一阶负责决定修正的朝向,二阶负责给修正幅度加刹车。
曲率稳定的,其实是更新节奏¶
还是盯着那条曲线看。
如果某一段曲线弯得很厉害,说明你站在当前点附近时,地形变化很快。这个时候你就算方向判断没错,步子迈太大,也很容易直接冲过最低点,跑到另一侧去。接下来下一轮又发现方向反了,再往回修,于是开始来回摆。
这就是很多人说的过冲和震荡。
反过来,如果某一段曲线很平,说明局部地形比较缓。这时候你每次都特别保守,只挪一点点,当然不会炸,但会收敛得很慢。
所以二阶梯度提供的,不是什么额外装饰,而是局部风险提示。
曲率大,说明路急,要慢一点。
曲率小,说明路平,可以放开一点。
量化里这个感觉其实很好懂。你在做横截面收益预测时,经常会碰到一小撮极端样本,比如几只最近突然暴涨、换手也极高的小盘股。它们会把局部误差拉得很大。如果你只看一阶信息,很容易顺着这点误差猛冲过去,把这段行情硬记成规律。二阶项的作用,就是让模型意识到:这里虽然有误差,但这块地形很可能比较险,别一下修太狠。
如果翻成量化里的话,一阶更像是在说:这批股票当前整体该往上修还是往下修。二阶更像是在说:这个判断到底稳不稳,修的时候要不要留余地。
你甚至可以把它想成调仓时的两个动作。先判断该不该加减仓,再判断这次是重手调,还是试探性地挪一点。XGBoost 里的二阶信息,管的主要就是后面这半步。
回到 XGBoost,它为什么非要把这件事算进去¶
讲到这里,再回头看 XGBoost 的训练过程,就顺了。
XGBoost 不是一口气长成一棵大树,而是一轮一轮往现有模型上补小树。第 \(t\) 轮时,模型已经有了当前预测 \(\hat y_i^{(t-1)}\),现在它准备再加一棵树 \(f_t(x_i)\),于是新预测变成:
真正的问题不是“我能不能再长一棵树”,而是:
这棵树加进去以后,损失到底会下降多少。
原始损失函数通常不好直接和树结构一起处理,所以 XGBoost 就在当前预测点附近做二阶泰勒展开:
这一步一做,事情就变了。
原来“加一棵树后损失怎么变”这个问题,是一团不太好直接处理的东西。现在它被改写成了一个局部二次目标。于是树的每个叶子节点,不再只是拍脑袋给一个输出值,而是可以根据当前落进这个叶子的样本的一阶、二阶信息,一起算出一个更合适的修正幅度。
如果第 \(j\) 个叶子的样本集合是 \(I_j\),记
那么这个叶子的最优输出就是:
这条式子其实就是上面那句口号在 XGBoost 里的落地版本。
\(G_j\) 这一坨一阶信息告诉模型,这个叶子里的样本整体上该往哪边修。
\(H_j\) 这一坨二阶信息告诉模型,这个叶子里的局部地形到底陡不陡,修正幅度该不该保守一点。
\(\lambda\) 再加一道 L2 正则,把叶子输出继续往回压一把,避免修得太凶。
所以 XGBoost 的强,不是它会补树,而是它每补一棵树,都在问同一件事:方向有没有看错,修正会不会过头。
把它翻译成量化研究里的话,其实就是:
这批股票当前整体是被低估了,还是高估了。
如果要修正,这次修正应该是大胆一点,还是保守一点。
这组样本看起来像是一个稳定结构,还是只是最近一段行情碰巧凑出来的噪音。
XGBoost 比较厉害的地方,就是它不是凭感觉回答这些问题,而是把这些问题都塞回目标函数里一起算。
分裂 gain 为什么也能算出来¶
叶子值能算,只是第一步。
树接下来还要决定一个更现实的问题:这个叶子要不要继续分裂。
如果不分裂,这批样本共用一个叶子值;如果分裂,左右两边就可以各用各的值。问题是,这一刀切下去,收益到底有没有大到值得付出更复杂的代价。
有了上面的局部二次目标,这件事也能继续算。于是 XGBoost 才会有那条经典的 gain 公式:
它本质上就是在算一笔账。
分裂以后,左右两个叶子各自能把损失往下压多少。
减去不分裂时原来那个叶子已经能做到多少。
再减去多长一个分支本身的复杂度代价 \(\gamma\)。
如果最后 gain 很大,说明这一刀值得切。如果很小,说明这刀带来的改进不够;如果是负的,就别切。
这就是为什么我说,XGBoost 很少在训练里浪费动作。它不是“反正能切就切”,而是每切一步都在算这一步有没有回报。
放到量化里也一样。你拿着一张因子表,模型准备按“市值 < 某个阈值”再切一刀。真正的问题不是“这个条件有没有故事”,而是“切完以后,预测误差是不是实打实地下去了”。gain 做的就是这件事。
比如现在一整个叶子里混着两类股票。一类是小市值、高换手、近期强势;另一类是大市值、低波动、机构持仓稳定。如果切开以后,两边的最优叶子值明显不一样,而且带来的损失下降足够大,那这刀就值得切。反过来,如果切开以后只是把样本分得更碎,但误差没真正降多少,那这一刀在量化里多半就对应着一句很熟的话:你是在制造解释,不是在提高预测。
在代码里,哪些参数在控制这件事¶
如果再往工程实现靠一点,你会发现上面这些直觉在代码里其实都有落点。
先看方向是怎么定义的。
你选的是平方误差、逻辑损失还是别的目标函数,决定了每个样本的 \(g_i\) 和 \(h_i\) 怎么算。说白了,你先规定什么叫误差,模型才能知道该怎么纠偏。
再看更新幅度和稳定性主要被谁控制。
eta或learning_rate:整体缩放每一轮叶子输出,越小越稳,但也越慢。lambda:L2 正则,直接压缩叶子权重,让更新更保守。min_child_weight:要求一个叶子里累计的二阶信息量足够大,才允许继续分裂。这个值越大,模型越不容易为了少数样本就激进地下刀。gamma:要求一次分裂带来的收益足够大,才值得多长一个分支。alpha:L1 正则,进一步压叶子输出,让模型别太激进。
你如果把这些参数和上面的图连起来看,会很清楚。
一阶和二阶提供的是局部方向感和路况信息。
eta、lambda、min_child_weight、gamma 这些参数,决定的是模型最后愿意把这一步走得多快、多猛、多冒险。
量化里调这些参数时,直觉上也可以这样理解。
eta小一点,相当于你承认市场噪音很多,每次只肯小修,不敢一步修满。lambda大一点,相当于你不太相信某个局部模式值得给出特别激进的预测分数。min_child_weight大一点,相当于你要求一组样本里得先有足够多、足够稳的“证据”,才允许模型单独给它开一个叶子。gamma大一点,相当于你告诉模型,不要为了讲一个边际很弱的小故事,就再多切一层树。
如果你做过因子挖掘,会发现这套直觉其实很顺手。很多候选模式在样本内看起来都有点道理,但真正难的是分清楚:它到底是一个能反复出现的结构,还是一段行情刚好配合出来的形状。XGBoost 这些参数,本质上就是在控制模型对“局部故事”的轻信程度。
再回到标题¶
很多 Kaggle 表格赛里,XGBoost 之所以能赢,当然有树模型擅长处理表格条件结构这层原因。
但更关键的原因是,它不是在盲目地一棵一棵往上叠树。它会在当前点附近先看斜率,再看曲率,把“往哪边修”和“该修多大”一起算进去,然后才决定叶子值和分裂值不值得做。
放到量化里看,这套逻辑尤其重要。因为量化数据最怕的从来不是模型不够复杂,而是模型太容易把阶段性行情、少数极端样本、偶然凑出来的条件组合,当成稳定规律。XGBoost 用一阶、二阶和正则化把每一步都卡得比较严,本质上是在尽量避免这种“修着修着就开始贴噪音”的事。
所以如果把整篇文章压成一句话,我会这样说:
XGBoost 常赢,不只是因为它会长树,而是因为它用一阶信息决定纠错方向,用二阶信息约束修正步子,让每一轮更新都更像一次算过账的修正,而不是盲目的试错。