# 勤奋的凯尔森同学10000h刻意练习Day12:XGBoost与LightGBM —— 同样是GBDT,凭什么它俩统治了Kaggle?从二阶泰勒到直方图加速

所属专栏: 凯尔森的10000h刻意练习
Slogan: 一个36岁程序员, 研究生(学硕),前世界500强top20软件开发Java工程师,用10000小时深耕AI, 目标Top10 AI资深工程师, 全程日更记录。

Kaggle结构化数据比赛里,排名前10的选手,几乎无一例外用XGBoost或LightGBM,为什么?

Day8讲了GBDT:串行建树,每棵拟合前面所有树的残差。原理不复杂,代码几十行就能写一个——那为什么工业界和竞赛圈几乎没人用"手搓GBDT",而是全都涌向XGBoost和LightGBM?答案不在算法原理,而在工程优化:XGBoost用二阶泰勒展开引入曲率信息、把正则化写进目标函数让树自动控制复杂度;LightGBM用直方图把分裂搜索从压到、用叶子优先策略把有限深度花在增益最大的地方。两者都在同一棵GBDT树上做文章,一个在精度方向深耕,一个在速度方向优化。光有强模型还不够,超参数调优是把潜力榨出来的最后一公里 —— 网格搜索、随机搜索、贝叶斯优化三件套,加上正确的调参优先级,才能事半功倍。

# 1.1 GBDT原理回顾

Day8讲过GBDT的核心:串行训练树,每棵树拟合前面所有树的残差(真实值减预测值)。

其中是第t棵树,拟合的是。

第 轮预测 = 前 轮预测 + 第 棵树提供的修正量。

更一般的形式用负梯度代替残差(当损失函数是MSE时负梯度就是残差):

第t棵树拟合,然后,是学习率。真实值为 ,预测值为 。

为什么用负梯度,而不直接用残差?

残差只是MSE损失下的特例:对求偏导,负梯度恰好等于。换成绝对值损失、Huber损失,或分类任务的交叉熵,残差不好定义了,但负梯度永远存在。用负梯度当统一接口,GBDT就能从回归推广到分类、排序等任何可导损失——这才是"梯度"提升的本意。

# 1.1.1 手推小例子:GBDT两轮拟合残差

下面用3个样本把"算残差 → 拟合残差 → 更新预测"这个循环手推两轮,看MSE如何一步步下降。

# 1. 设定数据与第0棵树

1 1 10
2 2 20
3 3 30

第0棵树通常是一棵只输出均值的"树":。

第1轮的残差(MSE的负梯度)就是:

残差
1 10 20 -10
2 20 20 0
3 30 20 10

初始损失:

# 2. 第1棵树:拟合残差

为了手算方便,假设每棵树足够深,每个样本各占一个叶子,能把残差完全拟合(树1对三个样本分别输出-10、0、10)。取学习率,更新预测:

树1输出 新残差
1 10 -10 -5
2 20 0 0
3 30 10 5

# 3. 第2棵树:继续拟合新残差

第2棵树拟合新残差[-5, 0, 5],同样以更新:

树2输出 新残差
1 10 15 -5 -2.5
2 20 20 0 0
3 30 25 5 2.5

# 4. 损失下降趋势

轮次 预测值 残差 MSE
第0棵(均值) [20, 20, 20] [-10, 0, 10] 66.67
+第1棵 [15, 20, 25] [-5, 0, 5] 16.67
+第2棵 [12.5, 20, 27.5] [-2.5, 0, 2.5] 4.17

规律很明显:每过一轮,残差都变成上一轮的倍,损失按固定比例衰减——树越多拟合越准,但学习率越小需要越多的树。这正是后面调参时learning_rate和n_estimators要绑在一起调的原因。

下面的XGBoost代码,就是把上面的手算过程——算梯度、建树、更新预测——工程化,并在梯度统计和特征扫描等步骤上做了并行优化。

# 1.2 XGBoost

XGBoost(eXtreme Gradient Boosting)在GBDT基础上加了正则化、二阶泰勒展开、并行化的特征扫描与梯度统计等优化。

# 1.2.1 正则化

