机器学习实战:用Python手把手教你实现聚合模型(附代码)

很多刚开始接触机器学习的朋友,都会遇到一个挺尴尬的情况:理论课听得明明白白,公式推导也能跟得上,可一旦要自己动手写代码,把那些模型从纸面搬到屏幕上,就有点无从下手了。特别是像聚合模型(Aggregation Model) 这类听起来有点“玄乎”的概念,什么投票法、加权平均、Bagging、Boosting,光看名字就让人头大。更别提还要自己用Python实现一遍了。

我自己刚开始学的时候也这样,总觉得聚合模型是那些竞赛大神或者大厂算法工程师才玩得转的高级货。后来在几个实际项目里硬着头皮用了几次,才发现它的核心思想其实特别朴素——“三个臭皮匠,顶个诸葛亮”。单个模型可能能力有限,容易看走眼(欠拟合)或者钻牛角尖(过拟合),但如果我们把多个模型的意见综合起来,往往能得到更稳、更准的判断。这就像我们做重要决定时,不会只听一个人的意见,而是会问问身边几个靠谱的朋友,再自己掂量掂量。

这篇文章,我就想抛开那些复杂的数学外壳,直接带大家用Python把几种主流的聚合模型亲手实现一遍。我们会从最简单的均匀投票开始,一步步写到线性加权融合,再到更高级的BaggingAdaBoost。我不假设你有很强的数学背景,但希望你写过一些Python,用过scikit-learn做过基础的分类回归。我们的目标很明确:让你看完就能自己跑通代码,真正理解这些模型是怎么“动起来”的,以后在自己的项目里也能用得顺手。

我会用到一个人工生成的分类数据集,这样数据干净,干扰少,我们能更专注在算法逻辑本身。所有代码都会附上详细的注释,并且告诉你哪些地方容易踩坑。咱们不搞“教科书式”的平铺直叙,就聊怎么把它写出来、跑起来、调好它。

1. 环境准备与数据生成

在动手写模型之前,我们得先把“战场”布置好。这里包括安装必要的Python库,以及生成一份用于实验的数据。我强烈建议你使用 Anaconda 来管理环境,它能避免很多包版本冲突的烦心事。

1.1 安装依赖库

我们主要会用到以下几个库:

  • NumPy & Pandas: 数据处理和矩阵运算的基石,没人能绕得开。
  • Scikit-learn: 机器学习界的“瑞士军刀”,我们不仅用它来对比结果,还会借用它的一些基础组件。
  • Matplotlib: 画图神器,模型效果好不好,一张图往往比一堆数字更直观。

你可以通过下面的命令一次性安装它们(如果你用pip的话):

pip install numpy pandas scikit-learn matplotlib

注意:如果你在安装过程中遇到速度慢的问题,可以尝试使用国内的镜像源,例如清华源:pip install -i https://pypi.tuna.tsinghua.edu.cn/simple package-name

1.2 生成模拟数据集

为了清晰地展示聚合模型如何工作,我们不使用复杂的真实数据集,而是自己构造一个。这样数据的分布和规律我们心知肚明,更容易观察模型的“行为”。

我们构造一个二维数据集,其正负样本大致被一条正弦曲线分开,但在边界处有大量重叠,人为制造一些分类难度。单个线性分类器(比如逻辑回归)很难把这类数据分好,这就给了聚合模型发挥的空间。

import numpy as np
import matplotlib.pyplot as plt

