跳转至


课程  因子投资  机器学习  Python  Poetry  ppw  tools  programming  Numpy  Pandas  pandas  算法  hdbscan  聚类  选股  Algo  minimum  numpy  回测  数据标准化  algo  FFT  模式识别  配对交易  GBDT  LightGBM  XGBoost  statistics  CDF  KS-Test  monte-carlo  VaR  过拟合  algorithms  machine learning  strategy  python  sklearn  pdf  概率  数学  面试题  量化交易  策略分类  风险管理  Info  interview  career  强化学习  监督学习  AI量化  复权  数据  tushare  akshare  xgboost  PCA  wavelet  时序事件归因  SHAP  深度学习  归一化  BN  LN  WN  machine-learning  quant-trading  dropout  kaggle  boosting  决策树  泰勒展开  openclaw  resources  quant  deep-learning  tcn  time-series  causal-convolution  交易实战  避坑指南  Figures  Behavioral Economics  graduate  arma  garch  人物  职场  Quantopian  figure  Banz  金融行业  买方  卖方  story  量化传奇  rsi  zigzag  穹顶压力  因子  ESG  因子策略  投资  策略  pe  ORB  Xgboost  Alligator  Indicator  factor  alpha101  alpha  技术指标  wave  algorithm  pearson  spearman  套利  LOF  白银  因子分析  Alphalens  涨停板  herd-behaviour  momentum  因子评估  review  SMC  聪明钱  trade  history  indicators  zscore  波动率  lightgbm  顶背离  另类数据  freshman  others  AI  DeepSeek  network  量子计算  金融交易  IBM  weekly  进化论  logic-factor  Agent Skills  Skills Marketplace  VS Code  Tushare  XtQuant  BaoStock  A股  量化  neutralization  basics  LLT  backtest  backtrader  研报  papers  UBL  金融阅读  免费资源  华尔街日报  WSJ  量化学习  quantlib  jupyter-notebook  scikit-learn  pypinyin  qmt  xtquant  blog  static-site  duckdb  工具  colors  free resources  barra  world quant  Alpha  openbb  risk-management  llm  prompt  CANSLIM  Augment  arsenal  copilot  vscode  code  量化数据存储  hdf5  h5py  cursor  augment  trae  Jupyter  jupysql  pyarrow  parquet  数据源  quantstats  几何收益  实盘  clickhouse  polars  滑动窗口  notebook  sqlite  sqlite-utils  fastlite  大数据  PyArrow  UV  Pydantic  Engineering  redis  remote-agent  AI-tools  Moonshot  回测,研报,tushare  dividend 

algo »

Kaggle 表格赛里,XGBoost 为什么总有竞争力?


Kaggle 表格赛里,XGBoost 为什么总有竞争力?

Kaggle 的表格赛里,能把 leaderboard 分数做上去的模型不少,但到了后面,大家最常用的还是 XGBoost 这类树模型。

很多人会把原因归到一句俗话上:表格数据适合树。这句话本身没有问题,但它只能解释树为什么常见,还不能解释 XGBoost 为什么经常更稳。

XGBoost 真正特别的地方,是它每补一棵树之前,都会先把这一步更新怎么算清楚。落到训练过程里,其实就是三个问题:

  • 这批样本当前该往哪边修
  • 这一轮应该修多少
  • 修到这里以后,这个节点还要不要继续分

下面就顺着这三步往下看:先看单个样本怎么更新,再看这一步怎么变成叶子值,最后看什么时候值得继续分裂。

先放到因子表里看

如果把 Kaggle 里最典型的 tabular 比赛翻成量化语言,其实很像横截面因子表。

每一行是一只股票,每一列是一个特征,目标可能是下周收益、超额收益,或者某种风险状态。

先拿一类样本举例:低估值、低波动的股票。假设模型总把它们下周的收益预测得偏低。

后面就围绕这批股票看三件事:

  • 既然老被低估,这一轮该怎么往回修
  • 这一步修正,最后会怎么落成叶子值
  • 修到这里以后,要不要再继续拆

先看误差怎么动

先别管树,只看单个样本的损失怎么变化。既然“低估值 + 低波动”这类股票老被低估,模型第一步就得回答:当前预测到底该怎么调。

如果预测目标是“下周收益”,那当前预测 \(\hat y\) 变成 \(\hat y + \Delta\) 以后,损失会怎么变。

训练时,每次更新其实都在回答两件事:该往哪边修,以及这一步该修多大。

一阶信息管方向,二阶信息管幅度。

单个样本怎么更新

要把这一步写出来,最直接的办法就是在当前点附近做一个局部近似,看看一小步 \(\Delta\) 会让损失怎么变:

\[ l(\hat y + \Delta) \approx l(\hat y) + g \Delta + \frac{1}{2} h \Delta^2 \]

这里的 \(g\) 是一阶导数,\(h\) 是二阶导数。式子里的 \(g\Delta\) 决定方向,\(\frac{1}{2} h \Delta^2\) 决定这一步不会走得太大。

把它再往下推一步,最低点就在

\[ \Delta^* = -\frac{g}{h} \]

这个式子已经把核心说得很清楚了:\(g\) 决定往哪边修,\(h\) 决定这一步能修多大。\(h\) 越大,更新幅度通常越小。

XGBoost局部二次近似

  • 黑色曲线:当前点附近的局部损失形状。
  • 橙色虚线:红点处的切线,对应一阶导数,只负责告诉你哪边是下坡。
  • 蓝色箭头:这一轮真正采用的更新量,从 \(\Delta=0\) 走到 \(\Delta^*=-g/h\)
  • 左图更弯,\(h\) 更大,所以步长更小;右图更平,所以步长可以更大。