XGBoost的目标函数加了对树复杂度的惩罚:

式中是样本数,是当前树的总棵数:第一项是所有样本在损失函数(回归常用MSE、分类常用交叉熵,分别在Day6、Day7见过)上的经验损失之和;第二项把棵树的复杂度全部加总惩罚。于是训练目标从单纯"拟合得准"变成"拟合得准且模型简单"。

总成本 = 预测错误的成本 + 模型复杂的成本

其中正则化项写清楚就是:

逐项拆解这两个惩罚项:

  • :叶子节点数,用于衡量树结构复杂度——叶子越多结构越复杂、越容易记住噪声,就是每多长一个叶子要付出的代价,越大叶子越少
  • :第j个叶子的权重(即该叶子输出的预测值),是对所有权重的L2正则,抑制某个叶子权重过大导致预测剧烈波动,越大权重越保守
  • 控制结构复杂度(树长多大),控制权重幅度(叶子输出多大),两者一外一内共同防过拟合

和经典GBDT的关键差别:经典GBDT的理论形式主要从经验损失角度描述boosting;XGBoost将树结构复杂度和叶子权重的正则项显式写入目标函数,并直接参与树结构优化——优化时自动权衡"拟合得准"和"树别太复杂"。

# 1.2.2 二阶泰勒展开

XGBoost对损失函数做二阶泰勒展开,在经典GBDT只用一阶梯度的基础上引入二阶信息:

其中是当前预测值一阶导(梯度),是当前预测值二阶导(海森)。逐项拆解:

  • :常数项,是上一轮已经产生的损失,本轮优化时可忽略
  • :一阶项,沿梯度方向修正预测——原生GBDT只用到这一层
  • :二阶项,刻画损失面在该点的"弯曲程度",自动惩罚迈得过大的步子
  • :复杂度惩罚,压住这棵树的叶子数和叶子权重

二阶信息带来了什么?

一阶梯度只告诉你"往哪个方向走",二阶海森还告诉你"这个方向的地形有多弯"——相当于从梯度下降升级到牛顿法,步长估计更精准。二阶信息提供了局部曲率,通常能更准确地估计一次更新带来的损失变化,并推导出更直接的叶子权重优化公式;但这并不意味着在所有数据、所有实现中都必然比一阶方法收敛更快。更关键的是,补上二阶项后,对叶子权重求导能得到解析解:

其中、分别是叶子内所有样本的、之和。这个公式直接给出了每个叶子该放什么权重,不用迭代搜索。

# 1.2.3 分裂增益公式

有了,就能算每次分裂值不值。把一棵树分成左右两个叶子,分裂前是一个叶子,分裂后是两个,增益定义为分裂后目标函数的减少量:

逐项拆解:

  • 前两项:左右子叶子的"得分",二阶导在分母里起的作用类似自适应学习率——大的地方(损失面弯曲厉害)步子自动缩小,避免过冲
  • 第三项:不分裂时的得分,减去它意味着"分裂必须比不分裂更好"
  • :分裂本身要付出代价(多一个叶子),只有增益超过才值得分裂

这套公式把"树怎么长、叶子放什么值、分裂划不划算"全变成了和的加减乘除——对每个候选分裂点,XGBoost只需维护左右子节点的和就能高效计算分裂增益;同时利用、可以直接得到叶子的最优权重。XGBoost仍然依赖learning_rate、max_depth、gamma等超参数,但二阶信息让叶子权重有了理论解析,不再靠人工试错步长。

原理讲透,下面上手训练第一个XGBoost模型,数据用sklearn自带的乳腺癌二分类数据集。

# 1.2.4 sklearn接口实战

import numpy as np
from sklearn.datasets import load_breast_cancer
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score, f1_score, roc_auc_score
import xgboost as xgb

data = load_breast_cancer()
X_train, X_test, y_train, y_test = train_test_split(
    data.data, data.target, test_size=0.2, random_state=42, stratify=data.target
)

