ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

XGBoost调参进阶:损失函数、分裂增益与贝叶斯优化实战

XGBoost调参进阶:损失函数、分裂增益与贝叶斯优化实战 我给自己定的十二天学习计划卡在今天终于有了一点“打通经脉”的感觉。前些天用 XGBoost 做二分类和回归基本停留在调包状态数据塞进去predict出来参数靠试错和复制别人的配置。到了第十一天晚上我盯着max_depth、gamma、min_child_weight这三个参数想了半天发现自己根本说不清楚它们分别控制树的哪一层结构更别说为什么调参时会互相打架。所以第十二天我决定干两件事第一把 XGBoost 的目标函数和分裂增益在纸上完整推一遍第二把手上的超参数搜索从网格搜索换成贝叶斯优化并且把优化过程可视化出来。这篇文章就是 Day 12 的记录适合已经能跑通 XGBoost 代码、但想补原理和调参方法论的人。如果你只是个刚入门的新手也能看懂因为我会把所有公式拆成生活化的话讲。 今天这篇会有点长但收获是实打实的。我尽量不绕弯子把树怎么分裂、贝叶斯优化怎么选点、可视化里最容易踩的坑一次说清楚。1. 从“调包侠”到“看懂分裂条件”为什么值得单独花一天1.1 前 11 天的能力盘点以及今天我到底要补哪三块拼图前 11 天我不是白过的。最基本的流程我已经很熟了读 CSV、清洗缺失值、划分训练测试集、用XGBClassifier或XGBRegressor训练、看准确率或 RMSE最后用plot_importance画一张特征重要性图。但问题出在“解释”层面。比如有人问我learning_rate设小之后为什么往往要把n_estimators调大gamma参数的默认值是 0为什么文档里说它越大模型越保守为什么随机搜索有时候比网格搜索效果还好贝叶斯优化又比随机搜索多了什么这些问题我答不全。答不全的根源是没理解提升树“加法模型”的本质也没理解贝叶斯优化背后的概率模型。所以 Day 12 只补三块一是 XGBoost 的数学骨架二是贝叶斯优化的基本逻辑三是把这两者可视化成图。可视化不是为了好看是为了让“原理”和“实验结果”对上号。一旦对上号以后再调参你下手就是有方向的而不是瞎猫碰死耗子。1.2 网格搜索和随机搜索我都试过痛点相当真实先说网格搜索为什么让我难受。假设我要调四个参数每个参数给 5 个候选值组合数就是 5 的 4 次方625 次。每次都用 5 折交叉验证去评估等于要训练 3000 多个模型。我原本以为这也没什么后来换了个稍大的数据集单次训练加验证要 8 秒3000 多次就是六七个小时。跑完之后我得到什么只是一张“这几个点分数最高”的表中间所有试过的点除了分数几乎没有任何信息。随机搜索比网格搜索好一些因为它不会老老实实把网格铺满而是随机撒点。当大部分参数对结果影响不大的时候随机搜索确实更容易碰到好区域。但随机搜索也有个大毛病它不利用已经尝试过的点。前一次测试发现learning_rate0.05附近分数明显更好可下一次随机依然可能在0.2附近浪费时间。贝叶斯优化的核心区别就在这里它会根据历史结果建立一个“哪个区域更可能取得好分数”的概率模型下一次选点专门往概率大的区域走。这正好适合 XGBoost 这种单次评估很贵、参数空间又连续的场景。2. XGBoost 的目标函数与分裂增益纸上推导一遍就忘不掉2.1 加法模型为什么是“每次只补残差”XGBoost 属于梯度提升树核心是一个“加法模型”。假设我们已经训练了 t-1 棵树那么第 t 棵树的加入方式是这样的最终的预测值 前面所有树的预测值 第 t 棵树的输出写成公式就是ŷ_i^(t) ŷ_i^(t-1) f_t(x_i)这里的f_t(x_i)就是第 t 棵树给第 i 个样本打的分。关键在于第 t 棵树并不是直接学“x 到 y 的完整映射”而是学“当前模型还差多少没预测对”。这个差值就是残差。更严谨地说它学的是损失函数在当前预测点上的负梯度方向但因为 XGBoost 用了二阶导数它比只学负梯度更精细。这种“每次只补残差”的思路放到生活里就像修路。第一天先把大坑填平第二天发现还有小裂缝第三天再补裂缝。每棵树都觉得自己只干了一点点活但合起来效果惊人。这也是提升树和随机森林最大的不同随机森林的每棵树都是独立去预测最后投票提升树的每棵树都在“纠正前任的错误”。2.2 从目标函数到分裂增益的完整推导逻辑XGBoost 的目标函数由两部分组成训练损失加上模型复杂度惩罚。Obj Σ L(y_i, ŷ_i) Σ Ω(f_t)其中Σ Ω(f_t)是正则项XGBoost 对每棵树的定义是Ω(f) γ * T 1/2 * λ * Σ w_j²T是叶子节点数w_j是叶子权重γ和λ是惩罚系数。叶子越多模型越复杂γ 惩罚越大叶子权重越大模型越激进λ 会把它拉回来。这一步很容易理解它给了模型一个“别长太胖”的压力防止死记硬背训练集。接下来是对损失函数做二阶泰勒展开。我们把损失函数在当前预测值ŷ_i^(t-1)附近展开得到每个样本的一阶梯度g_i和二阶梯度h_i。二阶梯度就是损失函数对预测值的二阶导。XGBoost 和传统 GBDT 的一个关键区别就在这传统 GBDT 只用一阶导信息XGBoost 使用了二阶导能同时考虑损失函数的斜率还有曲率。曲率大说明梯度变化快这一步走得就应该更小心。把常数项去掉之后目标函数化简成Obj Σ [g_i * w_q(x_i) 1/2 * h_i * w_q(x_i)²] γ*T 1/2*λ*Σ w_j²这里的w_q(x_i)是样本 i 被分到某个叶子后对应的叶子权重。我们把同一个叶子上的样本归到一起定义G_j Σ g_iH_j Σ h_i那么最优化这个目标函数时每个叶子的最优权重是w_j* -G_j / (H_j λ)这个公式说明叶子权重大小不只是由梯度决定还由二阶梯度H_j和惩罚项λ决定。H_j越大说明这个叶子上的样本损失曲率越高权重会被压缩λ 越大权重也被压得越厉害。这直接解释了为什么调大reg_lambda会让模型更保守。接下来是分裂增益。当一个节点要分裂成左右两个孩子节点时XGBoost 用下面的公式计算收益Gain 1/2 * [ G_L²/(H_Lλ) G_R²/(H_Rλ) - (G_LG_R)²/(H_LH_Rλ) ] - γ请你先别被这个公式吓到。它的意思非常直白分裂之后左右两个子节点的目标函数之和减去分裂之前父节点的目标函数收益大于某个阈值才值得分。阈值就是 γ。如果分裂带来的损失下降还抵不过 γ 的惩罚那这刀就不切了。我用一组假数据算过比光看公式印象深刻比如某个节点上G10H20λ1γ0.1。分裂后左边G_L6H_L8右边G_R4H_R12。代入之后左边的贡献是36/94右边是16/13≈1.23分裂前是100/21≈4.76括号里得到41.23-4.76≈0.47再乘以 0.5减去 γ 的 0.1最后 Gain 约等于 0.135大于 0说明这次分裂有点赚头。如果 Gain 小于等于 0这个分裂就会被放弃。记住这套逻辑之后gamma参数就好理解了它就是一个“分裂收益底线”。默认值是 0意味着哪怕只赚一点点也要分调大之后弱分裂会被过滤掉树变短模型更稳健但可能欠拟合。min_child_weight则是另一道门槛它要求每个叶子上的H_j累计值至少达到某个数否则也不让分。这相当于“叶子屁股底下要坐上足够多的人才能继续分家”。2.3 中文文档里经常一笔带过的三个细节第一学习率learning_rate本质上也是个正则项。每棵树学到的输出会被乘以一个小于 1 的系数再累加到总体预测上。这样做的代价是模型需要更多棵树才能达到同样效果但因为每棵树走得更小、更稳过拟合风险会降低。所以learning_rate调小时通常要同步增大n_estimators。这不是玄学这是加法模型的必然结果。第二subsample是行采样colsample_bytree是列采样。它们和随机森林里的 bagging 思想类似每棵树只看一部分样本、一部分特征让树和树之间的相关性降低。这里的代价是单棵树精度下降但因为最终是多棵树的加权融合整体泛化通常反而更好。用生活类比就是一个委员会里每个人信息不全反而少犯“大家一起错”的毛病。第三XGBoost 的“加权分位数草图”算法让它在直方图近似下能找到准最佳分裂点。早期 GBDT 在找分裂点时要把所有特征值排序数据大了很慢。XGBoost 会先把连续特征分成候选分位点再在这些候选点上评估增益。tree_methodhist走的就是这条路。理解了这一点你就明白为什么max_depth不是唯一的复杂度控制min_child_weight和gamma才是更细粒度的剪刀。3. 贝叶斯优化到底是什么以及为什么 XGBoost 调参比网格搜索省心3.1 从搜参数变成“边试边学”一次交叉验证就是一次现实抽血贝叶斯优化的目标函数是黑盒的。什么意思每次你选一组超参数要跑一次完整的交叉验证才知道效果。这个函数没有解析表达式你不能像求x²最小值那样求导或者直接算。它唯一的输入是一组参数唯一的输出是一个分数中间可能经历几十秒甚至几分钟的训练。这种目标函数在机器学习里到处都是因为“训练 验证”整个链路本身就是黑盒。贝叶斯优化的思路是既然真实函数很贵那我们就先用一个便宜的概率模型去猜“真实函数可能长什么样”然后根据这个猜测去选下一个点。这个便宜的概率模型叫代理模型最常用的是高斯过程。高斯过程会给出每一个参数组合对应的预测均值还会给出预测方差。预测均值代表“我估计这里能拿多少分”预测方差代表“我对这个估计有多不确定”。你可以这么理解你在黑夜里摸一块凸凹不平的地面手里只有一支手电筒。高斯过程帮你画出一张“地面高度预测图”同时标出哪些区域是照过很多次、非常确定的哪些区域是基本没照过、不确定的。然后贝叶斯优化会决定下一手电应该照哪。它既要顾及“照到高地的概率”也得顾“没探索过的地方可能更高”。这就是探索和利用的平衡。3.2 用“装修师傅量房间”讲清楚高斯过程如果你第一次接触高斯过程最友好的解释是把它当成一个“会自我评估的插值器”。普通插值可以穿过已知点但对没看过的位置没有把握判断。高斯过程不一样它不只会给你一个预测值还会给你一个信封状的置信区间。已知点附近置信区间窄远离已知点的地方置信区间宽。这就是为什么它能告诉你“哪里还需要再试”。放到调参场景假设当前已经测过 6 组参数。高斯过程会对整个参数空间生成一个平滑的函数分布在某些区域它敢打包票说分数不会好比如learning_rate0.3配合max_depth10附近显然过拟合在另一些区域它说不准比如你一直没试过min_child_weight7附近它会给出一个偏中性的预测价值和一个很大的不确定性。下一个点很可能就落在“高预测均值”和“高不确定性”交汇的地方。这正是网格搜索不具备的适应能力。3.3 采集函数怎么决定下一次去哪个点代理模型算完之后怎么从里面抽一个“下一批参数”这就轮到采集函数。最出名的是 Expected Improvement翻译成“期望提升”它计算的是EI(x) E[ max(f(x) - f_best, 0) ]f_best是目前看到的最好分数。这个公式的意思是站在当前最优成绩的肩上看每个候选点“有望提升多少”。如果一个点预测均值很高但不确定性也很高EI 可能很大如果一个点已经测过很多次确定它很烂那 EI 就接近 0。实际操作中贝叶斯优化库还会有一个kappa之类的参数来控制探索倾向。kappa越大越愿意抓不确定性kappa小则更稳妥。对 XGBoost 这种目标函数我通常先跑 5 个随机初始点让代理模型先见见世面再做 15 到 20 次正式迭代。初始点太少的话高斯过程连大方向都猜不准容易在局部最优里打转。下面是我实际搜索时给 XGBoost 划的参数边界大家可以参考参数范围说明n_estimators50 ~ 400树的数量必须取整数max_depth3 ~ 9单棵树深度必须取整数learning_rate0.01 ~ 0.3步长收缩min_child_weight1 ~ 10叶子最小二阶梯度累计subsample0.5 ~ 1.0行采样比例colsample_bytree0.5 ~ 1.0列采样比例reg_lambda0.5 ~ 5.0叶子权重 L2 惩罚4. 让原理“露出原形”Day 12 的三类可视化实践4.1 先把数据、指标和验证方式交代清楚为了避免读者稀里糊涂我先把实验配置钉死。今天用的数据集是sklearn内置的糖尿病数据集有 10 个特征、442 个样本任务是回归。这个数据集不大单次训练很快特别适合做原理演示。如果你要做实际项目比如基于 XGBoost 的空域交通复杂度建模思路完全一样只是数据量大了以后需要加上早停和分块训练。评价指标我用 RMSE越小越好。验证方式我用 5 折交叉验证。这里有个很重要的点贝叶斯优化的目标函数必须是“交叉验证分数”而不能只是训练集上的表现否则选出来的参数就是过拟合参数。我在实验里固定了随机种子random_state42确保每次优化的结果可复现。没有固定种子的调参实验等于在做不可重复的科学实验。4.2 单棵树可视化看分裂特征和叶子权重而不是只看装饰XGBoost 自带plot_tree函数一句话就能画出某棵树的内部结构。第一棵树的图里你能看到根节点选了哪个特征、阈值是多少左边走条件成立右边走条件不成立。叶子节点上的leaf值就是这棵树的输出权重。如果这个权重是负数说明在这个分支上的样本前几棵树已经普遍预测偏高了这棵树要把分数往下拉一点。很多人第一次看这图会觉得“树好多根本看不过来”。不用看全部看前两三棵就够了。因为第一棵树学的是主要残差结构最清晰越到后面的树学的是边角料分支乱很正常。另外plot_tree依赖 Graphviz你本机得装好对应的二进制工具否则会直接报错。我当时卡在这里卡了二十分钟还以为是代码问题。4.3 特征重要性对比默认排序可能会骗人plot_importance默认用的是importance_typeweight也就是统计每个特征被当作分裂特征的次数。这个指标有个毛病一个特征如果出现在很多浅树里可能只是因为它被反复拿来试但不代表它每次分裂都带来很大收益。我更推荐看gain也就是该特征所有分裂点的平均增益之和。增益越高说明这个特征在“减少损失”这件事上的贡献越大。我对比过两个指标结论经常不完全一致。有些特征 weight 很高但 gain 一般有些特征 weight 不高却能在几个关键节点带来大幅增益。实际看的时候我会两个图并排看。如果两个图共同指向同一批特征这批特征基本就是定海神针如果互相矛盾那就得靠部分依赖图再验证一下。4.4 偏依赖图变量之间怎么合作一张图全看清偏依赖图是让树模型“会说话”的重要工具。它会把某个特征取不同的值让其他特征保持不变观察模型预测值的平均变化。对线性模型来说这画出来就是一条斜线对 XGBoost它可能是一条曲线甚至带阶梯状。更进阶一点可以一次传入两个特征画出二维等高线热力图。比如把bp和s1两个特征同时变化看模型输出如何响应。你会很清楚地看到特征交互单个变量单独看可能影响不大但两个变量同时变大时预测值剧烈变化。这就是树模型天然捕捉交互作用的能力偏依赖图能把这个能力显式地摊开给你看。5. 贝叶斯优化目标曲面可视化与完整实验代码5.1 一个能跑起来的参考脚本下面这段代码我建议你直接复制到 Jupyter Notebook 里跑一遍。它分成两部分第一部分是普通的 XGBoost 回归训练加特征重要性可视化第二部分是贝叶斯优化搜索参数并画出优化过程。import numpy as np import pandas as pd import xgboost as xgb import matplotlib.pyplot as plt from sklearn.datasets import load_diabetes from sklearn.model_selection import cross_val_score, KFold from bayes_opt import BayesianOptimization # 固定随机种子保证实验可复现 X, y load_diabetes(return_X_yTrue) cv KFold(n_splits5, shuffleTrue, random_state42) # 1. 先训练一个默认配置的模型画出特征重要性 model xgb.XGBRegressor( n_estimators200, max_depth4, learning_rate0.05, subsample0.8, colsample_bytree0.8, reg_lambda1.0, tree_methodhist, random_state42, ) model.fit(X, y) fig, ax plt.subplots(figsize(10, 4)) xgb.plot_importance(model, importance_typegain, axax) plt.title(XGBoost Feature Importance by Gain) plt.show() # 2. 定义贝叶斯优化的目标函数 def xgb_cv(n_estimators, max_depth, min_child_weight, learning_rate, subsample, colsample_bytree, reg_lambda): params { n_estimators: int(n_estimators), max_depth: int(max_depth), min_child_weight: min_child_weight, learning_rate: learning_rate, subsample: subsample, colsample_bytree: colsample_bytree, reg_lambda: reg_lambda, tree_method: hist, } model xgb.XGBRegressor(**params, random_state42) scores cross_val_score(model, X, y, cvcv, scoringneg_root_mean_squared_error) return np.mean(scores) # 3. 设定参数边界 pbounds { n_estimators: (50, 400), max_depth: (3, 9), min_child_weight: (1, 10), learning_rate: (0.01, 0.3), subsample: (0.5, 1.0), colsample_bytree: (0.5, 1.0), reg_lambda: (0.5, 5.0), } optimizer BayesianOptimization( fxgb_cv, pboundspbounds, random_state42 ) optimizer.maximize(init_points5, n_iter20) print(optimizer.max)这段代码的xgb_cv每次调用都会做一次 5 折交叉验证返回负 RMSE越大越好因为 sklearn 的neg_root_mean_squared_error是负值。BayesianOptimization会自动把历史结果存下来optimizer.max会给出当前找到的最优参数组合和对应分数。一个非常容易踩的坑是n_estimators和max_depth在贝叶斯优化库里会被当作浮点数传入所以在目标函数里必须用int()包一层。我一开始没转模型直接报错说期望 int 类型得到 float。5.2 迭代过程可视化画出尝试过的点位和分数贝叶斯优化跑完之后optimizer.res里存了每一轮的参数和分数。你可以用下面的代码画一张“目标分数随迭代次数的变化”图targets [res[target] for res in optimizer.res] plt.figure(figsize(10, 4)) plt.plot(targets, o-) plt.xlabel(Iteration) plt.ylabel(CV Score (negative RMSE)) plt.title(Bayesian Optimization Iteration History) plt.show()这张图能让你判断优化是否收敛前几次迭代分数往往会跳来跳去因为初始点在随机探索后面曲线会逐步抬高并稳定下来。如果 20 次迭代后曲线还在明显上升说明预算不够应该加n_iter。如果曲线很早就不动了你要警惕是不是搜索边界太窄或者初始点太少导致代理模型学歪了。我跑出来的结果是默认配置的负 RMSE 大概在 -58 附近贝叶斯优化 25 次评估后拉到了 -54 左右相当于 RMSE 从 58 降到 54。提升没有想象中夸张但对这个数据集来说已经是不小的进步。真实项目中如果你前面几轮随机点就看到了一个高分区域优化的收益会更明显。5.3 为什么我看完代理模型曲面会调整搜索边界贝叶斯优化还有一个隐藏优势就是它能把你搜索过的点映射到参数子空间中。你可以检查最优点的位置如果某个参数的最优点正好落在你设定的边界上比如max_depth9正好是边界那说明边界可能设窄了模型还想要更深的树只不过你把它限制死了。我刚开始做贝叶斯优化时把learning_rate的范围设成0.05到0.2结果每次最优解都跑到0.05附近峰值落在左边界。我一开始还以为“0.05 就是最佳”后来把左边界放宽到0.01优化器就开始往更小的值探索最后找到了更好的分数。这就是可视化带给我的一个直接教训看见边界就不再往下走基本说明你的搜索空间定义失误或者说提前放弃了潜在地带。另外不要同时优化太多参数。参数维度太高时高斯过程需要的采样点数量会指数级增长。我的习惯是如果项目任务复杂先把树结构相关参数固定下来专门优化learning_rate、n_estimators、reg_lambda这三个跑一轮之后再放开来精修。这样做不仅仅是省算力更是让代理模型更容易拟合。6. 复盘 Day 12留给下一周的三条操作心得最后说点实在的体会。第一调 XGBoost 时不要只看单个参数。max_depth、gamma、min_child_weight这三兄弟共同限制树的生长分开调容易顾此失彼。先把max_depth固定在 4 到 6再让贝叶斯优化同时调gamma和min_child_weight比逐个网格搜索舒服得多。第二贝叶斯优化不是越快越好。init_points我建议至少给 5 个让代理模型有足够样本建立初始地形。正式迭代一次只加 15 到 20 次跑完看迭代曲线再决定要不要加跑。如果你一上来就init_points0, n_iter50前面十次几乎都是在乱猜后面才慢慢进入状态浪费的算力比多初始化几次还多。第三可视化会毫不留情地暴露数据问题。我在画特征重要性图和偏依赖图的时候发现有两个特征无论怎么调参数增益都低得离谱。后来回头查才发现这两个特征的原始数据分布极度偏斜缺失值又多树模型根本没用它们。这说明可视化不只是展示结果它其实是一种诊断工具。如果发现某个特征在所有树里都当不上分裂特征先去查数据质量再考虑是不是要特征工程。这周还剩两天我打算把贝叶斯优化里的 Expected Improvement 换成 Upper Confidence Bound 试试再对比一下不同采集函数在 XGBoost 调参上的差别。如果你也想深入建议从“固定两个参数、可视化三维代理曲面”开始亲眼看一次高斯过程怎么更新比看十篇文档都管用。
返回列表