集成学习思想:Bagging、Boosting、Stacking

一句话概述

集成学习(Ensemble Learning)通过组合多个弱学习器来构建一个强学习器,核心思想是「三个臭皮匠,顶个诸葛亮」。Bagging 通过并行训练降低方差,Boosting 通过串行训练降低偏差,Stacking 通过元学习器融合多个异构模型。这三种策略构成了现代机器学习中最强大的方法论体系,从随机森林到 XGBoost,几乎所有 Kaggle 竞赛冠军方案都离不开集成学习的影子。

💡 核心要点:① 集成学习的本质是组合多个模型以获取比单一模型更好的泛化性能 ② Bagging 并行训练、Boosting 串行训练、Stacking 分层训练三种范式各有适用场景 ③ 偏差-方差权衡是理解集成方法效果的底层逻辑 ④ 现代集成方法(如 XGBoost、LightGBM)已深度融入工业级 AI 流水线

教学与演示

一、集成学习的核心思想

是什么:集成学习(Ensemble Learning)是一种机器学习范式,它通过构建并结合多个学习器来完成学习任务。这些学习器可以是同质的(如多棵决策树),也可以是异质的(如决策树 + SVM + 逻辑回归)。当基学习器的准确率仅略高于随机猜测(即弱学习器)时,集成后的模型往往能取得远超单个强学习器的性能。

大白话 就像你不会只听一个人的意见就做决定,而是会综合多个专家的意见——集成学习就是让多个模型一起「投票」,最终结果往往更靠谱。

为什么:单个模型容易陷入过拟合或欠拟合。集成学习之所以有效,可以从统计学、计算学和表示论三个角度理解:从统计角度看,单个学习器可能选错假设空间,而集成多个学习器可以降低这个风险;从计算角度看,单个学习算法容易陷入局部最优,而集成可以结合多个局部最优找到更好的全局解;从表示角度看,真实假设可能不在任何单一假设空间中,但集成可以扩展表示能力。

怎么做

import numpy as np

# ========== 生成模拟的二分类数据集 ==========
np.random.seed(42)  # 固定随机种子,保证结果可复现
n_samples = 200  # 样本数量

