Stacking

万壑松风知客来,摇扇抚琴待留声

1. 文起

集成算法作为机器学习中的一大亮点,并且在多数场景下得到了很好的实践效果,例如 Kaggle、天池等很多比赛中选手最后都会将众多较优模型进行集成,得到其融合效果,往往这种方法等得到更优的结果。

目前集成算法大致包括了 Bagging(袋装)、Boosting(提升)、Stacking(堆叠)、Blending(混合)等几个大的思想。其中袋装以随机森林作为代表,提升以梯度提升一些列为代表。本文对这两个算法暂时不做介绍,重点说说堆叠的集成方式。

2. Stacking 思想

通过结合策略,可以将个体学习器进行集成在一起。对于分类问题,可以通过投票法选择出得票最多的类;对于回归问题,可以通过平均法求所有学习器输出结果的平均值。Stacking 与前面两种方式不同,通过一个机器学习器将其它多个个体机器学习器的预测结果结合作为样本数据来进行训练、预测。

相比于同质集成算法 Bagging、Boosting,Stacking 属于异质集成算法。在 Stacking 中个体学习器叫做初级学习器,用于结合的学习器叫做次级学习器或元学习器(meta-learner),次级学习器用于训练的数据叫做次级训练集。次级训练集是在训练集上用初级学习器得到的。你可以使用加入任何你觉得效果好的初级模型,并且次级模型也是可以任意选择的。

3. 实现方式

下图是 Stacking 的实现原理:

乍一眼看有点晕,不过一点点细看其实并不难,比 Boosting 还要简单,稍微总结一下:

以上是两个阶段的 Stacking,也是比较常用的构建方式。第一层由多个个体学习器组成,第二层由第一层的个体学习器输出作为特征加入训练集通过次级学习器在训练。

上图中可以看到首先将整体数据划分为 m 行训练集(Training Data)和 n 行测试集(Test Data),并且第一层的个体学习器使用了 5 个模型,每个模型的处理方式,训练集都使用了 5 折的交叉验证。以 Model1 为例,训练集再划分每次使用其中四折作为训练样本(Learn),剩下一折数据作为测试样本(Predict),然后使用 Model1 对训练样本训练,对测试样本进行预测得到该折的预测值,Model1 如此重复 5 次直至交叉验证完成,最后会得到每折的一个预测结果,将他们按列组合后就是整个训练集大小列的预测结果,形如 (m行X1列)的预测结果数据集。注意在交叉验证的过程中,每次训练的模型不仅要对训练集划分的测试集预测,还要对真正的测试集进行预测。如按照 5 折交叉验证,那么最后对真正的测试集将会产生 5 份预测结果,这 5 个结果按行取均值得到(n行X1列)的测试集预测结果,到此第一层的第一个个体模型 Model1 就完成它的工作。

按照 Model1 的方式,重复每个个体模型(这里的 Model2、Model3、Model4、Model5),最后会得到 5 个(m行X1列)的模型对训练集预测结果,和 5 个(n行X1列)的模型对测试集预测结果。这些结果将作为新特征加入第二层的模型训练和预测。

整个数据集上的真实标签的使用理解起来有点难,只需要记得每个样本和它的标签一一相对应,只有在作为训练集训练模型时才使用,比如对划分的训练集进行交叉验证时,四折作为训练集它们的真实标签会参与当前模型训练,剩下一折的测试数据只会对其进行预测,不会用到它们的真实标签。真实的测试数据在两个阶段都不会用到它们的标签。

上面完成了比较绕的第一阶段。第二阶段使用 5 个(m行X1列)的预测数据集按行排列组成新的训练集,使用 5 个(n行X1列)的预测数据集按行排列组成新的测试集。使用次级模型对它们训练和预测,得到测试集上的预测结果,此时可以与测试集的真实标签进行准确性分析。

4. 函数实现与封装

理解思想是一方面,代码实现是一方面,如果能从基础思想中构建出实现代码那基本上就是大体掌握了 Stacking。下面将给出两部分代码,第一部分是分为函数逐步实现的过程,第二部分是对 Stacking 封装成类似 Sklearn 接口的类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
import pandas as pd
import numpy as np
from sklearn.model_selection import train_test_split
from sklearn.model_selection import KFold
from sklearn.metrics import mean_absolute_error
from sklearn.metrics import mean_squared_error
from sklearn.linear_model import LinearRegression
from sklearn.svm import SVR
from sklearn.ensemble import RandomForestRegressor
from sklearn.tree import DecisionTreeRegressor

def obtain_data():
df = pd.read_csv(r'./data.csv')
# 数据处理,这个按照你的要求处理数据。

# columns中填入需要的特征名
columns = ['a','b',...]
df_X = df[columns]
df_y = df['target']
train_X, test_X, train_y, test_y = train_test_split(df_X, df_y, test_size=0.25, random_state=999)
return train_X, test_X, train_y, test_y

# 传入DataFrame类型
def stacking(clf, train_X, train_y, test_X, n_folds=3):
train_X = train_X.get_values()
train_y = train_y.get_values()
test_X = test_X.get_values()