def generate_synthetic_data(n_samples=500, noise=0.8, random_state=42):
    """
    生成一个非线性可分的二分类数据集。
    正负样本大致分布在一个正弦波形的两侧。

    参数:
        n_samples (int): 总样本数。
        noise (float): 噪声水平,控制数据点的分散程度。
        random_state (int): 随机种子,确保结果可复现。

    返回:
        X (np.ndarray): 特征矩阵,形状为 (n_samples, 2)。
        y (np.ndarray): 标签向量,形状为 (n_samples,),取值为 {1, -1}。
    """
    np.random.seed(random_state)
    t = np.linspace(0, 2 * np.pi, n_samples // 2)

    # 生成正类样本 (y = 1)
    X_pos = np.column_stack([
        t,
        np.sin(t) - noise * np.random.rand(len(t))
    ])
    y_pos = np.ones(len(t))

    # 生成负类样本 (y = -1)
    X_neg = np.column_stack([
        t,
        np.sin(t) + noise * np.random.rand(len(t))
    ])
    y_neg = -np.ones(len(t))

    # 合并并打乱顺序
    X = np.vstack([X_pos, X_neg])
    y = np.hstack([y_pos, y_neg])

    indices = np.arange(len(X))
    np.random.shuffle(indices)
    return X[indices], y[indices]

# 生成数据并可视化
X, y = generate_synthetic_data(n_samples=600, noise=0.9)
plt.figure(figsize=(8, 6))
plt.scatter(X[y==1, 0], X[y==1, 1], c='red', label='Class +1', alpha=0.6, edgecolors='w', s=50)
plt.scatter(X[y==-1, 0], X[y==-1, 1], c='blue', label='Class -1', alpha=0.6, edgecolors='w', s=50)
plt.xlabel('Feature 1')
plt.ylabel('Feature 2')
plt.title('Synthetic Binary Classification Dataset')
plt.legend()
plt.grid(True, linestyle='--', alpha=0.5)
plt.show()

运行上面的代码,你会得到一张类似下图的散点图。可以看到,红色点和蓝色点像两条缠绕的丝带,你中有我,我中有你。这就是我们给模型出的“考题”。

![生成的数据集散点图示意]

为了后续评估,我们把数据分成训练集和测试集:

from sklearn.model_selection import train_test_split

X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.25, random_state=42, stratify=y
)
print(f"训练集样本数: {X_train.shape[0]}, 测试集样本数: {X_test.shape[0]}")

2. 基础聚合方法:从投票到加权融合

聚合模型最直观的想法,就是收集多个“专家”(基模型)的意见,然后综合出一个最终决策。根据综合方式的不同,我们可以从简单到复杂,搭建出不同的聚合策略。

2.1 均匀投票法 (Uniform Voting / Majority Vote)

这是最简单粗暴的聚合方式。我们训练多个不同类型的基分类器,每个分类器对测试样本独立做出预测(+1或-1),然后统计票数,少数服从多数。在回归问题中,这就变成了直接对预测值取算术平均。

为什么这样做可能有效?想象一下,每个基分类器都会犯错,但犯错的模式和原因可能不同。通过投票,一个分类器的错误可能被其他分类器的正确判断所纠正。当然,这有个前提:基分类器之间要有一定的“差异性”。如果所有分类器都一模一样,那投票就失去了意义。

我们先来创建几个常见的基分类器:

from sklearn.linear_model import LogisticRegression
from sklearn.svm import SVC
from sklearn.tree import DecisionTreeClassifier
from sklearn.neighbors import KNeighborsClassifier
from sklearn.ensemble import RandomForestClassifier

# 初始化一组多样化的基分类器
base_estimators = {
    'Logistic Regression': LogisticRegression(C=1.0, random_state=42),
    'SVM (RBF Kernel)': SVC(kernel='rbf', probability=True, random_state=42), # 用probability=True以便后续可能用到预测概率
    'Decision Tree (深度5)': DecisionTreeClassifier(max_depth=5, random_state=42),
    '3-NN': KNeighborsClassifier(n_neighbors=3),
    'Random Forest (10棵树)': RandomForestClassifier(n_estimators=10, random_state=42)
}

现在,我们来实现一个通用的均匀投票聚合类:

class UniformVotingClassifier:
    """
    均匀投票聚合分类器。
    """
    def __init__(self, estimators):
        """
        参数:
            estimators (dict): 一个字典,键为估计器名称,值为初始化好的估计器对象。
        """
        self.estimators = estimators
        self.fitted_estimators = {}

    def fit(self, X, y):
        """训练所有基分类器。"""
        print("训练基分类器中...")
        for name, est in self.estimators.items():
            est.fit(X, y)
            self.fitted_estimators[name] = est
            print(f"  {name} 训练完成。")
        return self

    def predict(self, X):
        """对样本进行预测(硬投票)。"""
        # 收集所有预测结果
        predictions = np.zeros((X.shape[0], len(self.fitted_estimators)))
        for idx, (name, est) in enumerate(self.fitted_estimators.items()):
            predictions[:, idx] = est.predict(X)

        # 进行多数投票 (对于二分类,对每行求和,大于0则判为+1,否则为-1)
        # 因为我们用+1和-1作为标签,求和后看正负即可。
        aggregated = np.sum(predictions, axis=1)
        final_pred = np.where(aggregated >= 0, 1, -1)
        return final_pred

    def evaluate(self, X, y_true):
        """评估在给定数据集上的准确率。"""
        y_pred = self.predict(X)
        accuracy = np.mean(y_pred == y_true)
        return accuracy

让我们在训练集上训练这个投票模型,并在测试集上看看效果:

# 实例化并训练投票模型
voting_clf = UniformVotingClassifier(base_estimators)
voting_clf.fit(X_train, y_train)

# 评估每个基分类器以及投票模型的性能
print("\n--- 各模型在测试集上的准确率 ---")
test_accuracies = {}
for name, est in voting_clf.fitted_estimators.items():
    acc = np.mean(est.predict(X_test) == y_test)
    test_accuracies[name] = acc
    print(f"{name:25s}: {acc:.4f}")

# 评估投票模型
voting_acc = voting_clf.evaluate(X_test, y_test)
test_accuracies['Uniform Voting'] = voting_acc
print(f"{'Uniform Voting':25s}: {voting_acc:.4f}")

你可能会看到类似下面的输出。注意观察,投票模型(Uniform Voting)的准确率不一定是基分类器里最高的,但它通常比最差的基分类器好,并且很多时候能接近甚至超过最好的基分类器。这就是聚合的稳健性体现。

训练基分类器中...
  Logistic Regression 训练完成。
  SVM (RBF Kernel) 训练完成。
  Decision Tree (深度5) 训练完成。
  3-NN 训练完成。
  Random Forest (10棵树) 训练完成。

--- 各模型在测试集上的准确率 ---
Logistic Regression       : 0.7067
SVM (RBF Kernel)          : 0.7800
Decision Tree (深度5)      : 0.7467
3-NN                      : 0.7333
Random Forest (10棵树)     : 0.7867
Uniform Voting            : 0.7867

提示:均匀投票法实现简单,计算开销小,是提升模型稳定性的一个快速有效手段。在实践中的第一个基线模型之后,可以尝试加入投票法,看是否有稳定提升。

2.2 线性加权融合 (Linear Blending)

均匀投票法给每个基分类器赋予了相同的权重(一票)。但现实中,我们心里清楚,有些朋友的意见更值得参考,有些则可能经常不靠谱。线性加权融合就是给每个分类器分配一个权重 α_t,最终的决策是加权投票:G(x) = sign( Σ α_t * g_t(x) )

关键问题来了:权重α_t怎么定? 一个自然的想法是,让权重与分类器的性能挂钩。性能越好,权重越高。我们可以把寻找最优权重 α 本身建模成一个优化问题:最小化加权组合在某个验证集上的误差。

这里有一个非常重要的实践细节:绝不能使用训练集来学习权重α。因为基分类器 g_t 已经在训练集上训练过了,再用训练集去学权重,会导致严重的过拟合。我们必须使用一个独立的验证集(Validation Set),或者通过交叉验证的方式。

下面我们实现一个使用验证集学习权重的线性融合模型:

from sklearn.base import BaseEstimator, ClassifierMixin
from scipy.optimize import minimize