到这里讨论的还是单个样本。但树模型不是给每只股票单独配一个参数,它的做法是先把样本分到不同叶子里,再让同一叶子的样本共用一个输出值。

叶子值怎么算

回到 XGBoost 的训练过程,这个问题就会落到树结构上。它不是一次长成一棵完整的大树,而是一轮一轮往现有模型上加新树。

前面那个“这批股票该修多少”的问题,到了这里就会变成:新加的这棵树,应该把哪些样本分到同一个叶子里,以及这个叶子该输出多少。

\(t\) 轮时,模型已经有了当前预测 \(\hat y_i^{(t-1)}\),现在它准备再加一棵树 \(f_t(x_i)\),于是新预测变成:

XGBoost决策树原理图

  • 分裂节点:样本按条件往下走,不同条件对应不同分支。
  • 叶子节点:样本最后落到一个叶子里,拿到一个输出值 \(w_j\)。这个值表示这一轮要修多少。
  • 最下面那行 \(\hat y = \sum f_k(x)\):最终预测来自多棵树的输出累加,不是一棵树单独决定。

这张图对应的,其实就是从“单个样本怎么修”到“一组样本怎么修”的过渡。前面算的是单个样本的更新量,到了树里,就变成哪些样本分到一组,这一组统一给多少修正。

\[ \hat y_i^{(t)} = \hat y_i^{(t-1)} + f_t(x_i) \]

真正要算的不是“要不要加一棵树”,而是这棵树加进去以后,目标函数会下降多少。

原始损失函数通常不好直接和树结构一起处理,所以 XGBoost 就在当前预测点附近做二阶泰勒展开:

\[ l\left(y_i, \hat y_i^{(t-1)} + f_t(x_i)\right) \approx l\left(y_i, \hat y_i^{(t-1)}\right) + g_i f_t(x_i) + \frac{1}{2} h_i f_t(x_i)^2 \]

这样一来,“加一棵树以后损失怎么变”就被改写成了一个局部二次目标。

接下来就可以按叶子来处理样本了。既然不能给每只股票单独放一个 \(\Delta\),那就先把表现相近的样本分到同一个叶子里,再给这一组样本一个统一修正。

如果前面那批“低估值 + 低波动”的股票被分到同一个叶子里,模型就不会一只一只地修,而是直接给这一组一个共同的值。

如果第 \(j\) 个叶子的样本集合是 \(I_j\),记

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

那么这个叶子的最优输出就是:

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

这条式子读起来也很直接:

  • \(G_j\) 决定修正方向
  • \(H_j\) 限制修正幅度
  • \(\lambda\) 用来压住过大的叶子输出

到这里,前面单个样本上的 \(\Delta\),就落成了树里的叶子值。单个样本该怎么修,到了这里,变成的是这一组样本一起修多少。

分裂值不值得

叶子值解决的是一个问题:如果先不继续分,这个叶子该输出多少。接下来还剩下另一个问题:这个叶子值已经够了吗,还是还值得再分一次。

还是用前面的例子来看。这批“低估值 + 低波动”的股票虽然已经能共用一个叶子值,但它们内部可能还有差异。比如再按“高换手 / 低换手”分开,误差也许还会继续下降。

这时候需要比较两件事:

  • 不分裂,所有样本共用一个叶子值
  • 分裂,左叶子和右叶子分别计算自己的叶子值

如果分裂后目标函数下降得更多,那这次分裂就值得保留。上面那套局部二次目标,刚好可以把这个下降量直接算出来,这就是 XGBoost 的 gain 公式:

\[ \text{Gain} = \frac{1}{2} \left( \frac{G_L^2}{H_L + \lambda} + \frac{G_R^2}{H_R + \lambda} - \frac{G^2}{H + \lambda} \right) - \gamma \]

XGBoost分裂Gain示意图

  • 父节点的 \(G, H\):表示不分裂时,把这批样本放在一起算。
  • 左右叶子的 \(G_L, H_L\)\(G_R, H_R\):表示分裂后,左右两边分开算。
  • 最下面的 gain:就是“分开算”比“放在一起算”多带来的那部分下降量,再减去复杂度代价 \(\gamma\)

到这里,叶子值和 gain 的分工就很清楚了:

  • 叶子值回答:这一轮先怎么修
  • gain 回答:这个节点还有没有继续分裂的必要

参数在控制什么

到了参数层面,核心也没变,还是在管同一件事:不要让模型太轻易相信局部模式。对应到前面的主线,就是不要修得太猛,也不要太容易继续往下分。

  • eta:控制每一轮修正的幅度
  • lambdaalpha:压住过大的叶子输出
  • min_child_weightgamma:要求证据够多、收益够大,才允许继续分裂

再回到 XGBoost

很多 Kaggle 表格赛里,XGBoost 当然有树模型适合表格数据这层优势。但更关键的是,它不是一路往下拟合,而是每一步都会先把更新量和分裂收益算出来。

它每补一棵树,实际上都在做两层判断:这一轮该修多少,以及这个节点还有没有必要继续分裂。前一个问题靠一阶、二阶和叶子值,后一个问题靠 gain。

放到量化里看,这一点尤其重要。真正危险的往往不是模型不够复杂,而是模型太容易把阶段性行情、极端样本和偶然条件当成稳定规律。XGBoost 用一阶、二阶和正则化把每一步都卡得更严,本质上是在尽量延后这种失控。

XGBoost 总有竞争力,不只是因为它会长树,而是因为它每补一步之前,都会先把该怎么修、修多少、还要不要继续分清楚。