Boosting家族 – XGBoost分享

Boosting(提升方法) 是一种 集成学习(Ensemble Learning) 思想。
它的核心目标是:

把多个“弱分类器”(weak learners)叠加起来,形成一个“强分类器”(strong learner)。

Boosting 的基本思路

Boosting 的训练是逐步进行的(不同于随机森林的“并行训练”):

  1. 初始化模型
    用一个简单的模型(比如决策树)先预测。
  2. 计算误差(残差)
    看看模型预测得哪里错了。
  3. 训练下一个模型
    让新模型“专注”学习前面模型做错的样本。
  4. 加权组合多个模型
    把所有弱模型按照一定权重组合成一个最终强模型。
  5. 重复
    不断叠加,直到误差不再明显减少或达到设定次数。

一、什么是 XGBoost

XGBoost(Extreme Gradient Boosting) 是一种基于 梯度提升树(Gradient Boosting Decision Tree, GBDT) 的高效机器学习算法,
由陈天奇等人在 2016 年提出。

它的名字意思是:“极端梯度提升算法”——即在传统 GBDT 的基础上,进行了极致的优化。

二、核心思想

XGBoost 的核心思想与 Boosting 一致:

通过多棵“弱分类器”(通常是浅层决策树)逐步叠加,让后面的树去纠正前面树的错误,最终形成一个强大的预测模型。

三、基本工作流程

  1. 初始化模型
    设定初始预测值 F 0 ( x ) F_0(x) F0(x)

  2. 计算梯度和二阶梯度
    对每个样本计算当前模型预测误差的导数和二阶导(即残差信息)。

  3. 构建新的树(弱分类器)
    拟合这些梯度残差,构建一棵新的 CART 决策树。

  4. 计算叶子节点的权重

    w j = − ∑ g i ∑ h i + λ w_j = -\frac{\sum g_i}{\sum h_i + \lambda} wj=hi+λgi

    其中:

    • g i g_i gi 是样本 i i i 的一阶梯度
    • h i h_i hi 是样本 i i i 的二阶梯度
    • λ \lambda λ 是 L2 正则化系数

    这个公式确保了在考虑正则化的情况下,找到使目标函数最小化的最优权重。

  5. 更新模型

    F m ( x ) = F m − 1 ( x ) + η ⋅ h m ( x ) F _ { m } ( x ) = F _ { m - 1 } ( x ) + \eta \cdot h _ { m } ( x ) Fm(x)=Fm1(x)+ηhm(x)

    其中:

    • F m ( x ) F _ { m } ( x ) Fm(x) 是第 m m m 轮迭代后的集成模型
    • F m − 1 ( x ) F _ { m - 1 } ( x ) Fm1(x) 是第 m − 1 m-1 m1 轮迭代后的模型
    • η \eta η 是学习率(收缩系数)
    • h m ( x ) h _ { m } ( x ) hm(x) 是第 m m m 轮训练的基础学习器

    这个公式体现了梯度提升的核心思想:通过逐步添加新的基础学习器来改进模型。

  6. 重复以上步骤

    直到树的数量达到设定上限或损失函数不再明显下降。

PS: 数学上更详细的推导请参考视频 XGBoost损失推导

四、XGBoost 的目标函数

XGBoost 的训练目标是最小化以下带正则化的目标函数:

Obj = ∑ i l ( y i , y ^ i ) + ∑ k Ω ( f k ) \text{Obj} = \sum_{i} l(y_i, \hat{y}_i) + \sum_{k} \Omega(f_k) Obj=il(yi,y^i)+kΩ(fk)

其中:

  • l ( y i , y ^ i ) l(y_i, \hat{y}_i) l(yi,y^i):样本的损失函数(如平方误差、逻辑损失等);
  • Ω ( f k ) = γ T + 1 2 λ ∑ j w j 2 \Omega(f_k) = \gamma T + \frac{1}{2} \lambda \sum_j w_j^2 Ω(fk)=γT+21λjwj2:模型复杂度惩罚项。
    • T T T:树的叶子数;
    • λ \lambda λ:L2 正则化参数;
    • γ \gamma γ:每次分裂的惩罚项。

五、XGBoost 的主要优化点

优化方向说明
二阶梯度优化不仅使用一阶导,还使用二阶导信息,使得更新方向更精确
正则化项控制模型复杂度,防止过拟合
并行分裂搜索特征级并行计算,提高训练速度
稀疏感知算法自动处理缺失值和稀疏特征
缓存优化使用分块结构优化内存访问
交叉验证内置支持自动调参与早停策略(early stopping)

六、XGBoost 常用参数

参数名含义
n_estimators树的数量
max_depth树的最大深度
learning_rate学习率(控制每次提升幅度)
subsample训练样本的采样比例
colsample_bytree每棵树使用的特征比例
reg_lambdaL2 正则化参数
reg_alphaL1 正则化参数
gamma最小分裂损失的惩罚项