class LinearBlendingClassifier(BaseEstimator, ClassifierMixin):
    """
    线性加权融合分类器(用于二分类,标签为+1/-1)。
    使用验证集通过优化方法学习每个基分类器的最优权重。
    """
    def __init__(self, estimators, val_size=0.2, random_state=42):
        self.estimators = estimators  # 基分类器字典
        self.val_size = val_size
        self.random_state = random_state
        self.alphas_ = None  # 学习到的权重
        self.fitted_estimators_ = {}

    def _split_validation(self, X, y):
        """从训练集中分割出验证集。"""
        from sklearn.model_selection import train_test_split
        X_train, X_val, y_train, y_val = train_test_split(
            X, y, test_size=self.val_size, random_state=self.random_state, stratify=y
        )
        return X_train, X_val, y_train, y_val

    def _objective(self, alphas, predictions_val, y_val):
        """优化目标函数:加权组合在验证集上的0/1错误率。"""
        # 计算加权投票结果
        weighted_vote = np.dot(predictions_val, alphas)
        y_pred = np.where(weighted_vote >= 0, 1, -1)
        # 计算错误率
        error = np.mean(y_pred != y_val)
        return error

    def fit(self, X, y):
        # 1. 分割出验证集
        X_train, X_val, y_train, y_val = self._split_validation(X, y)
        print(f"训练集大小: {X_train.shape[0]}, 验证集大小: {X_val.shape[0]}")

        # 2. 在训练集上训练所有基分类器
        self.fitted_estimators_ = {}
        for name, est in self.estimators.items():
            est.fit(X_train, y_train)
            self.fitted_estimators_[name] = est

        # 3. 收集基分类器在验证集上的预测结果(硬标签,+1/-1)
        n_val = X_val.shape[0]
        n_est = len(self.estimators)
        predictions_val = np.zeros((n_val, n_est))
        for idx, (name, est) in enumerate(self.fitted_estimators_.items()):
            predictions_val[:, idx] = est.predict(X_val)

        # 4. 优化权重alpha,最小化验证集错误率
        # 初始权重设为均匀权重
        alpha_init = np.ones(n_est) / n_est
        # 添加简单约束:权重之和为1(非必须,但有助于稳定),每个权重大于等于0
        constraints = ({'type': 'eq', 'fun': lambda a: np.sum(a) - 1})
        bounds = [(0, None) for _ in range(n_est)]  # 权重非负

        result = minimize(
            fun=self._objective,
            x0=alpha_init,
            args=(predictions_val, y_val),
            method='SLSQP',
            bounds=bounds,
            constraints=constraints,
            options={'maxiter': 1000, 'ftol': 1e-9}
        )

        if not result.success:
            print(f"优化警告: {result.message}")
        self.alphas_ = result.x
        print("学习到的权重:")
        for idx, (name, _) in enumerate(self.fitted_estimators_.items()):
            print(f"  {name:25s}: {self.alphas_[idx]:.4f}")
        return self

    def predict(self, X):
        """使用学习到的权重进行加权投票预测。"""
        n_samples = X.shape[0]
        n_est = len(self.fitted_estimators_)
        predictions = np.zeros((n_samples, n_est))
        for idx, est in enumerate(self.fitted_estimators_.values()):
            predictions[:, idx] = est.predict(X)
        weighted_vote = np.dot(predictions, self.alphas_)
        return np.where(weighted_vote >= 0, 1, -1)

让我们来训练并评估这个线性融合模型:

# 实例化并训练线性融合模型
linear_blend_clf = LinearBlendingClassifier(base_estimators, val_size=0.25)
linear_blend_clf.fit(X_train, y_train)

# 评估
linear_blend_acc = np.mean(linear_blend_clf.predict(X_test) == y_test)
test_accuracies['Linear Blending'] = linear_blend_acc
print(f"\nLinear Blending 在测试集上的准确率: {linear_blend_acc:.4f}")

观察输出,你会看到每个基分类器被分配了不同的权重。性能好的分类器(如Random Forest)通常获得更高的权重,而性能较差的则权重较低。线性融合的准确率有潜力超过均匀投票,因为它更“聪明”地利用了不同分类器的优势。但是,这也取决于验证集的代表性以及优化过程是否成功。

方法测试准确率备注
Logistic Regression0.7067线性模型,对非线性数据拟合能力有限
SVM (RBF)0.7800核方法,处理非线性能力较强
Decision Tree0.7467容易过拟合,我们限制了深度
3-NN0.7333局部模型,对噪声敏感
Random Forest0.7867本身就是集成模型,表现稳定
Uniform Voting0.7867与最好的基模型持平
Linear Blending0.7933可能略优于均匀投票

