数据STUDIO

决胜法宝!集成学习 Stacking, Blending & Voting


如果你想提高 ML 模型的性能,也许你应该考虑集成机器学习!

我们听说过“团结就是力量”这句话,它的含义可以转移到生活的不同领域。有时,对特定问题的正确答案得到多个来源的支持。这就是集成学习的思想精髓,即将一组 ML 模型放在一起,来改进特定问题的解决方案。

本文中,我们将了解什么是集成学习,存在哪些集成类型,我们将专门讨论Voting和Stacking集成。

什么是集成学习?

集成学习 [1] 是指联合使用 ML 算法主要解决分类和/或回归问题。这些算法可以是相同类型(同质集成学习)或不同类型(异构集成学习)。 集成学习 执行各种专家或 ML 模型的战略组合,以提高使用单个弱模型获得的 有效性 [2] 。下图提供了一个关于不实现 集成学习 的模型和一个实现集成学习的模型的比较的可视化概览。

传统学习与集成学习的比较

有不同类型的 集成学习 技术,主要区别在于使用的模型类型(同质或异构模型)、数据采样(有或没有替换、k-fold等)和决策函数(投票、平均、元模型等)。因此, 集成学习 技术可以分为:

  • Bagging
  • Boosting
  • Stacking

除了这三个主要类别之外,还出现了两个重要的变体: Voting (它是Bagging的补充)和 Blending (Stacking的子类型)。尽管 Voting 和 Blending 分别是 Bagging 和 Stacking 的补充和子类型,但这些技术通常被视为 集成学习 的直接类型。

在这篇博客中,我们将专门讨论 Stacking,Blending 和 Voting 技术。

Stacking

Stacking 全称 Stacking Generalization ,它是 David H. Wolpert [3] 在 1992 年引入的一种方法,其关键是减少不同泛化器(即 ML 模型)的泛化误差。 Stacking Generalization 方法的总体思路是生成一个 元模型 (Meta-Model)。这样的 元模型 由一组ML 基础模型(即弱学习器)通过 k 折交叉验证技术的预测组成。最后,使用额外的 ML 模型(通常称为“最终估计器”或“最终学习器”)训练 元模型 。

Stacking Generalization 方法通常由 2 个训练阶段组成,称为“level 0”和“level 1”。值得一提的是,它可以根据需要添加尽可能多的levels。而在实践中,通常只使用 2 个level。第一阶段(level 0)的目的是为元模型生成训练数据,这是通过对第一阶段中定义的每个“弱学习器”实施 k 折交叉验证来实现的。这些“弱学习器”中的每一个的预测都被“Stacking”起来,以便建立这样的“新训练集”(元模型)。第二阶段(level 1)的目的是训练元模型,这种训练是通过已经确定的 “终结学习器” 进行的。

在下图中,是一个 Stacking Generalization Classifier 架构图,该分类器由 3 个基本模型(弱学习器)和一个最终估计器组成。
Stacking泛化架构

现在我们已经知道 Stacking Generalization 技术原理。接下来看一个如何在代码中执行此操作(该技术可以直接从 scikit-learn 实现,但是,为了使解释更具说明性,我们看看我们如何从刮)。

上下滑动查看更多源码

import numpy as np
from sklearn.datasets import load_breast_cancer
from sklearn.model_selection import train_test_split

from sklearn.tree import DecisionTreeClassifier
from sklearn.ensemble import RandomForestClassifier
from sklearn.neighbors import KNeighborsClassifier
from sklearn.ensemble import GradientBoostingClassifier
from sklearn.naive_bayes import GaussianNB
from sklearn.linear_model import LogisticRegression


