模型融合与模型堆叠,听到这两个词的时候,脑海中立马跳出来线上线下的机器学习比赛。这种情况已成为过去,现在随着算力提升和更廉价的机器,人们现在已经可以在生产中堆叠模型。例如,很容易部署多个神经网络模型,提供响应时间小于 500ms 的实时服务。
有时候,大网络大模型可以被一些小模型替代,提供相似性能的服务,但响应更快。在这种情况下,你会选择哪种模型?对我个人而言,我会选多个小模型,更快,同时性能差不多。
要记得这点,模型越小,运行速度越快,调整性能越容易。
模型融合就是组合不同的模型,可以通过输出的概率值结合在一起,最简单的方式是平均输出概率值,简单且高效。在这种方式下,各模型权重相等。
但要记得,要组合处理过程不会高度相似的模型。
如果模型结果不是概率值,你也可以组合预测值。最简单的方式是投票 Voting,假设有一个多分类任务,有三个目标类 [0, 1, 2],投票过程如下。
-
[0, 0, 1]:最高票类别为 0。
-
[0, 1, 2]:无最高票类别,随机选择一个类别。
-
[2, 2, 2]:最高票类别为 2。
实现代码如下。
import numpy as np
def mean_probs(probs):
return np.mean(probs, axis=1)
def vote_preds(preds):
idx = np.apply_along_axis(np.bincount, axis=1, arr=preds)
return np.argmax(idx, axis=1)
probs = np.random.random((3, 3))
print(mean_probs(probs))
preds = np.random.randint(0, 3, size=(3, 3))
print(vote_preds(preds))
输入的参数是二维数组,列表示不同模型的输出,行表示不同类型目标。
你可以根据需要修改计算过程。
另一种组合方式是按概率值排序。
import numpy as np
import pandas as pd
from scipy.stats import rankdata
def rank_probs(probs):
ranked = []
for i in range(probs.shape[1]):
rank_data = rankdata(probs[:, i])
ranked.append(rank_data)
ranked = np.column_stack(ranked)
return np.mean(ranked, axis=1)
为什么要做这种模型融合的处理?
上图显示了三个人猜测大象的高度,真实高度更接近三个猜测值的平均值。假设每个人都猜的很准,那么平均三个人的猜测结果会更准。当然,平均过程也可以加权计算。
假如你有一个 AUC 很高的随机森林模型,和一个稍微低一点 AUC 的逻辑回归模型,你可以按照 7:3 的权重,对模型结果做加权平均。假如还有一个 XGBoost 模型的 AUC 高于随机森林模型,可以按照 3:2:1 的权重,对模型结果做加权平均。那么这个权重是如何确定的?
假设有三个猴子和三个 0-1 的旋钮,每个猴子操作一个旋钮,我们把它们每次调整旋钮后的值当作 AUC。最终,猴子们会找到一个最佳组合方式,获得最好的 AUC 得分。这就是随机搜索的思路。
在搜索之前,你必须牢记以下融合规则:
-
第一,在融合前创建 Folds。
-
第二,在融合前创建 Folds。
是的,这是两条融合规则,我没写错。
第一步,创建 Folds,为了方便解释,将数据划分为两部分:Fold 1、Fold 2。在现实场景中,你需要更多的划分数量。
然后在 Fold 1 上训练不同模型,在 Fold 2 上做预测。然后,在 Fold 1 上训练,在 Fold 2 上预测。因此,我们得到了在所有训练数据上的预测结果。
接下来该融合模型,拿 Fold 1 和 Fold 1 上的预测结果,创建优化函数,寻找最小化误差或最大化 AUC 的权重值。然后在 Fold 2 上检测权重值是否有效。
计算代码如下。
import numpy as np
from functools import partial
from scipy.optimize import fmin
from sklearn import metrics
class OptimizeAUC:
def __init__(self):
self.coef_ = 0
def _auc(self, coef, x, y):
x_coef = x * coef
preds = np.sum(x_coef, axis=1)
auc = metrics.roc_auc_score(y, preds)
return -1 * auc
def fit(self, x, y):
auc_partial = partial(self._auc, x=x, y=y)
init_coef = np.random.dirichlet(
np.ones(x.shape[1]), size=1
)
self.coef_ = fmin(auc_partial, init_coef, disp=True)
def predict(self, x):
x_coef = x * self.coef_
return np.sum(x_coef, axis=1)
然后看一下该方法如何使用。
import xgboost as xgb
from sklearn.datasets import make_classification
from sklearn import (
ensemble, linear_model,
metrics, model_selection
)
x, y = make_classification(n_samples=10000, n_features=25)
xfold1, xfold2, yfold1, yfold2 = model_selection.train_test_split(
x, y, test_size=0.5, stratify=y
)
logreg = linear_model.LogisticRegression()
rf = ensemble.RandomForestClassifier()
xgbc = xgb.XGBClassifier()
logreg.fit(xfold1, yfold1)
rf.fit(xfold1, yfold1)
xgbc.fit(xfold1, yfold1)
pred_logreg = logreg.predict_proba(xfold2)[:, 1]
pred_rf = rf.predict_proba(xfold2)[:, 1]
pred_xgbc = xgbc.predict_proba(xfold2)[:, 1]
avg_pred = (pred_logreg + pred_rf + pred_xgbc) / 3
fold2_pred = np.column_stack((
pred_logreg, pred_rf, pred_xgbc, avg_pred
))
fold2_auc = []
for i in range(fold2_pred.shape[1]):
auc = metrics.roc_auc_score(yfold2, fold2_pred[:, i])
fold2_auc.append(auc)
print(f"Fold-2: LR AUC = {fold2_auc[0]}")
print(f"Fold-2: RF AUC = {fold2_auc[1]}")
print(f"Fold-2: XGB AUC = {fold2_auc[2]}")
print(f"Fold-2: Average Pred AUC = {fold2_auc[3]}")
logreg = linear_model.LogisticRegression()
rf = ensemble.RandomForestClassifier()
xgbc = xgb.XGBClassifier()
logreg.fit(xfold2, yfold2)
rf.fit(xfold2, yfold2)
xgbc.fit(xfold2, yfold2)
pred_logreg = logreg.predict_proba(xfold1)[:, 1]
pred_rf = rf.predict_proba(xfold1)[:, 1]
pred_xgbc = xgbc.predict_proba(xfold1)[:, 1]
avg_pred = (pred_logreg + pred_rf + pred_xgbc) / 3
fold1_pred = np.column_stack((
pred_logreg, pred_rf, pred_xgbc, avg_pred
))
fold1_auc = []
for i in range(fold2_pred.shape[1]):
auc = metrics.roc_auc_score(yfold1, fold1_pred[:, i])
fold1_auc.append(auc)
print(f"Fold-2: LR AUC = {fold1_auc[0]}")
print(f"Fold-2: RF AUC = {fold1_auc[1]}")
print(f"Fold-2: XGB AUC = {fold1_auc[2]}")
print(f"Fold-2: Average Pred AUC = {fold1_auc[3]}")
opt = OptimizeAUC()
opt.fit(fold1_pred[:, :-1], yfold1)
opt_preds_fold2 = opt.predict(fold2_pred[:, :-1])
auc = metrics.roc_auc_score(yfold2, opt_preds_fold2)
print(f"Optimized AUC, Fold 2 = {auc}")
print(f"Coefficients = {opt.coef_}")
opt = OptimizeAUC()
opt.fit(fold2_pred[:, :-1], yfold2)
opt_preds_fold1 = opt.predict(fold1_pred[:, :-1])
auc = metrics.roc_auc_score(yfold1, opt_preds_fold1)
print(f"Optimized AUC, Fold 1 = {auc}")
print(f"Coefficients = {opt.coef_}")
可以从结果中看到该方法比平均值更好。但有时候平均值会更好。
每个随机森林都是融合模型,它是一组决策树的组合。随机森林来自融合模型的 Bagging 思想,在 Bagging 中,创建数据的多个小的子集,训练多个模型,最终预测结果来自多个模型的预测结果组合,比如均值。
XGBoost 也是融合模型。所有梯度增强模型都是融合模型,因此都属于 boosting 模型。除了连续 Boosting 模型通过误差残差训练,以最小化之前模型的误差之外,Boosting 模型与 Bagging 模型类似。然后,Boosting 模型会完美拟合数据,因此对过拟合现象敏感。
上述代码片段中只处理单列之间的计算,真实情况不止于此,很多时候你需要处理多列概率之间的计算。例如,你要预测多分类中的某一类,你可以简单使用投票方法,但该方法通常不是最优的。如果你想要组合这些概率,你会得到两维数组,而不是之前二分类问题中的一维向量。在这种情况下,你需要改变优化器 Optimizer 与预测函数。
现在我们转向下一个有趣的话题,非常著名的模型堆叠 Stacking。
模型堆叠不是很困难的事,它很简单,如果你有正确的交叉验证,并保证整个模型训练过程中使用的数据 Folds 一致,那就不会有过拟合问题。
下边简单介绍模型堆叠思路。
-
将训练数据划分为 Folds。
-
训练一批模型 M1,M2,...,Mn。
-
创建全 Folds 的训练时验证预测值。
-
到此,完成 Level-1(L1)模型训练。
-
使用上述 Folds 预测值作为另一个模型的特征。到此为 Level-2(L2)模型。
-
使用同样的 Folds 来训练 L2 中使用的模型。
-
在训练集和测试集上创建 OOF(out of fold)预测值。
接下来可以重复 L1 部分,创建更多层模型。
有时候会看到术语 Blending 混合,该术语仅仅替换了 Fold 创建方法,使用 hold out fold 的方法做 L1 模型训练,即留出某些 Fold 不参与训练,只参与测试。
本章介绍的方法适用于任何机器学习问题:分类、回归、多标签分类等。