欢迎关注公众号:DeepL Newer

MENU

模型融合

April 4, 2020 • Read: 3251 • 数据挖掘与机器学习阅读设置

一般来说,通过融合多个不同模型的结果,可以提升最终的成绩,所以这以方法在各种数据竞赛中应用非常广泛。模型融合又可以从模型结果、模型自身、样本集等不同的角度进行融合。通过这篇文章,希望能让大家真正理解各种模型融合的方式及原理

首先,针对不同的任务(分类or回归),从简单的加权融合开始,介绍分类任务中的投票(Voting)原理和具体实现。然后介绍回归任务中的平均(Averaging)融合机制。之后简单介绍一下Boosting和Bagging以及两者的关联。最后重点介绍Stacking/Blending的原理和具体实现

大纲如下

  • 回归任务中的加权融合 与 分类任务中的Voting
  • Boosting/Bagging的原理及对比
  • Stacking/Blending构建多层模型(原理、实现和对比)
  • 总结

1. 回归任务中的加权融合 与 分类任务中的Voting

1.1 回归任务中的加权融合

单从字面上看很好理解,就是根据各个模型的最终预测表现分配不同的权重,以改变其队最终结果影响的大小。例如,对于正确率低的模型给予较小的权重,而正确率高的模型给予更高的权重。以下面的代码为例

# 生成一些简单的样本,test_predi代表第i个模型的预测值
test_pred1 = [1.2, 3.2, 2.1, 6.2]
test_pred2 = [0.9, 3.1, 2.0, 5.9]
test_pred3 = [1.1, 2.9, 2.2, 6.0]

# y_test_true 代表模型的真实值
y_test_true = [1, 3, 2, 6]

# 可以先看一下各个模型的预测结果
print('Pred1 MAE:',mean_absolute_error(y_test_true, test_pred1)) 
print('Pred2 MAE:',mean_absolute_error(y_test_true, test_pred2)) 
print('Pred3 MAE:',mean_absolute_error(y_test_true, test_pred3))

## 结果:
Pred1 MAE: 0.175
Pred2 MAE: 0.075
Pred3 MAE: 0.1

可以发现,第2个模型的误差更小,准确率更高,所以我们无论用什么方式,都应该给第二个模型的预测值赋予更高的权重

# 加权融合,权重的默认值是(1/n),n为模型个数,相当于默认使用平均加权融合
def weighted_method(test_pred1, test_pred2, test_pred3, w = [1/3, 1/3, 1/3]):
    weighted_result = w[0] * pd.Series(test_pred1) + w[1] * pd.Series(test_pred2) + w[2] * pd.Series(test_pred3)
    return weighted

# 根据上面的MAE,计算每个模型的权重(MAE越小,权重越大)
w = [0.3, 0.4, 0.3] # 这个权重我是自定义的,也可以使用一些其它方法,例如softmax
weighed_pred = weighted_method(test_pred1, test_pred2, test_pred3, w)
print('Weighted_pred MAE:',mean_absolute_error(y_test_true, Weighted_pre))   # 融合之后效果提高了一些

# 结果
Weighted_pre MAE: 0.0575

上述加权融合的技术是从模型结果的层面进行的,就是让每个模型跑一遍结果,然后对所有的结果进行融合,当然融合的方式不只有加权平均,还有例如平均、取中位数等,下面给出取平均和中位数的方式

# 定义结果的平均函数
def Mean_method(test_pre1,test_pre2,test_pre3):
    Mean_result = pd.concat([pd.Series(test_pre1),pd.Series(test_pre2),pd.Series(test_pre3)],axis=1).mean(axis=1)
    return Mean_result

## 定义结果的中位数平均函数
def Median_method(test_pre1,test_pre2,test_pre3):
    Median_result = pd.concat([pd.Series(test_pre1),pd.Series(test_pre2),pd.Series(test_pre3)],axis=1).median(axis=1)
    return Median_result

1.2 分类任务中的Voting

投票(Voting)是集成学习里面针对分类问题的一种结果融合策略。其基本思想是选择所有模型输出结果中,最多的那个类。即少数服从多数

在不改变模型的情况下,直接对不同模型的预测结果进行投票或者平均,这是一种简单却行之有效的融合方式。比如分类问题,假设有三个相互独立的模型,每个模型的正确率都是70%,采用少数服从多数的方式进行投票,那么最终的正确率将是