model = xgb.XGBClassifier(
    n_estimators=100,          # 树的数量,一共建 100 棵树
    max_depth=4,               # 树的最大深度
    learning_rate=0.1,          # 学习率
    subsample=0.8,             # 行采样比例,每棵树只用随机 80% 的样本训练 → 行采样,增加树之间的差异性
    colsample_bytree=0.8,      # 列采样比例,每棵树只用随机 80% 的特征 → 列采样,同样是降方差、防过拟合的手段
    reg_alpha=0.1,             # L1正则,倾向于把部分叶子的权重压到 0,产生稀疏效果
    reg_lambda=1.0,            # L2正则,抑制叶子权重过大,XGBoost 里最常用的一道"刹车"
    random_state=42,					 # 训练时的评估指标用对数损失
    eval_metric='logloss'
)
model.fit(X_train, y_train)
y_pred = model.predict(X_test)
# predict_proba返回形状为(n_samples, 2)的概率矩阵,两列分别是"属于0的概率"和"属于1的概率"。取[:, 1] 就是每条样本被判为类别(良性)的概率——这是算AUC必需的连续值输入
y_proba = model.predict_proba(X_test)[:, 1]

print(f"准确率: {accuracy_score(y_test, y_pred):.4f}")
print(f"F1:       {f1_score(y_test, y_pred):.4f}")
print(f"AUC:      {roc_auc_score(y_test, y_proba):.4f}")
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
准确率: 0.9474
F1:       0.9589
AUC:      0.9934
1
2
3

# 1.2.5 原生接口和DMatrix

XGBoost原生接口用DMatrix(优化过的数据结构),训练更快:

dtrain = xgb.DMatrix(X_train, label=y_train)
dtest = xgb.DMatrix(X_test, label=y_test)

params = {
    'objective': 'binary:logistic',
    'max_depth': 4,
    'learning_rate': 0.1,
    'subsample': 0.8,
    'colsample_bytree': 0.8,
    'eval_metric': 'logloss'
}

evals = [(dtrain, 'train'), (dtest, 'test')]
model = xgb.train(params, dtrain, num_boost_round=1000, evals=evals,
                  early_stopping_rounds=20, verbose_eval=False)

print("最佳迭代轮数:", model.best_iteration)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
最佳迭代轮数: 96
1

early_stopping_rounds=20表示如果连续20轮验证集指标没提升就停止,防止过拟合。设置了num_boost_round=1000但实际只跑到第96轮就早停了——这就是早停的性价比:不用人工猜树的数量,模型自己决定何时停。

sklearn接口 vs 原生DMatrix接口怎么选?

维度 sklearn风格(XGBClassifier) 原生DMatrix(xgb.train)
上手成本 低,和sklearn估计器一致,能进Pipeline 高,要手动构造DMatrix、写params字典
训练速度 略慢(多一层封装) 快,DMatrix为内部优化而生,省内存
功能完整度 常用参数齐全,部分高级功能不支持 全,官方新特性首发于此
与sklearn生态 完美兼容GridSearchCV/Pipeline/cross_val_score 不兼容,需要自己包一层

经验法则:需要进sklearn流水线、做交叉验证网格搜索就用XGBClassifier;追求极限速度、要用最新/高级特性就回原生DMatrix。Kaggle上排名靠前的方案几乎都走原生接口。

# 1.3 LightGBM

LightGBM是微软出品的GBDT实现,核心优化是直方图算法和叶子优先生长。

# 1.3.1 直方图算法

XGBoost的exact树方法建树时,每个节点要遍历所有样本的所有特征值来找最佳分裂点(预排序算法),复杂度。XGBoost从2.0起也默认提供了hist直方图方法,但LightGBM从设计之初就把直方图作为核心算法。LightGBM先把连续特征离散化成最多255个桶(直方图bin),分裂时只在桶边界找最佳分裂点:

连续值: [1.2, 3.5, 7.8, 2.1, 9.0, ...]
           ↓ 分桶
桶编号:  [ 0,   1,   3,   0,   4,  ...]   ← 最多255个桶

每个桶记录: 该桶内样本的梯度总和G、海森总和H、样本数
分裂时: 只在255个桶边界试,不用遍历每个样本
1
2
3
4
5
6

