Boosting家族 -- XGBoost分享
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 一致:
通过多棵“弱分类器”(通常是浅层决策树)逐步叠加,让后面的树去纠正前面树的错误,最终形成一个强大的预测模型。
三、基本工作流程
-
初始化模型
设定初始预测值 F0(x)F_0(x)F0(x)。 -
计算梯度和二阶梯度
对每个样本计算当前模型预测误差的导数和二阶导(即残差信息)。 -
构建新的树(弱分类器)
拟合这些梯度残差,构建一棵新的 CART 决策树。 -
计算叶子节点的权重
wj=−∑gi∑hi+λw_j = -\frac{\sum g_i}{\sum h_i + \lambda}wj=−∑hi+λ∑gi
其中:
- gig_igi 是样本 iii 的一阶梯度
- hih_ihi 是样本 iii 的二阶梯度
- λ\lambdaλ 是 L2 正则化系数
这个公式确保了在考虑正则化的情况下,找到使目标函数最小化的最优权重。
-
更新模型
Fm(x)=Fm−1(x)+η⋅hm(x)F _ { m } ( x ) = F _ { m - 1 } ( x ) + \eta \cdot h _ { m } ( x )Fm(x)=Fm−1(x)+η⋅hm(x)
其中:
- Fm(x)F _ { m } ( x )Fm(x) 是第 mmm 轮迭代后的集成模型
- Fm−1(x)F _ { m - 1 } ( x )Fm−1(x) 是第 m−1m-1m−1 轮迭代后的模型
- η\etaη 是学习率(收缩系数)
- hm(x)h _ { m } ( x )hm(x) 是第 mmm 轮训练的基础学习器
这个公式体现了梯度提升的核心思想:通过逐步添加新的基础学习器来改进模型。
-
重复以上步骤
直到树的数量达到设定上限或损失函数不再明显下降。
PS: 数学上更详细的推导请参考视频 XGBoost损失推导
四、XGBoost 的目标函数
XGBoost 的训练目标是最小化以下带正则化的目标函数:
Obj=∑il(yi,y^i)+∑kΩ(fk)\text{Obj} = \sum_{i} l(y_i, \hat{y}_i) + \sum_{k} \Omega(f_k)Obj=i∑l(yi,y^i)+k∑Ω(fk)
其中:
- l(yi,y^i)l(y_i, \hat{y}_i)l(yi,y^i):样本的损失函数(如平方误差、逻辑损失等);
- Ω(fk)=γT+12λ∑jwj2\Omega(f_k) = \gamma T + \frac{1}{2} \lambda \sum_j w_j^2Ω(fk)=γT+21λ∑jwj2:模型复杂度惩罚项。
- TTT:树的叶子数;
- λ\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 nptrain_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 optimbatch_size = 64
class OttoDataset(Dataset):def __init__(self, filepath, train=True):self.df = pd.read_csv(filepath)self.train = trainif self.train:self.features = self.df.iloc[:, 1:-1].valuesself.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:].valuesself.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.0for batch_idx, data in enumerate(train_loader, 0):inputs, target = dataoptimizer.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'].valuessubmission = 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()

可见排行榜分数