$$ 0.7*0.7*0.7+0.7*0.7*0.3*3=0.343+0.441=0.784 $$

融合后预测正确的情况有两种,一种是三个模型都预测对了,另一种是其中两个模型预测对了,有一个模型预测错了。对于这两种情况,由于少数服从多数的机制存在,会使得最终结果都对。3个0.7相乘对应的就是第一种情况,$0.7*0.7*0.3*3$对应的就是第二种情况

经过简单的投票后,正确率提升了8%。这是一个简单的概率问题——如果进行投票的模型越多,显然其结果将会更好,但前提条件是模型之间相互独立,结果之间没有相关性。越相近的模型进行融合,融合效果也会越差

sklearn中的VotingClassifier就实现了投票法。投票法的输出有两种类型:一种是直接输出类别标签,另一种是输出类别概率。前者叫做硬投票(Marjority/Hard voting),后者叫做软投票(Soft voting)。硬投票就是少数服从多数的原则,但有时候少数服从多数并不适用,更加合理的投票方式应该是有权值的投票。在现实生活中也有这样的例子,比如在唱歌比赛中,专业评审一人可以投10票,而观众一人只能投一票

简单介绍一下硬投票和软投票

  • 硬投票选择各个模型输出最多的标签,如果标签数量相同,那么按照升序的次序进行选择

hard voting的少数服从多数原则在上面这种情况似乎不太合理,虽然只有模型1和模型4结果为A,但它们俩的概率的高于90%,也就是说很确定结果为A,其它三个模型结果为B,但从概率来看,并不是很确定

  • 软投票是根据各个模型输出的类别概率来进行类别的预测。如果给定权重,则会得到每个类别概率的加权平均值;否则就是普通的算术平均值

下面以鸢尾花数据集简单的测试一下投票法和单个模型的效果对比

from sklearn.datasets import load_iris

from sklearn.linear_model import LogisticRegression
from sklearn.tree import DecisionTreeClassifier
from sklearn.svm import SVC
from sklearn.ensemble import VotingClassifier
from sklearn.ensemble import AdaBoostClassifier
from xgboost import XGBClassifier
from sklearn.ensemble import RandomForestClassifier

iris = load_iris()

x = iris.data
y = iris.target
x_train, x_test, y_train, y_test = train_test_split(x, y, test_size=0.3)

clf1 = XGBClassifier(learning_rate=0.1, n_estimators=150, max_depth=3, min_child_weight=2, subsample=0.7,
                     colsample_bytree=0.6, objective='binary:logistic')
clf2 = RandomForestClassifier(n_estimators=50, max_depth=1, min_samples_split=4,
                              min_samples_leaf=63,oob_score=True)
clf3 = SVC(C=0.1, probability=True)  # 软投票的时候,probability必须指定且为true

# 硬投票
eclf = VotingClassifier(estimators=[('xgb', clf1), ('rf', clf2), ('svc', clf3)], voting='hard')
for clf, label in zip([clf1, clf2, clf3, eclf], ['XGBBoosting', 'Random Forest', 'SVM', 'Voting']):
    scores = cross_val_score(clf, x, y, cv=5, scoring='accuracy')
    print("Accuracy: %0.2f (+/- %0.2f) [%s]" % (scores.mean(), scores.std(), label))

# 结果如下:
Accuracy: 0.96 (+/- 0.02) [XGBBoosting]
Accuracy: 0.33 (+/- 0.00) [Random Forest]
Accuracy: 0.95 (+/- 0.03) [SVM]
Accuracy: 0.94 (+/- 0.04) [Voting]

# 软投票只需要设置voting='soft'即可,这样最后的Voting正确率会成为0.96

投票法非常简单,但是这种方法也存在风险,很明显,如果融合的模型中有些结果并不是很好,就会把整体的结果往下拉

到此为止已经介绍了结果层面的融合方式。回归任务一般将多个模型的结果进行加权融合,分类任务一般采用投票法获取最终的结果

2. Boosting/Bagging的原理及对比

Boosting/Bagging这两个都是从样本集的角度考虑把多个若模型集成起来的一种方式,只不过两者在集成的时候有些区别。xgb、lgb就属于Boosting,而随机森林就是Bagging的方式