直方图算法的收益:

  • 速度:分裂搜索从压到,bins最多255,远小于样本数
  • 内存:用uint8存桶编号代替float64存原始值,内存降为约
  • 天然抗过拟合:分桶本身就是一种离散化,相邻的特征值被归到同一桶,等价于做了轻微的平滑

直方图做差加速:一个节点的直方图 = 父节点直方图 - 兄弟节点直方图。只需算一边的直方图,另一边做减法即可,直方图计算量减半。

# 1.3.2 叶子优先生长(Leaf-wise)

生长策略 方式 效果
Level-wise(XGBoost) 按层生长,同一层所有叶子同时分裂 树更对称,但可能浪费在低增益分裂上
Leaf-wise(LightGBM) 每次选增益最大的叶子分裂 同样的叶子数下增益更高,但可能过深(用max_depth限制)
Level-wise(同层全分裂)          Leaf-wise(选增益最大的叶子分裂)
     [根]                            [根]
    /     \                         /     \
  [L]     [R]                     [L]     [R]  ← 假设L增益更大
  / \     / \                     / \
[L1][L2][R1][R2]               [L1][L2]         ← 只分裂L,不分裂R
(4个都裂,不管增益)               [L1a][L1b]      ← 继续裂L1
                                  (叶子数相同,但增益集中)
1
2
3
4
5
6
7
8

# 1.3.3 LightGBM实战

import lightgbm as lgb
from sklearn.metrics import accuracy_score

model = lgb.LGBMClassifier(
    n_estimators=100,
    max_depth=4,
    learning_rate=0.1,
    num_leaves=31,           # 叶子数(Leaf-wise的关键参数)
    subsample=0.8,
    colsample_bytree=0.8,
    reg_alpha=0.1,
    reg_lambda=1.0,
    random_state=42,
    verbose=-1               # 关闭训练日志
)
model.fit(X_train, y_train)
y_pred = model.predict(X_test)
print(f"准确率: {accuracy_score(y_test, y_pred):.4f}")
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
准确率: 0.9561
1

# 1.3.4 原生接口

dtrain = lgb.Dataset(X_train, label=y_train)
dval = lgb.Dataset(X_test, label=y_test, reference=dtrain)

params = {
    'objective': 'binary',
    'metric': 'binary_logloss',
    'learning_rate': 0.1,
    'num_leaves': 31,
    'max_depth': 4,
    'verbose': -1
}

model = lgb.train(params, dtrain, num_boost_round=1000,
                  valid_sets=[dval], callbacks=[lgb.early_stopping(20, verbose=False)])
print("LightGBM最佳迭代:", model.best_iteration)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
LightGBM最佳迭代: 43
1

LightGBM只用了43轮就早停了(XGBoost用了96轮),说明在乳腺癌这个数据集上LightGBM收敛更快。两种接口的取舍和XGBoost一致:进sklearn流水线用LGBMClassifier,追求速度和最新特性用原生lgb.train。

# 1.4 XGBoost vs LightGBM vs CatBoost

维度 XGBoost LightGBM CatBoost
分裂算法 直方图(默认)/预排序 直方图 对称树
生长策略 Level-wise Leaf-wise 对称
类别特征 需手动编码 需手动编码 原生支持
训练速度 中 最快 中
内存占用 中 小 中
精度 高 高(对噪声略敏感) 高(默认就很好)
大数据量表现 中等数据最佳 大数据首选 中等数据最佳
调参难度 中,参数多 中,叶子数易爆 低,默认参数即很强
缺失值处理 自动学习分裂方向 自动 自动

CatBoost对类别特征处理最好,不需要手动做One-Hot,适合类别特征多的数据集。选择时考虑数据规模、特征类型、精度要求和训练时间:中等规模数据优先XGBoost/CatBoost,百万级以上样本优先LightGBM,类别特征多且不想费心编码直接上CatBoost。

# 1.5 超参数调优

模型参数分两类:

  • 结构参数:决定模型复杂度,如max_depth、num_leaves
  • 训练参数:决定训练过程,如learning_rate、n_estimators、subsample

# 1.5.1 网格搜索(GridSearchCV)

穷举所有参数组合,找到最优。参数多时计算量爆炸——3个参数各3个取值就是种组合,5折交叉验证就是135次训练。