七、XGBoost 的优点总结

优点说明
训练速度快支持多线程并行、缓存优化
泛化能力强正则化项防止过拟合
预测精度高使用二阶梯度信息
内存利用高效支持稀疏特征与缺失值
特征重要性分析可解释性强
兼容多任务分类、回归、排序都支持

八、简单 Python 示例

from xgboost import XGBClassifier
from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score
# 1. 加载数据
X, y = load_iris(return_X_y=True)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2)
# 2. 创建模型
model = XGBClassifier(n_estimators=100, learning_rate=0.1, max_depth=3)
# 3. 训练
model.fit(X_train, y_train)
# 4. 预测与评估
y_pred = model.predict(X_test)
print("Accuracy:", accuracy_score(y_test, y_pred))

问题:

  1. 损失越来越小的原因取决于你选的新模型,对吧?你怎么知道应该选择哪个模型去让这个差值越来越收敛?

    损失越来越小,是因为每一轮我们都在选择一个新的弱模型(比如一棵树),去最有效地拟合当前的残差(也就是上一次模型没学好的部分)。

  2. 为什么叶子节点值越大 树复杂度越高?

    简单直接的回答是:叶子节点值越大,意味着模型需要做出的预测偏差(残差)越大,为了拟合这些大的偏差,树就需要生长得更复杂(更深、有更多分支)。

直观理解:一个比喻:

想象一下你要教一个孩子识别动物。

  • 情况一(叶子节点值小):你给他看一张猫的图片,说:“这是猫。” 孩子可能只需要记住“有尖耳朵、胡须、喵喵叫”这几个简单特征就能认出来。这就像一棵简单的树,一两步就能做出正确判断。
  • 情况二(叶子节点值大):你给他看一张鸭嘴兽的图片,说:“这是鸭嘴兽。” 孩子会非常困惑,因为它像鸭子又像海狸还会下蛋!为了让他理解,你必须解释一大堆复杂的、例外的规则:“它是一只哺乳动物,但是会下蛋,嘴巴像鸭子,尾巴像海狸,生活在水边…”。这就像一棵复杂的树,需要很多层的if-else判断(规则)才能把这个“特殊案例”区分出来。

在模型中,一个“难以拟合”的样本(比如鸭嘴兽)对应的残差(真实值减去当前模型的预测值)就会很大。树模型通过创建新的分支(增加规则)来专门处理这些特殊样本,从而导致树的复杂度升高。这个新分支的叶子节点值,就是为了纠正这个大偏差而设置的,所以这个值本身也会比较大。

[OTTO分类问题](Otto Group Product Classification Challenge | Kaggle)

XGboost

import xgboost as xgb
import pandas as pd
import numpy as np
train_pth = './dataset/otto-group/train.csv'
test_pth = './dataset/otto-group/test.csv'
train_data = pd.read_csv(train_pth)
test_data = pd.read_csv(test_pth)
print("训练数据形状:", train_data.shape)
print("测试数据形状:", test_data.shape)
print("训练数据列名:", train_data.columns.tolist())
# 初始化训练数据集
X_train = train_data.iloc[:, 1:-1].values  # 特征列
y_train_original = train_data.iloc[:, -1].values  # 目标列
print("目标变量示例:", y_train_original[:10])
print("目标变量唯一值:", np.unique(y_train_original))
# 将类别标签转换为数字
unique_labels = sorted(list(set(y_train_original)))
label_to_idx = {label: idx for idx, label in enumerate(unique_labels)}
y_train = np.array([label_to_idx[label] for label in y_train_original])
print("转换后的目标变量:", y_train[:10])
print("类别数量:", len(unique_labels))
# 初始化测试数据集
X_test = test_data.iloc[:, 1:].values  # 跳过第一列(ID)
print("训练特征形状:", X_train.shape)
print("测试特征形状:", X_test.shape)
# 转换为 DMatrix 格式
dtrain = xgb.DMatrix(X_train, label=y_train)
dtest = xgb.DMatrix(X_test)
# 定义模型参数 - 改为输出概率
params = {
'booster': 'gbtree',          # 使用基于树的模型
'objective': 'multi:softprob', # 改为 softprob 输出概率
'num_class': len(unique_labels),  # 类别数量
'eta': 0.1,                   # 学习率
'max_depth': 6,               # 树的最大深度
'subsample': 0.8,             # 样本采样比例
'colsample_bytree': 0.8,      # 特征采样比例
'gamma': 0.1,                 # 节点分裂的最小损失
'lambda': 1.0,                # L2正则化系数
'seed': 42                    # 随机种子
}
# 模型训练
epoch_num = 1300
print("开始训练 XGBoost 模型...")
model = xgb.train(params, dtrain, epoch_num)
print("训练完成!")
# 模型预测 - 返回概率
y_pred_proba = model.predict(dtest)
print("预测概率矩阵形状:", y_pred_proba.shape)
print("预测概率示例(前5个样本):")
print(y_pred_proba[:5])
# 生成类别名称(Class_1 到 Class_9)
class_columns = [f'Class_{i+1}' for i in range(len(unique_labels))]
# 创建提交文件
if 'id' in test_data.columns:
ids = test_data['id']
else:
# 如果测试数据没有id列,使用行号(从1开始)
ids = range(1, len(y_pred_proba) + 1)
# 创建包含概率的DataFrame
submission_proba = pd.DataFrame(y_pred_proba, columns=class_columns)
submission_proba.insert(0, 'id', ids)  # 在第一列插入id
# 保存预测结果
submission_proba.to_csv('./dataset/otto-group/otto_xgboost_predictions_proba.csv', index=False)
print("概率预测结果已保存到 ./dataset/otto-group/otto_xgboost_predictions_proba.csv")

