1.融合入门:Voting与Averaging

发布时间 2023-07-18 16:28:15作者: 哎呦哎(iui)

1 认识模型融合

在机器学习竞赛界,流传着一句话:当一切都无效的时候,选择模型融合。这句话出自一位史上最年轻的Kaggle Master之口,无疑是彰显了模型融合这一技巧在整个机器学习世界的地位。如果说机器学习是人工智能技术中的王后,集成学习(ensemble Learning)就是王后的王冠,而坐落于集成学习三大研究领域之首的模型融合,则毫无疑问就是皇冠上的明珠,熠熠生辉,夺人眼球。

为什么模型融合如此受学者们青睐呢?模型融合在最初的时候被称为“评估器结合”,与集成算法(狭义)一样,它也是训练多个评估器、并将多个评估器以某种方式结合起来解决问题的机器学习方法。我们都知道,集成算法(狭义)是竞赛高分榜统治者,但在高分榜上最顶尖的那部分战队一定都使用了相当丰富的模型融合技巧,因为模型融合能够在经典集成模型的基础上进一步提升分数,使用模型融合技巧后、融合算法的效果常常可以胜过当前最先进的单一集成算法。正是这一有效的提分能力,让模型融合在竞赛界具有很高的地位。也因此,模型融合是提分的最终手段。
同样是使用多个评估器的技术,模型融合为什么能比集成算法获得更高的分数呢?上世纪90年代的多篇论文(Xu et al., 1992; Bauer & Kohavi, 1999; Optiz & Maclin, 1999)从多个角度给出了多个理由——

  • 直观印象:训练的基础不同

集成算法(狭义):青铜算法+人海战术

  • 以Bagging和Boosting代表的集成算法(狭义)可以说是使用青铜算法+人海战术。在集成时,我们一般使用数百棵决策树、甚至树桩这样的弱评估器,构成强大的学习能力。

  • 在这个过程中,由于评估器数量很多,因此每个评估器贡献自己的一小步(Boosting)、或者一小点意见(Bagging),没有任何弱评估器可以在数据集上直接给出优秀的结果。因此从本质上来说,集成算法还是一个算法,弱评估器本身不具备有效预测能力。


    模型融合:王者算法+精英敢死队
  • 模型融合则使用王者算法+精英敢死队战术。在融合时,我们往往构建最多数十个Xgboost、神经网络这样的强评估器,再设计出强大的规则来融合强评估器的结果。或者,我们使用少数几个很强的学习器处理特征,再让将新特征输入相对较弱的学习器,以此来提升模型表现。

  • 在这个过程中,由于评估器很少,所以每个评估器的贡献都很大,即便不融合,单一评估器也可以直接在数据集上给出优秀的结果。因此从本质上来说,模型融合是囊括多个算法的技巧,而并非一个算法。

不难发现:通过集成,我们让算法从弱到强,但通过融合,我们让算法从强到更强。面对同样的预测任务,集成算法会努力获得一个好结果,而模型融合则是将许多好结果融合在一起,得到更好的结果。

  • 统计方面:假设空间不同,泛化能力不同

集成算法(狭义):一维假设空间

  • 每个算法都是一套独一无二的、从数据到结论的计算流程,这一流程在统计学上被称为是“对样本与标签关系的假设”。例如,KNN假设,相同类别的样本在特征上应该相似。Boosting算法假设令单一损失函数最小的树一定可以组成令整体损失最小的树林。

  • 我们可以有不同的假设,但在有限的训练数据集上,我们很难看出哪个假设才是对贴近于真实样本标签关系的假设。这就是说,我们无法保证现在表现好的算法在未知数据集上表现也一定会好。因此使用单一假设/单一算法的缺点就很明显:虽然在当前数据集上当前假设表现最好,但如果其他假设才更贴近数据真实的情况,那当前选择出的模型的泛化能力在未来就无法得到保证了。


    模型融合:高维假设空间
  • 相对的,如果能够将不同的假设结合起来,就可以降低选错假设的风险。最贴近数据真实状况的假设,即便在当前数据集上表现并不是最好的,但在融合之后可以给与融合的结果更强大的泛化能力。

  • 同时,假设真正的数据规律并不能被现在的数据代表,那融合多个假设也能够拓展假设空间,不同假设最终能够更接近真实规律的可能性也越大。

因此,从统计学的角度来说,模型融合能够:(1) 降低选错假设导致的风险、(2)提升捕捉到真正数据规律的可能性、(3)提升具有更好泛化能力的可能性

  • 统计方面:假设空间不同,泛化能力不同

