连载的上一篇文章,小鱼使用 Bagging 的集成思想,根据政党的捐赠数据来预测政党类型(二分类任务),得到了如下的 ROC 曲线图:
通过将 SVM、KNN 等基础模型的预测结果取平均,得到了 Ensemble 后的 ROC-AUC Score 为:
>> print("Ensemble ROC-AUC score: %.3f" % roc_auc_score(ytest, P.mean(axis=1)))
Ensemble ROC-AUC score: 0.880
文章的最后,小鱼提出了一个疑问:这些基础模型有的预测结果好,有的预测能力一般,如果只是简单粗暴地将结果取平均值,是不公平的。
Bagging 中存在的问题
比如,其中有的基础模型会将大部分样本预测为多数类别,存在过拟合的风险。这一点,可以分析一下各模型预测结果中正负样本的分布情况。
注:REP 使用标签 1 表示,为正样本;DEM 使用标签 0 表示,为负样本。
p = P.apply(lambda x:1*(x>=0.5).value_counts(normalize=True))
p.index = ["DEM", "REP"]
p
输出结果:
测试集真实的分布情况:
>> ytest.value_counts(normalize=True)
0 0.756126
1 0.243874
Name: cand_pty_affiliation, dtype: float64
下面,我们以可视化的方式看一下少数类别的预测结果:
p.loc["REP",:].sort_values().plot(kind="bar")
plt.axhline(0.24, color="g", linewidth=1, alpha=0.3)
plt.title("REP FOR PREDICT")
plt.show()
从比例上来看,mlp-nn
和 logistic
将大部分样本预测成了多数类别 DEM ,因而少数类别占比较小。下面,尝试去掉 mlp-nn
来看下预测结果:
>> include = [c for c in P.columns if c not in ["mlp-nn"]]
>> print("Truncated ensemble ROC-AUC score: %.3f" % roc_auc_score(ytest, P.loc[:,include].mean(axis=1)))
Truncated ensemble ROC-AUC score: 0.877
效果似乎没有得到改善,反而变得糟糕了。这其实是因为每个算法模型有自己的缺点的同时,也存在优点,简单去掉的话并不是一个恰当的方式。
下面是关于 mlp-nn 和 logistics 模型在预测时将大部分样本预测为多数类别的论证:绘制各个基础模型预测结果的混淆矩阵,并打印召回率。
import seaborn as sns
from sklearn.metrics import confusion_matrix
plt.figure(figsize=(10,12), dpi=200)
j = 1
for i in range(len(P.columns)):
model_name = P.columns[i]
cnf_matrix = confusion_matrix(ytest, P.iloc[:,i]>0.5)
recall = cnf_matrix[1,1]/(cnf_matrix[1,0]+cnf_matrix[1,1])
print(f"模型 {model_name} 的召回率为 {recall:.2%}。")
plt.subplot(4,2,j)
plt.title(model_name)
sns.heatmap(cnf_matrix,annot=True,fmt='g')
j += 1
模型召回率:
模型 svm 的召回率为 69.28%。
模型 knn 的召回率为 80.27%。
模型 naive bayes 的召回率为 60.97%。
模型 mlp-nn 的召回率为 51.02%。
模型 random forest 的召回率为 81.35%。
模型 gbm 的召回率为 79.06%。
模型 logistic 的召回率为 51.41%。
预测混淆矩阵:
可以发现 mlp-nn 和 logistic 的召回率确实是很低的,只有 51% 。召回率不是我们评估模型的唯一标准,召回率高表示漏网之鱼少,还须要考虑到误杀的样本。因此,下面我们尝试使用 ROC-AUC Score 来淘汰两个模型。
从曲线下方面积(AUC 面积)来看,我们需要淘汰掉的是 svm 和 naive bayes :
>> include = [c for c in P.columns if c not in ["svm", "naive bayes"]]
>> print("Truncated ensemble ROC-AUC score: %.3f" % roc_auc_score(ytest, P.loc[:,include].mean(axis=1)))
Truncated ensemble ROC-AUC score: 0.884
ROC-AUC Score 由原来的 0.880 提升到了 0.884 。
Stacking 集成
上面我们为了优化结果,手动剔除一些模型,那有没有更智能的方式呢?能不能让机器自己决定怎么利用这些基础模型呢?答案就是 Stacking 集成,可以用下面的公式表示:
对于每一个基础模型,选择一个最合适的权重,但是这些权重我们来怎么定义呢?
那就让我们训练一个模型来定义这些权重吧!也就是说我们将基础模型预测的结果输入到另一个模型中,比如 LR 构建的分类器,这样对于每个基础分类器的预测结果,就可以乘以一个合理的权重值,能力强的基础分类器权重比例自然大,能力弱的分类器权重小。
第二阶段模型的预测结果也就是我们的最终结果。下面是 Stacking 集成思想的示意图:
其中第二个模型的输入为第一阶段各个基础模型的预测结果。下面,我们以代码的形式来实战一下 Stacking 集成。
1、定义基础模型
base_learners = get_models()
其中,get_models
函数在连载的上一篇文章中已经介绍过了,此处不再赘述。get_models
以键值对的形式返回基础模型的名称以及初始化的各类基础模型。
2、定义权重分配模型(第二阶段)
meta_learner = GradientBoostingClassifier(
n_estimators=1000,
loss="exponential",
max_features=4,
max_depth=3,
subsample=0.5,
learning_rate=0.005,
random_state=SEED
)
小鱼这里选中的是 GradientBoostingClassifier
分类器,大家可以尝试其它的分类器。
3、切分训练集数据
将基础模型中的训练集分成两部分:train_base
和 pred_base
,train_base
用来训练各个基础模型,xpred_base
输入基础模型,获得第一阶段的预测值;将第一阶段预测结果作为第二阶段模型的输入,ypred_base
作为第二阶段训练模型时的 y
。
注:第一阶段训练时使用过的数据,同时拿去做预测并将预测结果作为第二阶段的输入是毫无意义的。
代码实现:
xtrain_base, xpred_base, ytrain_base, ypred_base = train_test_split(x_over_sample_train, y_over_sample_train,
test_size=0.5, random_state=SEED)
4、训练基础模型
def train_base_learners(base_learners, x, y, verbose=True):
"""Train all base learners in the library."""
if verbose:
print("Fitting models.")
for i, (name, m) in enumerate(base_learners.items()):
if verbose: print("%s..." % name, end=" ", flush=False)
m.fit(x, y)
if verbose: print("done")
训练基础模型时,使用的训练集 x
y
分别为 xtrain_base
和 ytrain_base
:
>> train_base_learners(base_learners, xtrain_base, ytrain_base)
Fitting models.
svm... done
knn... done
naive bayes... done
mlp-nn... done
random forest... done
gbm... done
logistic... done
5、准备第二阶段 权重分配训练器 的训练数据
第二阶段训练器的输入即:
下面,我们来定义基础训练器的结果预测函数:其中,预测结果 P
的行数和预测样本的个数一致,列数则和基础训练器的个数相同,P
将作为第二阶段模型的输入特征。
def predict_base_learners(pred_base_learners, x, verbose=True):
"""Generate a prediction matrix."""
P = pd.DataFrame(np.zeros((x.shape[0], len(pred_base_learners))), columns=pred_base_learners.keys())
if verbose:
print("Generating base learner predictions.")
for i, (name,model) in enumerate(pred_base_learners.items()):
if verbose:
print("%s..." % name, end=" ", flush=False)
p = model.predict_proba(x)
P[name] = p[:,1]
if verbose:
print("done")
return P
将训练好的 base_learns
和 xpred_base
作为参数传入上述函数,得到基础分类器的预测结果:
>> P_base = predict_base_learners(base_learners, xpred_base)
Generating base learner predictions.
svm... done
knn... done
naive bayes... done
mlp-nn... done
random forest... done
gbm... done
logistic... done
P_base
如下:
6、进行第二阶段的训练,并得出分类结果
训练第二阶段的模型:
>> meta_learner.fit(P_base, ypred_base)
GradientBoostingClassifier(learning_rate=0.005, loss='exponential',
max_features=4, n_estimators=1000, random_state=666,
subsample=0.5)
定义函数 ensemble_predict
函数实现 Stacking 集成,先由基础模型进行预测得到 P_pred
,再有第二阶段模型 meta_learner
进行预测得到最终结果。
def ensemble_predict(base_learners, meta_learner, x, verbose=True):
"""Generate predictions from the ensemble."""
P_pred = predict_base_learners(base_learners, x, verbose)
return P_pred, meta_learner.predict_proba(P_pred)[:, 1]
在测试集上进行预测,并计算 ROC-AUC Score:
>> P_pred, p = ensemble_predict(base_learners, meta_learner, xtest)
>> print(f"\nEnsemble ROC-AUC score: {roc_auc_score(ytest, p):.3}")
Generating base learner predictions.
svm... done
knn... done
naive bayes... done
mlp-nn... done
random forest... done
gbm... done
logistic... done
Ensemble ROC-AUC score: 0.883
经过 Stacking 集成之后,ROC-AUC Score 由 0.880 提升到了 0.883,有提升,提升不多。我们可以想想上述六个步骤中,哪个步骤需要我们优化呢?
其实是第三步,切分训练集数据,将原训练集的数据一分为二之后,无论是训练基础模型,还是训练第二阶段的权重分配模型时,训练集数据都少了一半,这是非常浪费的。
那有没有什么方法既可以保证每次训练基础模型和训练权重分配模型的数据不同,而且不浪费任何数据呢?感兴趣的读者朋友可以先思考一下~小鱼在连载的下一篇文章中为您揭晓 (* ̄︶ ̄)
网友评论