模型融合
模型融合常常是竞赛取得胜利的关键!
具有差异性的模型融合往往能给结果带来很大的提升。虽然并不是每次使用模型融合都能起到很大的作用,但是就平常的竞赛经验而言,尤其是在最终成绩相差不大的情况下,模型融合的方法往往会成为取胜的关键之一。
作者从构建多样性、训练过程融合和训练结果融合三部分介绍不同模型融合方法的应用场景。
1 构建多样性
1.1 特征多样性
构建多个有差异的特征集并分别建立模型,可使特征存在于不同的超空间,从而建立的多个模型有不同的泛化误差,最终模型融合时可以起到互补的效果。在竞赛中,队友之间的特征集往往是不一样的,在分数差异不大的情况下,直接进行模型融合基本会获得不错的收益。
另外,像随机森林的max_features
,XGBoost
中的colsample_bytree
和LightGBM
中的feature_fraction
都是用来对训练集中的特征进行采样的,其实本质上就是构建特征的多样性。
1.2 样本多样性
样本多样性来自于不同的样本集,具体的做法是将数据集切分成多份,然后分别建立模型。我们知道很多树模型在训练时会进行采样,主要目的是防止过拟合,从而提升预测的准确性。
有时候将数据集切分为多份并不是随机进行的,而是根据具体的赛题数据进行切分,需要考虑如何切分可以构建最大限度的数据差异性,并用切分后的数据分别训练模型(2019年天池“全球城市计算AI挑战赛”)。
1.3 模型多样性
不同模型对数据的表达能力是不同的,比如FM(Factorization Machine,因子分解机)能够学习到特征之间的交叉信息,并且记忆性较强;树模型可以很好的处理连续特征和离散特征(比如LightGBM和CatBoost),并且对异常值也具有很好的健壮性。把这两类在数据假设、表征能力方面有差异的模型融合起来肯定会达到一定的效果。
还有很多其他构建多样性的方法,比如训练目标多样性、参数多样性和损失函数选择的多样性等,都能产生非常好的效果。
2 训练过程融合
模型融合的方式有两种,第一种是训练过程融合,例如随机森林和XGBoost
,这两种模型在训练种构造多个决策树进行融合,多个决策树可以看成是多个弱分类器,随机森林通过Bagging
的方式进行融合,XGboost
通过Boosting
的方式进行融合。
2.1 Bagging
Bagging
从训练集中有放回的取出数据,这些数据构成样本集,这也保证了训练集的规模不变,然后用样本集训练弱分类器。重复上述过程多次,取平均值或者采用投票机制获取模型融合的最终结果。
Bagging
通过减小误差之间的差来减少分类器的方差。换言之Bagging
可以降低过拟合的风险。Bagging
算法的效率来自于训练数据的不同,各模型之间存在很大的差异,并且在加权融合的过程中可使训练数据的错误相互抵消。
可以选择相同的分类器进行训练,也可以选择不同的分类器。
2.2 Boosting
Boosting
的思想其实并不难理解,首先训练一个弱分类器,并把这个弱分类器分错类的样本记录下来,同时给予这个弱分类器一定的权重;然后建立一个新的弱分类器给予前面的错误样本进行训练,同样,我们也给予这个分类器一个权重。重复上面的过程,直到弱分类器的性能达到某一指标,例如当建立的新弱分类器并不会使准确度显著提升的时候停止迭代。最后将这些弱分类器各自乘上相应的权重并全部加起来,得到最后的强分类器。
基于Boosting
的算法AdaBoost
、LightGBM
、XGBoost
和CatBoost
。
3 训练结果融合
模型融合的第二种方式是训练结果融合,主要分为加权法、Stacking
和Blending
,这些方法都可以有效的提高模型的整体预测能力。
3.1 加权法
加权法对于一系列任务(比如分类和回归)和评价指标(如AUC、MSE或Logloss)都是很有效的,比如我们有10个算法模型都预测到了结果,直接对这10个结果取平均值或者给予每个算法不同的权重,即得到了融合结果。加权法通常还能减少过拟合,因为每个模型的结果可能存在一定的噪声。
对于分类问题,可以使用的方法有投票法和加权法等。
对于回归问题,可以使用的加权法有算术平均和几何平均等,当评分规则是SMAPE(平均绝对百分比误差)时,选择算数平均会使模型融合的结果偏大,这不符合SMAPE的直觉,越小的值对评分影响越大,所以选择几何平均,能够使结果偏向小值。
对于排序问题如果是使用MRR
作为评价指标,命名的位置越靠前,得分也越高。因此我们不仅进行加权融合,还需要让结果偏向小值,这时候就要对结果进行转换,然后再用加权法进行融合,一般而言使用的转换方式是log变换
,如果以AUC作为排序指标,一般使用排序均值的融合思路。
3.2 Stacking融合
使用加权法进行融合虽然简单,但需要人工来确定权重,因此可以考虑更加智能的方式,通过新的模型来学习每个分类器的权重。我们假设有两层分类器,如果在第一层中某个特定的基分类器错误的学习了特征空间的某个区域,这种错误的学习行为可能会被第二层分类器检测到,这与其他分类器的学习行为一样,可以纠正不恰当的训练。上述过程就是Stacking
融合的基本思想。
这里注意两点:第一,构建的新模型一般是简单模型,比如逻辑回归这样的线性模型;第二,使用多个模型进行Stacking融合会有比较好的效果。
Stacking
融合使用基模型的预测结果作为第二层模型的输入,我们不能简单地使用完整的训练集数据来训练基模型,这会产生基分类器在预测时就已经看到测试集的风险,因此在提供预测结果时出现过拟合问题。所以我们使用Out-of-Fold
的方式进行预测,也就是使用K折交叉验证的方式来预测结果。
4 实战案例
完成Stacking融合的操作需要构造多个模型的预测结果,一般是三个以上,这里选择ExtraTreesRegressor
、RandomForestRegressor
、Ridge
和Lasso
作为基分类器,Ridge
作为最终分类器。
from sklearn.ensemble import ExtraTreesRegressor
from sklearn.ensemble import RandomForestRegressor
from sklearn.metrics import mean_squared_error
from sklearn.linear_model import Ridge, Lasso
from math import sqrt
from sklearn.model_selection import KFold
import pandas as pd
# 五折交叉验证
kf = KFold(n_splits=5, shuffle=True, random_state=2023)
train = pd.read_csv('train.csv')
test = pd.read_csv('test.csv')
all_data = pd.concat((train,test))
all_data = pd.get_dummies(all_data)
# 填充缺失值
all_data = all_data.fillna(all_data.mean())
# 数据切分
x_train = all_data[:train.shape[0]]
x_test = all_data[train.shape[0]:]
y_train = train.SalePrice
构建一个针对sklearn
中模型的功能类,初始化参数然后训练和预测。
class SklearnWrapper(object):
def __init__(self, clf, seed=0, params=None):
params['random_state'] = seed
self.clf = clf(**params)
def train(self, x_train, y_train):
self.clf.fit(x_train, y_train)
def predict(self, x):
return self.clf.predict(x)
封装交叉验证函数,这段代码的可复用性强。
import numpy as np
def get_oof(clf):
oof_train = np.zeros((x_train.shape[0], ))
oof_test = np.zeros((x_test.shape[0], ))
oof_test_skf = np.empty((5, x_test.shape[0]))
scores = []
for i, (train_index, valid_index) in enumerate(kf.split(x_train, y_train)):
trn_x, trn_y, val_x, val_y = x_train.iloc[train_index], y_train[train_index], \
x_train.iloc[valid_index], y_train[valid_index]
clf.train(trn_x, trn_y)
oof_train[valid_index] = clf.predict(val_x)
oof_test_skf[i, :] = clf.predict(x_test)
score_single = sqrt(mean_squared_error(val_y, oof_train[valid_index]))
scores.append(score_single)
print(f'mean: {np.mean(scores)}')
oof_test[:] = oof_test_skf.mean(axis=0)
return oof_train.reshape(-1, 1), oof_test.reshape(-1, 1)
接下来是基分类器训练和预测的部分代码,可预测四个模型的验证集结果和测试集结果,并辅助最后一步的Stacking融合。
et_params = {
'n_estimators': 100,
'max_features': 0.5,
'max_depth': 12,
'min_samples_leaf':2
}
rf_params = {
'n_estimators': 100,
'max_features': 0.2,
'max_depth': 12,
'min_samples_leaf': 2
}
rd_params = {'alpha': 10}
ls_params = {'alpha': 0.005}
et = SklearnWrapper(clf=ExtraTreesRegressor, seed=2023, params=et_params)
rf = SklearnWrapper(clf=RandomForestRegressor, seed=2023, params=rf_params)
rd = SklearnWrapper(clf=Ridge, seed=2020, params=rd_params)
ls = SklearnWrapper(clf=Lasso, seed=2020, params=ls_params)
print('使用极限树:')
et_oof_train, et_oof_test = get_oof(et)
print('使用随机森林:')
rf_oof_train, rf_oof_test = get_oof(rf)
print('使用岭回归:')
rd_oof_train, rd_oof_test = get_oof(rd)
print('使用Lasso回归:')
ls_oof_train, ls_oof_test = get_oof(ls)
最后是Stacking部分,使用Ridge模型,当然也可以尝试树模型这类更加复杂的模型。
def stack_model(oof_1, oof_2, oof_3, oof_4, predictions_1, predictions_2, predictions_3, predictions_4, y):
train_stack = np.hstack([oof_1, oof_2, oof_3, oof_4])
test_stack = np.hstack([predictions_1, predictions_2, predictions_3, predictions_4])
oof = np.zeros((train_stack.shape[0]))
predictions = np.zeros((test_stack.shape[0]))
scores = []
for fold_, (trn_idx, val_idx) in enumerate(kf.split(train_stack, y)):
trn_data, trn_y = train_stack[trn_idx], y[trn_idx]
val_data, val_y = train_stack[val_idx], y[val_idx]
clf = Ridge(random_state=2020)
clf.fit(trn_data, trn_y)
oof[val_idx] = clf.predict(val_data)
predictions += clf.predict(test_stack) / 5
score_single = sqrt(mean_squared_error(val_y, oof[val_idx]))
scores.append(score_single)
print(f'{fold_ + 1} / {5}', score_single)
print(f'mean: {np.mean(scores)}')
return oof, predictions
Stacking融合
print('使用Stack:')
oof_stack, predictions_stack = stack_model(et_oof_train, rf_oof_train, rd_oof_train, ls_oof_train, et_oof_test, rf_oof_test, rd_oof_test, ls_oof_test, y_train)