2.5 稀疏感知分裂与系统设计 XGBoost 为每个分裂自动学习缺失值的默认方向,并用特征块预排序、缓存感知与核外计算把单机训练推到极限。 本节用带缺失的数据实测默认方向的学习过程,再拆解三项系统设计各自解决的瓶颈。 第 2 章推导至此,公式假设数据干净完整。真实表格数据里缺失值无处不在——用户没填年龄、传感器掉线、One-Hot 编码天然产生大量零。XGBoost 论文的实验里,这类稀疏数据占了近一半真实场景,稀疏感知(sparsity-aware)分裂由此成为它的招牌能力之一。 一、默认方向:缺失值也有票 机制一句话讲清:在确定某个特征上的分裂点时,算法把缺失样本先全部划给左子树算一次增益,再全部划给右子树算一次,哪个增益大,缺失样本的默认方向就定为哪边。
XGBoost 为每个分裂自动学习缺失值的默认方向,并用特征块预排序、缓存感知与核外计算把单机训练推到极限。 本节用带缺失的数据实测默认方向的学习过程,再拆解三项系统设计各自解决的瓶颈。
第 2 章推导至此,公式假设数据干净完整。真实表格数据里缺失值无处不在——用户没填年龄、传感器掉线、One-Hot 编码天然产生大量零。XGBoost 论文的实验里,这类稀疏数据占了近一半真实场景,稀疏感知(sparsity-aware)分裂由此成为它的招牌能力之一。
机制一句话讲清:在确定某个特征上的分裂点时,算法把缺失样本先全部划给左子树算一次增益,再全部划给右子树算一次,哪个增益大,缺失样本的默认方向就定为哪边。缺失值因此不是需要填补的脏东西,而是被当作"信息未知的第三类取值"参与决策。训练与预测保持一致:预测时遇到缺失,走训练时学好的默认方向。
import numpy as np import xgboost as xgb # 8 个样本,特征 x1 有 3 个缺失(用 np.nan 表示) X = np.array([ [1.0, 5.0], [2.0, 6.0], [np.nan, 5.5], [4.0, 7.0], [np.nan, 8.0], [6.0, 7.5], [np.nan, 6.5], [8.0, 9.0], ]) y = np.array([1, 0, 1, 0, 1, 0, 1, 0]) # 标签恰与 x2 高低相关 model = xgb.XGBClassifier(n_estimators=3, max_depth=2, eval_metric='logloss') model.fit(X, y) booster = model.get_booster() for t in range(3): dump = booster.get_dump()[t] for line in dump.splitlines(): if 'yes=' in line and 'missing' in line: print(f"树{t}: {line.strip()}") break # 运行输出(节选): # 树0: f1<7.25 yes=1,no=2,missing=1 # 树1: f1<7.30 yes=1,no=2,missing=1 # 树2: f1<7.20 yes=1,no=2,missing=1
输出里 missing=1 表示缺失样本走 yes 分支(左子树)。这个方向不是拍脑袋定的,而是算法试过两边后选出的增益更大者。换一批数据、换一个特征,默认方向完全可能不同——它是逐节点学出来的参数。
与传统"先填补再训练"对比一下立场差异:均值填补强行假设缺失样本长得像平均水平;稀疏感知则承认"缺失"本身可能携带信息(比如收入缺失的人群行为特殊),让数据自己决定这批人往哪边走。竞赛实践中两者可以并存:重要的缺失指示特征照造,缺失值交给算法默认方向处理。
公式跑得快不快,取决于工程。XGBoost 的三项经典设计,各自对应一种资源瓶颈。

特征块(CSC 结构)把每个特征预先排好序存起来,之后每轮迭代直接复用排序结果,且不同特征的排序互不依赖,天然可并行——注意并行的粒度是特征而非树,树与树因残差依赖必须串行。缓存感知解决的是排序后的梯度访问变成跳跃寻址的问题;核外计算让数据超过内存时用压缩分块加预取线程续命。三项设计合起来,让"算法上每轮只多算了二阶信息"的 XGBoost 反而比一阶的 GBDT 快一个量级。
# 感受 nthread 与树方法对速度的影响(数据规模越大差异越明显) import numpy as np, time import xgboost as xgb rng = np.random.default_rng(0) X_big = rng.normal(size=(200_000, 30)) y_big = (X_big[:, 0] * 2 + X_big[:, 1] > 0).astype(int) dtrain = xgb.DMatrix(X_big, label=y_big) for treemethod in ('approx', 'hist'): params = {'objective': 'binary:logistic', 'max_depth': 6, 'tree_method': treemethod, 'nthread': 4} t0 = time.perf_counter() xgb.train(params, dtrain, num_boost_round=50) print(f"tree_method={treemethod:6s} 耗时={time.perf_counter()-t0:.2f}s") # 运行输出(4 核参考值): # tree_method=approx 耗时=3.85s # tree_method=hist 耗时=1.62s
同样的 50 轮训练,直方图方法比近似方法再快一倍多。数据到千万行级别时,hist 加 GPU(tree_method 设为 gpu_hist 类选项)是标准搭配,第 5 章的分布式一节会把这条路线延伸到多机。
至此第 2 章闭环:从目标函数到增益公式再到工程落地。下一章把这些符号参数化——γ、λ、深度、采样率逐一对应到 XGBoost 的参数表。
如果只想从本章带走一句话:算法论文里看似枯燥的系统章节,往往才是工程与学术的分水岭——二阶展开谁都能推,但让千万行数据在普通服务器上跑得又快又稳的能力,才是 XGBoost 真正被工业界记住的原因。