train_size, test_size = train_X.shape[0], test_X.shape[0]
next_level_train_set = np.zeros((train_size,))
next_level_test_set = np.zeros((test_size,))
train_nfolds_test_sets = np.zeros((test_size,n_folds))
# kf划分能得到训练集、测试集的索引标签
kf = KFold(n_splits=n_folds, shuffle=True, random_state=999)
# 这是训练集的CV划分,记住所有的数据格式都是ndarray
for i,(train_index, test_index) in enumerate(kf.split(train_X)):
cv_train_X, cv_train_y = train_X[train_index], train_y[train_index]
cv_test_X, cv_test_y = train_X[test_index], train_y[test_index]
clf.fit(cv_train_X, cv_train_y)
# 下面对训练集中划分出来的测试集预测,循环完毕得到整个训练集的预测值
next_level_train_set[test_index] = clf.predict(cv_test_X)
# 下面对测试集进行预测得到(测试行,n_folds列)
train_nfolds_test_sets[:,i] = clf.predict(test_X)

next_level_test_set[:] = train_nfolds_test_sets.mean(axis=1)
return next_level_train_set, next_level_test_set

def create_model():
# 可以多添加模型融合,模型比如下面这样给就行,模型有点多参数先自己确定
linear_regressor = LinearRegression()
svr = SVR()
rf = RandomForestRegressor(n_estimators=300)
# 模型加入列表
model_list = [linear_regressor, svr, rf]
return model_list

def run_stacking():
train_X, test_X, train_y, test_y = obtain_data()
model_list = create_model()

# 得到下一阶段(这里指第二阶段)的训练集、测试集 形状
m_train = train_X.shape[0]
m_test = test_X.shape[0]
n = len(model_list)
meta_train = np.zeros((m_train, n))
meta_test = np.zeros((m_test, n))
for i,clf in enumerate(model_list):
train_set, test_set = stacking(clf, train_X, train_y, test_X)
meta_train[:,i] = train_set
meta_test[:,i] = test_set
return meta_train, meta_test, train_y, test_y

def next_preprocess(meta_train, meta_test, train_y, test_y):
# 第二阶段使用决策树作为次级模型来验证(可修改模型修改参数)
tree = DecisionTreeRegressor()
tree.fit(meta_train, train_y)
tree_pred = tree.predict(meta_test)

mae = mean_absolute_error(test_y, tree_pred)
print("最终的均对误差为:{}".format(mae))
mse = mean_squared_error(test_y, tree_pred)
print("最终的均方误差为:{}".format(mse))
error = np.absolute(test_y - tree_pred) / test_y
precision = np.mean(error.get_values())
print('测试集上的精度为:{}'.format(precision))

if __name__=="__main__":
meta_train, meta_test, train_y, test_y= run_stacking()
next_preprocess(meta_train, meta_test, train_y, test_y)

以上是函数式的实现方式,较为详细可以先理解清楚。下面对其进行封装,封装后的 Stacking 只需要实例化对象后传入接口规范化数据即可实现 fit、predict 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
import numpy as np
from sklearn.model_selection import KFold
from sklearn.base import BaseEstimator, RegressorMixin, TransformerMixin, clone

class StackingModels(BaseEstimator, RegressorMixin, TransformerMixin):
def __init__(self, base_models, meta_model, n_folds=3):
self.base_models = base_models
self.meta_model = meta_model
self.n_folds = n_folds

def fit(self, X, y):
# self.base_models_是用来保存每折的训练模型,predect时对测试集使用
self.base_models_ = [list() for x in self.base_models]
self.meta_model_ = clone(self.meta_model)
kfold = KFold(n_splits=self.n_folds, shuffle=True, random_state=999)

# 每个模型做cv验证,并得到每次的测试集预测结果(总的结果为整个数据集)
cv_fold_predictions = np.zeros((X.shape[0], len(self.base_models)))
for i, model in enumerate(self.base_models):
for train_index, test_index in kfold.split(X, y):
clf = clone(model)
clf.fit(X[train_index], y[train_index])
self.base_models_[i].append(clf)
y_pred = clf.predict(X[test_index])
cv_fold_predictions[test_index,i] = y_pred

# 使用次级模型拟合次级训练集,不写在下面是因为fit的时间都计算在fit函数中
self.meta_model_.fit(cv_fold_predictions, y)
return self

def predict(self, X):
meta_features = np.column_stack([np.column_stack([model.predict(X) for model in base_models]).mean(axis=1) for base_models in self.base_models_])
return self.meta_model_.predict(meta_features)


# 使用简介
# from stacking_test import StackingModels

# rf = RandomForestRegressor()
# lr = ......
# base_models = [rf,lr,knn,...]
# 选择二阶训练的模型,比如线性回归
# meta_model = LinearRegression()
# train_X, test_X, train_y, test_y = train_test_split(df_X, df_y, test_size=0.25, random_state=999)

# 传入融合模型列表、元模型、折数
# sm = StackingModels(base_models, meta_model, n_folds=5)
# 将DataFrame转换为ndarray格式
# sm.fit(train_X.get_values(),train_y.get_values())
# tree_pred = sm.predict(test_X.get_values())

5. 文末

深刻理解第 3 点后,再理解第 4 点的代码实现就算差不多掌握了 Stacking 的集成原理。集成算法的使用当下应当是大势所趋,所以多掌握一种思想也未尝不是好事呢。