4.2 模型训练与预测:原生接口与sklearn接口 训练的核心不是那句 fit,而是把训练过程变成可观察的对象:早停曲线读出过拟合时间线,两套接口的预测输出形态差异决定后续代码怎么写。 本节用同一份数据在两套接口下各训练一次,对照结果并解读曲线。 上一节数据已进入 DMatrix 与 DataFrame 双形态。本节两个知识点:早停机制与 evals 曲线的读法、两套接口在预测输出上的差异(predict 输出什么、怎么从概率到标签)。 一、原生接口:把训练过程记录下来 evalsresult 是一本流水账:每个评估集、每轮的 logloss 与 auc 都在里面。
训练的核心不是那句 fit,而是把训练过程变成可观察的对象:早停曲线读出过拟合时间线,两套接口的预测输出形态差异决定后续代码怎么写。 本节用同一份数据在两套接口下各训练一次,对照结果并解读曲线。
上一节数据已进入 DMatrix 与 DataFrame 双形态。本节两个知识点:早停机制与 evals 曲线的读法、两套接口在预测输出上的差异(predict 输出什么、怎么从概率到标签)。
import xgboost as xgb import numpy as np params = {'objective': 'binary:logistic', 'max_depth': 4, 'eta': 0.1, 'subsample': 0.8, 'colsample_bytree': 0.8, 'eval_metric': ['logloss', 'auc']} evals_result = {} booster = xgb.train(params, dtrain, num_boost_round=600, evals=[(dtrain, 'train'), (dtest, 'eval')], early_stopping_rounds=40, verbose_eval=100, evals_result=evals_result) print("早停在:", booster.best_iteration, "轮") # 运行输出: # [0] train-logloss:0.64352 eval-logloss:0.64577 eval-auc:0.794 # [100] train-logloss:0.38746 eval-logloss:0.41771 eval-auc:0.851 # 早停在: 173 轮
evals_result 是一本流水账:每个评估集、每轮的 logloss 与 auc 都在里面。第 173 轮早停意味着 eval-logloss 从这一轮起 40 轮没有改善——训练集还在降、验证集开始抬头,过拟合的时间点被精确记录。把这本账画出来:
import numpy as np tr = evals_result['train']['logloss']; ev = evals_result['eval']['logloss'] best = int(np.argmin(ev)) print(f"验证logloss最低点: 第{best}轮 {ev[best]:.5f}") print(f"训练结束时: 训练{tr[-1]:.5f} vs 验证{ev[min(best+40, len(ev)-1)]:.5f}") # 运行输出: # 验证logloss最低点: 第173轮 0.41222 # 训练结束时: 训练0.35102 vs 验证0.41861
训练损失 0.351 与验证损失 0.419 的缺口就是过拟合程度。缺口随轮数单调拉大,早停相当于在缺口代价与拟合收益之间取均衡点。曲线是诊断书,不是装饰品:若两条曲线贴得很近且都高,是欠拟合,该开容量(第 3 章三步走第一步);若很早分叉,该收闸门。

from xgboost import XGBClassifier from sklearn.metrics import roc_auc_score clf = XGBClassifier(n_estimators=600, max_depth=4, learning_rate=0.1, subsample=0.8, colsample_bytree=0.8, eval_metric='logloss', early_stopping_rounds=40) clf.fit(X_tr_enc, y_tr, eval_set=[(X_te_enc, y_te)], verbose=False) proba = clf.predict_proba(X_te_enc)[:, 1] label = clf.predict(X_te_enc) print(f"停在 {clf.best_iteration} 轮 AUC={roc_auc_score(y_te, proba):.4f}") print("predict 直接给标签:", label[:8]) # 运行输出: # 停在 171 轮 AUC=0.8523 # predict 直接给标签: [0 1 0 0 1 0 0 0]
与原生接口停在 173 轮相比差两轮,来自实现细节差异(随机数消费顺序),可以忽略。真正的差别在预测输出:原生 predict 在 binary:logistic 下输出概率,要标签需自己设阈值;sklearn 接口的 predict 直接给标签(内部按 0.5 切),predict_proba 给概率。下游代码若混用两者,"拿概率当标签"是高频事故——概率 0.47 与 0.49 都会被误当成负例处理,排序业务里损失巨大。
# 原生接口的预测输出是概率 p_raw = booster.predict(dtest, iteration_range=(0, booster.best_iteration + 1)) print("原生predict前5个输出:", np.round(p_raw[:5], 3)) # 运行输出: 原生predict前5个输出: [0.078 0.612 0.145 0.094 0.553]
阈值不必是 0.5。流失场景里漏一个流失客户的代价远高于误留一个,最优阈值应按业务代价矩阵在验证集上搜索——这件事属于下一节的评估话题。
💡 关键直觉:早停的意义不只是省时间。它是把"模型复杂度"从离散参数(树数)变成数据驱动的连续量,本质上是把第 2 章的正则思想从"每棵树内部"延伸到"森林的长度"。
训练完成的模型还不能直接说"好"。下一节做正式评估与选型:交叉验证、多指标、阈值搜索一条龙。