集成算法(狭义):一维假设空间

  • 每个算法都是一套独一无二的、从数据到结论的计算流程,这一流程在统计学上被称为是“对样本与标签关系的假设”。例如,KNN假设,相同类别的样本在特征上应该相似。Boosting算法假设令单一损失函数最小的树一定可以组成令整体损失最小的树林。

  • 我们可以有不同的假设,但在有限的训练数据集上,我们很难看出哪个假设才是对贴近于真实样本标签关系的假设。这就是说,我们无法保证现在表现好的算法在未知数据集上表现也一定会好。因此使用单一假设/单一算法的缺点就很明显:虽然在当前数据集上当前假设表现最好,但如果其他假设才更贴近数据真实的情况,那当前选择出的模型的泛化能力在未来就无法得到保证了。


    模型融合:高维假设空间
  • 相对的,如果能够将不同的假设结合起来,就可以降低选错假设的风险。最贴近数据真实状况的假设,即便在当前数据集上表现并不是最好的,但在融合之后可以给与融合的结果更强大的泛化能力。

  • 同时,假设真正的数据规律并不能被现在的数据代表,那融合多个假设也能够拓展假设空间,不同假设最终能够更接近真实规律的可能性也越大。

因此,从统计学的角度来说,模型融合能够:(1) 降低选错假设导致的风险、(2)提升捕捉到真正数据规律的可能性、(3)提升具有更好泛化能力的可能性

现在,使用比较广泛的融合方式有如下4种:

  • 均值法Averaging:适用于回归类算法,将每个评估器的输出做平均,类似于Bagging中回归的做法。我们已经在之前的《集成算法》公开课中给大家讲解过Averaging。

  • 投票法Voting:适用于分类算法,按每个评估器的输出进行投票,类似于Bagging中分类的做法。

  • 堆叠法Stacking:使用一个/多个算法在训练集上输出的某种结果作为下一个算法的训练数据。其中比较著名的是gbdt+lr,其中逻辑回归lr使用gbdt输出的树结构数据作为训练数据。就是使用我们很多的强学习器,然后在我们的训练集上输出我们的训练结果,然后把我们的预测结果拼拼凑凑拼成一个新的数据集,然后拿这个新的数据集作为下一个算法的训练集。一共有两层算法,第一层算法是负责输出训练集上的测试结果,第二次负责拿这个训练结果进行训练输出最终结果。

  • 改进堆叠法Blending:一种特殊的stacking,使用一个/多个算法在验证集上输出的某种结果作为下一个算法的训练数据。

另外还有其他在论文中出现过,但并未被广泛使用的各种融合方法,例如无限集成(Unlimited Ensemble)、代数集成(Algebraic Method)、行为知识空间法(behavior knowldge space)等。这些方法都基于非常严谨的数学过程建立,数学功底不错的小伙伴可以查看周志华教授的《集成学习:基础与算法》书籍。接下来的3天,我们将一一说明讲解上述融合方法。

2 投票法Voting

2.1 五大类投票方法

投票法是适用于分类任务的模型融合方法,也是在直觉上来说最容易理解的方法。简单来说,投票法对所有评估器输出的结果按类别进行计数,出现次数最多的类别就是融合模型输出的类别。假设现在面临3分类问题,有5个强学习器,对于任意样本\(i\)而言,这些强学习器的输出类别分别如下所示:

1号分类器:3

2号分类器:3

3号分类器:2

4号分类器:3

5号分类器:2

在5个学习器的结果来看,类别3出现3次,类别2出现2次,类别1一次也没有出现,因此出现次数最多的类别3就是该融合模型在样本\(i\)上的预测类别。这种方式其实就是让所有评估器对类别进行投票、并且让少数如从多数,我们在KNN以及Bagging算法(如随机森林分类)当中都见过这种投票方式,应该相当容易理解。在实际机器学习执行过程当中,我们可以使用多种、丰富的投票方式:

  • 相对多数投票 vs 绝对多数投票

相对多数投票就是上面所提到的少数服从多数,即只要有一个备选类别占比较多即可。与之相对的是绝对多数投票:


绝对多数投票要求至少有50%或以上的分类器都输出了同一类别,否则就拒绝预测。例如在上面5个分类器中,至少要有2.5个分类器都输出同一类别,样本才能够进行输出具体类别,否则则输出“不确定”,相当于增加一类输出类别。


这种投票方法在现在听起来非常奇怪,为什么我们要允许算法拒绝预测呢?事实上,绝对多数投票是现实中非常常见的投票方法(例如公司关键议程的通过、某些公司董事长的选举),这种投票方法不会轻易抹杀少数派的意见,而会尽量为更多的人争取利益。因此,当我们需要对决策更加谨慎时,我们就会选择绝对多数投票。如果票数最多的类别没有占比超过50%,则重新投票。


在机器学习当中,绝对多数投票可以帮助我们衡量当前投票的置信程度。票数最多的类别占比越高,说明融合模型对当前样本的预测越有信心。当票数最多的类别占比不足50%时,重新投票对于机器学习来说效率太低,因此我们往往会选择另一种方案:我们可以使用相对较弱的学习器进行绝对多数投票融合、并将绝对多数投票中被输出为“不确定”的样本交由学习能力更强的模型进行预测,相当于将“困难样本”交由复杂度更高的算法进行预测,这也是一种融合方式。

  • 硬投票 vs 软投票

大多数分类模型的模型输出结果可以有两种表示方式:

  • 表示为二值或多值标记,即算法输出值为[0,1]或[1,2,3]这样的具体类别。

  • 表示为类别概率,即算法输出值为(0,1)之间的任意浮点数,越靠近1则说明算法对当前类别预测的置信度越高。我们需要规定阈值、或使用softmax函数的规则来将浮点数转换为具体的类别。