注意:线性融合的优化问题可能陷入局部最优,且对验证集的质量敏感。在实际中,更常用且稳健的方法是下一节要讲的Stacking,或者直接使用现成的集成算法如Random Forest、Gradient Boosting。

3. 高级聚合:Bagging与Boosting实战

前面两种方法(投票和加权)都属于 Blending,即先训练好一堆基模型,然后再聚合。还有另一大类方法,被称为 Aggregation-Learning,它们在学习的过程中同时进行聚合,让基模型为了“共同的目标”而协作。其中最著名的代表就是 BaggingBoosting

3.1 Bagging (Bootstrap Aggregating)

Bagging 的核心思想是:利用数据随机性来创造多样性。我们只有一份训练集,如何得到多个不同的基模型呢?Bagging 的做法是通过 Bootstrap 采样(有放回抽样),从原始训练集中生成多个不同的子数据集,然后用同一个学习算法在每个子数据集上训练一个模型,最后将这些模型的预测结果进行平均(回归)或投票(分类)。

这种方法特别适用于 高方差、低偏差 的模型(如深度决策树、神经网络),因为通过平均可以显著降低方差。最著名的Bagging算法就是 随机森林(Random Forest),它在对数据行进行Bootstrap采样的同时,还对特征列进行随机采样,进一步增加基模型间的差异性。

下面,我们不直接调用RandomForestClassifier,而是手动实现一个简化的Bagging分类器,以理解其工作原理:

class SimpleBaggingClassifier:
    """
    一个简化的Bagging分类器实现。
    """
    def __init__(self, base_estimator, n_estimators=10, max_samples=1.0, random_state=42):
        """
        参数:
            base_estimator: 基学习器对象(需支持fit和predict)。
            n_estimators (int): 基学习器的数量。
            max_samples (float or int): 每个子训练集的样本数比例或绝对数。
            random_state (int): 随机种子。
        """
        self.base_estimator = base_estimator
        self.n_estimators = n_estimators
        self.max_samples = max_samples
        self.random_state = random_state
        self.estimators_ = []

    def _bootstrap_sample(self, X, y):
        """生成一个Bootstrap样本。"""
        n_samples = X.shape[0]
        sample_size = int(self.max_samples * n_samples) if isinstance(self.max_samples, float) else self.max_samples
        indices = np.random.choice(n_samples, size=sample_size, replace=True)  # 有放回抽样
        return X[indices], y[indices]

    def fit(self, X, y):
        np.random.seed(self.random_state)
        self.estimators_ = []
        print(f"训练 {self.n_estimators} 个基学习器...")
        for i in range(self.n_estimators):
            # 1. Bootstrap采样
            X_sample, y_sample = self._bootstrap_sample(X, y)
            # 2. 克隆基学习器并训练
            estimator = clone(self.base_estimator)
            estimator.fit(X_sample, y_sample)
            self.estimators_.append(estimator)
            if (i+1) % 5 == 0:
                print(f"  已完成 {i+1}/{self.n_estimators}")
        return self

    def predict(self, X):
        """对所有基学习器预测结果进行投票。"""
        predictions = np.zeros((X.shape[0], len(self.estimators_)))
        for idx, est in enumerate(self.estimators_):
            predictions[:, idx] = est.predict(X)
        # 多数投票
        aggregated = np.sum(predictions, axis=1)
        return np.where(aggregated >= 0, 1, -1)

让我们用一棵深度较大的决策树作为基学习器,看看Bagging如何提升其性能:

from sklearn.base import clone
from sklearn.tree import DecisionTreeClassifier

# 使用一个深度较大、容易过拟合的决策树作为基学习器
deep_tree = DecisionTreeClassifier(max_depth=15, min_samples_split=5, random_state=42)

# 单个深度决策树的表现
single_tree = deep_tree.fit(X_train, y_train)
single_tree_acc = np.mean(single_tree.predict(X_test) == y_test)
print(f"单个深度决策树测试准确率: {single_tree_acc:.4f}")