# 生成两个高斯分布簇,作为两个类别的特征
X_class0 = np.random.randn(n_samples // 2, 2) + np.array([-1, -1])  # 类别0:中心在(-1,-1)
X_class1 = np.random.randn(n_samples // 2, 2) + np.array([1, 1])    # 类别1:中心在(1,1)
X = np.vstack([X_class0, X_class1])  # 纵向拼接特征矩阵
y = np.hstack([np.zeros(n_samples // 2), np.ones(n_samples // 2)])  # 标签:前一半为0,后一半为1
# 打乱数据顺序,避免模型学到顺序偏差
indices = np.arange(n_samples)
np.random.shuffle(indices)
X, y = X[indices], y[indices]

# ========== 实现弱分类器:决策树桩 ==========
def decision_stump(X, y, sample_weights=None):
    """
    单层决策树:仅选一个特征和一个阈值做二分类
    X: (n_samples, n_features) 特征矩阵
    y: (n_samples,) 标签向量
    sample_weights: 样本权重(Boosting 时会用到)
    返回: (最佳特征索引, 最佳阈值, 小于阈值预测的类别, 大于阈值预测的类别)
    """
    n_samples, n_features = X.shape
    if sample_weights is None:
        sample_weights = np.ones(n_samples) / n_samples  # 默认均匀权重
    best_error = float('inf')  # 初始化最小错误率为无穷大
    best_stump = None  # 存储最佳决策树桩的参数
    
    for feat_idx in range(n_features):  # 遍历每个特征
        # 生成候选阈值:取该特征的最小值到最大值之间的等分点
        thresholds = np.linspace(X[:, feat_idx].min(), X[:, feat_idx].max(), 20)
        for thresh in thresholds:  # 遍历每个候选阈值
            for polarity in [-1, 1]:  # 遍历两种极性方向
                # polarity=1: 小于阈值预测类别1;polarity=-1: 小于阈值预测类别0
                pred = np.where(X[:, feat_idx] < thresh, 
                               (polarity + 1) / 2,   # 小于阈值时的预测
                               (1 - polarity) / 2)    # 大于等于阈值时的预测
                # 计算加权错误率
                error = np.sum(sample_weights[y != pred])
                if error < best_error:
                    best_error = error
                    best_stump = (feat_idx, thresh, polarity)
    return best_stump

# ========== Bagging 实现(并行训练多个基学习器) ==========
class SimpleBagging:
    """简单 Bagging 实现:Bootstrap 采样 + 多个决策树桩投票"""
    def __init__(self, n_estimators=10):
        self.n_estimators = n_estimators  # 基学习器数量
        self.stumps = []  # 存储每个基学习器(决策树桩)
    
    def fit(self, X, y):
        """训练:对每个基学习器,自助采样后训练决策树桩"""
        n_samples = X.shape[0]
        self.stumps = []
        for _ in range(self.n_estimators):
            # Bootstrap 采样:有放回地随机抽取 n_samples 个样本索引
            bootstrap_idx = np.random.choice(n_samples, n_samples, replace=True)
            X_boot, y_boot = X[bootstrap_idx], y[bootstrap_idx]
            # 在 Bootstrap 样本上训练一个决策树桩
            stump = decision_stump(X_boot, y_boot)
            self.stumps.append(stump)
    
    def predict(self, X):
        """预测:所有基学习器投票,取多数"""
        n_samples = X.shape[0]
        votes = np.zeros(n_samples)
        for feat_idx, thresh, polarity in self.stumps:
            # 每个决策树桩给出预测(0 或 1)
            pred = np.where(X[:, feat_idx] < thresh, 
                           (polarity + 1) / 2, (1 - polarity) / 2)
            votes += pred  # 累加票数
        # 多数投票:超过半数基学习器预测为1,则最终预测为1
        return (votes >= self.n_estimators / 2).astype(int)

# ========== Boosting 实现(串行训练,关注错分样本) ==========
class SimpleAdaBoost:
    """简单 AdaBoost 实现:串行训练,错误样本增加权重"""
    def __init__(self, n_estimators=10):
        self.n_estimators = n_estimators  # 迭代轮数
        self.alphas = []   # 每个基学习器的权重系数
        self.stumps = []   # 每个基学习器(决策树桩)
    
    def fit(self, X, y):
        n_samples = X.shape[0]
        # 将标签从 {0,1} 映射到 {-1,1},方便 AdaBoost 权重更新公式
        y_signed = 2 * y - 1
        sample_weights = np.ones(n_samples) / n_samples  # 初始样本权重均匀分布
        
        for _ in range(self.n_estimators):
            # 在加权数据上训练决策树桩
            stump = decision_stump(X, y, sample_weights)
            feat_idx, thresh, polarity = stump
            self.stumps.append(stump)
            
            # 计算当前基学习器的预测值(-1 或 1)
            pred_signed = np.where(X[:, feat_idx] < thresh,
                                  polarity, -polarity)
            # 计算加权错误率
            error = np.sum(sample_weights[y_signed != pred_signed])
            error = max(error, 1e-10)  # 防止除零
            
            # 计算基学习器权重 alpha:错误率越低,alpha 越大
            alpha = 0.5 * np.log((1 - error) / error)
            self.alphas.append(alpha)
            
            # 更新样本权重:错误样本权重增大,正确样本权重减小
            sample_weights *= np.exp(-alpha * y_signed * pred_signed)
            sample_weights /= np.sum(sample_weights)  # 归一化
    
    def predict(self, X):
        """预测:加权投票(每个基学习器按其 alpha 系数加权)"""
        n_samples = X.shape[0]
        final_score = np.zeros(n_samples)
        for (feat_idx, thresh, polarity), alpha in zip(self.stumps, self.alphas):
            pred_signed = np.where(X[:, feat_idx] < thresh,
                                  polarity, -polarity)
            final_score += alpha * pred_signed  # 加权累加
        return (final_score > 0).astype(int)  # 符号决定最终类别

# ========== Stacking 实现(分层训练,用元学习器融合) ==========
class SimpleStacking:
    """简单 Stacking 实现:第一层多个学习器,第二层用逻辑回归融合"""
    def __init__(self, base_learners, meta_learner=None):
        self.base_learners = base_learners  # 第一层基学习器列表
        self.meta_learner = meta_learner    # 第二层元学习器
    
    def fit(self, X, y):
        """训练:先用基学习器预测,再用元学习器学习如何融合这些预测"""
        n_samples = X.shape[0]
        n_base = len(self.base_learners)
        
        # 第一层:训练每个基学习器
        for learner in self.base_learners:
            learner.fit(X, y)
        
        # 生成元特征:每个基学习器的预测结果
        meta_features = np.zeros((n_samples, n_base))
        for i, learner in enumerate(self.base_learners):
            meta_features[:, i] = learner.predict(X)
        
        # 第二层:元学习器学习如何组合基学习器的输出
        if self.meta_learner is None:
            # 简单的逻辑回归作为元学习器
            from sklearn.linear_model import LogisticRegression
            self.meta_learner = LogisticRegression()
        self.meta_learner.fit(meta_features, y)
    
    def predict(self, X):
        """预测:基学习器先预测,元学习器再做最终决策"""
        n_base = len(self.base_learners)
        meta_features = np.zeros((X.shape[0], n_base))
        for i, learner in enumerate(self.base_learners):
            meta_features[:, i] = learner.predict(X)
        return self.meta_learner.predict(meta_features)

# ========== 演示:三种方法的准确率对比 ==========
# 使用 sklearn 的决策树桩作为基学习器(这里仅演示概念)
print("集成学习三种范式对比演示")
print("=" * 50)
print("Bagging 思想:并行训练 → 降低方差 → 适合高方差模型(如决策树)")
print("Boosting 思想:串行训练 → 降低偏差 → 适合高偏差模型(如线性模型)")
print("Stacking 思想:分层训练 → 学习如何组合 → 适合异构模型融合")
\text{集成学习的泛化误差分解}\(\text{Bias-Variance Decomposition: } E[(f(x) - y)^2] = \underbrace{(E[f(x)] - \bar{y})^2}_{\text{Bias}^2} + \underbrace{E[(f(x) - E[f(x)])^2]}_{\text{Variance}} + \underbrace{\sigma^2}_{\text{Noise}}\)
大白话 偏差就是「瞄得准不准」,方差就是「手抖不抖」。Bagging 让你多用几把尺子量(减少手抖),Boosting 让你每次改进瞄准的位置(减少瞄准误差),Stacking 则让一个更聪明的人综合所有人的意见。

什么用:在 AI 应用中,集成学习几乎是所有竞赛和工业部署的标配。Kaggle 竞赛中超过 80% 的冠军方案使用了某种形式的集成。在工业场景中,推荐系统的 CTR 预估、金融风控的欺诈检测、医疗影像的病灶识别等核心任务都依赖于集成方法获得更高的准确率和更稳定的预测。

哪些坑:集成学习会增加计算开销和模型复杂度,推理延迟也会上升。Boosting 对异常值敏感,容易过拟合噪声。Stacking 如果设计不当,元学习器可能学到基学习器的系统性偏差而非真正的信号。此外,集成模型的可解释性远不如单一模型(如单棵决策树)。

二、Bagging:自助聚合

是什么:Bagging(Bootstrap Aggregating)由 Leo Breiman 于 1996 年提出。它通过 Bootstrap 采样(有放回抽样)从原始训练集中生成多个不同的训练子集,在每个子集上独立训练一个基学习器,最后对所有基学习器的预测结果进行投票(分类)或平均(回归)。

大白话 就像你让 10 个学生每人随机抽取一部分试卷来学习,然后让他们各自判断新题目——他们学的材料略有不同,但综合投票的结果往往比任何一个单独判断都准。

为什么:对于不稳定的学习器(如决策树——训练数据的微小变化可能导致树结构大幅改变),Bagging 能显著降低方差。从数学上看,假设每个基学习器方差为 σ²,两两相关系数为 ρ,则集成后均值方差为 ρσ² + (1-ρ)σ²/B。当 B(基学习器数量)增大时,方差趋近于 ρσ²。关键在于降低基学习器之间的相关性 ρ,Bootstrap 采样和随机特征选择都是为此。

怎么做

import numpy as np

# ========== 演示 Bootstrap 采样过程 ==========
np.random.seed(0)
original_data = np.array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])  # 原始10个样本
n_samples = len(original_data)

print("原始数据:", original_data)
print("\n=== Bootstrap 采样演示 ===")
for i in range(3):
    # 有放回地随机抽取 n_samples 个样本
    bootstrap_sample = np.random.choice(original_data, size=n_samples, replace=True)
    # 统计被选中和未被选中的样本
    selected = set(bootstrap_sample)
    not_selected = set(original_data) - selected
    print(f"第{i+1}次采样: {sorted(bootstrap_sample)}")
    print(f"  未被选中的样本: {sorted(not_selected)} (作为袋外验证集)")

# ========== OOB(袋外)误差估计 ==========
print("\n=== 袋外误差(OOB Error)估计 ===")
print("每个样本被选中的概率: 1 - (1 - 1/n)^n ≈ 1 - 1/e ≈ 63.2%")
print("每个样本未被选中的概率: (1 - 1/n)^n ≈ 1/e ≈ 36.8%")
print("OOB 样本(未被选中的 36.8%)可作为验证集,无需额外划分数据")

# 随机森林中 OOB 评分的简单演示
n_estimators = 100
oob_count = np.zeros(len(original_data))  # 每个样本作为 OOB 的次数计数
for _ in range(n_estimators):
    bootstrap_idx = np.random.choice(len(original_data), len(original_data), replace=True)
    for j in range(len(original_data)):
        if j not in bootstrap_idx:
            oob_count[j] += 1
print(f"经过 {n_estimators} 轮 Bagging 后,每个样本平均作为 OOB 的次数: {oob_count.mean():.1f}")
\text{Bagging 方差缩减推导}\(\text{Var}(\frac{1}{B}\sum_{i=1}^{B} X_i) = \rho \sigma^2 + \frac{1 - \rho}{B}\sigma^2\)
大白话 Bagging 的本质是:用不同的数据子集训练多个模型,然后取平均值。只要这些模型不是完全相关的(即它们的错误不完全相同),平均之后方差就会降低。

三、Boosting:自适应提升

是什么:Boosting 是一种串行集成方法,其核心思想是:先训练一个基学习器,然后根据其表现调整训练样本的分布(增加错分样本的权重),再训练下一个基学习器,如此迭代。最终将所有基学习器加权组合。最经典的代表是 AdaBoost(Freund & Schapire, 1995)和 Gradient Boosting(Friedman, 2001)。

大白话 Boosting 就像一个老师给学生补课——每次考试后,把错题拿出来重点复习,下次考试这些错题占的分更重。经过多次反复,学生就能把难点一个个攻克。

为什么:Boosting 的理论基础来自 PAC 学习理论(Probably Approximately Correct learning)。一个关键定理是:只要基学习器的准确率略高于 50%(即弱学习器),通过 Boosting 就可以将其提升为任意精度的强学习器。AdaBoost 使用指数损失函数,通过前向分步加法模型(Forward Stagewise Additive Modeling)逐步逼近最优解。Gradient Boosting 则推广了这一框架——在任意可微损失函数下,每一步拟合损失函数的负梯度方向(即残差)。

怎么做

import numpy as np

# ========== AdaBoost 的指数损失与权重更新机制 ==========
def adaboost_weight_update_demo():
    """
    演示 AdaBoost 中样本权重的更新过程
    """
    n_samples = 5
    # 初始样本权重:每个样本同等重要
    w = np.ones(n_samples) / n_samples
    # 假设第一轮分类器在 5 个样本上的预测正确性(True=正确, False=错误)
    correct = np.array([True, False, True, True, False])
    # 计算加权错误率
    error = w[~correct].sum()
    print(f"加权错误率: {error:.3f}")
    
    # 计算分类器权重 alpha
    alpha = 0.5 * np.log((1 - error) / error)
    print(f"分类器权重 alpha: {alpha:.3f}")
    print(f"alpha > 0 说明分类器优于随机猜测,alpha < 0 说明不如随机猜测")
    
    # 更新样本权重:错误样本权重乘以 e^alpha,正确样本乘以 e^{-alpha}
    for i in range(n_samples):
        if correct[i]:
            w[i] *= np.exp(-alpha)  # 正确样本降低权重
        else:
            w[i] *= np.exp(alpha)   # 错误样本增大权重
    w /= w.sum()  # 归一化
    print(f"更新后的样本权重: {w}")
    print(f"错误样本的新权重之和: {w[~correct].sum():.3f} (等于0.5,因为 alpha 的选取使下次错误率为0.5)")

adaboost_weight_update_demo()

# ========== Gradient Boosting 的残差拟合思想 ==========
print("\n=== Gradient Boosting 残差拟合演示 ===")
# 模拟数据:y = 2x + sin(3x) + noise
np.random.seed(1)
x = np.linspace(0, 5, 20)
y_true = 2 * x + np.sin(3 * x) + np.random.normal(0, 0.3, 20)

# 第一步:用均值作为初始预测
f0 = np.mean(y_true)
print(f"第0步:初始预测(均值) f0 = {f0:.3f}")
residual = y_true - f0  # 残差 = 真实值 - 当前预测值
print(f"残差(负梯度方向): {residual[:5]} ... (前5个)")

# 梯度提升的核心:每一步新学习器去拟合残差
# 残差恰好是平方损失 L = 1/2(y-f)^2 关于 f 的负梯度: -∂L/∂f = y - f = residual
print("\n核心思想:每步新加的学习器 h_m(x) 去拟合当前模型的残差")
print("更新规则: f_m(x) = f_{m-1}(x) + η · h_m(x)")
print("其中 η 是学习率(shrinkage),控制每一步的贡献大小,通常取 0.01~0.1")
print("小学习率 + 多迭代 = 更好的泛化能力(这就是 XGBoost 的核心技巧之一)")
\text{AdaBoost 指数损失与加法模型}\(L(y, F(x)) = \exp(-y F(x)), \quad F_m(x) = F_{m-1}(x) + \alpha_m h_m(x)\)
大白话 AdaBoost 每次关注上次犯错的样本,Gradient Boosting 则更进一步——每次关注上次预测值和真实值之间的差距(残差),让新模型去填补这个差距。就像画画一样,先画个轮廓,再逐步添加细节。

四、Stacking:堆叠泛化

是什么:Stacking(Stacked Generalization)由 David Wolpert 于 1992 年提出。它使用两层结构:第一层是多个异构的基学习器(可以是不同类型的模型),第二层是一个元学习器(meta-learner),元学习器的输入是第一层各模型的预测输出,任务是学习如何最优地组合这些预测。

大白话 Bagging 和 Boosting 是让「同类型的专家」一起工作,Stacking 则是让「不同类型的专家」(数学家、物理学家、统计学家)各自给出意见,然后由一位「总工程师」决定如何综合这些意见。

为什么:不同模型的「错误模式」不同——决策树容易在边界处犯错,SVM 在核函数选择不当时容易有偏差,线性模型则有系统性偏差。Stacking 通过让元学习器看到每个基模型的预测模式,学习哪些基模型在哪些区域更可靠,从而实现比简单平均或投票更好的融合效果。关键技巧包括使用 K 折交叉验证生成元特征(避免数据泄露)和使用概率输出而非硬分类结果。

怎么做

import numpy as np

# ========== Stacking 中 K 折交叉验证生成元特征的演示 ==========
def generate_meta_features_with_cv():
    """
    演示如何使用 K 折交叉验证生成元特征(第一层输出),
    这是 Stacking 避免数据泄露的关键技巧
    """
    # 模拟数据:10个样本,3个特征
    X = np.array([[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]], dtype=float)
    y = np.array([0, 0, 0, 1, 1, 1, 1, 1, 1, 1])
    
    n_folds = 5  # K=5 折
    fold_size = len(X) // n_folds  # 每折大小
    
    # 模拟一个基分类器的预测
    # 注意:在实际 Stacking 中,对每一折,用训练折训练模型,预测验证折
    print("=== Stacking 元特征生成(K折交叉验证) ===")
    print(f"数据集大小: {len(X)}, K={n_folds}, 每折 {fold_size} 个样本")
    
    meta_features = np.zeros(len(X))  # 存储第一层的输出(元特征)
    for fold in range(n_folds):
        val_start = fold * fold_size
        val_end = (fold + 1) * fold_size
        print(f"第{fold+1}折: 验证集索引 [{val_start}:{val_end}],其余为训练集")
        # 实际场景中:用训练集训练基学习器,然后预测验证集
        # 这里简化:直接用简单规则模拟预测
        val_indices = list(range(val_start, val_end))
        for idx in val_indices:
            meta_features[idx] = 1.0 if X[idx, 0] > 15 else 0.0
    
    print(f"生成的元特征(第一层输出): {meta_features}")
    print("这些元特征 + 原始标签将作为第二层元学习器的训练数据")
    print("关键:验证集样本没有参与对应基学习器的训练,避免了信息泄露")

generate_meta_features_with_cv()

# ========== 三种集成方法的总结对比 ==========
print("\n" + "=" * 50)
print("三种集成方法的对比总结")
print("=" * 50)
print("Bagging:  并行 | 降低方差 | 同质基学习器 | 简单投票/平均")
print("Boosting: 串行 | 降低偏差 | 同质基学习器 | 加权投票")
print("Stacking: 分层 | 利用异构优势 | 异质基学习器 | 元学习器融合")
print("\n选择建议:")
print("  - 模型方差大(如深决策树)→ Bagging / 随机森林")
print("  - 模型偏差大(如浅决策树)→ Boosting / GBDT")
print("  - 有多个不同类型的好模型 → Stacking")
\text{Stacking 的元学习器优化目标}\(\min_{\theta} \sum_{i=1}^{n} L(y_i, g_\theta(z_i^{(1)}, z_i^{(2)}, \ldots, z_i^{(K)}))\)
大白话 Stacking 的精髓在于:不是简单地把各个模型的预测平均一下,而是让一个额外的模型去学习「什么时候该信哪个模型」。比如,决策树在数据密集区判断准,SVM 在边界区分好——元学习器就会学会在不同区域给不同模型分配不同权重。

什么用:Stacking 在 Kaggle 竞赛中极为常见,尤其当参赛者拥有多个表现良好但类型不同的模型时。在工业界,推荐系统中常将深度学习模型、逻辑回归模型和因子分解机的预测结果通过 Stacking 融合,提高 CTR 预估精度。

哪些坑:Stacking 的复杂度最高,训练和调参都很耗时。如果基学习器之间高度相关(都犯类似的错误),Stacking 的优势会大打折扣。元特征如果不用交叉验证生成而是直接用训练数据预测,会导致严重的数据泄露和过拟合。

概念关系图谱

方法基学习器关系训练方式主要解决的误差代表算法
Bagging同质/独立并行方差(Variance)随机森林
Boosting同质/依赖串行偏差(Bias)AdaBoost, XGBoost
Stacking异质/独立两层表示能力竞赛常用融合
简单平均任意并行方差集成平均

重点答疑

Q1: Bagging 和 Boosting 的根本区别是什么?

Bagging 的基学习器是并行、独立训练的,目标是降低方差(适合高方差低偏差的复杂模型如决策树);Boosting 的基学习器是串行、依赖训练的(后一个学习器依赖前一个的结果),目标是降低偏差(适合高偏差低方差的简单模型如浅决策树)。从权重角度看,Bagging 中各基学习器权重相等,Boosting 中每个基学习器按其性能有不同的权重。

Q2: 为什么 Boosting 比 Bagging 更容易过拟合?

Boosting 使用串行训练,每一步都在试图纠正前面所有步骤的错误。当数据中有噪声时,Boosting 可能会过度关注噪声样本,导致模型逐渐「记住」噪声模式而非数据的真实分布。这在训练迭代次数过多时尤其明显。相比之下,Bagging 并行训练多个模型取平均,对噪声天然有平滑和稀释作用,因此抗过拟合能力更强。

Q3: Stacking 为什么需要用交叉验证来生成元特征?

如果直接用基学习器在全部训练数据上训练后对训练集做预测作为元特征,那么基学习器已经「见过」这些数据,元特征中包含了过拟合的信息。用交叉验证可以确保:每个样本的元特征来自一个未在训练中包含该样本的基学习器,从而得到「无偏」的元特征,避免信息泄露。

章节单词汇总

英文音标术语/释义
Ensemble/ɑːnˈsɑːmbəl/集成;将多个模型组合成一个更强的模型
Bagging/ˈbæɡɪŋ/Bootstrap Aggregating 的缩写;自助聚合
Boosting/ˈbuːstɪŋ/提升;串行训练,重点修正前面的错误
Bootstrap/ˈbuːtstræp/自助法;有放回地随机抽样
Variance/ˈveriəns/方差;模型预测的波动程度
Bias/ˈbaɪəs/偏差;模型预测与真实值的系统性差距
Stacking/ˈstækɪŋ/堆叠泛化;用元学习器融合多个模型的输出
Meta-learner/ˈmetə ˈlɜːrnər/元学习器;学习如何组合基学习器的模型
Residual/rɪˈzɪdʒuəl/残差;真实值与预测值之间的差距
Shrinkage/ˈʃrɪŋkɪdʒ/收缩;通过小学习率控制每步的更新幅度

面试练习

Q1 [单选] Bagging 的主要目的是什么?

  • A. 降低模型的偏差
  • B. 降低模型的方差
  • C. 降低模型的复杂度
  • D. 增加模型的训练速度
解答:Bagging(Bootstrap Aggregating)通过并行训练多个基学习器并取平均/投票,主要降低模型的方差。由于各基学习器在不同 Bootstrap 样本上训练,它们的错误存在一定独立性,集成后方差显著降低。

Q2 [单选] AdaBoost 中,当一个样本被错分后,其权重会如何变化?

  • A. 权重减小
  • B. 权重增大
  • C. 权重不变
  • D. 权重变为零
解答:AdaBoost 的核心机制是增加错分样本的权重,使后续的基学习器更加关注这些难分类的样本。正确分类的样本权重会减小。

Q3 [多选] 以下哪些属于集成学习方法?

  • A. 随机森林(Random Forest)
  • B. XGBoost
  • C. 支持向量机(SVM)
  • D. Stacking
解答:随机森林是 Bagging 的典型代表,XGBoost 是 Gradient Boosting 的实现,Stacking 是另一种集成范式。SVM 是单一模型,不属于集成学习。

Q4 [单选] 在 Bagging 中,每个基学习器的训练数据是如何获得的?

  • A. 使用全部原始数据
  • B. 通过 Bootstrap 采样(有放回抽样)
  • C. 使用原始数据的互斥子集
  • D. 使用原始数据加上噪声
解答:Bagging 使用 Bootstrap 采样——从原始训练集中有放回地随机抽取与原始数据集大小相同的样本。每个样本约有 63.2% 的概率被选中,36.8% 成为袋外(OOB)样本。

Q5 [单选] 关于 Boosting,以下说法正确的是?

  • A. 基学习器是串行训练的,后一个依赖前一个的结果
  • B. 基学习器是并行训练的,互不依赖
  • C. 所有基学习器的权重是相等的
  • D. Boosting 主要用于降低模型的方差
解答:Boosting 是串行训练方式,每个基学习器依赖于之前所有学习器的结果(通过样本权重或残差)。各基学习器的权重由其性能决定,各不相同。Boosting 主要降低偏差。

Q6 [多选] Stacking 相比 Bagging 和 Boosting 的独特之处在于?

  • A. 可以组合不同类型的基学习器(如决策树 + SVM)
  • B. 使用元学习器学习如何融合各基学习器的输出
  • C. 通常需要用交叉验证来生成元特征
  • D. 训练速度比 Bagging 更快
解答:Stacking 的核心优势是支持异质基学习器、使用元学习器进行智能融合、需要用交叉验证防止数据泄露。但 Stacking 的训练复杂度通常比其他方法更高而非更低。

Q7 [单选] 偏差-方差分解中,不可约误差(Noise)是指什么?

  • A. 模型复杂度带来的误差
  • B. 数据本身的噪声,任何模型都无法消除
  • C. 训练集和测试集分布不同带来的误差
  • D. 算法随机性带来的误差
解答:不可约误差 σ² 是数据生成过程中的固有噪声,即使模型完全正确也无法消除。它代表了预测任务的理论性能上限。

Q8 [单选] Gradient Boosting 中,每一步新加的模型拟合的是什么?

  • A. 原始标签 y
  • B. 前一个模型的预测值
  • C. 当前模型的残差(损失函数的负梯度)
  • D. 样本权重
解答:Gradient Boosting 每步拟合的是残差,即损失函数关于当前预测值的负梯度方向。对于平方损失 L=½(y−f)²,负梯度恰好等于残差 y−f。这使得每步新模型都在「填补」当前模型的不足之处。

Q9 [多选] 以下哪些是防止集成学习过拟合的常用方法?

  • A. 限制 Boosting 的迭代轮数(早停)
  • B. 使用较小的学习率(shrinkage/learning rate)
  • C. 在 Bagging 中引入随机特征选择(如随机森林)
  • D. 尽可能增加基学习器的数量
解答:早停和学习率控制是 Boosting 防止过拟合的关键。随机特征选择增加基学习器多样性,降低方差。但单纯增加基学习器数量并不能防止过拟合,Boosting 中反而可能加剧过拟合。

Q10 [单选] 一个样本在 Bagging 的 Bootstrap 采样中从未被选中的概率约为多少?

  • A. 50%
  • B. 63.2%
  • C. 36.8%
  • D. 10%
解答:每次采样中某个样本被选中的概率是 1/n,不被选中的概率是 (1−1/n)。经过 n 次独立采样后,该样本从未被选中的概率是 (1−1/n)^n,当 n 较大时趋近于 1/e ≈ 36.8%。这些样本构成了袋外(OOB)验证集。