Baggingとその偽物(?)の効果を調べてみた

# 経緯

cross validationをしたときに、作ったモデルがもったいないので、「作ったモデルの結果の平均を最終結果にする」という似非Baggingを時々やっていたものの、ちゃんとBootstrapサンプリングした場合に比べて性能が悪かったりするんだろうか、と不安に思っていたので、ちゃんと調べてみることに。


# お題

手ごろな大きさと難易度ということで、MNIST。オーバーフィッティングしやすいように訓練データを1万件にして残りを検証データに。

弱学習器はLRとRFとMLP。

CVとBaggingで、弱学習器の数は5, 10, 15。(Baggingとしては少な目？)


# 結果

結構ばらついたので、seedを変えつつ3回やってみました。

|         |val_acc(1回目)|val_acc(2回目)|val_acc(3回目)|val_acc(平均)|
|:--------|-------------:|-------------:|-------------:|------------:|
|lr-base  |         85.2 |         85.4 |         85.2 |        85.3 |
|lr-cv5   |         87.5 |         86.8 |         87.3 |        87.2 |
|lr-cv10  |         87.1 |         86.9 |         87.2 |        87.1 |
|lr-cv15  |         86.8 |         86.6 |         86.9 |        86.8 |
|lr-bag5  |         87.7 |         87.2 |         87.5 |        87.5 |
|lr-bag10 |         87.9 |         87.6 |         88.3 |        87.9 |
|lr-bag15 |         88.0 |         88.1 |         88.5 |        88.2 |

|         |val_acc(1回目)|val_acc(2回目)|val_acc(3回目)|val_acc(平均)|
|:--------|-------------:|-------------:|-------------:|------------:|
|rf-base  |         95.0 |         94.9 |         94.9 |        94.9 |
|rf-cv5   |         95.1 |         94.9 |         94.9 |        95.0 |
|rf-cv10  |         95.3 |         95.1 |         95.1 |        95.2 |
|rf-cv15  |         95.4 |         95.2 |         95.2 |        95.2 |
|rf-bag5  |         94.7 |         94.6 |         94.6 |        94.6 |
|rf-bag10 |         94.8 |         94.6 |         94.7 |        94.7 |
|rf-bag15 |         94.9 |         94.7 |         94.7 |        94.7 |

|         |val_acc(1回目)|val_acc(2回目)|val_acc(3回目)|val_acc(平均)|
|:--------|-------------:|-------------:|-------------:|------------:|
|nn-base  |         93.9 |         93.2 |         93.6 |        93.6 |
|nn-cv5   |         95.3 |         95.5 |         95.2 |        95.3 |
|nn-cv10  |         95.8 |         95.9 |         95.7 |        95.8 |
|nn-cv15  |         96.0 |         96.1 |         96.0 |        96.0 |
|nn-bag5  |         95.1 |         95.0 |         95.1 |        95.1 |
|nn-bag10 |         95.3 |         95.5 |         95.3 |        95.4 |
|nn-bag15 |         95.4 |         95.6 |         95.4 |        95.5 |


# 結論

- LRはBaggingの方が良さそう？
- RFを更に(少数で)Baggingするのは逆効果っぽい？
- MLPはCVの方が良さそう？

だいぶ僅差なので細かいところはよく分からないけど、とりあえず「CVの結果の平均」はそう悪くなさそう。

# ソースコード

```py3
"""
アンサンブルのお試しコード
"""

import numpy as np
import sklearn.base
import sklearn.ensemble
import sklearn.metrics
import sklearn.model_selection
import sklearn.linear_model
import sklearn.neural_network
import sklearn.datasets


class CVAggregatingClassifier:

    def __init__(self, base_estimator, n_fold):
        self.base_estimator = base_estimator
        self.n_fold = n_fold
        self.estimators_ = []
        self.cv_score_ = 0

    def fit(self, X, y):
        self.estimators_ = []
        score_list = []
        count_list = []
        skf = sklearn.model_selection.StratifiedKFold(n_splits=self.n_fold, shuffle=True)
        for train, test in skf.split(X, y):
            estimator = sklearn.base.clone(self.base_estimator)
            estimator.fit(X[train], y[train])
            pred = estimator.predict(X[test])
            score = sklearn.metrics.accuracy_score(y[test], pred)
            score_list.append(score)
            count_list.append(len(y[test]))
            self.estimators_.append(estimator)
        self.cv_score_ = np.average(score_list, weights=count_list)

    def predict(self, X):
        return np.argmax(self.predict_proba(X), axis=-1)

    def predict_proba(self, X):
        return np.mean([e.predict_proba(X) for e in self.estimators_], axis=0)


if __name__ == '__main__':
    np.random.seed(3456)

    mnist = sklearn.datasets.fetch_mldata('MNIST original')
    X, y = mnist.data, mnist.target
    X_train, X_test, y_train, y_test = sklearn.model_selection.train_test_split(X, y, test_size=60000.0 / len(y))
    print('train size={} test size={}'.format(len(X_train), len(X_test)))

    factories = [
        ('lr', lambda: sklearn.linear_model.LogisticRegression(n_jobs=-1)),
        ('rf', lambda: sklearn.ensemble.RandomForestClassifier(n_estimators=100, n_jobs=-1)),
        ('nn', lambda: sklearn.neural_network.MLPClassifier(hidden_layer_sizes=(1000, 1000))),
    ]
    for base_name, factory in factories:
        estimators = [
            ('base', factory()),
            ('cv5', CVAggregatingClassifier(factory(), n_fold=5)),
            ('cv10', CVAggregatingClassifier(factory(), n_fold=10)),
            ('cv15', CVAggregatingClassifier(factory(), n_fold=15)),
            ('bag5', sklearn.ensemble.BaggingClassifier(factory(), n_estimators=5, oob_score=True)),
            ('bag10', sklearn.ensemble.BaggingClassifier(factory(), n_estimators=10, oob_score=True)),
            ('bag15', sklearn.ensemble.BaggingClassifier(factory(), n_estimators=15, oob_score=True)),
        ]
        for name, estimator in estimators:
            estimator.fit(X_train, y_train)
            y_pred = estimator.predict(X_test)
            acc = sklearn.metrics.accuracy_score(y_test, y_pred)

            if hasattr(estimator, 'oob_score_'):
                print('{}-{:5s}: val_acc={:.2f} oob_score={:.2f}'.format(base_name, name, acc * 100, estimator.oob_score_ * 100))
            elif hasattr(estimator, 'cv_score_'):
                print('{}-{:5s}: val_acc={:.2f} cv_score={:.2f}'.format(base_name, name, acc * 100, estimator.cv_score_ * 100))
            else:
                print('{}-{:5s}: val_acc={:.2f}'.format(base_name, name, acc * 100))
```