# Bagging集成后的表现
bagging_clf = SimpleBaggingClassifier(base_estimator=deep_tree, n_estimators=50, max_samples=0.8)
bagging_clf.fit(X_train, y_train)
bagging_acc = np.mean(bagging_clf.predict(X_test) == y_test)
test_accuracies['Bagging (50 trees)'] = bagging_acc
print(f"Bagging集成 (50棵树) 测试准确率: {bagging_acc:.4f}")

你很可能看到,单个深度决策树因为过拟合,在测试集上表现平平甚至较差。而经过Bagging集成后,准确率得到了显著且稳定的提升。这就是“众人拾柴火焰高”的力量,通过平均化,抑制了单个模型过拟合的“噪声”,保留了学习的“信号”。

3.2 AdaBoost (Adaptive Boosting)

如果说Bagging是“民主投票”,那么Boosting,特别是AdaBoost,就更像是“名师辅导”。它的核心思想是:序列化地训练一系列弱学习器,每个学习器都专注于纠正前一个学习器犯的错误

AdaBoost 的工作流程非常巧妙:

  1. 初始化所有训练样本的权重为相等值。
  2. 对于每一轮 t=1 to T: a. 用当前的样本权重训练一个弱学习器 g_t。 b. 计算这个弱学习器的加权错误率 ε_t。 c. 根据错误率计算该弱学习器的权重 α_t(错误率越低,权重越高)。 d. 更新样本权重:增加被 g_t 分类错误样本的权重,减少分类正确样本的权重。这样,下一轮的学习器就会更关注那些难分的样本。
  3. 将所有弱学习器按其权重 α_t 进行线性组合,得到最终模型。

AdaBoost 的神奇之处在于,即使每个弱学习器只比随机猜测好一点点(例如准确率51%),通过这样的自适应加权组合,最终能产生一个非常强大的模型。

下面,我们实现一个以决策树桩(Decision Stump) 作为弱学习器的AdaBoost分类器。决策树桩是只做一次分裂的决策树,是最弱的分类器之一,非常适合用来演示AdaBoost如何“化腐朽为神奇”。

class DecisionStump:
    """决策树桩(单层决策树)弱分类器。"""
    def __init__(self):
        self.feature_index = None
        self.threshold = None
        self.polarity = 1  # 指示哪一边被预测为正类
        self.alpha = None

    def predict(self, X):
        n_samples = X.shape[0]
        X_column = X[:, self.feature_index]
        predictions = np.ones(n_samples)
        if self.polarity == 1:
            predictions[X_column < self.threshold] = -1
        else:
            predictions[X_column >= self.threshold] = -1
        return predictions

class AdaBoostCustom:
    """自定义的AdaBoost分类器(使用决策树桩)。"""
    def __init__(self, n_estimators=50):
        self.n_estimators = n_estimators
        self.stumps = []

    def fit(self, X, y):
        n_samples, n_features = X.shape
        # 1. 初始化样本权重
        w = np.ones(n_samples) / n_samples

        for t in range(self.n_estimators):
            # 2. 训练一个弱分类器(决策树桩),使其最小化加权错误率
            stump = DecisionStump()
            min_error = float('inf')

            # 遍历所有特征和可能的阈值,寻找最佳分裂点
            for feature_i in range(n_features):
                X_column = X[:, feature_i]
                thresholds = np.unique(X_column)
                for threshold in thresholds:
                    for polarity in [1, -1]:
                        # 计算当前分裂下的预测和加权错误
                        p = 1
                        predictions = np.ones(n_samples)
                        if polarity == 1:
                            predictions[X_column < threshold] = -1
                        else:
                            predictions[X_column >= threshold] = -1

                        misclassified = w[(predictions != y)]
                        error = np.sum(misclassified)

                        if error < min_error:
                            min_error = error
                            stump.feature_index = feature_i
                            stump.threshold = threshold
                            stump.polarity = polarity

            # 3. 计算该弱分类器的权重 alpha_t
            EPS = 1e-10  # 防止除零
            stump.alpha = 0.5 * np.log((1.0 - min_error + EPS) / (min_error + EPS))

            # 4. 更新样本权重
            predictions = stump.predict(X)
            w *= np.exp(-stump.alpha * y * predictions)
            w /= np.sum(w)  # 归一化

            self.stumps.append(stump)

            # 可选:提前停止,如果错误率已经为0
            if min_error <= 0:
                print(f"第 {t+1} 轮后训练错误率为0,提前停止。")
                break
        return self

    def predict(self, X):
        stump_preds = [stump.alpha * stump.predict(X) for stump in self.stumps]
        y_pred = np.sum(stump_preds, axis=0)
        return np.sign(y_pred)