基于此,我们可以将投票分成两种类型:


硬投票:将不同类别的出现次数进行计数,出现次数最多的类别就是投票结果


软投票:将不同类别下的概率进行加和,概率最高的类别就是投票结果。基于概率的软投票可以衡量投票的置信程度,某一类别下的概率越高,说明模型对该类别的预测结果越有信心。

具体来看,延续3分类、5个分类器的假设,当使用二值标记时我们有:

1号分类器:3

2号分类器:3

3号分类器:2

4号分类器:3

5号分类器:2

当使用类别概率时,我们有:

分类器 1的概率 2的概率 3的概率
1号 0.1 0.4 0.5
2号 0.2 0.35 0.45
3号 0.1 0.6 0.3
4号 0.3 0.3 0.4
5号 0.4 0.5 0.1

在软投票中,我们对所有分类器上每一个类别的概率进行加和:

分类器 1的概率 2的概率 3的概率
1号 0.1 0.4 0.5
2号 0.2 0.35 0.45
3号 0.1 0.6 0.3
4号 0.3 0.3 0.4
5号 0.2 0.7 0.1
加和 0.09 2.35 1.75

加和之后,概率占比最高的是类别2。因此基于软投票,算法输出的类别为类别2。不难发现,软投票和硬投票得出的结果可能不同,这是因为软投票有一种“窥探心灵”的能力(也就是衡量置信度的能力)。其实我们会发现在1号,2号,4号分类器中虽然标签给了3,但是给2的概率还是很高的。

如果使用硬头票,那毫无疑问输出类别应该是3。但当我们查看具体的预测概率时我们就会发现,给与类别3较高概率的分类器(1号、2号与4号)往往也给与了类别2较高的概率,只不过这些分类器稍稍倾向于类别3。然而,给出类别2的分类器(3号、5号)往往会给与类别2非常高的概率,这说明这两个分类器对于自己输出的类别非常有信心。这种情况导致对概率进行加和后,我们发现类别2的概率是最高的。在5个分类器进行投票时,有三个分类器认为类别2是真实标签的可能性很高,另外两个分类器认为类别2就是真实标签,那毫无疑问选择输出类别2是整个团队置信度到达顶峰的选项。

在实际进行投票法融合时,我们往往优先考虑软投票 + 相对多数投票方案,因为软投票方案更容易在当前数据集上获得好的分数。然而,如果我们融合的算法都是精心调参过的算法,那软投票方案可能导致过拟合。因此具体在使用时,需要依情况而定。

  • 加权投票

除了基于概率/基于类别进行投票、以及使用绝对多数/相对多数进行投票之外,还有一种可以与上述方式结合的投票方法,即加权投票。加权投票是在投票过程中赋予不同分类器权重的投票,具体表现在:

  • 对于硬投票,加权投票可以改变每一个分类器所拥有的票数。

  • 对于软投票,加权投票可以改变每一个分类器所占的权重,令概率的普通加和变成加权求和。

具体来看,硬投票情况下,我们有:

分类器 输出类别 原始票数 权重 加权票数
1 3 1 0.3 0.3
2 3 1 0.8 0.8
3 2 1 0.5 0.5
4 3 1 0.6 0.6
5 2 1 0.2 0.2

这个权重那一列加和不需要等于1

现在,我们再按照类别进行计数,类别3的票数为(0.3 + 0.8 + 0.6 = 1.7票),而类别2的票数为(0.5 + 0.2 = 0.7)票,因此票数占比更多的是类别3,融合模型输出类别3。

在软投票情况下,我们有:

分类器 1的概率 2的概率 3的概率 权重
1号 0.1 0.4 0.5 0.3
2号 0.2 0.35 0.45 0.8
3号 0.1 0.6 0.3 0.5
4号 0.3 0.3 0.4 0.6
5号 0.2 0.7 0.1 0.2

则加权求和结果可以计算为:

image

不难发现,当调整权重之后,最终输出的结果依然是类别2,但是加权之后整体算法对于类别2的置信度是降低的、对于类别3的置信度是提升的。因此,修改权重很可能会影响融合模型最终的输出。通常来说,我们会给与表现更好的分类器更多的权重,但并不会给与它过多的权重,否则会过拟合

大多数时候,我们可能会主观地认为,加权后的模型比未加权的模型更强大,因为加权求和比普通求和更复杂、加权平均也比普通平均更复杂。但事实上,无论是从实验还是理论角度,加权方法并没有表现出比一般普通方法更强的泛化能力。在某些数据上,加权会更有效,但如果强评估器之间的结果差异本来就不大,那加权一般不会使模型结果得到质的提升。

2.2 使用sklearn实现投票法

sklearn中不支持绝对多数投票

在sklearn的集成学习板块中,拥有数个帮助我们实现各类融合方法的类,其中,我们可以很容易使用下面的类来实现投票法:

class sklearn.ensemble.VotingClassifier(estimators, *, voting='hard', weights=None, n_jobs=None, flatten_transform=True, verbose=False)

