从零开始学AI22——集成学习
集成学习核心思想是“三个臭皮匠,顶个诸葛亮”,通过组合多个弱模型提升整体性能。三大流派:Bagging(如随机森林)并行训练、投票降方差,适合抗噪稳定场景;Boosting(如XGBoost)串行纠错、逐次优化,精准度高;Stacking多层融合,专家会诊,追求极致精度。在结构化数据领域,集成学习(尤其XGBoost、随机森林)是工业界与竞赛首选,广泛应用于风控、推荐、广告点击率预测等任务。非结构化数据(图像、语音)则由深度学习主导。数学本质:Bagging降低方差,Boosting通过梯度下降减偏差。代码实证显示,GBDT通过不断拟合残差逼近最优解,效果显著。
简单理解
集成学习 (Ensemble Learning)的核心思想就一句话:“三个臭皮匠,顶个诸葛亮”。
在机器学习里,我们不仅可以训练一个模型(比如一棵决策树),我们还可以训练一大堆模型,然后让它们组团来做决定。
通常,这一大堆模型(臭皮匠)合起来的效果,会比单独任何一个模型(哪怕是诸葛亮)都要好。
集成学习的“三大门派”
根据这堆模型怎么组团,集成学习分成了三个主要流派:
1. Bagging (装袋法) —— “民主投票”
- 代表作:随机森林 (Random Forest)
- 口号:大家各学各的,最后投票。
- 怎么做:
- 找 100 个学生(模型)。
- 给每个学生发不一样的复习资料(随机采样训练数据)。
- 大家同时开始独立学习(并行训练),互不干扰。
- 考试时,大家一起做同一道题。
- 结果:少数服从多数。如果 80 人选 A,20 人选 B,那就选 A。
- 优点:极其稳定!就算有个别学生学傻了(过拟合),大部分人还是正常的,整体结果不会跑偏。
2. Boosting (提升法) —— “接力补课”
- 代表作:AdaBoost, GBDT, XGBoost, LightGBM
- 口号:专门盯着错题学。
- 怎么做:
- 先让第 1 个学生做题。
- 第 1 个学生做错的题目,会被挑出来,加倍重视。
- 第 2 个学生专门去学这部分做错的题。
- 第 2 个学生又做错的题,交给第 3 个学生去攻克……
- 结果:是一步步串行训练出来的。
- 优点:极其精准!后面的学生都是为了弥补前面学生的不足而存在的,所以准确率通常比 Bagging 更高(打比赛的神器)。
3. Stacking (堆叠法) —— “专家会诊”
- 代表作:各类比赛的冠军方案常用。
- 口号:用一个老板来管这帮打工的。
- 怎么做:
- 第一层:训练一堆不同的模型(SVM、决策树、神经网络...),让它们分别预测结果。
- 第二层:训练一个**“元模型” (Meta-Model)**(通常是逻辑回归)。
- 结果:第二层的模型把第一层所有模型的预测结果当作输入特征,学习如何组合它们才能得到最佳答案。
- 比如:老板发现 A 模型擅长做数学,B 模型擅长做语文,老板就会根据题目类型,动态分配权重。
一句话总结
- Bagging (随机森林):并行训练,少数服从多数,为了防过拟合(降方差)。
- Boosting (XGBoost):串行训练,死磕错题,为了提准确率(降偏差)。
- Stacking:多层结构,集百家之长,为了刷榜拿冠军。
应用场景
集成学习(尤其是Random Forest和XGBoost/LightGBM)是目前工业界和竞赛圈的绝对主力。
在深度学习(Deep Learning)统治图像和语音领域的同时,集成学习统治了几乎所有的表格数据(Tabular Data)领域。
只要你的数据是存放在 Excel 或 SQL 数据库里的结构化数据,首选一定是集成学习。
1. 随机森林 (Random Forest) 的应用场景
关键词:稳定、抗噪、无脑好用(不用调参效果也不错)。
- 金融风控(银行/互金):
- 场景:信用卡反欺诈、贷款违约预测。
- 优势:银行数据通常噪音很大(脏数据多),且对稳定性要求极高。随机森林不容易受异常值影响,哪怕有个别数据填错了,几十棵树投票也能把错误抵消掉。
- 医疗病理分析:
- 场景:基因测序分类、疾病预测。
- 优势:医疗数据通常特征维度极高(几万个基因),样本数却很少(几百个病人)。随机森林处理高维数据能力强,且能给出特征重要性 (Feature Importance),告诉医生哪个基因最关键。
- 电商推荐系统的粗排(Recall):
- 场景:从百万商品里快速筛选出几千个候选集。
- 优势:可以并行计算,速度快,适合处理大规模数据。
2. XGBoost / LightGBM 的应用场景
关键词:精准、快、打比赛神器、工业界核心。
(这是目前数据挖掘岗位的必备技能)
- 搜索与广告点击率预估 (CTR Prediction):
- 场景:决定给用户展示哪个广告,或者淘宝搜出来什么商品排在前面。
- 优势:这种场景对准确率要求极高(提升 0.1% 的点击率就是几亿的收入)。Boosting 算法通过死磕残差(Residual),能把预测误差压到极致。
- 销售预测 / 库存管理:
- 场景:预测下个月这件衣服能卖多少件?
- 优势:这类问题通常包含时间序列特征和各种复杂的非线性关系。GBDT 类算法能很好地拟合这些非线性规律。
- 数据竞赛 (Kaggle/天池):
- 场景:几乎所有非图像/视频类的比赛。
- 地位:如果你打比赛不用 XGBoost/LightGBM/CatBoost,你基本进不了前 10%。它们就是当今表格数据领域的“屠龙刀”。
3. Stacking 的应用场景
关键词:刷分、极致精度、复杂。
- 高精尖的预测任务:
- 场景:顶级对冲基金的股价预测、地震波预测、精密仪器故障预测。
- 优势:当单一模型(无论是 XGBoost 还是 神经网络)都无法再提升哪怕 0.001 的精度时,就把它们堆叠起来(Stacking)。虽然训练很慢、模型很重、部署很麻烦,但在赢者通吃的领域,这点提升值得付出代价。
集成学习什么时候不好用?
- 非结构化数据:
- 图像、视频、音频:这些领域是CNN/Transformer(深度学习)的天下。集成学习处理像素级别的特征效果很差。
- 自然语言处理 (NLP):
- 虽然以前有用,但现在基本也被BERT/GPT(Transformer)取代了。
- 对实时性要求极高且资源受限(如嵌入式设备):
- 虽然 LightGBM 已经很快了,但如果要在一块小芯片上跑几千棵树,内存和耗电量可能还是扛不住。这时候简单的逻辑回归或单棵决策树可能更合适。
总结
- 处理 Excel 表格数据?→→XGBoost / LightGBM(首选)。
- 数据太脏、怕过拟合?→→随机森林。
- 处理图片/声音?→→ 左转去找深度学习。
数学推导
集成学习的数学推导主要分为两派:
- Bagging (随机森林)的推导核心是统计学中的偏差-方差分解 (Bias-Variance Decomposition)。
- Boosting (GBDT/XGBoost)的推导核心是加法模型 (Additive Model)和前向分步算法 (Forward Stagewise Algorithm)。
我们重点讲这两个核心逻辑。
一、 Bagging 的数学原理:降低方差 (Variance Reduction)
核心问题:为什么把一堆“差不多”的模型平均一下,效果就会变好?
假设我们有 nn 个独立的随机变量(模型预测值) X1,X2,...,XnX1,X2,...,Xn。
假设它们都服从正态分布,方差都是 σ2σ2。
单个模型的方差:
Var(Xi)=σ2Var(Xi)=σ2
(这意味着单个模型很容易受数据波动影响,发挥不稳定)集成模型的方差(取平均):
令 Xˉ=1n∑XiXˉ=n1∑Xi。
根据方差的性质(独立变量和的方差等于方差的和):
Var(Xˉ)=Var(1n∑Xi)=1n2∑Var(Xi)=1n2⋅nσ2=σ2nVar(Xˉ)=Var(n1∑Xi)=n21∑Var(Xi)=n21⋅nσ2=nσ2
推导结论:
通过对 nn 个独立模型取平均,整体的方差变成了原来的1nn1!
这就是随机森林为什么特别稳的数学原因。它通过“民主投票”极大地降低了模型的**过拟合(高方差)**风险。
(注:实际中模型很难完全独立,所以方差降低幅度没有 1/n 这么完美,但依然效果显著)
二、 Boosting 的数学原理:拟合残差 (Residual Fitting)
核心问题:为什么一个个接力学,就能越来越准?
Boosting 的本质是一个加法模型:
Fm(x)=∑t=1mαtht(x)Fm(x)=∑t=1mαtht(x)
- Fm(x)Fm(x):最终的强学习器。
- ht(x)ht(x):第 tt 步训练出来的弱学习器(小树)。
- αtαt:第 tt 棵树的权重。
我们以GBDT (Gradient Boosting Decision Tree)为例,它的推导最直观。
推导过程:
第一步:训练第一棵树 h1(x)h1(x) 去拟合真实值 yy。
- 预测值:F1(x)=h1(x)F1(x)=h1(x)
- 残差 (Residual):r1=y−F1(x)r1=y−F1(x)
第二步:训练第二棵树 h2(x)h2(x)。
- 关键点:这棵树的目标不再是拟合 yy,而是拟合残差 r1r1。
- 它想让 h2(x)≈y−F1(x)h2(x)≈y−F1(x)。
- 更新强模型:F2(x)=F1(x)+h2(x)F2(x)=F1(x)+h2(x)。
第 m 步(数学泛化):
我们定义一个损失函数 L(y,F(x))L(y,F(x))(比如均方误差)。
我们要找到第 mm 棵树 hmhm,使得加入它之后,整体损失 LL 最小。利用泰勒展开(一阶展开):
L(y,Fm−1(x)+hm(x))≈L(y,Fm−1(x))+∂L∂F⋅hm(x)L(y,Fm−1(x)+hm(x))≈L(y,Fm−1(x))+∂F∂L⋅hm(x)为了让损失变小(小于上一轮的损失),我们需要让这一项变负:
∂L∂F⋅hm(x)<0∂F∂L⋅hm(x)<0最快让它变负的方法,就是让 hm(x)hm(x) 沿着负梯度方向走:
hm(x)≈−∂L(y,Fm−1(x))∂Fm−1(x)hm(x)≈−∂Fm−1(x)∂L(y,Fm−1(x))
推导结论:
GBDT 的每一棵新树,都在拟合上一轮模型的负梯度(对于均方误差来说,负梯度正好就是残差 y−y^y−y^)。
通过不断地把“负梯度”加进去,总误差就会一步步趋近于 0。
- XGBoost 的改进:它在泰勒展开时,保留了二阶导数(Hessian矩阵),不仅看梯度下降的方向,还看曲率,所以走得更准、更快。
三、 总结对比
| 维度 | Bagging (随机森林) | Boosting (GBDT/XGBoost) |
|---|---|---|
| 数学目标 | 降低方差 (Variance) | 降低偏差 (Bias) |
| 模型关系 | 并行独立 (iid) | 串行依赖 (Sequential) |
| 核心操作 | 平均 / 投票 | 梯度下降 (在函数空间) |
| 推导逻辑 | 统计学大数定律 | 优化理论 (泰勒展开) |
一句话总结数学本质:
- Bagging是通过平摊风险来求稳。
- Boosting是通过梯度下降来求准。
代码实例
我们将通过一个**波士顿房价预测(回归任务)**的案例,手写实现Boosting (GBDT)的核心逻辑,并与 Sklearn 的标准库进行对比。
我们将深入到“残差”层面,让你亲眼看到每一棵树是如何去拟合上一棵树剩下的“烂摊子”的。
Python 代码实现
请确保安装了sklearn,numpy,matplotlib。
import numpy as np import matplotlib.pyplot as plt from sklearn.tree import DecisionTreeRegressor from sklearn.ensemble import GradientBoostingRegressor, RandomForestRegressor from sklearn.metrics import mean_squared_error from sklearn.datasets import fetch_california_housing from sklearn.model_selection import train_test_split # ========================================== # 1. 准备数据 (加州房价数据集) # ========================================== # 为了方便演示和画图,我们只取前 500 个样本,且只用 1 个特征 (MedInc: 收入中位数) housing = fetch_california_housing() X = housing.data[:500, [0]] # 只取第0列特征 y = housing.target[:500] # 房价 # 排序 X 以便画图好看 (不影响训练) sort_idx = X.flatten().argsort() X = X[sort_idx] y = y[sort_idx] # 划分训练测试集 X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42) # ========================================== # 2. 手写 GBDT (Gradient Boosting) 核心逻辑 # ========================================== print(">>> 手写 GBDT 训练过程展示") # 超参数 n_estimators = 3 # 我们只训练 3 棵树,方便一步步看 learning_rate = 1.0 # 学习率设为 1.0 (为了让效果更明显) # 存储每一棵小树 trees = [] # 初始预测值 (通常设为 y 的平均值,这里简化为 0) f_m = np.zeros(len(y_train)) plt.figure(figsize=(15, 5)) # --- 开始串行训练 --- for i in range(n_estimators): # 1. 计算残差 (Residual) # 核心公式: r = y_true - y_current_pred # 对于均方误差(MSE),负梯度正好就是残差 residuals = y_train - f_m # 2. 训练一棵小树去拟合这个残差 (不是拟合 y !) # 这里的树深度通常很浅 (weak learner) tree = DecisionTreeRegressor(max_depth=2, random_state=42) tree.fit(X_train, residuals) trees.append(tree) # 3. 更新强学习器的预测值 # F_new(x) = F_old(x) + lr * tree.predict(x) r_pred = tree.predict(X_train) f_m += learning_rate * r_pred # --- 可视化这一步 --- plt.subplot(1, 3, i+1) # 画出残差数据点 plt.scatter(X_train, residuals, color='r', marker='+', label='Residuals (Errors)') # 画出这棵树拟合的曲线 # 为了画平滑曲线,生成一点测试数据 X_plot = np.linspace(X.min(), X.max(), 100).reshape(-1, 1) y_plot = tree.predict(X_plot) plt.plot(X_plot, y_plot, 'g-', linewidth=3, label=f'Tree {i+1} Fitting') plt.title(f"Step {i+1}: Fitting Residuals\nResidual Range: [{residuals.min():.1f}, {residuals.max():.1f}]") plt.xlabel("Feature (Income)") plt.legend() plt.grid(True, alpha=0.3) plt.tight_layout() plt.show() # --- 手写模型的最终预测 --- y_pred_manual = np.zeros(len(y_test)) for tree in trees: y_pred_manual += learning_rate * tree.predict(X_test) mse_manual = mean_squared_error(y_test, y_pred_manual) print(f"手写 GBDT (3棵树) MSE: {mse_manual:.4f}") # ========================================== # 3. 对比 Sklearn 官方实现 # ========================================== print("\n>>> Sklearn 官方模型对比") # GBDT gbdt = GradientBoostingRegressor(n_estimators=3, learning_rate=1.0, max_depth=2, random_state=42) gbdt.fit(X_train, y_train) y_pred_gbdt = gbdt.predict(X_test) mse_gbdt = mean_squared_error(y_test, y_pred_gbdt) print(f"Sklearn GBDT (3棵树) MSE: {mse_gbdt:.4f}") # 随机森林 (Bagging 对比) rf = RandomForestRegressor(n_estimators=3, max_depth=2, random_state=42) rf.fit(X_train, y_train) y_pred_rf = rf.predict(X_test) mse_rf = mean_squared_error(y_test, y_pred_rf) print(f"Sklearn RF (3棵树) MSE: {mse_rf:.4f}") # ========================================== # 4. 最终效果可视化 (整体拟合曲线) # ========================================== plt.figure(figsize=(10, 6)) plt.scatter(X_test, y_test, color='black', alpha=0.5, label='True Data') # 为了画线 X_grid = np.linspace(X.min(), X.max(), 500).reshape(-1, 1) # GBDT 预测线 y_grid_gbdt = gbdt.predict(X_grid) plt.plot(X_grid, y_grid_gbdt, 'r-', linewidth=2, label='GBDT Prediction') # RF 预测线 y_grid_rf = rf.predict(X_grid) plt.plot(X_grid, y_grid_rf, 'b--', linewidth=2, label='Random Forest Prediction') plt.title("GBDT vs Random Forest Fitting Curve") plt.xlabel("MedInc") plt.ylabel("House Price") plt.legend() plt.show()代码深度解析
1. 手写 GBDT 的三张子图
运行代码后,你会看到三张连着的图,这就是 Boosting 的灵魂:
- Step 1:第一棵树在拟合原始数据的误差。此时残差很大(比如在 -2 到 2 之间)。
- Step 2:第二棵树不看原始房价了,它只看 Step 1 剩下的残差。你会发现 Y 轴的范围变小了(误差变小了)。
- Step 3:第三棵树继续拟合 Step 2 剩下的残差。
- 结论:每一棵树都在做一个“修补匠”,把上一棵树没预测准的地方(残差)给补上。最后把这三棵树的预测值加起来,就是一个很准的预测。
2. GBDT vs RF 的对比
在最后的对比图中:
- Random Forest (蓝虚线):线条通常比较平滑(或阶梯状更明显),因为它是在取平均,倾向于“中庸”,方差小。
- GBDT (红实线):线条会试图去捕捉数据的每一个拐点(拟合得更紧密),因为它在不断修正误差,偏差小。
3. 为什么 Sklearn 和手写结果略有不同?
你会发现mse_manual和mse_gbdt非常接近但不是完全一样(比如 0.98 vs 0.96)。
这是因为 Sklearn 的GradientBoostingRegressor在内部做了一些初始化优化(比如初始预测值不是 0,而是训练集的均值或中位数),而且分裂节点的算法细节可能有微小差异。但核心的“拟合残差”逻辑是完全一致的。