现在,让我们见证一下AdaBoost如何将一堆“弱不禁风”的决策树桩,组合成一个强大的分类器:

# 训练自定义的AdaBoost
adaboost_custom = AdaBoostCustom(n_estimators=100)
adaboost_custom.fit(X_train, y_train)
adaboost_custom_acc = np.mean(adaboost_custom.predict(X_test) == y_test)
test_accuracies['AdaBoost (Custom)'] = adaboost_custom_acc
print(f"自定义AdaBoost (100个树桩) 测试准确率: {adaboost_custom_acc:.4f}")

# 作为对比,使用scikit-learn内置的AdaBoost(通常性能更强)
from sklearn.ensemble import AdaBoostClassifier
sklearn_adaboost = AdaBoostClassifier(
    base_estimator=DecisionTreeClassifier(max_depth=1), # 决策树桩
    n_estimators=100,
    random_state=42
)
sklearn_adaboost.fit(X_train, y_train)
sklearn_ada_acc = np.mean(sklearn_adaboost.predict(X_test) == y_test)
test_accuracies['AdaBoost (sklearn)'] = sklearn_ada_acc
print(f"Scikit-learn AdaBoost 测试准确率: {sklearn_ada_acc:.4f}")

你会发现,即使基学习器如此简单,AdaBoost依然能取得非常不错的分类效果,甚至可能接近或超过之前更复杂的模型。这就是自适应提升的魅力所在。

4. 模型对比、可视化与实战建议

我们已经实现了从基础到高级的多种聚合模型。是时候把它们放在一起,做个全面的比较,并看看它们的决策边界到底有何不同。

4.1 性能对比汇总

让我们用一个表格来清晰展示所有模型在测试集上的表现:

import pandas as pd

# 将准确率字典转换为DataFrame并排序
results_df = pd.DataFrame(list(test_accuracies.items()), columns=['Model', 'Test Accuracy'])
results_df = results_df.sort_values('Test Accuracy', ascending=False).reset_index(drop=True)
print("\n=== 所有模型测试准确率排名 ===")
print(results_df.to_string(index=True))

这个排名会直观地告诉你,在这个特定数据集上,哪种聚合策略最有效。通常,AdaBoostRandom Forest / Bagging 会占据前列,而简单的投票法也能提供一个不错的基线。

4.2 决策边界可视化

“一图胜千言”。绘制不同模型的决策边界,能帮助我们理解它们是如何看待这个分类问题的。

def plot_decision_boundary(clf, X, y, title, ax):
    """在指定坐标轴上绘制分类器的决策边界。"""
    # 创建网格点
    x_min, x_max = X[:, 0].min() - 0.5, X[:, 0].max() + 0.5
    y_min, y_max = X[:, 1].min() - 0.5, X[:, 1].max() + 0.5
    xx, yy = np.meshgrid(np.arange(x_min, x_max, 0.02),
                         np.arange(y_min, y_max, 0.02))

    # 预测整个网格
    Z = clf.predict(np.c_[xx.ravel(), yy.ravel()])
    Z = Z.reshape(xx.shape)

    # 绘制决策边界和训练样本
    ax.contourf(xx, yy, Z, alpha=0.4, cmap=plt.cm.RdBu)
    ax.scatter(X[y==1, 0], X[y==1, 1], c='red', marker='o', edgecolors='k', s=30, label='Class +1')
    ax.scatter(X[y==-1, 0], X[y==-1, 1], c='blue', marker='s', edgecolors='k', s=30, label='Class -1')
    ax.set_xlim(xx.min(), xx.max())
    ax.set_ylim(yy.min(), yy.max())
    ax.set_title(title)
    ax.set_xlabel('Feature 1')
    ax.set_ylabel('Feature 2')
    ax.legend(loc='upper right')

