Boosting家族 – XGBoost分享
Boosting(提升方法) 是一种 集成学习(Ensemble Learning) 思想。
它的核心目标是:
把多个“弱分类器”(weak learners)叠加起来,形成一个“强分类器”(strong learner)。
Boosting 的基本思路
Boosting 的训练是逐步进行的(不同于随机森林的“并行训练”):
- 初始化模型
用一个简单的模型(比如决策树)先预测。 - 计算误差(残差)
看看模型预测得哪里错了。 - 训练下一个模型
让新模型“专注”学习前面模型做错的样本。 - 加权组合多个模型
把所有弱模型按照一定权重组合成一个最终强模型。 - 重复
不断叠加,直到误差不再明显减少或达到设定次数。
一、什么是 XGBoost
XGBoost(Extreme Gradient Boosting) 是一种基于 梯度提升树(Gradient Boosting Decision Tree, GBDT) 的高效机器学习算法,
由陈天奇等人在 2016 年提出。
它的名字意思是:“极端梯度提升算法”——即在传统 GBDT 的基础上,进行了极致的优化。
二、核心思想
XGBoost 的核心思想与 Boosting 一致:
通过多棵“弱分类器”(通常是浅层决策树)逐步叠加,让后面的树去纠正前面树的错误,最终形成一个强大的预测模型。
三、基本工作流程
初始化模型
设定初始预测值 F 0 ( x ) F_0(x) F0(x)。计算梯度和二阶梯度
对每个样本计算当前模型预测误差的导数和二阶导(即残差信息)。构建新的树(弱分类器)
拟合这些梯度残差,构建一棵新的 CART 决策树。计算叶子节点的权重
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 正则化系数
这个公式确保了在考虑正则化的情况下,找到使目标函数最小化的最优权重。
更新模型
F m ( x ) = F m − 1 ( x ) + η ⋅ h m ( x ) F _ { m } ( x ) = F _ { m - 1 } ( x ) + \eta \cdot h _ { m } ( x ) Fm(x)=Fm−1(x)+η⋅hm(x)
其中:
- F m ( x ) F _ { m } ( x ) Fm(x) 是第 m m m 轮迭代后的集成模型
- F m − 1 ( x ) F _ { m - 1 } ( x ) Fm−1(x) 是第 m − 1 m-1 m−1 轮迭代后的模型
- η \eta η 是学习率(收缩系数)
- h m ( x ) h _ { m } ( x ) hm(x) 是第 m m m 轮训练的基础学习器
这个公式体现了梯度提升的核心思想:通过逐步添加新的基础学习器来改进模型。
重复以上步骤
直到树的数量达到设定上限或损失函数不再明显下降。
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=i∑l(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_lambda | L2 正则化参数 |
reg_alpha | L1 正则化参数 |
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))
问题:
损失越来越小的原因取决于你选的新模型,对吧?你怎么知道应该选择哪个模型去让这个差值越来越收敛?
损失越来越小,是因为每一轮我们都在选择一个新的弱模型(比如一棵树),去最有效地拟合当前的残差(也就是上一次模型没学好的部分)。
为什么叶子节点值越大 树复杂度越高?
简单直接的回答是:叶子节点值越大,意味着模型需要做出的预测偏差(残差)越大,为了拟合这些大的偏差,树就需要生长得更复杂(更深、有更多分支)。
直观理解:一个比喻:
想象一下你要教一个孩子识别动物。
- •情况一(叶子节点值小):你给他看一张猫的图片,说:“这是猫。” 孩子可能只需要记住“有尖耳朵、胡须、喵喵叫”这几个简单特征就能认出来。这就像一棵简单的树,一两步就能做出正确判断。
- •情况二(叶子节点值大):你给他看一张鸭嘴兽的图片,说:“这是鸭嘴兽。” 孩子会非常困惑,因为它像鸭子又像海狸还会下蛋!为了让他理解,你必须解释一大堆复杂的、例外的规则:“它是一只哺乳动物,但是会下蛋,嘴巴像鸭子,尾巴像海狸,生活在水边…”。这就像一棵复杂的树,需要很多层的
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()

可见排行榜分数

浙公网安备 33010602011771号