subsample从1.0降到0.6使回归RMSE降低13.7%,采样方法uniform和gradient_based的差异及适用场景说明。
XGBoost 的 subsample 与 sampling_method:那个悄然拯救你回归模型的 Bagging 杠杆
作者:Shakti Tiwari — Nifty 期权交易员、XGBoost 专家(optiontradingwithai.in)
免责声明:Shakti Tiwari 持有 NISM-Series-XII 认证,并非 SEBI 注册研究分析师。内容仅供教育用途。
XGBoost 的 subsample 控制每轮提升迭代所使用的训练行数比例——它是作用于每棵树的行级 Bagging。在我们观察的 5 折交叉验证中,保持默认值 1.0 是回归任务最差的设置(RMSE 0.4611),而 0.6 以 RMSE 0.4057 胜出——误差降低约 13.7%。分类准确率几乎没变(所有取值下从 0.9063 到 0.9072)。配套的 sampling_method(uniform vs gradient_based)决定如何选取行:uniform 等概率抽取,gradient_based 对难拟合的行过采样。将 subsample 调优在 0.6–0.9 区间内,永远不要盲目保持 1.0。
大多数人会调 max_depth、eta 和 lambda,然后就收工了。他们从来不碰 subsample。这是个大错——尤其在你做交易或预测的时候。
事情是这样的。梯度提升树模型一次添加一棵树,每棵树纠正上一棵树的残差。如果每棵树都看到所有行(subsample = 1.0),树之间就会高度相关——它们都学到同一份数据的相同特性。这就带来了方差,而方差正是让你的样本外数字在到期日崩掉的元凶。
subsample 打破了这种相关性。每棵树在一份随机抽取的行上训练,所以集成模型的行为有点像 Bagging 模型:每棵树不同,它们的误差部分相互抵消,泛化能力得到提升。在我们的数据中,从 1.0 → 0.6 这一步改动就让回归误差削减了两位数。这不是四舍五入的巧合;这是「你信任的模型」与「你做模拟交易然后祈祷的模型」之间的差距。
是的,这对我们运行的双层引擎也很重要。我们的在线系统是双层的:Layer 1 = Dhan WebSocket 实时捕获(shadow/predict-only,未经经纪商授权绝不交易);Layer 2 = EOD 审计的 XGBoost/LightGBM 训练核心(walk-forward,成本与滑点模型)。本文讨论的参数在 Layer 2 中调优。所以 subsample 是一个 Layer-2 旋钮,在日终审计折上调优——不是在实时 tick 流上乱转的东西。
Samajh lo:subsample 是你可能一直没用的最便宜的正则化手段。
研究问题与假设
问题:在固定的 5 折交叉验证框架下,subsample ∈ {0.4, 0.6, 0.8, 0.9, 1.0} 在(a)回归 RMSE 和(b)分类准确率上表现如何,其他所有参数保持不变?
假设:更小的 subsample → 更多的行级随机性 → 更低的方差 → 更好的样本外表现,但仅限在一定程度上。低于某个阈值后树会饿死(每轮数据太少)导致欠拟合,所以我们预期是一条 U 形(或至少是非单调的)曲线,存在一个最优点,而不是一条直线。
我们还想验证第二个更微妙的说法:行抽样在回归和分类任务上表现是否不同。数据给出了响亮的回答——见结果部分。
数据与方法框
该框架是确定性的(全局 RNG 种子 42,折种子 0)。下面的数字都是观察值,不是理论值,也不是插值结果。
观察到的 5 折交叉验证表
默认的 subsample=1.0 是这里最差的回归设置。RMSE 0.4611 比 0.6 最优值(0.4057)高出约 13.7%。「默认 = 安全」对这个杠杆来说是错误的。
默认的 subsample=1.0 是这里最差的回归设置。RMSE 0.4611 比 0.6 最优值(0.4057)高出约 13.7%。「默认 = 安全」对这个杠杆来说是错误的。
最优点在 ~0.6 附近。回归 RMSE 在 subsample 0.6 时触底至 0.4057,0.8 紧随其后(0.4194,高 3.4%)。良好的区间大致在 0.6–0.8。
最优点在 ~0.6 附近。回归 RMSE 在 subsample 0.6 时触底至 0.4057,0.8 紧随其后(0.4194,高 3.4%)。良好的区间大致在 0.6–0.8。
太小反而有害——欠拟合是真实存在的。降到 0.4 会使 RMSE 回升到 0.4140(比 0.6 高 2.0%)。每棵树只看到 40% 的行,所以它无法干净地捕捉 X0²/sin(X1) 的结构。确认是非单调的,正如假设所言。
太小反而有害——欠拟合是真实存在的。降到 0.4 会使 RMSE 回升到 0.4140(比 0.6 高 2.0%)。每棵树只看到 40% 的行,所以它无法干净地捕捉 X0²/sin(X1) 的结构。确认是非单调的,正如假设所言。
分类准确率基本持平。波动范围仅为 0.9063 → 0.9072(0.0009 的窗口)。在这个数据集上,行抽样不是控制分类准确率的旋钮——它是回归泛化的杠杆。
分类准确率基本持平。波动范围仅为 0.9063 → 0.9072(0.0009 的窗口)。在这个数据集上,行抽样不是控制分类准确率的旋钮——它是回归泛化的杠杆。
边缘处指标不一致。subsample 0.9 给出最佳 clf ACC(0.9072)但回归 RMSE 是第三差的(0.4233)。根据你实际部署的指标来调优——不要盲目地在不同目标间做平均排名。
边缘处指标不一致。subsample 0.9 给出最佳 clf ACC(0.9072)但回归 RMSE 是第三差的(0.4233)。根据你实际部署的指标来调优——不要盲目地在不同目标间做平均排名。
曲线是 U 形的,不是线性的。1.0(差)→ 0.6(最佳)→ 0.4(再次变差)。「越小越好」的直觉在拐点以下就失效了。
曲线是 U 形的,不是线性的。1.0(差)→ 0.6(最佳)→ 0.4(再次变差)。「越小越好」的直觉在拐点以下就失效了。
可复现性(代码)
精确的搜索过程,可直接运行(单个参数约需几分钟;完整的 A2–A7 运行约 50 分钟):
import numpy as np, xgboost as xgb, json
def make_reg(n=20000, d=24, seed=1):
r = np.random.default_rng(seed)
X = r.normal(0, 1, size=(n, d))
y = (X[:,0]**2 + np.sin(X[:,1]) + X[:,2]*X[:,3]
+ 0.5*X[:,4] - 0.3*X[:,5] + r.normal(0,0.3,n))
return X, y
def make_clf(n=20000, d=24, seed=2):
r = np.random.default_rng(seed)
X = r.normal(0, 1, size=(n, d))
score = (X[:,0]*0.8 + X[:,1]**2*0.5 - X[:,2]*0.6
+ np.tanh(X[:,3]) + r.normal(0,0.4,n))
y = (score > 0).astype(int)
return X, y
def kfold(X, y, n_split=5, seed=0):
idx = np.arange(len(y)); r2 = np.random.default_rng(seed); r2.shuffle(idx)
parts = np.array_split(idx, n_split)
for i in range(n_split):
te = parts[i]; tr = np.concatenate([parts[j] for j in range(n_split) if j!=i])
yield tr, te
Xr, yr = make_reg(); Xc, yc = make_clf()
results = {}
def oos_reg(param, values, fixed):
out={}
for v in values:
p = dict(max_depth=4, eta=0.1, subsample=0.8, colsample_bytree=0.8,
objective='reg:squarederror', verbosity=0)
p.update(fixed); p[param]=v
oos=[]
for tr,te in kfold(Xr, yr):
dtr=xgb.DMatrix(Xr[tr], label=yr[tr]); dte=xgb.DMatrix(Xr[te], label=yr[te])
bst=xgb.train(p, dtr, num_boost_round=500, evals=[(dte,'te')],
early_stopping_rounds=30, verbose_eval=False)
pred=bst.predict(dte, iteration_range=(0, bst.best_iteration+1))
oos.append(np.sqrt(np.mean((yr[te]-pred)**2)))
out[str(v)]=round(float(np.mean(oos)),4)
return out
## Same shape for oos_clf with objective='binary:logistic', metric = accuracy
results['A5_sub_reg'] = oos_reg('subsample', [0.4,0.6,0.8,0.9,1.0], {})
## results['A5_sub_clf'] = oos_clf('subsample', [0.4,0.6,0.8,0.9,1.0], {})
json.dump(results, open('a5_sub.json','w'), indent=2)
要开启 sampling_method(这个配套旋钮),scikit-learn API 更清晰:
import xgboost as xgb
## Default uniform row-draw, the setting used in the sweep above
reg_uniform = xgb.XGBRegressor(
n_estimators=500, max_depth=4, eta=0.1,
subsample=0.6, colsample_bytree=0.8,
sampling_method='uniform', early_stopping_rounds=30)
## Gradient-based: oversample hard rows (needs hist/gpu_hist tree method)
clf_grad = xgb.XGBClassifier(
n_estimators=500, max_depth=4, eta=0.1,
subsample=0.8, colsample_bytree=0.8,
sampling_method='gradient_based', tree_method='hist',
early_stopping_rounds=30)
运行它,你会复现上面的表格。垃圾进垃圾出——但这里的数字是确定的。
深度解析:sampling_method — Uniform vs Gradient-Based
subsample 回答的是多少行;sampling_method 回答的是哪些行。根据 doc/parameter.rst,有两种模式,它们完全改变了随机性的性质。
uniform(默认)。每行以相同的概率 subsample 被抽中进入给定的提升轮次。这是经典的 bootstrap 式 Bagging:抽取对模型当前拟合得多差毫不知情。这就是上面搜索过程所使用的设置,当你只想无假设地获得方差 reduction 时这是正确的默认选择。
gradient_based。在这种情况下,行 i 被选中的概率与它的梯度 |g_i| 的幅度成正比——即当前残差有多大。对于 reg:squarederror,梯度实际上就是负残差(y_i − ŷ_i),所以模型当前偏差很大的行比已经拟合得很紧的行被抽中的概率大得多。效果:每棵新树都朝着难以拟合的行倾斜。这在精神上更接近 boosting 自身的逻辑(关注残差),但应用在行采样层面而非树权重层面。
两个来自文档和源码的实际约束:
gradient_based 只在 subsample < 1 时生效。当 subsample=1.0 时没有重采样的必要,所以该参数无效。
它需要 tree_method='hist' 或 'gpu_hist'。基于梯度的抽样建立在直方图树构建器之上;精确方法('exact')和近似方法('approx')不支持它。在实践中,这意味着:如果你想用 gradient_based,请设置 tree_method='hist'(或者让 XGBoost 在现代版本中默认使用它)。
什么时候应该选择 gradient_based 而不是 uniform?考虑不平衡的难度。如果 5% 的行携带了 50% 的残差(期权 IV 曲面中的厚尾、稀有的波动会话、真正更难点的小众类别),uniform 会因概率而持续欠采样那些顽固的行,而 gradient_based 则会刻意让它们被过度代表。权衡:如果那些大梯度只是随机异常值,它可能会过度聚焦于噪声,所以用我们验证 subsample 的相同方式验证它——基于部署指标的 out-of-fold 验证。
纸上的数字是冰冷的。下面是我实际如何逐步阅读上面的表格,这样你可以在自己的数据上做同样的事情。
以默认值为锚点。从 subsample=1.0,RMSE 0.4611 开始。这是你的"无 bagging"基线。所有度量都相对于它来衡量。
向左走。0.9 → 0.4233(更好)。0.8 → 0.4194(更好)。0.6 → 0.4057(地板)。误差在减少行数的过程中下降——方差正在被削减。
越过拐点。0.4 → 0.4140。误差再次上升。我们从"刚刚好的随机性"跨入了"每棵树数据不足"。那个回升就是你的欠拟合警告灯。
找到谷值,而非边缘。最小值在 0.6,而非测试的最小值(0.4)。一个常见的业余错误是看到"越小到目前为止越好"就选了 0.4——但曲线已经转向了。始终绘制完整的扫描曲线;永远不要用单步爬山法调参。
检查第二个指标。分类 ACC 在整个扫描过程中是平的(0.9063–0.9072)。所以如果你的部署指标是准确率,subsample 会是一个你可以留在接近 1.0 以节省计算量的自由参数。调参预算有限——把它花在指标会变动的地方。
注意分歧。0.9 对分类最好,但对回归只是中等水平。如果你有多目标排序,按部署优先级加权,不要天真地平均。
这六步阅读可以推广到每一个连续的 XGBoost 旋钮:锚定、扫描、找谷值、尊重拐点、交叉检查第二个指标、按部署权重排序。这里的形状——一个干净的 U 型——是一个表现良好的正则化参数的教科书级签名。
我们原本预期 subsample 能提升两个指标。它没有。分类列在整个扫描过程中保持无聊的平缓。这是对"bagging 帮助一切"心智模型的反面证据。
为什么会这样?在这个合成分类任务中,决策边界相当平滑,标签噪声(分数上的 N(0,0.4))是对称的。丢弃行几乎不会改变一个点落在阈值的哪一边,所以准确率是稳定的。相比之下,回归对连续曲面拟合得好坏很敏感——而相关树(subsample 1.0)会过拟合那个曲面的起伏,使 RMSE 膨胀。所以这个"失败"实际上是最具启发性的结果:subsample 是一种方差税,你需要在指标惩罚方差的场景下付出——回归比平衡分类受到的惩罚更多。
我们也曾半预期 0.4 会击败 0.6。它没有——0.4 欠拟合了。这一点很有用;它否定了"更多随机性 = 总是更好"的说法。
合成数据。目标来自已知非线性 + 高斯噪声。真实的 Nifty 期权链特征是更厚尾的、有缺口且依赖状态的。曲线的形状(U 型,默认值最差)是可迁移的教训;确切的 0.6 最优点不是在你数据上的承诺。
单一固定配置。max_depth=4, eta=0.1, colsample_bytree=0.8 保持不变。subsample 与 colsample_*(下一篇文章,A6)和 eta 有交互——更深的树或不同的学习率会使最优点偏移。
sampling_method 这里没有扫描。本次运行使用 uniform。gradient_based 是我们描述但未在表格中做 CV 的独立轴。
单一种子家族。Fold seed 0, global 42。用 20k 行和 5 折,估计是稳定的,但我们没有做自助法置信区间。
永远不要未经测试就上线 subsample=1.0。把 0.6–0.9 作为强制网格点。我们的数据显示默认值可能让你损失约 14% 的回归误差。
根据你的部署指标调参。如果你的部署指标是 RMSE/MAE,把搜索中心放在 0.6–0.8。如果是在平衡准确率上且你的任务像我们这样平滑,subsample 是低优先级的——把调参预算花在其他地方。
警惕欠拟合。如果降低 subsample 后误差越过拐点上升了,停止——你已经让树饿死了。0.4 在这里已经太薄了。
与 colsample_bytree(A6)配合使用。行 + 列子采样是互补的随机性。它们一起比单独任何一个都能更有效地解耦树。不要调了一个就把另一个盲目冻结在 0.8。
对困难、不平衡的行使用 gradient_based。如果你的损失由少数顽固的预测错误行主导(类别不平衡、厚尾),sampling_method='gradient_based' 配合 tree_method='hist' 会基于大梯度行重新加权抽样——有效地告诉 XGBoost"在当前你错得最多的地方多用些树"。
把 subsample 留在第二层。根据两层说明,这是一个日终审计的训练核心旋钮,用成本和滑点模型进行前向验证——不是实时 tick 参数。
问:subsample 和 colsample_bytree 一样吗?不一样。subsample 对每个 boosting 轮次采样行(训练实例);colsample_bytree 对每棵树采样特征/列。不同维度的随机性。A6 会讲到列。
问:我可以设置 subsample > 1.0 吗?不能。有效范围是 (0, 1]。1.0 意味着"使用所有行"。subsample 中没有行的过采样;要做到那一点你需要看 gradient_based 重加权,而不是 > 1 的值。
问:sampling_method='gradient_based' 实际上做什么?根据 doc/parameter.rst,不是均匀地抽取行,而是 XGBoost 用每个行梯度的大小(当前模型在其上的错误程度)对每行加权,然后按比例采样。难以拟合的行被过度采样。它需要 tree_method='hist' 或 'gpu_hist'。
问:为什么分类准确率没有变动?因为在这个任务中准确率是对平滑边界上粗糙的、基于阈值的指标;丢弃行不会翻转很多预测。回归 RMSE 作为连续且对方差敏感的指标,响应很强。
问:我应该在小型数据集(n=500)上使用 subsample 吗?谨慎使用。在 0.6 时你给每棵树只有 300 行——严重欠拟合的风险。数据越薄,你应该把 subsample 保持得越高(0.8–1.0)。我们的 20k 行结果不会线性迁移到小数据集上。
问:subsample 会减慢训练吗?略微。采样行是廉价的;更大的成本是更低的 subsample 可能需要更多轮次才能收敛。有了 early-stopping 它会自动限制。
Profiles: about.me · optiontradingwithai.in · github · whatsapp · x/twitter
dmlc/xgboost GitHub 仓库 — github.com/dmlc/xgboost (~28.7k★)。参数语义的主要来源。
doc/parameter.rst — 定义 subsample(行子采样比例,范围 (0,1],默认 1)和 sampling_method(uniform / gradient_based)。
doc/tutorials/param_tuning.rst — 调参指导;推荐 subsample + colsample_bytree 作为随机正则化。
观察到的 5 折 CV 结果:a2_a7_results.json,键为 A5_sub_reg / A5_sub_clf(本文中的数字)。
你提供的第三部分(Section 3/3)中,正文章节内容为空。Message 内容全部为作者署名信息、系列文章链接和社交媒体链接:
这些不属于文章正文结构(Introduction、Core Concepts、Technical Deep-Dive 等),属于元数据/签名区块,不在翻译范围内。
如需翻译正文,请提供实际章节内容,例如:
我会按要求完整翻译每一段落,保留所有格式与图片标记。