# 选择几个代表性模型进行可视化
models_to_plot = {
    'Single Deep Tree': single_tree,
    'Uniform Voting': voting_clf,
    'Bagging (50 trees)': bagging_clf,
    'AdaBoost (Custom)': adaboost_custom,
    'AdaBoost (sklearn)': sklearn_adaboost
}

fig, axes = plt.subplots(2, 3, figsize=(15, 10))
axes = axes.ravel()
for idx, (name, model) in enumerate(models_to_plot.items()):
    plot_decision_boundary(model, X_train, y_train, name, axes[idx])
# 隐藏最后一个多余的子图
axes[-1].axis('off')
plt.tight_layout()
plt.show()

观察这些决策边界图,你能得到很多洞见:

  • 单个深度决策树:边界非常崎岖、复杂,是典型的过拟合表现,试图记住每一个训练样本。
  • 均匀投票:边界变得平滑了一些,不同分类器的意见相互抵消了部分噪声。
  • Bagging:边界更加平滑和合理,方差显著降低,泛化能力增强。
  • AdaBoost:边界通常也很平滑,但可能以一种更自适应、更聚焦于困难样本的方式形成。我们自定义的版本可能边界更简单,而sklearn的版本可能更精细。

4.3 实战应用建议与避坑指南

在真实项目中应用聚合模型,光会写代码还不够,以下几点经验可能对你有帮助:

  1. 基模型的选择与多样性

    • Bagging:选择高方差、低偏差的模型作为基学习器效果最好,如未剪枝的决策树、神经网络。因为Bagging主要目的是降低方差。
    • Boosting:选择弱学习器,如浅层决策树(树桩)。Boosting可以逐步降低偏差。
    • Blending/Stacking:尽可能选择不同类型的模型(线性模型、树模型、核方法等),以增加多样性。如果所有基模型都相似,集成收益甚微。
  2. 计算成本与效率

    • Bagging 的基模型可以并行训练,非常适合分布式计算。
    • Boosting 是序列化的,难以并行,训练更慢。但像 XGBoost、LightGBM 等现代库有高度优化的实现。
    • 对于超大规模数据,考虑使用 随机子空间特征采样数据采样 来减少每个基模型训练的数据量。
  3. 过拟合风险

    • Bagging 本身是降低过拟合的。
    • Boosting 如果迭代轮次太多,很容易过拟合。必须使用验证集早停(Early Stopping)或交叉验证来确定最优的 n_estimators
    • Stacking 的第二层模型如果太复杂,也容易过拟合。通常使用简单的线性模型作为元学习器。
  4. 工具库推荐

    • Scikit-learnVotingClassifier, BaggingClassifier, AdaBoostClassifier, RandomForestClassifier, GradientBoostingClassifier。入门和原型开发首选。
    • XGBoost / LightGBM / CatBoost:工业级Boosting实现,在速度、精度和功能上都非常强大,是竞赛和生产的标配。
    • MLxtend:提供了方便的 StackingClassifierStackingRegressor 实现。
  5. 一个常见的误区:认为集成模型总是比单个模型好。这不一定。如果你的单个模型已经非常强大且泛化能力极佳(例如一个精心调参的深度神经网络),集成的提升可能很小,而计算成本却大增。永远从建立一个好的基线模型开始,再考虑是否需要以及如何集成。

在我经历的项目中,对于表格数据,梯度提升树(如LightGBM) 通常是效果最好的“开箱即用”选择。对于图像、文本等深度学习占主导的领域,模型集成(如对多个训练周期的模型 checkpoint 进行集成)仍然是提升比赛排名和最终产品精度的有效手段。关键是要理解你所用方法背后的思想,并根据数据和问题特点灵活运用,而不是机械地套用。

更多推荐