参数 说明
estimators 需要使用投票法融合的分类评估器及分类评估器的名称。(一般两到三个)
可输入单一评估器,或使用列表打包多个评估器。
voting 投票方式,默认使用相对多数投票方法。
输入字符串'hard',选择硬投票,输入字符串'soft',选择软投票。当选择软投票方法时,只能接受可以输出概率值的算法,如SVM等间隔度量算法则不在可以融合的范围之内。
weights 权重,默认为None,表示不对评估器进行加权。如果输入权重,则使用列表打包多个权重。
flatten_transform 当使用软投票时,可以通过该参数选择输出的概率结构。如果为True,最终则输出结构为(n_samples, n_estimators * n_classes)的二维数组。如果为False,最终则输出结构为(n_samples,n_estimators,n_classes)的三维数组。
n_jobs, verbose 线程数与模型监控

1 导入所需的工具库,确认数据

#常用工具库
import re
import numpy as np
import pandas as pd
import matplotlib as mlp
import matplotlib.pyplot as plt
import time

#算法辅助 & 数据
import sklearn
from sklearn.model_selection import KFold, cross_validate
from sklearn.datasets import load_digits #分类 - 手写数字数据集
from sklearn.datasets import load_boston
from sklearn.model_selection import train_test_split

#算法(单一学习器)
from sklearn.neighbors import KNeighborsClassifier as KNNC
from sklearn.neighbors import KNeighborsRegressor as KNNR
from sklearn.tree import DecisionTreeRegressor as DTR
from sklearn.tree import DecisionTreeClassifier as DTC
from sklearn.linear_model import LinearRegression as LR
from sklearn.linear_model import LogisticRegression as LogiR
from sklearn.ensemble import RandomForestRegressor as RFR
from sklearn.ensemble import RandomForestClassifier as RFC
from sklearn.ensemble import GradientBoostingRegressor as GBR
from sklearn.ensemble import GradientBoostingClassifier as GBC
from sklearn.naive_bayes import GaussianNB
import xgboost as xgb

#融合模型
from sklearn.ensemble import VotingClassifier
from sklearn.ensemble import VotingRegressor

我们使用sklearn自带的手写数字数据集:

data = load_digits()
X = data.data
y = data.target

X.shape

np.unique(y) #10分类

image
分割数据集

Xtrain,Xtest,Ytrain,Ytest = train_test_split(X,y,test_size=0.2,random_state=1412)

我们将在Xtrain,Ytrain上进行交叉验证,并在Xtest,Ytest上进行最后的测试。我们的目标是:交叉验证的结果尽量好(理论泛化能力强),同时测试集上的结果也需要尽量好(理论泛化能力的验证,但不完全严谨)。当两者不可兼得时,我们优先考虑交叉验证的结果(理论泛化能力强)

2 定义交叉验证所需要的函数

def individual_estimators(estimators):
    """
    对模型融合中每个评估器做交叉验证,对单一评估器的表现进行评估
    """
    for estimator in estimators:
        cv = KFold(n_splits=5,shuffle=True,random_state=1412)
        results = cross_validate(estimator[1],Xtrain,Ytrain
                             ,cv = cv
                             ,scoring = "accuracy"
                             ,n_jobs = -1
                             ,return_train_score = True
                             ,verbose=False)
        test = estimator[1].fit(Xtrain,Ytrain).score(Xtest,Ytest)
        print(estimator[0]
          ,"\n train_score:{}".format(results["train_score"].mean())
          ,"\n cv_mean:{}".format(results["test_score"].mean())
          ,"\n test_score:{}".format(test)
          ,"\n")
def fusion_estimators(clf):
    """
    对融合模型做交叉验证,对融合模型的表现进行评估
    """
    cv = KFold(n_splits=5,shuffle=True,random_state=1412)
    results = cross_validate(clf,Xtrain,Ytrain
                             ,cv = cv
                             ,scoring = "accuracy"
                             ,n_jobs = -1
                             ,return_train_score = True
                             ,verbose=False)
    test = clf.fit(Xtrain,Ytrain).score(Xtest,Ytest)
    print("train_score:{}".format(results["train_score"].mean())
          ,"\n cv_mean:{}".format(results["test_score"].mean())
          ,"\n test_score:{}".format(test)
         )

其中第一个是对单个评估器进行交叉验证的函数,下面那个是对融合之后的评估器进行交叉验证的函数

3 建立基于交叉验证的benchmark、做模型选择

其中benchmark是找一个跑的很好的单个模型做对比。
一般在模型融合之前,我们需要将所有可能的算法都先简单运行一次,然后从中选出表现较好的算法作为融合的基础。同时,我们还可能在分数最高的单一算法上进行精确的调优,找到单一算法可以实现的最好分数来作为benchmark。毕竟,融合要求使用多个算法、自然也会在运算时间和算力上有所要求,如果单一算法的结果能够胜过融合,那我们优秀选择单一算法。在公开课当中,鉴于有限的时间,我为大家挑选了几个算法用于展示投票法融合的实力。我们使用备选分类器中的逻辑回归作为benchmark。