2.1 Boosting

Boosting是将各种弱分类器串联起来的集成学习方式,每一个分类器的训练都依赖于前一个分类器的结果。串联(顺序运行)的方式导致了运行速度比较慢。和所有融合方式一样,它不会考虑各个弱分类器的内部结构,只是对训练数据(样本集)和连接方式进行操纵,以获得更小的误差。其基本思想是一种迭代的方法,每次训练的时候都更关心分类错误的样例,给这些分类错误的样例增加更大的权重,下一次迭代的目标就是更容易辨别出上一轮分类错误的样例。最终将这些弱分类器进行加权相加。引用加州大学Alex lhler教授的两页PPT

Boosting可以这么理解,比如我想用很多模型M1,M2,...,Mn去预测二手车的价格,但是这些模型的具体工作是这样安排的。首先让M1先训练然后预测价格,等M1预测完了之后,M2的训练是对M1训练的改进和提升,即优化M1没有做好的事情。同样,M3会基于M2的结果再次进行优化,这样一直到Mn。这就是所谓的串联,即在训练过程中这K个模型之间是有依赖关系的,当引入第i个模型的时候,实际上是对前i-1个模型进行优化。最终的预测结果是对这k个模型结果的一个大组合

Boosting家族的代表有A打Boost、GBDT、xgboost、lightbgm等,但是这些模型之间还是有区别的,可以分成AdaBoost流派和GBDT流派。比如AdaBoost,在引入M2的时候,其实它关注的是M1预测不好的那些样本,这些样本在M2训练的时候,会加大权重。后面的模型引入也都是这个道理, 即关注前面模型预测不好的那些样本;而GBDT,包括后面的xgboost这些,它们则是更加聚焦于残差,即M2引入的时候,它关注的是M1的所有预测结果与真实结果之间的差距,它想减少这个差距,后面的模型引入也是这个道理,即关注前面模型预测结果与真实结果之间的差距,然后一步一步的进行缩小

2.2 Bagging

Bagging是Bootstrap Aggregating的缩写。这种方法也不对模型本身进行操作,而是作用于样本集上。采用的是随机有放回的选择性训练数据,然后构造分类器,最后进行组合。与Boosting方法中个分类器之间相互依赖和串行运行不同,Bagging方法中的学习器之间不存在强依赖关系,而是同时生成并运行

其基本思路为:

  • 在样本集中进行K轮有放回的抽样,每次抽取n个样本,得到K个训练集;
  • 分别用K个训练集训练得到K个模型
  • 对得到的K个模型预测结果用投票或平均的方式进行融合

在这里,训练集的选取可能不会包含所有样本集,未被包含的数据将成为包外数据,用来进行包外误差的泛化估计。每个模型的训练过程中,每次训练集可以取全部的特征进行训练,也可以随机取部分特征进行训练。例如极具代表性的随机森林算法就是每次随机选取部分特征

下面仅从思想层面介绍随机森林算法:

  • 在样本集中进行K轮有放回的抽样,每次抽取n个样本,得到K个训练集,其中n一般远小于总样本数量
  • 选取训练集,在整体特征集M中选取部分特征集m构建决策树,其中$m<<M$
  • 在构造每颗决策树的过程中,按照选取最小的基尼指数进行分裂节点的选取,构建决策树。决策树的其它节点都采取相同的分裂规则进行构建,直到该节点的所有训练样例都属于同一类或达到树的最大深度
  • 重复上述步骤,得到随机森林
  • 多颗决策树同时进行预测,对结果进行投票或平均得到最终的分类结果

多次随机选择的过程,使得随机森林不容易过拟合且有很好的抗干扰能力

2.3 Boosting和Bagging的比较

下面从不同角度对Boosting和Bagging进行比较

  • 优化方式

在机器学习中,我们训练一个模型的过程通常是将Loss最小化的过程。但是单单最小化Loss并不能保证模型在解决一般化的问题时能够最优,甚至不能保证模型可用,也就是模型泛化能力不够。训练数据集的Loss与一般化数据集的Loss之间的差异被称为generalization error

$$ error = Bias + Variance $$

$Variance$过大会导致模型过拟合,而$Bias$过大会导致模型欠拟合