epoch = 100

在这里插入图片描述

epoch = 1000

在这里插入图片描述

epoch = 2000

在这里插入图片描述

epoch = 5000

在这里插入图片描述

epoch = 10000

在这里插入图片描述

最佳:

epoch = 1000

在这里插入图片描述

Linear neural network

import torch
import pandas as pd
import numpy as np
from torch.utils.data import Dataset
from torch.utils.data import DataLoader
import torch.nn.functional as F
import torch.optim as optim
batch_size = 64
class OttoDataset(Dataset):
def __init__(self, filepath, train=True):
self.df = pd.read_csv(filepath)
self.train = train
if self.train:
self.features = self.df.iloc[:, 1:-1].values
self.labels = self.df.iloc[:, -1].values
#   标签编码(后面待学习)
self.unique_labels = sorted(list(set(self.labels)))
self.label_to_idx = {label: idx for idx, label in enumerate(self.unique_labels)}
self.labels = torch.tensor([self.label_to_idx[label] for label in self.labels], dtype=torch.long)
else:
self.features=self.df.iloc[:, 1:].values
self.labels = None
# print(self.labels)
self.features = torch.tensor(self.features, dtype=torch.float32)
def __getitem__(self, idx):
if self.train:
return self.features[idx], self.labels[idx]
else:
return self.features[idx]
def __len__(self):
return len(self.features)
class Model(torch.nn.Module):
def __init__(self):
super(Model, self).__init__()
self.linear1 = torch.nn.Linear(93, 128)
self.linear2 = torch.nn.Linear(128, 64)
self.linear3 = torch.nn.Linear(64, 32)
self.linear4 = torch.nn.Linear(32, 16)
self.linear5 = torch.nn.Linear(16, 9)
self.dropout = torch.nn.Dropout(0.2)
def forward(self, x):
x = F.relu(self.linear1(x))
x = F.relu(self.linear2(x))
x = F.relu(self.linear3(x))
x = F.relu(self.linear4(x))
return self.linear5(x)
train_dataset = OttoDataset('./dataset/otto-group/train.csv', train=True)
train_loader = DataLoader(train_dataset,
batch_size=batch_size,
shuffle=True,
num_workers=2)
test_dataset = OttoDataset('./dataset/otto-group/test.csv', train=False)
test_loader = DataLoader(test_dataset,
batch_size=batch_size,
shuffle=False,
num_workers=2)
model = Model()
criterion = torch.nn.CrossEntropyLoss()
optimizer = optim.SGD(model.parameters(), lr=0.001, momentum=0.5)
def train(epoch):
model.train()
running_loss = 0.0
for batch_idx, data in enumerate(train_loader, 0):
inputs, target = data
optimizer.zero_grad()
output = model(inputs)
loss = criterion(output, target)
loss.backward()
optimizer.step()
running_loss += loss.item()
if batch_idx % 300 == 299:
print('[%d, %5d] loss: %.3f' % (epoch + 1, batch_idx + 1, running_loss / 300))
running_loss = 0.0
def test():
model.eval()
all_preds = []
test_ids= []
with torch.no_grad():
for data in test_loader:
outputs = model(data)
# 转换为概率分布
probs = F.softmax(outputs, dim=1).cpu().numpy()
all_preds.append(probs)
all_preds = np.vstack(all_preds)
test_df = pd.read_csv('./dataset/otto-group/test.csv')
test_ids = test_df['id'].values
submission = pd.DataFrame(all_preds, columns=[
'Class_1', 'Class_2', 'Class_3', 'Class_4',
'Class_5', 'Class_6', 'Class_7', 'Class_8', 'Class_9'
])
submission.insert(0, 'id', test_ids)
submission.to_csv('submission.csv', index = False)
print("预测结果已保存到 submission.csv")
if __name__ == '__main__':
for epoch in range(100):
train(epoch)
test()

在这里插入图片描述

可见排行榜分数

在这里插入图片描述