集成学习原理
概述
集成学习(ensemble learning),它本身不是一个单独的机器学习算法,而是通过构建并结合多个机器学习器来完成学习任务。
其工作原理是:对于训练集数据,我们先产生一组“个体学习器”,再通过某种策略将它们结合起来得到最终输出,就可以最终形成一个强学习器,以达到博采众长的目的。就像在医学上,面对一个新型的或罕见的疾病时,通常会组织多个医学“专家”会诊,通过结合这些专家的意见,最终给出治疗方案。
集成学习通过将多个学习器进行结合,常常可获得比单一学习器显著优越的泛化性能。这对“弱学习器”尤为明显,因此集成学习的很多理论研究都是针对弱学习器进行的。但是实践中处于种种考虑,例如希望使用较少的个体学习器,或是重用关于常见学习器的一些经验等,人们往往会使用比较强的学习器。
如何产生并结合“好而不同”的个体学习器,是集成学习研究的核心。
也就是说,集成学习有两个主要的问题需要解决,
- 第一是如何得到若干个个体学习器,
- 第二是如何选择一种结合策略,将这些个体学习器集合成一个强学习器。
下面,我们从两个部分来理解集成学习:个体学习器的生成、学习器结合策略。
基学习器生成
首先,生成的基学习器要满足两个条件:
- 基分类器的判断准确率要超过随机分类器
- 基学习器之间具有差异性
在集成学习中,根据个体学习器的生成方式不同,可以分为Bagging算法和Boosting算法。
Boosting算法
Boosting算法通过顺序地给训练集中的数据重新加权创造不同的基础学习器。
其核心思想是,
- 先从初始训练集训练出一个基学习器,
- 再根据基学习器的表现对训练样本分布进行调整,使得先前基学习器做错的训练样本在后续受到更多关注,
- 然后基于调整后的样本分布来训练下一个基学习器;
- 如此重复进行,直至基学习器数目达到事先指定的值M,
- 最终这M个基学习器进行加权结合。
具体来说,
- 训练开始时,所有的数据项都被初始化为同一个权重,
- 在初始化后,每次增强的迭代会生成一个适应加权之后训练数据集的基础学习器。
- 每一次迭代的错误率会计算出来,而正确划分的数据项的权重会被降低,相反,错误划分的数据项的权重将会增大。
Boosting算法的最终模型是一系列基础学习器的线性组合,并且系数依赖于各个基础学习器的表现。
Boosting算法有很多版本,目前使用最广泛的是AdaBoost算法(Adaptive Boosting自适应性提升法)和GBDT算法(梯度提升决策树Gradient Boosting Decision Tree)。
如下图所示,对于包含m个基模型的Boosting算法,依次利用训练样本对其进行学习。在每个基模型中,样本的权重是不同的,对第i+1个基模型来说,第i个基模型会对每个样本进行评估,预测错误的样本,其权重会增加;反之,则减小。训练好每一个分类器后,对每一个分类器结果进行线性加权,从而得到最终结果。
#AdaBoosting
from sklearn.ensemble import AdaBoostClassifier
clf = AdaBoostClassifier(n_estimators=100)
scores = cross_val_score(clf,iris.data,iris.target,cv=5)
scores.mean()
Bagging算法
Bagging算法原理如下图,
- 通过对训练数据集有放回地抽取产生多个子训练集,
- 并在每个子训练集上训练一个基模型,
- 最终分类结果是由多个基模型的分类结果通过合并器产生的。
#使用bagging
from sklearn.ensemble import BaggingClassifier #回归用BaggingRegressor
from sklearn.tree import DecisionTreeClassifier
bagging_clf = BaggingClassifier(DecisionTreeClassifier()
,n_estimators=500 #基评估器数量
,max_samples=100 #每个子模型使用的样本数
,bootstrap=True #放回取样
# ,oob_score = True #使用袋外数据做测试集,调用bagging_clf.oob_score_得到分数
,n_jobs= -1 #使用所有核进行训练
,max_features=2 #针对特征随机采样
,bootstrap_features=True #特征放回取样
)
bagging_clf.fit(X_train, y_train)
bagging_clf.score(X_test, y_test)
随机森林是bagging的一个特化进阶版,
- 所谓的特化是因为随机森林的弱学习器都是决策树。
- 所谓的进阶是随机森林在bagging的样本随机采样基础上,又加上了特征的随机选择,
其基本思想没有脱离bagging的范畴。
小结
Bagging法和Boosting法最显著的不同的是
- Boosting法产生的个体学习器之间存在强依赖关系,一系列个体学习器基本都需要串行生成
- Bagging法产生的个体学习器之间不存在强依赖关系,一系列个体学习器可以并行生成
关于Bagging法和Boosting法的更详细比较见下图所示:
这两种方法之后都是将若干个分类器结合为一个分类器,只是结合方式不同,最终得到不同的效果。
将不同的分类算法套入此算法框架中在一定程度上可以提高原单一分类器的分类效果,但是也增大了计算量。
将决策树与这些算法框架进行结合得到新的算法如下:
- 装袋法 + 决策树 = 随机森林
- 自适应性提升法 + 决策树 = 提升树
- 梯度提升法 + 决策树 = GBDT
基学习器结合策略
在一般经验中,如果把好坏不等的东西掺到一起,那么通常结果会是比最坏的要好一些,比最好的要坏一些。集成学习如何把多个学习器结合起来,获得比单一学习器更好的性能呢?这就是所谓的“结合策略”
将个体学习器结合在一起的时候使用的方法叫做结合策略。常用的结合策略有平均法、投票法、学习法。
平均法
对于数值类的回归预测问题,通常使用的结合策略是平均法,也就是说将每个基学习器的结果进行简单平均或者加权平均。
加权平均的权重一般是从训练数据中学习得到,现实任务中的训练样本通常不充分或存在噪声,这将使得学出的权重不完全可靠。尤其是对规模比较大的集成来说,要学习的权重比较多,较容易导致过拟合。因此,实验和应用均显示出,加权平均法未必一定优于简单平均法。
一般而言,在个体学习器性能相差较大时宜使用加权平均法,而在个体学习器性能相近时宜使用简单平均法。
投票法
对于分类问题的预测,我们通常使用的是投票法,基学习器分类结果的多数作为最终结果。
- 最简单的投票法是相对多数投票法,也就是我们常说的少数服从多数,也就是T个弱学习器的对样本x的预测结果中,数量最多的类别\(c_i\)为最终的分类类别。如果不止一个类别获得最高票,则随机选择一个做最终类别。
- 稍微复杂的投票法是绝对多数投票法,也就是我们常说的要票过半数。在相对多数投票法的基础上,不光要求获得最高票,还要求票过半数。否则会拒绝预测。
- 更加复杂的是加权投票法,和加权平均法一样,每个弱学习器的分类票数要乘以一个权重,最终将各个类别的加权票数求和,最大的值对应的类别为最终类别。
下面代码将三种算法组成的集成算法和算法本身进行比较:
#投票分类器
from sklearn import datasets
from sklearn.model_selection import cross_val_score
from sklearn.linear_model import LogisticRegression
from sklearn.naive_bayes import GaussianNB
from sklearn.tree import DecisionTreeClassifier
from sklearn.ensemble import RandomForestClassifier
from sklearn.ensemble import VotingClassifier
iris = datasets.load_iris() #三分类问题,150条数据,4个特征
X,y = iris.data[:,1:3],iris.target
#初始化3个子模型
clf1 = LogisticRegression(solver='lbfgs'
,multi_class='multinomial'
,random_state=1)
clf2 = DecisionTreeClassifier(min_samples_leaf=10
,min_samples_split=0.1)
clf3 = GaussianNB()
#初始化集成模型
eclf = VotingClassifier(estimators=[('lr', clf1)
,('rf', clf2)
,('gnb',clf3)],
voting = 'hard') #hard voting 少数服从多数
#soft voting 平均法
for clf, label in zip([clf1, clf2, clf3, eclf], ['逻辑回归','决策树','朴素贝叶斯','集成学习']):
#计算交叉验证各个模型的得分
scores = cross_val_score(clf, X, y, cv=5, scoring='accuracy')
print("模型{0}的平均准确率为{1:0.2f},准确率方差为{2:0.2f} ".format(label,scores.mean(),scores.std()))
学习法stacking
上两节的方法都是对弱学习器的结果做平均或者投票,相对比较简单,但是可能学习误差较大,于是就有了学习法这种方法,对于学习法,代表方法是stacking,当使用stacking的结合策略时, 我们不是对弱学习器的结果做简单的逻辑处理,而是通过使用一个学习器来进行结合。在stacking算法中,我们把个体学习器叫做初级学习器,用于结合的学习器叫做次级学习器或元学习器(meta-learner)。次级学习器用于训练的数据叫做次级训练集。次级训练集是在训练集上用初级学习器得到的。
stacking 就是将一系列模型(也称基模型)的输出结果作为新特征输入到其他模型,这种方法由于实现了模型的层叠,即第一层的模型输出作为第二层模型的输入,第二层模型的输出作为第三层模型的输入,依次类推,最后一层模型输出的结果作为最终结果。本文会以两层的 stacking 为例进行说明。
Stacking学习的过程如下图所示:
在训练阶段,次级训练集是利用初级学习器产生的,若直接用初级学习器的训练集来产生次级训练集,则过拟合的风险会比较大;我们一般有两种方法来处理这个问题:
- 次级模型尽量选择简单的线性模型,这也是分类问题时logistic回归模型频繁出现在stacking第二层分类器的原因,除了模型原理简单以外,logistic回归还可以添加正则化项来避免过拟合。
- 利用K折交叉验证,使用训练初始学习器未使用的样本来产生次级学习器。
由于stacking中的每个基模型都需要对数据集进行划分后进行交叉训练,如果为每个模型都写这部分的代码会显得非常冗余,因此这里提供一种简便实现 stacking 的思路。
具体做法就是先实现一个父类,父类中实现了交叉训练的方法,因为这个方法对所有模型都是一致的,然后声明两个方法,train和predict,由于采用的基模型不同,这两个方法的具体实现也不同,因此需要在子类中实现。
import numpy as np
from sklearn.model_selection import KFold
class BasicModel(object):
"""Parent class of basic models"""
def train(self, x_train, y_train, x_val, y_val):
"""return a trained model and eval metric of validation data"""
pass
def predict(self, model, x_test):
"""return the predicted result of test data"""
pass
def get_oof(self, x_train, y_train, x_test, n_folds = 5):
"""K-fold stacking"""
num_train, num_test = x_train.shape[0], x_test.shape[0]
oof_train = np.zeros((num_train,))
oof_test = np.zeros((num_test,))
oof_test_all_fold = np.zeros((num_test, n_folds))
aucs = []
KF = KFold(n_splits = n_folds, random_state=2021)
for i, (train_index, val_index) in enumerate(KF.split(x_train)):
print('{0} fold, train {1}, val {2}'.format(i,
len(train_index),
len(val_index)))
x_tra, y_tra = x_train[train_index], y_train[train_index]
x_val, y_val = x_train[val_index], y_train[val_index]
model, auc = self.train(x_tra, y_tra, x_val, y_val)
aucs.append(auc)
oof_train[val_index] = self.predict(model, x_val)
oof_test_all_fold[:, i] = self.predict(model, x_test)
oof_test = np.mean(oof_test_all_fold, axis=1)
print('all aucs {0}, average {1}'.format(aucs, np.mean(aucs)))
return oof_train, oof_test
上面最重要的就是进行 K-fold 训练的get_oof 方法,该方法最终返回训练集和测试集在基模型上的预测结果,也就是两个一维向量,长度分别是训练集和测试集的样本数。
下面以两个基模型为例进行 stacking,分别是 xgboost 和 lightgbm,这两个模型都只需要实现BasicModel中的train和predict方法.
第一个基模型xgboost:
import xgboost as xgb
class XGBClassifier(BasicModel):
def __init__(self):
"""set parameters"""
self.num_rounds=1000
self.early_stopping_rounds = 15
self.params = {
'objective': 'binary:logistic',
'eta': 0.1,
'max_depth': 8,
'eval_metric': 'auc',
'seed': 0,
'silent' : 0
}
def train(self, x_train, y_train, x_val, y_val):
print('train with xgb model')
xgbtrain = xgb.DMatrix(x_train, y_train)
xgbval = xgb.DMatrix(x_val, y_val)
watchlist = [(xgbtrain,'train'), (xgbval, 'val')]
model = xgb.train(self.params,
xgbtrain,
self.num_rounds)
watchlist,
early_stopping_rounds = self.early_stopping_rounds)
return model, float(model.eval(xgbval).split()[1].split(':')[1])
def predict(self, model, x_test):
print('test with xgb model')
xgbtest = xgb.DMatrix(x_test)
return model.predict(xgbtest)
第二个基模型lightgbm:
import lightgbm as lgb
class LGBClassifier(BasicModel):
def __init__(self):
self.num_boost_round = 2000
self.early_stopping_rounds = 15
self.params = {
'task': 'train',
'boosting_type': 'dart',
'objective': 'binary',
'metric': {'auc', 'binary_logloss'},
'num_leaves': 80,
'learning_rate': 0.05,
# 'scale_pos_weight': 1.5,
'feature_fraction': 0.5,
'bagging_fraction': 1,
'bagging_freq': 5,
'max_bin': 300,
'is_unbalance': True,
'lambda_l2': 5.0,
'verbose' : -1
}
def train(self, x_train, y_train, x_val, y_val):
print('train with lgb model')
lgbtrain = lgb.Dataset(x_train, y_train)
lgbval = lgb.Dataset(x_val, y_val)
model = lgb.train(self.params,
lgbtrain,
valid_sets = lgbval,
verbose_eval = self.num_boost_round,
num_boost_round = self.num_boost_round)
early_stopping_rounds = self.early_stopping_rounds)
return model, model.best_score['valid_0']['auc']
def predict(self, model, x_test):
print('test with lgb model')
return model.predict(x_test, num_iteration=model.best_iteration)
下一个步骤就是将这两个基模型的输出作为第二层模型的输入,这里选用的第二层模型是 LogisticsRegression.
首先需要将各个基模型的输出 reshape 和 concatenate 成合适的大小:
lgb_classifier = LGBClassifier()
lgb_oof_train, lgb_oof_test = lgb_classifier.get_oof(x_train, y_train, x_test)
xgb_classifier = XGBClassifier()
xgb_oof_train, xgb_oof_test = xgb_classifier.get_oof(x_train, y_train, x_test)
input_train = [xgb_oof_train, lgb_oof_train]
input_test = [xgb_oof_test, lgb_oof_test]
stacked_train = np.concatenate([f.reshape(-1, 1) for f in input_train], axis=1)
stacked_test = np.concatenate([f.reshape(-1, 1) for f in input_test], axis=1)
然后用第二层模型进行训练和预测:
from sklearn.linear_model import LinearRegression
final_model = LinearRegression()
final_model.fit(stacked_train, y_train)
test_prediction = final_model.predict(stacked_test)