from sklearn.model_selection import GridSearchCV

param_grid = {
    'max_depth': [3, 5, 7],
    'learning_rate': [0.01, 0.1, 0.3],
    'n_estimators': [100, 200]
}

grid = GridSearchCV(
    xgb.XGBClassifier(random_state=42, eval_metric='logloss'),
    param_grid, cv=5, scoring='accuracy', n_jobs=-1
)
grid.fit(X_train, y_train)
print("最佳参数:", grid.best_params_)
print("最佳分数:", f"{grid.best_score_:.4f}")
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
最佳参数: {'learning_rate': 0.1, 'max_depth': 3, 'n_estimators': 200}
最佳分数: 0.9758
1
2

# 1.5.2 随机搜索(RandomizedSearchCV)

随机采样参数组合,比网格搜索高效,尤其是参数空间大时——50次随机搜索往往比27次网格搜索找到的参数更好,因为它不浪费在"死角"区域。

from sklearn.model_selection import RandomizedSearchCV
from scipy.stats import randint, uniform

param_dist = {
    'max_depth': randint(3, 10),
    'learning_rate': uniform(0.01, 0.3),
    'n_estimators': randint(100, 500),
    'subsample': uniform(0.6, 0.4),
    'colsample_bytree': uniform(0.6, 0.4)
}

rand_search = RandomizedSearchCV(
    xgb.XGBClassifier(random_state=42, eval_metric='logloss'),
    param_distributions=param_dist, n_iter=50,
    cv=5, scoring='accuracy', n_jobs=-1, random_state=42
)
rand_search.fit(X_train, y_train)
print("最佳参数:", {k: (v if isinstance(v, int) else round(v, 4)) for k, v in rand_search.best_params_.items()})
print("最佳分数:", f"{rand_search.best_score_:.4f}")
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
最佳参数: {'colsample_bytree': 0.9531, 'learning_rate': 0.0666, 'max_depth': 3, 'n_estimators': 198, 'subsample': 0.8801}
最佳分数: 0.9802
1
2

随机搜索(0.9802)比网格搜索(0.9758)还高了一点,试的组合少却搜到了更好的参数——因为网格搜索只覆盖了离散的几个点,随机搜索在连续空间里找到了网格"缝隙"之间的好参数。

# 1.5.3 贝叶斯优化

网格和随机搜索都不利用历史结果——每次采样和之前的结果无关。贝叶斯优化用高斯过程或TPE(树结构Parzen估计器)建模"参数 → 性能"的后验分布,每次基于已有结果选最有潜力的参数组合尝试。评估昂贵时尤其值钱:50次贝叶斯搜索往往比100次随机搜索更接近全局最优。

# pip install optuna
import optuna
from sklearn.model_selection import cross_val_score

def objective(trial):
    params = {
        'max_depth': trial.suggest_int('max_depth', 3, 10),
        'learning_rate': trial.suggest_float('learning_rate', 0.01, 0.3, log=True),
        'n_estimators': trial.suggest_int('n_estimators', 100, 500),
        'subsample': trial.suggest_float('subsample', 0.6, 1.0),
        'colsample_bytree': trial.suggest_float('colsample_bytree', 0.6, 1.0),
        'reg_alpha': trial.suggest_float('reg_alpha', 1e-8, 10.0, log=True),
        'reg_lambda': trial.suggest_float('reg_lambda', 1e-8, 10.0, log=True)
    }
    model = xgb.XGBClassifier(**params, random_state=42, eval_metric='logloss')
    scores = cross_val_score(model, X_train, y_train, cv=5, scoring='accuracy')
    return scores.mean()

study = optuna.create_study(direction='maximize', sampler=optuna.samplers.TPESampler(seed=42))
study.optimize(objective, n_trials=50)
print("最佳参数:", {k: (v if isinstance(v, int) else (round(v, 6) if v >= 1e-4 else f"{v:.2e}")) for k, v in study.best_params.items()})
print("最佳分数:", f"{study.best_value:.4f}")
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
最佳参数: {'max_depth': 7, 'learning_rate': 0.03582, 'n_estimators': 454, 'subsample': 0.801126, 'colsample_bytree': 0.874477, 'reg_alpha': '1.09e-08', 'reg_lambda': '1.76e-07'}
最佳分数: 0.9802
1
2

