【实战篇】集成算法建模(五)

连载的上一篇文章中,小鱼使用 Stacking 集成思想建模,在政党二分类预测任务中得到的 ROC-AUC Score 最新值为 0.883 :

>> 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

不过呢在文章的最后,小鱼向大家提出了一个疑问:在训练第一阶段的模型和第二阶段的模型时,我们都只用了原始训练集的一半数据。

因为一阶段的基础模型需要先训练一次,然后用新的数据预测,将预测结果输入到模型二,进行二阶段模型的训练。

因此,我们将原始的训练集数据一分为二,但这样会造成数据的浪费,要知道数据资源是很宝贵的,我们随随便便用掉一般实在是可惜。

那有没有什么办法可以解决这个问题呢?答案就是我们的 CV 交叉验证。

交叉验证

下面,小鱼以 3 折的交叉验证为例来说说什么是 Cross validation 以及怎么使用交叉验证。

在构造第二阶段的输入数据时,首先将原始训练集切分成 3 分,即 KFold(3) ;接下来交叉组合各组数据,进行基础模型的训练和预测:

  • 使用 ①② 数据集作为训练集,训练基础模型,并使用 号数据集进行预测,保存好 的预测结果和对应的真实标签。

  • 使用 ①③ 数据集作为训练集,训练基础模型,并使用 号数据集进行预测,保存好 的预测结果和对应的真实标签。

  • 使用 ②③ 数据集作为训练集,训练基础模型,并使用 号数据集进行预测,保存好 的预测结果和对应的真实标签。

:在进行交叉验证时,每组在训练基础模型时,必须使用全新的基础模型,而不是上一组训练过的模型。

最后,我们分别将每组交叉验证的预测结果 ③②① 以及真实标签 y3,y2,y1 按顺序进行组合,作为xtrainytrain 输入到第二阶段的模型进行训练。

③ + ② + ① 拼起来不就是全部的原始训练集吗?所以说使用交叉验证的方式来构建 Stacking 集成第二阶段模型的输入可以利用所有样本,避免数据资源的浪费。

代码实现

下面,小鱼从代码层面来实现使用了交叉验证的 Stacking 集成:

from sklearn.base import clone

def stacking(base_learners, meta_learner, X, y, kfold):
    """Simple training routine for stacking."""
    # Train final base learners for test time
    print("Fitting Final Base Learners...", end="")
    train_base_learners(base_learners, X, y, verbose=False)
    print("Done")
    
    # Generate predictions for training meta learners
    cv_preds, cv_y = [], []
    for i, (train_index, test_index) in enumerate(kfold.split(X)):
        fold_xtrain, fold_ytrain = X.iloc[train_index,:], y.iloc[train_index]
        fold_xtest, fold_ytest = X.iloc[test_index,:], y.iloc[test_index]
        
        fold_base_learners = {name: clone(model) for name, model in base_learners.items()}
        
        train_base_learners(fold_base_learners, fold_xtrain, fold_ytrain, verbose=False)
        fold_P_base = predict_base_learners(fold_base_learners, fold_xtest, verbose=False)
        
        cv_preds.append(fold_P_base)
        cv_y.append(fold_ytest)
        
        print(f"CV Fold {i+1} Done")
    
    print("CV-Predictions Done")
    
    # Be careful to get rows in the right order
    cv_preds = pd.concat(cv_preds)
    cv_y = pd.concat(cv_y)
    
    # Train meta learner
    print("Fitting Meta Learner...", end="")
    meta_learner.fit(cv_preds, cv_y)
    print("Done")
    
    return base_learners, meta_learner

函数有点长,小鱼一点一点来为大家解读,也可以借此机会再理解一下 Stacking 集成。stacking 函数返回值为第一阶段训练好的基础分类器 base_learners 和 第二阶段训练好的权重分配分类器 meta_learner

根据返回值的需求,我们的 stacking 函数也分成两部分:

  • 训练基础分类器
  • 训练第二阶段分类器

其中,Stacking 接收的参数依次为基础模型 base_learners 、第二阶段的初始化算法模型 meta_learner 、原始训练集特征 X 、原始训练集标签 y 以及 K折的交叉验证对象 kfold

接下来,是训练基础分类器的代码:

    print("Fitting Final Base Learners...", end="")
    train_base_learners(base_learners, X, y, verbose=False)
    print("Done")

由于我们接下来马上会使用交叉验证构建第二阶段的输入,因此训练第一阶段的模型时,放心地使用全部的训练集数据 Xy 就好。

然后就是 CV 构造第二阶段输入的代码了:

    cv_preds, cv_y = [], []
    for i, (train_index, test_index) in enumerate(kfold.split(X)):
        fold_xtrain, fold_ytrain = X.iloc[train_index,:], y.iloc[train_index]
        fold_xtest, fold_ytest = X.iloc[test_index,:], y.iloc[test_index]
        
        fold_base_learners = {name: clone(model) for name, model in base_learners.items()}
        
        train_base_learners(fold_base_learners, fold_xtrain, fold_ytrain, verbose=False)
        fold_P_base = predict_base_learners(fold_base_learners, fold_xtest, verbose=False)
        
        cv_preds.append(fold_P_base)
        cv_y.append(fold_ytest)
        
        print(f"CV Fold {i+1} Done")