Bagging方法主要通过降低Variance来降低error,Boosting方法主要通过降低Bias来降低error

Bagging方法采用多个不完全相同的训练集训练多个模型,最后结果取平均,由于$E[\frac{\sum X_i}{n}]=E[X_i]$,所以最终结果的Bias于单个模型的Bias很相近,一般不会显著降低Bias。而对于Variance,有

$$ \begin{align*} Var[\frac{\sum X_i}{n}]&=\frac{Var[X_i]}{n}, \ 子模型互相独立\\ Var[\frac{\sum X_i}{n}]&=Var[X_i], \ 子模型完全相同 \end{align*} $$

Bagging的多个子模型由不完全相同的数据集训练而成,子模型间有一定的相关性但又不完全独立,所以其结果在上述两式的中间状态,因此可以在一定程度上降低Variance,从而使得总error减小

Boosting方法从优化角度来说, 是用forward-stagewise这种贪心法去最小化损失函数$L(y, \sum a_if_i(x))$。forward-stagewise就是在迭代的第n步,求解新的子模型$f(x)$及步长$a$来最小化$L(y,f_{n-1}(x)+af(x))$,这里的$f_{n-1}(x)$是前n步得到的子模型的和。因此Boosting在最小化损失函数,Bias自然逐步下降,而由于模型之间的强相关性,所以并不能显著降低Variance

  • 样本选择

Bagging:训练集是在原始集中有放回选取的,从原始集中选出的各轮训练集之间是独立的

Boosting:每一轮的训练集不变,只是训练集中每个样本在分类器中的权重发生变化。而权值是根据上一轮的分类结果进行调整的

  • 样本权重

Bagging:使用均匀取样,每个样本的权重相等

Boosting:根据错误率不断调整样本的权重,错误率越大则权重越大

  • 预测函数

Bagging:所有预测函数的权重相等

Boosting:每个弱分类器都有相应的权重,误差小的分类器权重更大

  • 并行计算

Bagging:各个预测函数可以并行生成

Boosting:理论上各个预测函数只能顺序生成,因为后一个模型参数需要前一轮模型的结果

更多详情可以参考这篇博客模型融合方法学习总结

3. Stacking/Blending构建多层模型(原理、实现和对比)

3.1 Stacking

Stacking的本质是一种分层的结构,用了大量的基分类器,将其预测的结果作为下一层输入的特征,这样的结构使得它比相互独立训练的模型能够获得更多的特征

下面以一种易于理解但不会实际使用的两层stacking方法为例,简要说明其结构和工作原理