有意思的是,在乳腺癌这个较小(455个训练样本)的数据集上,三种调参方法的最佳分数分别是0.9758、0.9802、0.9802,差距很小。这符合一个规律:数据量越小,调参的边际收益越低——因为交叉验证本身在小数据上方差大,参数的微小改善被随机波动淹没了。数据量大时贝叶斯优化的优势才真正体现出来。

# 1.5.4 调参优先级

按这个顺序调,收益递减:

  1. learning_rate + n_estimators:先固定learning_rate=0.1,调n_estimators到合适值(用早停);然后减小learning_rate,增大n_estimators
  2. max_depth / num_leaves:控制树复杂度,XGBoost的max_depth一般3~10;LightGBM的num_leaves一般31~255,且应
  3. min_child_weight / min_data_in_leaf:叶子最小样本数,防过拟合
  4. subsample + colsample_bytree:采样比例,0.6~0.9,正则化
  5. reg_alpha + reg_lambda:正则化强度

调参顺序记住了,还需要工具回答"调到哪算够":学习曲线和验证曲线就是判断欠拟合、过拟合与参数敏感度的仪表盘。

# 1.6 学习曲线与验证曲线

import matplotlib.pyplot as plt
# validation_curve函数的核心作用:指定一个超参数和它的候选值,自动做交叉验证,返回每组参数下训练集和验证集的评分,用来直观展示超参数对模型泛化能力的影响
from sklearn.model_selection import validation_curve

# 候选树深。默认 max_depth=6,这里从 3 起步、步长 2 扫到 11,覆盖了"浅树→深树"的完整区间
param_range = [3, 5, 7, 9, 11]
train_scores, val_scores = validation_curve(
    xgb.XGBClassifier(random_state=42, eval_metric='logloss'),
    X_train, y_train, param_name='max_depth', param_range=param_range,
    cv=5, scoring='accuracy'
)

plt.plot(param_range, train_scores.mean(axis=1), label='训练集')
plt.plot(param_range, val_scores.mean(axis=1), label='验证集')
plt.xlabel('max_depth'); plt.ylabel('准确率')
plt.legend(); plt.show()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
max_depth  训练集准确率  验证集准确率
    3        1.0000      0.9692
    5        1.0000      0.9758
    7        1.0000      0.9714
    9        1.0000      0.9714
   11        1.0000      0.9714
1
2
3
4
5
6

训练集准确率从depth=3开始就是1.0(全对),但验证集在depth=5达到峰值后开始回落——这就是过拟合信号:训练集早就背全了,继续加深只是让验证集不再提升甚至回落。最佳max_depth应在5附近。

# 1.7 完整调参实战

把"基线 → 调参 → 评估 → 解释"整条链路走一遍,数据用加州房价(做二分类:房价是否高于中位数)。

import numpy as np
from sklearn.datasets import fetch_california_housing
from sklearn.model_selection import train_test_split, cross_val_score
from sklearn.metrics import accuracy_score, f1_score, roc_auc_score
import xgboost as xgb
import optuna

housing = fetch_california_housing()
X = housing.data
y = (housing.target > np.median(housing.target)).astype(int)
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.2, random_state=42, stratify=y
)

# 1. 基线模型(全默认参数)
baseline = xgb.XGBClassifier(random_state=42, eval_metric='logloss')
baseline.fit(X_train, y_train)
print(f"基线准确率: {baseline.score(X_test, y_test):.4f}")

# 2. Optuna调参(30次试验)
def objective(trial):
    params = {
        'max_depth': trial.suggest_int('max_depth', 3, 10),
        'learning_rate': trial.suggest_float('learning_rate', 0.01, 0.3, log=True),
        'n_estimators': trial.suggest_int('n_estimators', 100, 500),
        'subsample': trial.suggest_float('subsample', 0.6, 1.0),
        'colsample_bytree': trial.suggest_float('colsample_bytree', 0.6, 1.0),
        'reg_lambda': trial.suggest_float('reg_lambda', 1e-3, 10.0, log=True)
    }
    model = xgb.XGBClassifier(**params, random_state=42, eval_metric='logloss')
    return cross_val_score(model, X_train, y_train, cv=5, scoring='accuracy').mean()

