相关文章
【机器学习】集成学习 (Ensemble Learning) (一) —— 导引
【机器学习】集成学习 (Ensemble Learning) (二) —— Bagging 与 Random Forest
【机器学习】集成学习 (Ensemble Learning) (三) —— Boosting 与 Adaboost + GBDT
目录
2.3 堆叠法 (Stacking) 与 融合法 (Blending)
2.3 堆叠法 (Stacking) 与 融合法 (Blending)
2.3.1 Stacking
Stacking 学习几个不同的弱学习器,并通过训练一个元模型来组合这些弱模型的输出,作为最终的预测结果。
Stacking 与 Bagging、Boosting 相比,主要 差异 如下:
- Stacking 主要使用 异质弱学习器(不同模型),而 Bagging 和 Boosting 主要使用 同质弱学习器
- Stacking 训练一个 元模型 来组合基础模型,而 Bagging 和 Boosting 则 根据 确定性算法 组合弱学习器
因此,为构建 Stacking 模型,需定义至少两部分(通常是 2 层)模型
- 多个需要拟合的 弱学习器模型
- 一个用于组合各弱学习器模型的 元模型
例如,对于分类问题,可选 KNN、Logistic Regression、SVM 等作为弱分类器,并采用学习神经网络 NN 作为元模型。然后,神经网络将会把三个弱学习器的输出作为输入,并返回基于该输入的最终预测。所以,假设要拟合由 L 个弱学习器组成的 Stacking 集成模型,须遵循以下步骤:
- 将训练数据分为两组;
- 选择 L 个弱学习器,用它们拟合第一组数据;
- 使 L 个学习器中的每个学习器对第二组数据中的观测数据进行预测;
- 在第二组数据上拟合元模型,使用弱学习器做出的预测作为输入。
在前面的步骤中,将数据集一分为二,因为对用于训练弱学习器的数据的预测与元模型的训练 不相关。因此,将数据集分成两部分的一个 明显缺点 是:只有一半的数据用于训练基础模型,另一半数据用于训练元模型。
为克服这种限制,可使用某种 K 折交叉训练 方法(类似于 K 折交叉验证的做法)。这样所有的观测数据都可用来训练元模型:对于任意观测数据,弱学习器的预测都是通过在 K-1折数据(不包含已考虑的观测数据)上训练这些弱学习器的实例来完成的。换言之,它在 K-1 折数据上训练,在剩下 1 折数据上预测。迭代地重复改过程,就可得到对任 1 折观测数据的预测结果。这样一来,就可为数据集中的每个观测数据生成相关的预测,然后使用所有这些预测结果训练元模型。
Stacking 从数据集中训练出初级学习器,然后 ”生成“ 一个新的数据集用于训练次级学习器。由于深度学习模型一般需要较长的训练周期,如果硬件设备不允许建议选取留出法,如果需要追求精度可以使用交叉验证的方法。加之为防止过拟合,可采用 K 折交叉验证 求解。假设采用 5 折交叉验证,每个模型都要做满 5 次训练和预测,对于每一次:
- 从 80% 的数据训练得到一个模型 ht,然后预测训练集剩下的 20%,同时也要预测测试集。
- 每次有 20% 的训练数据被预测,5 次后正好每个训练样本都被预测过了。
- 每次都要预测测试集,因此最后测试集被预测 5 次,最终结果取 5 次的平均。
一个示例:
Stacking 算法
Stacking 的实现
(1) 最基本的使用方法,即使用前面分类器产生的特征输出 或 概率输出 作为 meta-classifier 的输入数据
from sklearn.datasets import load_iris from sklearn.preprocessing import StandardScaler from sklearn import model_selection from sklearn.linear_model import LogisticRegression from sklearn.neighbors import KNeighborsClassifier from sklearn.naive_bayes import GaussianNB from sklearn.ensemble import RandomForestClassifier from mlxtend.classifier import StackingClassifier from sklearn.metrics import accuracy_score from sklearn.metrics import confusion_matrix from sklearn.metrics import classification_report import warnings; warnings.filterwarnings(action='ignore') # 载入iris数据集 iris = load_iris() X = iris.data[:, :5] y = iris.target print('5 features = ', X[:5, :]) print('5 targets = ', y[:5]) print('\n') # 实现Stacking集成 def StackingMethod(X, y): ''' Stacking方法实现分类 INPUT -> 特征, 分类标签 ''' scaler = StandardScaler() # 标准化转换 scaler.fit(X) # 训练标准化对象 traffic_feature= scaler.transform(X) # 转换数据集 feature_train, feature_test, target_train, target_test = model_selection.train_test_split(X, y, test_size=0.3, random_state=0) # 弱分类器 clf1 = LogisticRegression(random_state=1) # 弱分类器 - Logistic 回归 clf2 = RandomForestClassifier(random_state=1) # 若分类器 - 随机森林 clf3 = GaussianNB() # 弱分类器 - 先验为高斯分布的朴素贝叶斯 # Stacking 集成 sclf = StackingClassifier(classifiers=[clf1, clf2, clf3], # use_probas=True, 类别概率值作为meta-classfier的输入 # average_probas=False, 是否对每一个类别产生的概率值做平均 meta_classifier=LogisticRegression()) sclf.fit(feature_train, target_train) # 模型测试 predict_results = sclf.predict(feature_test) conf_mat = confusion_matrix(target_test, predict_results) print(f"test accuracy score: {accuracy_score(predict_results, target_test)}") print(f"confusion_matrix: {conf_mat}") print(f"classification_report: {classification_report(target_test, predict_results)}") # 5折交叉验证 for clf, label in zip([clf1, clf2, clf3, sclf], ['Logistic Regression', 'Random Forest', 'naive Bayes', 'StackingModel']): scores = model_selection.cross_val_score(clf, X, y, cv=5, scoring='accuracy') print("Accuracy: %0.2f (+/- %0.2f) [%s]" % (scores.mean(), scores.std(), label)) return sclf if __name__ == '__main__': model = StackingMethod(X, y)
5 features = [[5.1 3.5 1.4 0.2] [4.9 3. 1.4 0.2] [4.7 3.2 1.3 0.2] [4.6 3.1 1.5 0.2] [5. 3.6 1.4 0.2]] 5 targets = [0 0 0 0 0] test accuracy score: 0.9777777777777777 confusion_matrix: [[16 0 0] [ 0 17 1] [ 0 0 11]] classification_report: precision recall f1-score support 0 1.00 1.00 1.00 16 1 1.00 0.94 0.97 18 2 0.92 1.00 0.96 11 accuracy 0.98 45 macro avg 0.97 0.98 0.98 45 weighted avg 0.98 0.98 0.98 45 Accuracy: 0.97 (+/- 0.02) [Logistic Regression] Accuracy: 0.97 (+/- 0.02) [Random Forest] Accuracy: 0.95 (+/- 0.03) [naive Bayes] Accuracy: 0.97 (+/- 0.02) [StackingModel]
(2) 另一种方法,对训练集中的特征维度进行操作,此时不是给每一个基分类器全部的特征,而是给不同的基分类器分配不同的特征。如基分类器 1 训练前半部分特征,基分类器2 训练后半部分特征(可通过 sklearn 的 pipelines 实现),最终通过 StackingClassifier 组合起来。
from sklearn.datasets import load_iris from sklearn.preprocessing import StandardScaler from sklearn import model_selection from sklearn.linear_model import LogisticRegression from mlxtend.feature_selection import ColumnSelector from mlxtend.classifier import StackingClassifier from sklearn.pipeline import make_pipeline from sklearn.metrics import accuracy_score from sklearn.metrics import confusion_matrix from sklearn.metrics import classification_report import warnings; warnings.filterwarnings(action='ignore') # 载入iris数据集 iris = load_iris() X = iris.data y = iris.target print('5 features = ', X[:5, :]) print('5 targets = ', y[:5]) print('\n') iris = load_iris() X = iris.data y = iris.target # 实现Stacking集成 def StackingMethod(X, y): ''' Stacking方法实现分类 INPUT -> 特征, 分类标签 ''' scaler = StandardScaler() # 标准化转换 scaler.fit(X) # 训练标准化对象 traffic_feature = scaler.transform(X) # 转换数据集 feature_train, feature_test, target_train, target_test = model_selection.train_test_split(X, y, test_size=0.3, random_state=0) pipe1 = make_pipeline(ColumnSelector(cols=(0, 2)), LogisticRegression()) pipe2 = make_pipeline(ColumnSelector(cols=(1, 2, 3)), LogisticRegression()) sclf = StackingClassifier(classifiers=[pipe1, pipe2], meta_classifier=LogisticRegression()) sclf.fit(feature_train, target_train) # 模型测试 predict_results = sclf.predict(feature_test) conf_mat = confusion_matrix(target_test, predict_results) print(f"test accuracy score: {accuracy_score(predict_results, target_test)}") print(f"confusion_matrix: {conf_mat}") print(f"classification_report: {classification_report(target_test, predict_results)}") return sclf if __name__ == '__main__': model = StackingMethod(X, y)
5 features = [[5.1 3.5 1.4 0.2] [4.9 3. 1.4 0.2] [4.7 3.2 1.3 0.2] [4.6 3.1 1.5 0.2] [5. 3.6 1.4 0.2]] 5 targets = [0 0 0 0 0] test accuracy score: 0.9777777777777777 confusion_matrix: [[16 0 0] [ 0 17 1] [ 0 0 11]] classification_report: precision recall f1-score support 0 1.00 1.00 1.00 16 1 1.00 0.94 0.97 18 2 0.92 1.00 0.96 11 accuracy 0.98 45 macro avg 0.97 0.98 0.98 45 weighted avg 0.98 0.98 0.98 45
Stacking 的特点
- 它可以帮你打败当前学术界性能最好的算法
- 有可能将集成的知识迁移到到简单的分类器上
- 自动化的大型集成策略可通过添加正则项有效的对抗过拟合,且无需太多的调参和特征选择,所以原则上 Stacking 非常适合 “懒人”
- 这是目前提升机器学习效果最好的方法,或者说是最效率的方法 Human Ensemble Learning
2.3.2 Stacking 与 神经网络 NN
Stacking 不是万能药,但往往很有效。通过与神经网络的对比,可以从另一个角度加深对 Stacking 的理解。
(1) Stacking是一种表示学习 (representation learning)
表示学习 指的是 模型从原始数据中自动抽取有效特征的过程,如深度学习。关于表示学习的理解可以参考:阿萨姆:人工智能(AI)是如何处理数据的?原始数据可能是杂乱无规律的。在 Stacking 中,通过第一层的多个学习器后,有效的特征被学习出来了。从该角度看,Stacking 的第一层即为特征抽取的过程。
在相关研究中,一些图示可说明情况:上排是未经 Stacking 的数据,下排是经过 Stacking (多个无监督学习算法) 处理后的数据。可见,红色和蓝色的数据在下排中分界更为明显。(* 数据经过了压缩处理)。此例表明,有效的 Stacking 可有效地抽取原始数据中的有用特征。
(2) Stacking 和神经网络从某种角度看异曲同工,神经网络也可被视为是一种集成学习
一方面,Stacking 的学习能力主要来自对特征的表示学习,思路同神经网络。这也是为什么上述说成 “第一层”、“最后一层”。
另一方面,神经网络也可被视为是一种集成学习,主要取决于不同神经元、网络层对不同特征的不同理解。从浅层到深层可看作是一种从具体到抽象的过程。
Stacking 的第一层可等价于神经网络的前 n-1 层隐藏层,而 Stacking 的最终分类层可类比于神经网络最后的输出层。不同之处在于,Stacking 中不同的分类器通过异质来体现对于不同特征的表示,神经网络则是从同质到异质的过程 且 有分布式表示的特点 (distributed representation)。Stacking 中应也有分布式的特点,主要表现在多个分类器的结果并非完全不同,而有很大程度的相同之处。
但同时这也提出了一个挑战,多个分类器应尽量在保证效果好的同时尽量不同,Stacking 对基分类器的两个要求:
- 差异化 (diversity) 要大
- 准确性 (accuracy) 要高
(3) Stacking 的输出层为什么用逻辑回归?
Stacking 的有效性主要来自于特征抽取。而 表示学习 中,如影随形的问题就是 过拟合,试回想深度学习中的过拟合问题。
周志华教授也曾重申了 Stacking 的过拟合问题。因为第二层的特征来自于对于第一层数据的学习,那么 第二层数据中的特征中不应包括原始特征,以降低过拟合的风险。例如:
- 第二层数据特征来源:仅包含第一层学习到的特征
- 第二层数据特征来源:既包含第一层学习到的特征,又包含输入第一层的原始特征
另一个例子是,Stacking 中一般都用交叉验证来避免过拟合,足可见这个问题的严重性。
为降低过拟合风险,第二层分类器应较简单,广义线性如逻辑回归就是一个不错的选择,当然 MLR 也是。在特征提取的过程中,已经使用了复杂的非线性变换,因此在输出层就不需要复杂的分类器了。这一点可类比神经网络的激活函数或输出层,都是相对简单的函数,一点原因就是无需复杂函数且能控制复杂度。
因此,Stacking 的输出层无需过分复杂的函数。此外,用逻辑回归作为第二层分类器还有额外的好处:
- 逻辑回归配合 L1 正则化可进一步防止过拟合
- 逻辑回归配合 L1 正则化可选择有效特征,从第一层的学习器中删除不必要的分类器,节省运算开销。
- 逻辑回归的输出结果还可被理解为概率
(4) Stacking 是否需要多层?第一层的分类器是否越多越好?
综上可知,Stacking 的表示学习的效果不是来自于多层堆叠,而是源于不同学习器对不同特征的学习能力 及其有效的结合。通常,2 层 Stacking 足够了,更多层则会面临更加复杂的过拟合问题,且收益有限。
第一层分类器的数量对于特征学习应该有所帮助,从经验角度看,异质基分类器越多越好。即使有所重复和高依赖性,也依然可以通过特征选择来处理,问题不大。
这提出了 另一个 Stacking 与深度学习的差别:
- Stacking 需要宽度,深度学习不需要
- 深度学习需要深度,而 Stacking不需要
但 Stacking 和深度学习都均需面临:
- 黑箱与解释性问题
- 严重的过拟合问题
若 Stacking 和深度学习都是一种表示学习,如何选择?这可能和样本量有关:
- 小样本上深度学习不具备可操作性,Stacking 或许可以
- 大样本上 Stacking 的效率理当不如深度学习,这也是一种取舍
2.3.3 Blending
Blending 与 Stacking 大致相同,只是 Blending 的主要区别在于训练集不是通过 K-Fold 的 CV 策略来获得预测值从而生成第二阶段模型的特征,而是建立一个 Holdout 集,例如10% 的训练数据,第二阶段的 stacker 模型就基于第一阶段模型对这 10% 训练数据的预测值进行拟合。说白了,就是把 Stacking 流程中的 K-Fold CV 改成 HoldOut CV。
流程上,Blending 相较 Stacking 更简单些,大致为:
- 将数据划分为训练集和测试集 (test_set),其中训练集需要再次划分为训练集 (train_set) 和验证集 (val_set);
- 创建第一层的多个弱学习器模型,其既可为同质,也可为异质;
- 使用 train_set 训练步骤 2 中的多个模型,然后用训练好的模型预测 val_set 和 test_set 得到 val_predict, test_predict1;
- 创建第二层的模型,使用 val_predict 作为训练集训练第二层的模型;
- 使用第二层训练好的模型对第二层测试集 test_predict1 进行预测,该结果为整个测试集的结果。
Blending 优点
- 比 Stacking 简单(因为不用进行 K 次的交叉验证来获得 stacker features)
- 避开了一个信息泄露问题:generlizers 和 stacker 使用了不一样的数据集
- 在团队建模过程中,无需给队友分享自己的随机种子
Blending 缺点
- 使用了很少的数据(是划分 hold-out 作为测试集,并非 cv)
- 可能会过拟合(其实大概率是第一点导致的)
- Stacking 使用多次的 CV 会比较稳健
参考文献