logi = LogiR(max_iter=3000, n_jobs=8) #初始情况下给与一个较大的max_iter,方便迭代到收敛
fusion_estimators(logi) #正处于过拟合状态,需要调整

image

4 构建多组分类器,进行融合

  • 第一组分类器:放任自由,收敛为主,有较高过拟合风险
    先定义每个单个模型
clf1 = LogiR(max_iter = 3000,random_state=1412,n_jobs=8) #逻辑回归
clf2 = RFC(n_estimators= 100,random_state=1412,n_jobs=8) #随机森林
clf3 = GBC(n_estimators= 100,random_state=1412)  #GBDT

estimators = [("Logistic Regression",clf1), ("RandomForest", clf2), ("GBDT",clf3)]
clf = VotingClassifier(estimators,voting="soft")

然后再测试单个模型中每个模型的效果

individual_estimators(estimators)

''
Logistic Regression 
 train_score:1.0 
 cv_mean:0.9666061749903212 
 test_score:0.9527777777777777 

RandomForest 
 train_score:1.0 
 cv_mean:0.9763574332171892 
 test_score:0.9833333333333333 

GBDT 
 train_score:1.0 
 cv_mean:0.9686943476577623 
 test_score:0.9388888888888889 
''

image
不难发现,每个模型在训练集上的分数都达到了1,但交叉验证分数大约在96%~97%之间,与逻辑回归的benchmark展现出来的结果相同。模型都存在一定的过拟合情况,其中GBDT在测试数据上表现出来的泛化能力有很大的问题,需要较为激进的调整。

模型融合的测试:

fusion_estimators(clf)

''
train_score:1.0 
 cv_mean:0.9777390631049168 
 test_score:0.9805555555555555
''

很明显,通过集成模型的表现有了显著提升,交叉验证分数(97.77%)比任何单一学习器都高,且在测试集上的表现提升到了98.05%,可见当前数据下模型融合的效果十分显著。

benchmark 基础融合
5折交叉验证 0.9666 0.9777(↑)
测试集结果 0.9527 0.9805(↑)

当然了,并不是任意评估器、任意数据上都能够看到如此一目了然的结果。如果你进行投票或平均融合之后,融合的结果反而没有单个算法好,那你可能是落入了投票法与平均法会失效的几大陷阱:

  1. 评估器之间的学习能力/模型表现差异太大。在融合中,一个表现很差的模型会拉低整个融合模型的上限,尤其是回归类算法,当一个模型的表现很差时,平均法得出的结果很难比最好的单一算法还好。因此我们必须要使用表现相似的模型进行融合。如果你的评估器中有拖后腿的模型,无论这个模型有多么先进,都应该立刻把它剔除融合模型。

  2. 评估器在类型上太相似,比如、全是树模型、都是Boosting算法,或都是线性评估器等。如果评估器类别太相似,模型融合会发挥不出作用,这在直觉上其实很好理解:如果平均/投票的评估器都一致,那融合模型最终得出的结果也会与单个评估器一致。

  3. 对评估器进行了过于精密的调优。一般来说,我们可能会认为,先对模型进行调优后再融合,能够进一步提升模型的表现。经过粗略调优的评估器融合确实能提升模型表现,但如果对评估器进行过于精密的调优,可能会让融合后的算法处于严重过拟合的状态。因此,一般我们不会在评估器上进行太精准的调优。
  • 第二组分类器:略微调参(非精细化调参),限制过拟合
    模型融合是一个可能加剧过拟合的手段,因此我们必须保证每一个学习器本身的过拟合不严重,为此我们需要对模型进行抗过拟合的处理。需要注意的是,扛过拟合可能会削弱模型的预测效果,因此我们必须根据过拟合的情况、泛化能力的展现来进行选择。对于逻辑回归,我们需要缩小参数C,对随机森林我们选择max_depth,对GBDT我们则选择max_features。其中,森林的树深需要使用tree_模块来查看:
#查看随机森林中的深度
clf2.fit(Xtrain,Ytrain)
for i in clf2.estimators_[:100]:
    print(i.tree_.max_depth)

image
我们查看会发现基本没有超过15的,所以我们设置随机森林的max_depth=12,对于GBDT设置max_depth没有用的,所以我们设置max_features="sqrt"

clf1 = LogiR(max_iter = 3000, C=0.1, random_state=1412,n_jobs=8) #这一组合可能说明我们的max_iter设置得太大了
clf2 = RFC(n_estimators= 100,max_depth=12,random_state=1412,n_jobs=8)
clf3 = GBC(n_estimators= 100,max_features="sqrt",random_state=1412)

estimators = [("Logistic Regression",clf1), ("RandomForest", clf2), ("GBDT",clf3)]
clf = VotingClassifier(estimators,voting="soft")
individual_estimators(estimators)

''
Logistic Regression 
 train_score:1.0 
 cv_mean:0.9666085946573751 
 test_score:0.9638888888888889 

RandomForest 
 train_score:1.0 
 cv_mean:0.972875532326752 
 test_score:0.9805555555555555 