class Ensemble:
    def __init__(self):
        self.x_train = None
        self.x_test = None
        self.y_train = None
        self.y_test = None
        self.k = 5

    def load_data(self):
        x, y = load_breast_cancer(return_X_y=True)
        self.x_train, self.x_test, self.y_train, self.y_test = train_test_split(x, y, test_size=0.3, random_state=23)

    def StackingClassifier(self):

        # Define weak learners
        weak_learners = [('dt', DecisionTreeClassifier()),
                         ('knn', KNeighborsClassifier()),
                         ('rf', RandomForestClassifier()),
                         ('gb', GradientBoostingClassifier()),
                         ('gn', GaussianNB())]

        # Final learner or meta model
        final_learner = LogisticRegression()

        train_meta_model = None
        test_meta_model = None

        # Start stacking
        for clf_id, clf in weak_learners:
            # 基于k-fold对每个分类器的预测
            predictions_clf = self.k_fold_cross_validation(clf)

            # 基于level 0 训练的各分类器测试集预测
            test_predictions_clf = self.train_level_0(clf)

            # Stack预测,构成数据模型的输入数据
            if isinstance(train_meta_model, np.ndarray):
                train_meta_model = np.vstack((train_meta_model, predictions_clf))
            else:
                train_meta_model = predictions_clf

            # 来自测试集的Stack预测将形成元模型的测试数据
            if isinstance(test_meta_model, np.ndarray):
                test_meta_model = np.vstack((test_meta_model, test_predictions_clf))
            else:
                test_meta_model = test_predictions_clf

        # Transpose train_meta_model
        train_meta_model = train_meta_model.T

        # Transpose test_meta_model
        test_meta_model = test_meta_model.T

        # Training level 1
        self.train_level_1(final_learner, train_meta_model, test_meta_model)

    def k_fold_cross_validation(self, clf):

        predictions_clf = None

        # Number of samples per fold
        batch_size = int(len(self.x_train) / self.k)

        # Stars k-fold cross validation
        for fold in range(self.k):

            # Settings for each batch_size
            if fold == (self.k - 1):
                test = self.x_train[(batch_size * fold):, :]
                batch_start = batch_size * fold
                batch_finish = self.x_train.shape[0]
            else:
                test = self.x_train[(batch_size * fold): (batch_size * (fold + 1)), :]
                batch_start = batch_size * fold
                batch_finish = batch_size * (fold + 1)

            # test & training samples for each fold iteration
            fold_x_test = self.x_train[batch_start:batch_finish, :]
            fold_x_train = self.x_train[[index for index in range(self.x_train.shape[0]) if
                                         index not in range(batch_start, batch_finish)], :]

            # test & training targets for each fold iteration
            fold_y_test = self.y_train[batch_start:batch_finish]
            fold_y_train = self.y_train[
                [index for index in range(self.x_train.shape[0]) if index not in range(batch_start, batch_finish)]]

            # Fit current classifier
            clf.fit(fold_x_train, fold_y_train)
            fold_y_pred = clf.predict(fold_x_test)

            # Store predictions for each fold_x_test
            if isinstance(predictions_clf, np.ndarray):
                predictions_clf = np.concatenate((predictions_clf, fold_y_pred))
            else:
                predictions_clf = fold_y_pred

        return predictions_clf

    def train_level_0(self, clf):
        # Train in full real training set
        clf.fit(self.x_train, self.y_train)
        # Get predictions from full real test set
        y_pred = clf.predict(self.x_test)

        return y_pred

    def train_level_1(self, final_learner, train_meta_model, test_meta_model):
        # 用最终学习器或元模型进行训练
        final_learner.fit(train_meta_model, self.y_train)
        # 从meta_model中获取训练和测试精度
        print(f"Train accuracy: {final_learner.score(train_meta_model, self.y_train)}")
        print(f"Test accuracy: {final_learner.score(test_meta_model, self.y_test)}")


if __name__ == "__main__":
    ensemble = Ensemble()
    ensemble.load_data()
    ensemble.StackingClassifier()
!欢迎关注 @公众号:机器学习研习院 及 @公众号:数据STUDIO
!更多优质内容等你发现!

Stacking Generalization 的关键部分之一是使用 k-fold交叉验证 来生成 元模型 训练数据。但是,有一个变体,我们可以省略 k-fold交叉验证 而只使用 “ one-holdout set ”,这种小而显着的变体称为“ Blending ”。

Blending

Blending 是一种源自 Stacking Generalization 的技术。唯一的区别是在 Blending 中, k-fold 交叉验证技术 不用于生成元模型的训练数据。Blending实现了 “ one-holdout set ”,即一小部分训练数据(验证)进行预测,这些预测将被“Stacking”形成元模型的训练数据。此外,从测试数据进行预测以形成元模型测试数据。

在下图中,我们可以看到使用 3 个基本模型(弱学习器)和最终分类器的 Blending 架构。蓝色框表示用于生成预测(黄色框)以形成元模型的那部分训练数据。绿色框表示用于生成预测以形成元模型测试数据的测试数据(紫色框)。
Blending架构
现在我们已经熟悉了 Blending 架构,再来看看我们是如何在代码中做到这一点的:

上下滑动查看更多源码

import numpy as np
from sklearn.datasets import load_breast_cancer
from sklearn.model_selection import train_test_split

from sklearn.tree import DecisionTreeClassifier
from sklearn.ensemble import RandomForestClassifier
from sklearn.neighbors import KNeighborsClassifier
from sklearn.ensemble import GradientBoostingClassifier
from sklearn.naive_bayes import GaussianNB
from sklearn.linear_model import LogisticRegression