使用 kfold.split(x) 切分特征,遍历每组结果时,可以分别得到每组数据的训练集索引 train_index 和验证集索引 test_index ,使用位置索引 iloc 即可选出每组数据的训练集和测试集。

        fold_xtrain, fold_ytrain = X.iloc[train_index,:], y.iloc[train_index]
        fold_xtest, fold_ytest = X.iloc[test_index,:], y.iloc[test_index]

接下来的一行代码也非常关键:

        fold_base_learners = {name: clone(model) for name, model in base_learners.items()}

对于交叉验证的每组训练,必须使用 clone 函数克隆出一组全新的基础模型,这些全新的模型没看过任何训练集数据,就和刚初始化时一样。

然后使用这些全新的基础模型 fold_base_learners 进行模型的训练,并预测第一阶段结果。

        train_base_learners(fold_base_learners, fold_xtrain, fold_ytrain, verbose=False)
        fold_P_base = predict_base_learners(fold_base_learners, fold_xtest, verbose=False)

最后将该组数据第一阶段预测值和对应的真实值保存下来,完成 1 次 for 循环。

        cv_preds.append(fold_P_base)
        cv_y.append(fold_ytest)

for 循环结束后,分别将交叉验证的所有结果、真实值各自拼接起来,用于二阶段权重分配模型的训练:

    # Be careful to get rows in the right order
    cv_preds = pd.concat(cv_preds)
    cv_y = pd.concat(cv_y)
    
    # Train meta learner
    print("Fitting Meta Learner...", end="")
    meta_learner.fit(cv_preds, cv_y)
    print("Done")

下面,小鱼调用上述 stacking 函数,参数 kfold 为 2 折交叉验证:

from sklearn.model_selection import KFold

cv_base_learners, cv_meta_learner = stacking(
    get_models(), clone(meta_learner), 
    x_over_sample_train, y_over_sample_train, 
    KFold(2)
)

P_pred, p = ensemble_predict(cv_base_learners, cv_meta_learner, xtest, verbose=False)
print(f'\nEnsemble ROC-AUC score: {roc_auc_score(ytest, p):.3}')

输出结果:

Fitting Final Base Learners...Done
CV Fold 1 Done
CV Fold 2 Done
CV-Predictions Done
Fitting Meta Learner...Done

Ensemble ROC-AUC score: 0.867

输出结果反而下降了?其实,这里是因为小鱼想快点得到结果,所以只使用了 2 折的交叉验证,试想一下,对于 2 折的交叉验证,每次训练基础模型时,只用了一半的数据。

我们在测试集、训练集划分时多采用 8:2 、7:3 这样的比例,以保证训练集的数据更充分。

下面,小鱼直接使用 mlens.ensemble 模块下的 SuperLearner 来进行并行地交叉验证训练,指定 10 折的交叉验证,这样每组交叉验证将会使用 90% 的数据作为训练集。

初始化并行训练的 SuperLearner ,并添加基础模型和第二阶段模型:

from mlens.ensemble import SuperLearner

# Instantiate the ensemble with 10 folds
sl = SuperLearner(
    folds=10,
    random_state=SEED,
    verbose=2,
    backend="multiprocessing"
)

# Add the base learners and the meta learner
sl.add(list(base_learners.values()), proba=True) 
sl.add_meta(meta_learner, proba=True)

使用 SuperLearner 进行训练和预测:

# Train the ensemble
sl.fit(x_over_sample_train, y_over_sample_train)

# Predict the test set
p_sl = sl.predict_proba(xtest)

print(f"\nSuper Learner ROC-AUC score: {roc_auc_score(ytest, p_sl[:, 1]):.3}")

输出结果:

Fitting 2 layers
Processing layer-1             done | 00:04:34
Processing layer-2             done | 00:00:05
Fit complete                        | 00:04:41

Predicting 2 layers
Processing layer-1             done | 00:02:03
Processing layer-2             done | 00:00:01
Predict complete                    | 00:02:05

Super Learner ROC-AUC score: 0.883

至此,使用了交叉验证的 Stacking 集成就算完成了~

说明

细心的读者可能疑问:使用了交叉验证的 Stacking 集成和没使用之前结果一样,ROC-AUC Score 还是 0.883。

这是因为小鱼为了快速地得到结果,在 【实战篇】集成算法建模(一) 进行原始数据集切分的时候,只取出 5% 的数据作为训练集,也就是 5000 条数据,过采样之后大概 6000 多条数据,所以训练集的数据整体就太小了。

最后,我们对比一下 Bagging 集成和 Stacking 集成的 ROC-AUC 曲线:

>> p_sl[:,1].reshape(-1,1).shape
(95000, 1)
>> P.mean(axis=1).shape
(95000,)
>> plot_roc_curve(ytest, p_sl[:,1].reshape(-1,1), P.mean(axis=1), ["Super Learner"], "Simple average")

绘制结果:

从结果来看,似乎 Stacking 集成思想的 Super Learner 只比 Bagging 集成思想的 Simple Average 好一点点,甚至差不多。

其实,小鱼的集成算法建模系列文章中,未对特征做标准化、归一化等预处理操作,并且也没有进行细节的调参,我们只是重点讲述了集成思想的应用。

感兴趣的读者朋友,可以尝试添加数据预处理和调参,再来对比 Bagging 和 Stacking 集成的效果,想必对比会更加明显。

以上就是集成算法建模系列文章的全部内容啦~祝你学习愉快 (*^▽^*)

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容