GBDT 
 train_score:1.0 
 cv_mean:0.9777342237708091 
 test_score:0.9722222222222222 

''

image
针对逻辑回归的调整效果不是非常明显,交叉验证的分数并未得到明显提升,但测试集上的结果上升,说明模型的泛化能力变得更加稳定了。但C参数是很少小于0.5的,我们现在得出的结果可能说明模型的权重是非常非常小的数,所以我们必须给与很小的C才能够加强过拟合的影响。随机森林的过拟合变得更加严重了,这说明我们限制过拟合对模型带来的伤害大于对模型的帮助,因此随机森林的过拟合是失败的,应该取消这种过拟合限制。GBDT的过拟合调整是非常成功的,交叉验证分数上升的同时,测试集上的分数也得到了大幅提升。结论是,我们保留对逻辑回归和GBDT的调整,撤销对随机森林的调整:

clf1 = LogiR(max_iter = 3000, C=0.1, random_state=1412,n_jobs=8) #这一组合可能说明我们的max_iter设置得太大了
clf2 = RFC(n_estimators= 100,random_state=1412,n_jobs=8)
clf3 = GBC(n_estimators= 100,max_features="sqrt",random_state=1412)

estimators = [("Logistic Regression",clf1), ("RandomForest", clf2), ("GBDT",clf3)]
clf = VotingClassifier(estimators,voting="soft")
fusion_estimators(clf)

""
train_score:1.0 
 cv_mean:0.9777463221060783 
 test_score:0.9861111111111112
""
benchmark 基础融合 抗过拟合
5折交叉验证 0.9666 0.9777(↑) 0.9777(-)
测试集结果 0.9527 0.9805(↑) 0.9861(↑)
image
可以看到,经过过拟合调整后,模型的交叉验证分数与测试集上的分数都上升了,不过交叉验证分数是轻微上升(从0.9777392上升至0.9777463),这轻微的上升可以忽略不计。测试集上的分数则有了0.06%的提升,是一个显著的进步。

5 构建多样性

在各个模型的过拟合情况较好、且模型稳定的情况下,我们要从哪个角度入手开始提升融合模型的表现呢?虽然我们不了解投票法的数学细节,但我们十分了解Bagging算法,比如随机森林的数学细节——我在《2022机器学习正课》当中为大家详细证明了,为什么随机森林的结果会好于单个算法,并且列出了随机森林的结果好于单个算法的关键条件:评估器之间相互独立。评估器之间的独立性越强,则模型从平均/投票当中获得的方差减少就越大,模型整体的泛化能力就越强。

无论是投票法还是平均法,都与Bagging算法有异曲同工之妙,因此我们相信“独立性”也有助于提升投票融合与平均融合的效果。在模型融合当中,独立性被称为“多样性”(diversity),评估器之间的差别越大、彼此之间就越独立,因此评估器越多样,独立性就越强。完全独立的评估器在现实中几乎不可能实现,因为不同的算法执行的是相同的预测任务,更何况大多数时候算法们都在相同的数据上训练,因此评估器不可能完全独立。但我们有以下关键的手段,用来让评估器变得更多样、让评估器之间相对独立:

  • 训练数据多样性:完成多组有效的特征工程,使用不同的特征矩阵训练不同的模型。该方法一般能够得到很好的效果,但如何找出多组有效的特征工程是难题。

  • 样本多样性:使用相同特征矩阵,但每次训练时抽样出不同的样本子集进行训练。当数据量较小时,抽样样本可能导致模型效果急剧下降。

  • 特征多样性:使用相同特征矩阵,但每次训练时抽样出不同的特征子集进行训练。当特征量较小时,抽样特征可能导致模型效果急剧下降。

  • 随机多样性/训练多样性:使用相同的算法,但使用不同的随机数种子(会导致使用不同的特征、样本、起点)、或使用不同的损失函数、使用不同的不纯度下降量等。这一方法相当于是在使用Bagging集成。

  • 算法多样性:增加类型不同的算法,如集成、树、概率、线性模型相混合。但需要注意的是,模型的效果不能太糟糕,无论是投票还是平均法,如果模型效果太差,可能大幅度降低融合的结果。

增加多样性的操作或多或少都阻止了模型学习完整的数据,因此会削弱模型对数据的学习,可能降低模型的效果。因此我们使用多样性时,需要时刻关注着模型的结果。

5.1 多种多样性混合

#逻辑回归没有增加多样性的选项
clf1 = LogiR(max_iter = 3000, C=0.1, random_state=1412,n_jobs=8)
#增加特征多样性与样本多样性
clf2 = RFC(n_estimators= 100,max_features="sqrt",max_samples=0.9, random_state=1412,n_jobs=8)
#特征多样性,稍微上调特征数量
clf3 = GBC(n_estimators= 100,max_features=16,random_state=1412) 

#增加算法多样性,新增决策树、KNN、贝叶斯
clf4 = DTC(max_depth=8,random_state=1412)
clf5 = KNNC(n_neighbors=10,n_jobs=8)
clf6 = GaussianNB()