study = optuna.create_study(direction='maximize', sampler=optuna.samplers.TPESampler(seed=42))
study.optimize(objective, n_trials=30)

# 3. 用最佳参数训练
best_model = xgb.XGBClassifier(**study.best_params, random_state=42, eval_metric='logloss')
best_model.fit(X_train, y_train)
y_pred = best_model.predict(X_test)
y_proba = best_model.predict_proba(X_test)[:, 1]

print(f"调参后准确率: {accuracy_score(y_test, y_pred):.4f}")
print(f"调参后F1:      {f1_score(y_test, y_pred):.4f}")
print(f"调参后AUC:     {roc_auc_score(y_test, y_proba):.4f}")

# 4. 特征重要性
print("\n特征重要性:")
for name, imp in sorted(zip(housing.feature_names, best_model.feature_importances_), key=lambda x: -x[1]):
    print(f"{name:15s}: {imp:.4f}")
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
基线准确率: 0.9111
调参后准确率: 0.9150
调参后F1:      0.9149
调参后AUC:     0.9720

特征重要性:
MedInc         : 0.3628
Longitude      : 0.1572
Latitude       : 0.1530
AveOccup       : 0.1075
HouseAge       : 0.0682
AveRooms       : 0.0661
Population     : 0.0444
AveBedrms      : 0.0409
1
2
3
4
5
6
7
8
9
10
11
12
13
14

基线到调参后只涨了0.39个百分点(0.9111 → 0.9150)——XGBoost的默认参数本来就很强,在中等规模数据上调参的边际收益有限。但特征重要性给出了有价值的信息:家庭收入中位数(MedInc)占了36.3%的重要性,经纬度(Longitude + Latitude)合计31%,这三项就解释了模型的近七成决策——和加州房价的地理分布规律完全吻合。

# 1.8 小结

# 选型速查

场景 选择
中等数据(<10万),要精度 XGBoost(默认参数就很强)
大数据(10万~千万级),要速度 LightGBM(直方图+Leaf-wise)
类别特征多,不想手动编码 CatBoost(原生支持)
需要进sklearn Pipeline做交叉验证 XGBClassifier / LGBMClassifier
Kaggle冲榜,追求极限 原生DMatrix/lgb.train + Optuna
防过拟合的第一道防线 early_stopping(性价比最高)

# 今天的认知升级:三棵树的进化逻辑

XGBoost和LightGBM不是两个独立发明,而是对同一棵GBDT树做不同方向的工程优化。把它们的差异串起来看,会发现一条清晰的进化逻辑:

  • GBDT的问题:经典理论形式只用一阶梯度,步长靠学习率猜;正则化依赖具体实现,理论目标函数不显式包含
  • XGBoost的解法:补二阶导→引入曲率,步长估计更准;写正则项→树复杂度自动控制;叶子权重有解析解→权重确定有理论依据
  • LightGBM的解法:直方图→分裂搜索从压到;Leaf-wise→把有限深度花在增益最大的叶子;直方图做差→计算量再减半

两者不是替代关系,而是互补:XGBoost在"每棵树的质量"上深耕,LightGBM在"建树的速度"上优化。在结构化数据上,它们至今仍是最强的基线模型——深度学习在图像、文本上碾压一切,但表格数据上梯度提升树长期占据优势。这不是因为树有多先进,而是因为表格数据的特征语义(数值大小、类别无序、缺失普遍)天然适合树的切分逻辑,而神经网络的连续乘加反而不好处理。

调参方法的演进也遵循同一个逻辑:网格搜索穷举但不聪明,随机搜索随机但不浪费,贝叶斯优化用历史信息指导下一步——从"盲目试"到"越试越准"。这和GBDT本身"每棵树修正上一棵的错误"的思想一脉相承:用好已知信息,让下一步比上一步更优。

最近更新: 2026/9/30
 |  Copyright © -Present Kelsen All Rights Reserved. | Author KelsenJing