class Ensemble:
    def __init__(self):
        self.x_train = None
        self.x_test = None
        self.y_train = None
        self.y_test = None

    def load_data(self):
        x, y = load_breast_cancer(return_X_y=True)
        self.x_train, self.x_test, self.y_train, self.y_test = train_test_split(x, y, test_size=0.15, random_state=23)
        self.x_train, self.x_val, self.y_train, self.y_val = train_test_split(self.x_train, self.y_train, test_size=0.3,
                                                                              random_state=23)

    def BlendingClassifier(self):

        # Define weak learners
        weak_learners = [('dt', DecisionTreeClassifier()),
                         ('knn', KNeighborsClassifier()),
                         ('rf', RandomForestClassifier()),
                         ('gb', GradientBoostingClassifier()),
                         ('gn', GaussianNB())]

        # Final learner or meta model
        final_learner = LogisticRegression()

        train_meta_model = None
        test_meta_model = None

        # Start stacking
        for clf_id, clf in weak_learners:

            # 基于k-fold对每个分类器的预测
            val_predictions, test_predictions = self.train_level_0(clf)

            # 堆叠预测,构成数据模型的输入数据
            if isinstance(train_meta_model, np.ndarray):
                train_meta_model = np.vstack((train_meta_model, val_predictions))
            else:
                train_meta_model = val_predictions

            # 来自测试集的Stack预测将形成元模型的测试数据
            if isinstance(test_meta_model, np.ndarray):
                test_meta_model = np.vstack((test_meta_model, test_predictions))
            else:
                test_meta_model = test_predictions

        # Transpose train_meta_model
        train_meta_model = train_meta_model.T

        # Transpose test_meta_model
        test_meta_model = test_meta_model.T

        # Training level 1
        self.train_level_1(final_learner, train_meta_model, test_meta_model)

    def train_level_0(self, clf):
        # Train with base x_train
        clf.fit(self.x_train, self.y_train)

        # 为抵制集生成预测(验证)这些预测将构建元模型的输入
        val_predictions = clf.predict(self.x_val)

        # 为原始测试集生成预测
        # 这些预测将用于测试元模型
        test_predictions = clf.predict(self.x_test)

        return val_predictions, test_predictions

    def train_level_1(self, final_learner, train_meta_model, test_meta_model):
        # 用最终学习器或元模型进行训练
        final_learner.fit(train_meta_model, self.y_val)

        # 从meta_model中获取训练和测试精度
        print(f"Train accuracy: {final_learner.score(train_meta_model, self.y_val)}")
        print(f"Test accuracy: {final_learner.score(test_meta_model, self.y_test)}")


if __name__ == "__main__":
    ensemble = Ensemble()
    ensemble.load_data()
    ensemble.BlendingClassifier()
!欢迎关注 @公众号:机器学习研习院 及 @公众号:数据STUDIO
!更多优质内容等你发现!

正如我们所见, Blending 架构比 Stack Generalization 更简单、更紧凑。省略 k-fold 交叉验证可以使我们优化处理时间。

现在我们已经了解了 Stacked Generalization 架构及其工作原理以及其变种( Blending )。那么有人就有疑问了,哪种技术更好?我应该什么时候应用Stacking或Blending?其实这将 100% 取决于我们需要解决的任务、当前的数据量以及可用的计算能力和内存。

最后,这个简单、直观且有时可能是一个不错选择的 集成学习 技术,那就是 Voting !

Voting

这种集成类型是最直观和容易理解的。 Voting分类器 是 集成学习 的同质和异质类型,即基分类器可以是相同类型或不同类型。如前所述,这种类型的集成也可以作为 bagging 的扩展(例如Random Forest)。

Voting分类器 的架构由多个 ML 模型组成,其预测以两种不同的方式进行评估: 硬投票 和 软投票 。在 硬投票模式 中,选择“得票最多”的预测。在下图中,演示了 硬投票模式 中的 Voting分类器 。

“硬”模式下的投票分类器

另一方面,软模式下的 Voting分类器 考虑每个 ML 模型输出的概率,这些概率将被 加权和平均 ,因此获胜的类别将是 具有最高加权和平均概率 的类别。在下图中,演示了 软投票模式 中的 Voting分类器 。

“软”模式下的投票分类器

现在我们知道了 Voiting Classifier 是如何工作的,现在看看如何在代码中做到这一点。在这种情况下,由于它是一种简单直观的集成技术(与 Stacking 或 Blending 相比),我们利用 scikit-learn 提供的功能来实现 Voting 。

上下滑动查看更多源码

from sklearn.datasets import load_breast_cancer
from sklearn.model_selection import train_test_split

from sklearn.tree import DecisionTreeClassifier
from sklearn.neighbors import KNeighborsClassifier
from sklearn.linear_model import LogisticRegression

from sklearn.ensemble import VotingClassifier
from sklearn.metrics import accuracy_score


class Ensemble:
def __init__(self):
        self.x_train = None
        self.x_test = None
        self.y_train = None
        self.y_test = None