#新增随机多样性,相同的算法更换随机数种子
clf7 = RFC(n_estimators= 100,max_features="sqrt",max_samples=0.9, random_state=4869,n_jobs=8)
clf8 = GBC(n_estimators= 100,max_features=16,random_state=4869)

estimators = [("Logistic Regression",clf1), ("RandomForest", clf2)
              , ("GBDT",clf3), ("Decision Tree", clf4), ("KNN",clf5) 
              , ("Bayes",clf6), ("RandomForest2", clf7), ("GBDT2", clf8)
             ]
clf = VotingClassifier(estimators,voting="soft")
individual_estimators(estimators)
""
Logistic Regression 
 train_score:1.0 
 cv_mean:0.9666085946573751 
 test_score:0.9638888888888889 

RandomForest 
 train_score:1.0 
 cv_mean:0.9735699767711964 
 test_score:0.9777777777777777 

GBDT 
 train_score:1.0 
 cv_mean:0.9777414827719705 
 test_score:0.9722222222222222 

Decision Tree 
 train_score:0.9509369962538313 
 cv_mean:0.8552821331784747 
 test_score:0.8527777777777777 

KNN 
 train_score:0.9832990502137966 
 cv_mean:0.9763429152148664 
 test_score:0.9833333333333333 

Bayes 
 train_score:0.8295060354940024 
 cv_mean:0.8086285327138987 
 test_score:0.7888888888888889 

RandomForest2 
 train_score:1.0 
 cv_mean:0.9749491869918699 
 test_score:0.9833333333333333 

GBDT2 
 train_score:1.0 
 cv_mean:0.9770446186604724 
 test_score:0.975 
""
fusion_estimators(clf)

""
train_score:1.0 
 cv_mean:0.9826098528842431 
 test_score:0.9805555555555555
""

我们会发现效果变得更差了

5.2 剔除表现不良的算法,继续观察

estimators = [("Logistic Regression",clf1), ("RandomForest", clf2)
              , ("GBDT",clf3), ("Decision Tree", clf4), ("KNN",clf5) 
              #, ("Bayes",clf6) 贝叶斯在训练集上的分数很低,这说明模型的学习能力不足
              , ("RandomForest2", clf7), ("GBDT2", clf8)
             ]
clf = VotingClassifier(estimators,voting="soft")
individual_estimators(estimators)

"""
Logistic Regression 
 train_score:1.0 
 cv_mean:0.9666085946573751 
 test_score:0.9638888888888889 

RandomForest 
 train_score:1.0 
 cv_mean:0.9735699767711964 
 test_score:0.9777777777777777 

GBDT 
 train_score:1.0 
 cv_mean:0.9777414827719705 
 test_score:0.9722222222222222 

Decision Tree 
 train_score:0.9509369962538313 
 cv_mean:0.8552821331784747 
 test_score:0.8527777777777777 

KNN 
 train_score:0.9832990502137966 
 cv_mean:0.9763429152148664 
 test_score:0.9833333333333333 

RandomForest2 
 train_score:1.0 
 cv_mean:0.9749491869918699 
 test_score:0.9833333333333333 

GBDT2 
 train_score:1.0 
 cv_mean:0.9770446186604724 
 test_score:0.975 
"""
fusion_estimators(clf)
""
train_score:1.0 
 cv_mean:0.9833067169957413 
 test_score:0.9833333333333333
""

5.3 尝试精简多样性

estimators = [("Logistic Regression",clf1), ("RandomForest", clf2)
              , ("GBDT",clf3), ("Decision Tree", clf4), ("KNN",clf5) 
              #, ("Bayes",clf6), ("RandomForest2", clf7), ("GBDT2", clf8)
             ]
clf = VotingClassifier(estimators,voting="soft")
individual_estimators(estimators)
""
Logistic Regression 
 train_score:1.0 
 cv_mean:0.9666085946573751 
 test_score:0.9638888888888889 

RandomForest 
 train_score:1.0 
 cv_mean:0.9735699767711964 
 test_score:0.9777777777777777 

GBDT 
 train_score:1.0 
 cv_mean:0.9777414827719705 
 test_score:0.9722222222222222 

Decision Tree 
 train_score:0.9509369962538313 
 cv_mean:0.8552821331784747 
 test_score:0.8527777777777777 

KNN 
 train_score:0.9832990502137966 
 cv_mean:0.9763429152148664 
 test_score:0.9833333333333333 

""
fusion_estimators(clf)
""
train_score:1.0 
 cv_mean:0.9819129887727449 
 test_score:0.9888888888888889
""
benchmark 基础融合 抗过拟合 复合多样性 精简多样性
5折交叉验证 0.9666 0.9777(↑) 0.9777(-) 0.98331(↑) 0.9819(↓)
测试集结果 0.9527 0.9805(↑) 0.9861(↑) 0.98333(↓) 0.9889(↑)