假设我们有三个基模型M1,M2,M3和一个元模型M4,有训练集train和测试集test,则:

  1. 用训练集train训练基模型M1(M1.fit(train)),然后分别在train和test上做预测,得到P1(M1.predict(train))和T1(M1.predict(test)
  2. 用训练集train训练基模型M2(M2.fit(train)),然后分别在train和test上做预测,得到P2(M2.predict(train))和T2(M2.predict(test)
  3. 用训练集train训练基模型M3(M3.fit(train)),然后分别在train和test上做预测,得到P3(M3.predict(train))和T3(M3.predict(test)

这样第一层的模型就训练结束了,接下来

  1. 把P1,P2,P3进行合并组成新的训练集train2,把T1,T2,T3进行合并组成新的测试集test2
  2. 用新的训练集train2训练元模型M4(M4.fit(train2)),然后在test2上进行预测得到最终的预测结果Y_pred(M4.predict(test2)

这样第二层训练预测就得到了最终的预测结果。这就是两层堆叠的一种基本的原始思路。Stacking本质上就是这么直接的思路,但是直接这样做,对于训练集和测试集分布不那么一致的情况下是有问题的,其问题在于用训练集训练原始模型,又接着用训练的模型去预测训练集,这不明摆着会过拟合吗?因此,现在的问题变成如何降低再训练的过拟合问题。一般有两种解决方法

  • 次级模型尽量选择简单的线性模型
  • 第一层训练模型使用交叉验证的方式

第一种方法很容易理解,重点是看第二种方法到底是怎么做的,先放一张图体会一下

以5折交叉验证为例

  1. 首先将训练集分成5份
  2. 对于每一个基模型$i$来说,用其中4份进行训练,然后用另一份训练集作验证集进行预测,得到$P_i$的一部分,然后再用测试集进行预测得到$T_i$的一部分,这样5轮下来之后,验证集的预测值就会拼接成一个完整的$P$,测试集的label值取个平均就会得到一个完整的$T$
  3. 所有的$P_i$合并就得到了下一层的训练集train2,所有的$T_i$合并就得到了下一层的测试集test2
  4. 利用train2训练第二层的模型,然后在test2上得到预测结果,就是最终的结果

Stacking的过程可以用下面的图表示

3.1.1 回归中的stacking

我们先拿之前的回归例子看看Stacking的效果

# 生成一些简单的样本数据, test_predi代表第i个模型的预测值
train_reg1 = [3.2, 8.2, 9.1, 5.2]
train_reg2 = [2.9, 8.1, 9.0, 4.9]
train_reg3 = [3.1, 7.9, 9.2, 5.0]

# y_test_true代表模型的真实值
y_train_true = [3, 8, 9, 5]

test_pred1 = [1.2, 3.2, 2.1, 6.2]
test_pred2 = [0.9, 3.1, 2.0, 5.9]
test_pred3 = [1.1, 2.9, 2.2, 6.0]

y_test_true = [1, 3, 2, 6]

def Stacking_method(train_reg1, train_reg2, train_reg3, y_train_true, test_pred1, test_pred2, test_pred3, model_L2 = LinearRegression()):
    model_L2.fit(pd.concat([pd.Series(train_reg1), pd.Series(train_reg2), pd.Series(train_reg3)], axis=1).values, y_train_true)
    Stacking_result = model_L2.predict(pd.concat([pd.Series(test_pre1), pd.Series(test_pre2), pd.Series(test_pre3)], axis=1).values)
    return Stacking_result
 
model_L2 = LinearRegression()
Stacking_pre = Stacking_method(train_reg1, train_reg2, train_reg3, y_train_true,
                               test_pre1, test_pre2, test_pre3, model_L2)
print('Stacking_pre MAE:', mean_absolute_error(y_test_true, Stacking_pre))


## 结果:
Stacking_pre MAE: 0.04213

这里的逻辑就是把第一层模型在训练集上的预测值,当作第二层训练集的特征,第一层模型在测试集上的预测值,当作第二层测试集的特征,然后在第二层建立一个简单的线性模型进行训练

可以发现最终误差相对于之前进一步提升了,这里需要注意的是,第二层的模型不宜选的过于复杂,否则会导致模型过拟合

接下来介绍一款强大的stacking工具StackingCVRegressor,这是一种继承学习的元回归器,首先导入

from mxltend.regressor import StackingCVRegressor

在标准stacking过程中,拟合一级回归器的时候,如果使用了第二级回归器输入的相同训练集,就会导致过拟合。但是,StackingCVRegressor使用了"非折叠预测"的概念:数据被分成K折,并且在K个连续的循环中,使用K-1折来拟合第一级回归器(即K折交叉验证的StackingRegressor)。在每一轮中(一共K轮),一级回归器先后被应用于在每次迭代中还未用过的1个子集,然后将得到的预测叠加起来作为输入数据提供给二级回归器。在StackingCVRegressor训练完之后,一级回归器拟合整个数据集以获得最佳预测。其实这就是前面我们介绍的原理

具体API及参数如下:

StackingCVRegressor(regressors,meta_regressor,cv = 5,shuffle = True,use_features_in_secondary = False)

  • regressors:基回归器,列表的形式,第一层模型。例如我打算第一层用xgb和lgb,第二层用线性模型,那么这里就应该写[xgb,lgb]
  • meta_regressor:元回归器,可以理解为第二层的模型,一般不能太复杂,例如使用一个普通的线性模型lr
  • cv:交叉验证策略,默认是5折交叉验证
  • use_features_in_secondary:默认是False,表示第二层的回归器只接受第一层回归器的结果进行训练和预测。如果设置为True,表示第二层的回归器不仅接收第一层回归器的结果,还接收原始的数据集一块进行训练
  • shuffle:是否打乱样本的顺序
  • 训练依然是用.fit(x, y),但这里的x和y要求是数组,所以如果是DataFrame,需要np.array()一下,并且x的shape应该是(n_samples,n_features),y的shape应该是(n_samples)
  • 预测依然是用.predict(x_test),只不过x_test也是数组,形状和上面的一样

3.1.2 分类中的Stacking

这里以鸢尾花数据集为例,首先手动实现一下stacking加深理解,然后使用mlxtend.classifier.StackingClassifier实现

from sklearn.ensemble import RandomForestClassifier
from sklearn.ensemble import ExtraTreesClassifier, GradientBoostingClassifier
from sklearn.metrics import roc_auc_score

# 创建数据集
iris = load_iris()
data_0 = iris.data
data = data_0[:100, :]

target_0 = iris.target
target = target_0[:100]

# 模型融合中用到的单个模型
clfs = [LogisticRegression(solver='lbfgs'),
        RandomForestClassifier(n_estimators=5, n_jobs=-1, criterion='gini'),
        ExtraTreesClassifier(n_estimators=5, n_jobs=-1, criterion='gini'),
        ExtraTreesClassifier(n_estimators=5, n_jobs=-1, criterion='entropy'),
        GradientBoostingClassifier(learning_rate=0.05, subsample=0.5, max_depth=6, n_estimators=5)]

# 切分一部分数据作为训练集
X, X_predict, y, y_predict = train_test_split(data, target, test_size=0.3, random_state=1)

dataset_blend_train = np.zeros((X.shape[0], len(clfs)))   # 每个模型的预测作为第二层的特征
dataset_blend_test = np.zeros((X_predict.shape[0], len(clfs)))

# 5折stacking
n_splits = 5
skf = StratifiedKFold(n_splits)
skf = skf.split(X, y)

for j, clf in enumerate(clfs):
    # 依次训练各个单模型
    dataset_blend_test_j = np.zeros((X_predict.shape[0], 5))
    for i, (train, test) in enumerate(skf):
        # 5——fold交叉训练,使用第i个部分作为预测, 剩余的部分来训练模型, 获得其预测的输出作为第i部分的新特征。
        X_train, y_train, X_test, y_test = X[train], y[train], X[test], y[test]
        clf.fit(X_train, y_train)
        y_submission = clf.predict_proba(X_test)[:,1]
        dataset_blend_train[test, j] = y_submission
        dataset_blend_test_j[:, j] = clf.predict_proba(X_predict)[:, 1]
        
    # 对于测试集, 直接用这k个模型的预测值均值作为新的特征
    dataset_blend_test[:, j] = dataset_blend_test_j.mean(1)
    print("val auc Score: %f" % roc_auc_score(y_predict, dataset_blend_test[:, j]))

clf = LogisticRegression(solver='lbfgs')
clf.fit(dataset_blend_train, y)
y_submission = clf.predict_proba(dataset_blend_test)[:,1]

print("Val auc Score of Stacking: %f" % (roc_auc_score(y_predict, y_submission)))

## 结果如下:
val auc Score: 1.000000
val auc Score: 0.500000
val auc Score: 0.500000
val auc Score: 0.500000
val auc Score: 0.500000
Val auc Score of Stacking: 1.000000

StackingClassifier的API及参数如下:

StackingClassifier(classifiers, meta_classifier, use_probas=False, average_probas=False, verbose=0, use_features_in_secondary=False), 这里的参数和上面的StackingCVRegressor基本上差不多

  • classifiers:基分类器, 数组形式[clf1, clf2, clf3], 每个基分类器的属性被存储在类属性 self.clfs_
  • meta_classifier:目标分类器,即第二层的分类器
  • use_probas:bool (default: False) 。如果设置为True, 那么目标分类器的输入就是前面分类输出的类别概率值而不是类别标签
  • average_probas:bool (default: False)。用来设置上一个参数当使用概率值输出的时候是否使用平均值
  • verbose:int, optional (default=0)。用来控制使用过程中的日志输出,当 verbose = 0时,什么也不输出;verbose = 1时,输出回归器的序号和名字;verbose = 2时,输出详细的参数信息
  • use_features_in_secondary:bool (default: False)。如果设置为True,那么最终的目标分类器就被基分类器产生的数据和最初的数据集同时训练。如果设置为False,最终的分类器只会使用基分类器产生的数据训练。
  • 常用方法: .fit().predict()
from mlxtend.classifier import StackingCVClassifier
# 上面的这个操作,如果换成StackingClassifier, 是这样的形式:
clf1 = RandomForestClassifier(n_estimators=5, n_jobs=-1, criterion='gini')
clf2 = ExtraTreesClassifier(n_estimators=5, n_jobs=-1, criterion='gini')
clf3 = ExtraTreesClassifier(n_estimators=5, n_jobs=-1, criterion='entropy')
clf4 = GradientBoostingClassifier(learning_rate=0.05, subsample=0.5, max_depth=6, n_estimators=5)
clf5 = LogisticRegression(solver='lbfgs')

sclf = StackingCVClassifier(classifiers=[clf1, clf2, clf3, clf4], meta_classifier=clf, cv=3)
sclf.fit(X, y)
# 5这交叉验证
#scores = cross_val_score(sclf, X, y, cv=3, scoring='accuracy')

y_submission = sclf.predict(X_predict)

print("Val auc Score of Stacking: %f" % (roc_auc_score(y_predict, y_submission)))

3.2 Blending

Blending是一种和Stacking很相像的模型融合方式,它与Stacking的区别在于训练集不是通过K-Fold的策略来获得预测值,而是先建立一个Holdout(留出集)

3.2.1 Blending版本一(单纯的Holdout)

单纯的Holdout就是直接把训练集分成两部分,70%作为新的训练集,30%作为验证集,然后用这70%的训练集分别训练第一层的模型,然后在30%的验证集上进行预测,把预测的结果作为第二层模型的训练集特征,这是训练部分。预测部分就是把真正的测试集先用第一层的模型预测,把预测结过作为第二层测试集的特征进行第二层的预测。 过程图如下:

看这个图应该很容易理解。这种方法实现起来也比较容易,基本和stacking的代码差不多,只不过少了内层的循环而已,毕竟每个模型不用交叉验证了

#创建训练的数据集
#创建训练的数据集
data_0 = iris.data
data = data_0[:100,:]

target_0 = iris.target
target = target_0[:100]
 
#模型融合中使用到的各个单模型
clfs = [LogisticRegression(solver='lbfgs'),
        RandomForestClassifier(n_estimators=5, n_jobs=-1, criterion='gini'),
        RandomForestClassifier(n_estimators=5, n_jobs=-1, criterion='entropy'),
        ExtraTreesClassifier(n_estimators=5, n_jobs=-1, criterion='gini'),
        #ExtraTreesClassifier(n_estimators=5, n_jobs=-1, criterion='entropy'),
        GradientBoostingClassifier(learning_rate=0.05, subsample=0.5, max_depth=6, n_estimators=5)]
 
#切分一部分数据作为测试集
X, X_predict, y, y_predict = train_test_split(data, target, test_size=0.3, random_state=2020)

#切分训练数据集为d1,d2两部分
X_d1, X_d2, y_d1, y_d2 = train_test_split(X, y, test_size=0.5, random_state=2020)
dataset_d1 = np.zeros((X_d2.shape[0], len(clfs)))
dataset_d2 = np.zeros((X_predict.shape[0], len(clfs)))
 
for j, clf in enumerate(clfs):
    #依次训练各个单模型
    clf.fit(X_d1, y_d1)
    y_submission = clf.predict_proba(X_d2)[:, 1]
    dataset_d1[:, j] = y_submission
    #对于测试集,直接用这k个模型的预测值作为新的特征。
    dataset_d2[:, j] = clf.predict_proba(X_predict)[:, 1]
    #print("val auc Score: %f" % roc_auc_score(y_predict, dataset_d2[:, j]))

#融合使用的模型
clf = GradientBoostingClassifier(learning_rate=0.02, subsample=0.5, max_depth=6, n_estimators=30)
clf.fit(dataset_d1, y_d2)
y_submission = clf.predict_proba(dataset_d2)[:, 1]
print("Val auc Score of Blending: %f" % (roc_auc_score(y_predict, y_submission)))

# 结果:
Val auc Score of Blending: 1.000000
3.2.2 Blending版本二(Holdout交叉)

第二种引入了交叉验证的思想,也就是每个模型看到的Holdout集合并不一样。说白了,就是把Stacking流程中的K-Fold CV改成HoldOut CV。第二阶段的stacker模型就基于第一阶段模型对这30%训练数据的预测值进行拟合

  1. 在第一层中, 用70%的训练集训练多个模型, 然后去预测那30%的数据得到预测值$P_i$, 同时也预测test集得到预测值$T_i$。这里注意,那30%的数据每个模型并不是一样,也是类似于交叉验证的那种划分方式,只不过stacking那里是每个模型都会经历K折交叉验证,也就是有多少模型,就会有多少次K折交叉验证,而blending这里是所有模型合起来只经历了一次K折交叉验证(看下图就容易懂了)
  2. 第二层直接对$P_i$进行合并,作为新的训练集train2,test集的预测值$T_i$合并作为新的测试集test2,然后训练第二层的模型

Blending的过程训练和预测过程可以使用下图来表示:

Blending的优势在于:

  • Blending比较简单,而Stacking相对比较复杂;
  • 能够防止信息泄露:generalizers和stackers使用不同的数据;

Blending缺点在于:

  • 只用了整体数据的一部分;
  • 最终模型可能对留出集(holdout set)过拟合;

Blending没有找到实现好的工具,需要自己写

# 模型融合中用到的单个模型
clfs = [LogisticRegression(solver='lbfgs'),
        RandomForestClassifier(n_estimators=5, n_jobs=-1, criterion='gini'),
        ExtraTreesClassifier(n_estimators=5, n_jobs=-1, criterion='gini'),
        ExtraTreesClassifier(n_estimators=5, n_jobs=-1, criterion='entropy'),
        GradientBoostingClassifier(learning_rate=0.05, subsample=0.5, max_depth=6, n_estimators=5)]

# 切分一部分数据作为训练集
X, X_predict, y, y_predict = train_test_split(data, target, test_size=0.3, random_state=2020)

dataset_blend_train = np.zeros((int(X.shape[0]/n_splits), len(clfs)))   # 每个模型的预测作为第二层的特征
dataset_blend_test = np.zeros((X_predict.shape[0], len(clfs)))

# 5折stacking
n_splits = 5
skf = StratifiedKFold(n_splits)
skf = skf.split(X, y)

fold = {}
for i, (train, test) in enumerate(skf):
    fold[i] = (X[train], y[train], X[test], y[test])
Y_blend = []
for j, clf in enumerate(clfs):
    # 依次训练各个单模型
    dataset_blend_test_j = np.zeros((X_predict.shape[0], 5))
    
    # 5——fold交叉训练,使用第i个部分作为预测, 剩余的部分来训练模型, 获得其预测的输出作为第i部分的新特征。
    X_train, y_train, X_test, y_test = fold[j]
    clf.fit(X_train, y_train)
    dataset_blend_train[:, j] =  clf.predict(X_test)
    Y_blend.extend(y_test)
    
    # 对于测试集,直接用这k个模型的预测值作为新的特征
    dataset_blend_test[:, j] = clf.predict(X_predict)
        
    print("val auc Score: %f" % roc_auc_score(y_predict, dataset_blend_test[:, j]))

dataset_blend_train = dataset_blend_train.T.reshape(70, -1)
dataset_blend_test = np.mean(dataset_blend_test, axis=1).reshape(-1, 1)
Y_blend = np.array(Y_blend).reshape(-1, 1)

clf = LogisticRegression(solver='lbfgs')
clf.fit(dataset_blend_train, Y_blend)
y_submission = clf.predict(dataset_blend_test)

print("Val auc Score of Stacking: %f" % (roc_auc_score(y_predict, y_submission)))

## 结果:
Val auc Score of Stacking: 1.000000

4. 总结

这篇文章是基于已经调参好的模型去研究如何发挥出模型更大的性能。从模型的结果、样本集的集成和模型自身融合三个方面去整理。模型的结果方面,对于回归问题,我们可以对模型的结果进行加权融合等方式;对于分类问题,我们可以使用Voting的方式去得到最终的结果。样本集的集成技术方面,我们学了Boosting和Bagging方式,都是把多个弱分类器进行集成的技术,但是两者是不同的。模型自身的融合方面, 我们学习了Stacking和Blending的原理及具体实现方法,介绍了mlxtend库里面的模型融合工具

Archives Tip
QR Code for this page
Tipping QR Code
Leave a Comment