def load_data(self):
        x, y = load_breast_cancer(return_X_y=True)
        self.x_train, self.x_test, self.y_train, self.y_test = train_test_split(x, y, test_size=0.25, random_state=23)

    @staticmethod
def __Classifiers__(name=None):
# See for reproducibility
        random_state = 23

if name == 'decision_tree':
return DecisionTreeClassifier(random_state=random_state)
if name == 'kneighbors':
return KNeighborsClassifier()
if name == 'logistic_regression':
return LogisticRegression(random_state=random_state, solver='liblinear')

def __DecisionTreeClassifier__(self):

# Decision Tree Classifier
        decision_tree = Ensemble.__Classifiers__(name='decision_tree')

# Train Decision Tree
        decision_tree.fit(self.x_train, self.y_train)

def __KNearestNeighborsClassifier__(self):

# K-Nearest Neighbors Classifier
        knn = Ensemble.__Classifiers__(name='kneighbors')

# Train K-Nearest Neighbos
        knn.fit(self.x_train, self.y_train)

def __LogisticRegression__(self):

# Decision Tree Classifier
        logistic_regression = Ensemble.__Classifiers__(name='logistic_regression')

# Init Grid Search
        logistic_regression.fit(self.x_train, self.y_train)

def __VotingClassifier__(self):

# Instantiate classifiers
        decision_tree = Ensemble.__Classifiers__(name='decision_tree')
        knn = Ensemble.__Classifiers__(name='kneighbors')
        logistic_regression = Ensemble.__Classifiers__(name='logistic_regression')

# Voting Classifier initialization
        vc = VotingClassifier(estimators=[('decision_tree', decision_tree),
                                          ('knn', knn), ('logistic_regression',
                                                         logistic_regression)], voting='soft')

# Fitting the vc model
        vc.fit(self.x_train, self.y_train)

# Getting train and test accuracies from meta_model
        y_pred_train = vc.predict(self.x_train)
        y_pred = vc.predict(self.x_test)

        print(f"Train accuracy: {accuracy_score(self.y_train, y_pred_train)}")
        print(f"Test accuracy: {accuracy_score(self.y_test, y_pred)}")


if __name__ == "__main__":
    ensemble = Ensemble()
    ensemble.load_data()
    ensemble.__VotingClassifier__()
!欢迎关注 @公众号:机器学习研习院 及 @公众号:数据STUDIO
!更多优质内容等你发现!

在上面的代码中,我们创建了一个包含不同分类器的类,它们是: 决策树 、 K-最近邻 、 逻辑回归 和 Voting分类器 。使用“ breast_cancer ”玩具数据集来比较 弱分类器 和 集成分类器 之间的有效性,并且每个分类器均使用其默认参数。

代码中,我们定义了将要使用的 弱分类器 ,并且将这些分类器以一种简单的方式作为参数传递给 Voting分类器 。此处我们使用的是“ hard ”模式。最后训练了集成模型,并得到的:

Decision Tree, Train accuracy: 1
Decision Tree, Test accuracy: 0.958
K-Nearest Neighbors, Train accuracy: 0.930
K-Nearest Neighbors, Test accuracy: 0.946
Logistic Regression, Train accuracy: 0.953
Logistic Regression, Test accuracy: 0.951
Voting Classifier, Train accuracy: 0.981
Voting Classifier, Test accuracy: 0.965
从结果看,Voting分类器的测试准确率略好于弱分类器。值得一提的是,尽管 投票分类器 是提高模型准确性的绝佳选择,但由于各种因素(包括处理时间),它可能并不总是最佳选择。

写在最后

本文中,我们了解了 集成学习 是什么以及它最常用的技术。并且深入研究了 Stacking 、 Blending 和 Voting 集成技术。其中 Blending 是一种源自 Stacking 的技术。

其实,我们有许多技术可以提供 ML 模型的准确性和有效性,而每个数据科学家都可以选择最适合他们问题的技术,可以从以下几个因素方面考虑:可用数据量、要进行的学习类型、可用计算处理量等。

参考资料

[1] 集成学习: http://www.scholarpedia.org/article/Ensemble_learning [2] 有效性: https://tjzhifei.github.io/links/EMFA.pdf [3] David H. Wolpert: https://www.sciencedirect.com/science/article/abs/pii/S0893608005800231

🏴‍☠️宝藏级🏴‍☠️ 原创公众号『 数据STUDIO 』内容超级硬核。公众号以Python为核心语言,垂直于数据科学领域,包括 可戳 👉 Python | MySQL | 数据分析 | 数据可视化 | 机器学习与数据挖掘 | 爬虫 等,从入门到进阶!
长按👇关注- 数据STUDIO -设为星标,干货速递