在增加算法多样性的过程中,我们尝试了多种算法组合。我们发现朴素贝叶斯对算法的伤害大于贡献,只要将朴素贝叶斯包括在融合算法内,算法的表现就持续停留在97%左右,无法继续上升。因此我们删除了朴素贝叶斯算法。同时,包含随机多样性的算法组合在交叉验证与测试集结果上的表现高度一致,其中测试集结果略有降低、交叉验证结果提升了不少,这是一组可以使用的结果。但考虑到运算的效率,我们尽量不增加运算缓慢的集成算法,因此我们又将随机多样性删除,观察只包含5个算法的融合模型,最终交叉验证上的结果略有降低、测试集结果突破新高,这也是一组可以使用的结果。从模型的稳定性来考虑,还是包含随机多样性的组合更好,但为了更快的运算速率,我们可以使用精简多样性继续往下计算。至此,我们的融合模型就构建完毕了,接下来我们来调整融合模型。

6. 分类器加权

对分类器加权是一个常见的操作,但如何选择权重却是整个模型融合过程中最令人头疼的问题——头疼主要在于,没有可以完全依赖的理论基础或数学公式去进行权重推导(或者说推导出来的权重基于过多假设、无法使用),同时费了很大精力求解出的权重可能对模型的效果完全没有影响。因此,在融合中加权是一个不经济的选项。但一般来说,我们还是会尝试几组权重来探索一下,模型是否还有提升的空间。

在机器学习算法中,只有一类算法对于评估器权重有自己的见解,那就是Boosting算法。在大部分Boosting集成过程中,我们会对每一个弱评估器求解其权重,并让权重作为迭代的一部分构建模型。那我们通常如何决定模型权重呢?在AdaBoost和XGBoost当中,我们都设立了用于衡量单个弱评估器置信度的某个指标,如果一个弱评估器的置信度越高,我们给与这个评估器的权重就越大。在Boosting算法中,置信度往往使用损失函数或损失函数的某种变体进行衡量,如果损失函数越大,则说明评估器的置信度越低,反之,则说明评估器的置信度越高。我们可以沿用这个思路,在模型融合中,我们会考虑的第一组权重,就是模型实际评估结果之间的比例。

如果模型评估指标是准确率这样的正向指标,则直接使用准确率作为权重。如果模型评估指标是MSE这样的负向指标,则使用1-指标或负指标作为权重。这样做的风险在于,模型可能陷入严重过拟合,但值得一试:

  • 第一种选项:使用各个模型交叉验证结果本身作为权重,有过拟合风险
estimators = [("Logistic Regression",clf1), ("RandomForest", clf2)
              , ("GBDT",clf3), ("Decision Tree", clf4), ("KNN",clf5)]
clf_weighted = VotingClassifier(estimators,voting="soft",weights=[0.96660,0.97357,0.97774,0.85528,0.97634])
fusion_estimators(clf_weighted)
""
train_score:1.0 
 cv_mean:0.9826098528842431 
 test_score:0.9888888888888889
""

可以看到,模型在交叉验证上的效果提升了!模型没有陷入过拟合,这是一个很好的消息。接下来,我们可以尝试使用更粗糙的权重来提升模型表现。一般来说,这个操作可以一定程度上缓解模型的过拟合:

  • 第二种选项:稍微降低权重精度,或许可以一定程度上抵消过拟合
clf_weighted = VotingClassifier(estimators,voting="soft",weights=[0.95,0.95,0.95,0.85,0.98])
fusion_estimators(clf_weighted)
""
train_score:1.0 
 cv_mean:0.9826098528842431 
 test_score:0.9888888888888889
""

可以看到,模型的表现并无变化,这说明对当前数据来说,精确的权重与粗略的权重差异不大。接下来,我们可以尝试调整模型中效果最好、或效果最差的算法的权重。一般,可以尝试加大效果好的算法的权重,减小效果差的算法的权重:

  • 第三种选项:加大效果好的算法的权重,减小效果差的算法的权重
clf_weighted = VotingClassifier(estimators,voting="soft",weights=[0.95,0.95,0.95,0.85,1.2]) #增大
fusion_estimators(clf_weighted)
""
train_score:1.0 
 cv_mean:0.9826098528842431 
 test_score:0.9861111111111112

""

不难发现,过拟合开始发生了,测试集上的结果开始降低。因此我们选择不加大效果好的算法的权重。

clf_weighted = VotingClassifier(estimators,voting="soft",weights=[0.95,0.95,0.95,0.3,0.98]) #减小
fusion_estimators(clf_weighted)
""
train_score:1.0 
 cv_mean:0.9833067169957413 
 test_score:0.9888888888888889
""

模型达到了目前为止,在5折交叉验证及测试分数上的最高值。现在我们给与决策树的权重非常小,说明现在决策树只是在提供多样性,在实际预测方面做出的贡献较少。提供多样性可以让模型的泛化能力增强,让实际预测方面的贡献变小又可以降低决策树本身较低的预测分数带来的影响。至此,我们就得到了一个很好的融合结果。

benchmark 基础融合 抗过拟合 复合多样性 精简多样性 权重调整
5折交叉验证 0.9666 0.9777(↑) 0.9777(-) 0.98331(↑) 0.9819(↓) 0.9833
测试集结果 0.9527 0.9805(↑) 0.9861(↑) 0.98333(↓) 0.9889(↑) 0.9889