CUBE SUGAR CONTAINER

技術系のこと書きます。

Python: アンサンブル学習の Voting を試す

今回は機械学習におけるアンサンブル学習の一種として Voting という手法を試してみる。 これは、複数の学習済みモデルを用意して多数決などで推論の結果を決めるという手法。 この手法を用いることで最終的なモデルの性能を上げられる可能性がある。 実装については自分で書いても良いけど scikit-learn に使いやすいものがあったので、それを選んだ。

sklearn.ensemble.VotingClassifier — scikit-learn 0.20.2 documentation

使った環境は次の通り。

$ sw_vers                                                  
ProductName:    Mac OS X
ProductVersion: 10.14.1
BuildVersion:   18B75
$ python -V                    
Python 3.7.1

下準備

まずは今回使うパッケージをインストールしておく。

$ pip install scikit-learn tqdm 

とにかく混ぜてみる

とりあえず、最初は特に何も考えず複数のモデルを使って Voting してみる。

以下のサンプルコードでは乳がんデータセットを使って Voting を試している。 使ったモデルはサポートベクターマシン、ランダムフォレスト、ロジスティック回帰、k-最近傍法、ナイーブベイズの五つ。 モデルの性能は 5-Fold CV を使って精度 (Accuracy) について評価している。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

from collections import defaultdict

import numpy as np
from tqdm import tqdm
from sklearn import datasets
from sklearn.ensemble import RandomForestClassifier
from sklearn.ensemble import VotingClassifier
from sklearn.linear_model import LogisticRegression
from sklearn.neighbors import KNeighborsClassifier
from sklearn.metrics import accuracy_score
from sklearn.svm import SVC
from sklearn.model_selection import StratifiedKFold
from sklearn.naive_bayes import GaussianNB


def main():
    # 乳がんデータセットを読み込む
    dataset = datasets.load_breast_cancer()
    X, y = dataset.data, dataset.target

    # voting に使う分類器を用意する
    estimators = [
        ('svm', SVC(gamma='scale', probability=True)),
        ('rf', RandomForestClassifier(n_estimators=100)),
        ('logit', LogisticRegression(solver='lbfgs', max_iter=10000)),
        ('knn', KNeighborsClassifier()),
        ('nb', GaussianNB()),
    ]

    accs = defaultdict(list)

    skf = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
    for train_index, test_index in tqdm(list(skf.split(X, y))):
        X_train, X_test = X[train_index], X[test_index]
        y_train, y_test = y[train_index], y[test_index]

        # 分類器を学習する
        voting = VotingClassifier(estimators)
        voting.fit(X_train, y_train)

        # アンサンブルで推論する
        y_pred = voting.predict(X_test)
        acc = accuracy_score(y_test, y_pred)
        accs['voting'].append(acc)

        # 個別の分類器の性能も確認してみる
        for name, estimator in voting.named_estimators_.items():
            y_pred = estimator.predict(X_test)
            acc = accuracy_score(y_test, y_pred)
            accs[name].append(acc)

    for name, acc_list in accs.items():
        mean_acc = np.array(acc_list).mean()
        print(name, ':', mean_acc)


if __name__ == '__main__':
    main()

上記に適当な名前をつけて実行してみる。 それぞれのモデルごとに計測した性能が出力される。

$ python voting.py 
100%|███████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 5/5 [00:03<00:00,  1.64it/s]
voting : 0.957829934590227
svm : 0.9385148133897653
rf : 0.9648787995382839
logit : 0.949134282416314
knn : 0.9314659484417083
nb : 0.9401923816852635

なんと Voting するよりもランダムフォレスト単体の方が性能が良いという結果になってしまった。 このように Voting するからといって必ずしも性能が上がるとは限らない。 例えば今回のように性能が突出したモデルがあるなら、それ単体で使った方が良くなる可能性はある。 あるいは、極端に性能が劣るモデルがあるならそれは取り除いた方が良いかもしれない。 それ以外には、次の項目で説明するモデルの重み付けという手もありそう。

モデルに重みをつける

性能が突出したモデルを単体で使ったり、あるいは劣るモデルを取り除く以外の選択肢として、モデルの重み付けがある。 これは、多数決などで推論結果を出す際に、特定のモデルの意見を重要視・あるいは軽視するというもの。 scikit-learn の VotingClassifier であれば weights というオプションでモデルの重みを指定できる。

次のサンプルコードでは、ランダムフォレストとロジスティック回帰の意見を重要視するように重みをつけてみた。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

from collections import defaultdict

import numpy as np
from tqdm import tqdm
from sklearn import datasets
from sklearn.ensemble import RandomForestClassifier
from sklearn.ensemble import VotingClassifier
from sklearn.linear_model import LogisticRegression
from sklearn.neighbors import KNeighborsClassifier
from sklearn.metrics import accuracy_score
from sklearn.svm import SVC
from sklearn.model_selection import StratifiedKFold
from sklearn.naive_bayes import GaussianNB


def main():
    dataset = datasets.load_breast_cancer()
    X, y = dataset.data, dataset.target

    estimators = [
        ('svm', SVC(gamma='scale', probability=True)),
        ('rf', RandomForestClassifier(n_estimators=100)),
        ('logit', LogisticRegression(solver='lbfgs', max_iter=10000)),
        ('knn', KNeighborsClassifier()),
        ('nb', GaussianNB()),
    ]

    accs = defaultdict(list)

    skf = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
    for train_index, test_index in tqdm(list(skf.split(X, y))):
        X_train, X_test = X[train_index], X[test_index]
        y_train, y_test = y[train_index], y[test_index]

        # 分類器に重みをつける
        voting = VotingClassifier(estimators,
                                  weights=[1, 2, 2, 1, 1])
        voting.fit(X_train, y_train)

        y_pred = voting.predict(X_test)
        acc = accuracy_score(y_test, y_pred)
        accs['voting'].append(acc)

        for name, estimator in voting.named_estimators_.items():
            y_pred = estimator.predict(X_test)
            acc = accuracy_score(y_test, y_pred)
            accs[name].append(acc)

    for name, acc_list in accs.items():
        mean_acc = np.array(acc_list).mean()
        print(name, ':', mean_acc)


if __name__ == '__main__':
    main()

上記を実行してみよう。 先ほどよりも Voting したときの性能は向上している。 ただ、やはりランダムフォレスト単体での性能には届いていない。

$ python weight.py
100%|███████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 5/5 [00:03<00:00,  1.59it/s]
voting : 0.9613697575990766
svm : 0.9385148133897653
rf : 0.9666487110427088
logit : 0.949134282416314
knn : 0.9314659484417083
nb : 0.9401923816852635

Seed Averaging

先ほどの例では、モデルに重み付けしてみたものの結局ランダムフォレストを単体で使った方が性能が良かった。 とはいえ Voting は一つのアルゴリズムだけを使う場合にも性能向上につなげる応用がある。 それが、続いて紹介する Seed Averaging という手法。 これは、同じアルゴリズムでも学習に用いるシード値を異なるものにしたモデルを複数用意して Voting するというやり方。

次のサンプルコードでは、Voting で使うアルゴリズムはランダムフォレストだけになっている。 ただし、初期化するときのシード値がそれぞれ異なっている。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

from collections import defaultdict

import numpy as np
from tqdm import tqdm
from sklearn import datasets
from sklearn.ensemble import RandomForestClassifier
from sklearn.ensemble import VotingClassifier
from sklearn.metrics import accuracy_score
from sklearn.model_selection import StratifiedKFold


def main():
    dataset = datasets.load_breast_cancer()
    X, y = dataset.data, dataset.target

    # Seed Averaging
    estimators = [
        ('rf1', RandomForestClassifier(n_estimators=100, random_state=0)),
        ('rf2', RandomForestClassifier(n_estimators=100, random_state=1)),
        ('rf3', RandomForestClassifier(n_estimators=100, random_state=2)),
        ('rf4', RandomForestClassifier(n_estimators=100, random_state=3)),
        ('rf5', RandomForestClassifier(n_estimators=100, random_state=4)),
    ]

    accs = defaultdict(list)

    skf = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
    for train_index, test_index in tqdm(list(skf.split(X, y))):
        X_train, X_test = X[train_index], X[test_index]
        y_train, y_test = y[train_index], y[test_index]

        voting = VotingClassifier(estimators)
        voting.fit(X_train, y_train)

        y_pred = voting.predict(X_test)
        acc = accuracy_score(y_test, y_pred)
        accs['voting'].append(acc)

        for name, estimator in voting.named_estimators_.items():
            y_pred = estimator.predict(X_test)
            acc = accuracy_score(y_test, y_pred)
            accs[name].append(acc)

    for name, acc_list in accs.items():
        mean_acc = np.array(acc_list).mean()
        print(name, ':', mean_acc)


if __name__ == '__main__':
    main()

上記を実行してみよう。

$ python sa.py 
100%|███████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 5/5 [00:03<00:00,  1.37it/s]
voting : 0.9683878414774914
rf1 : 0.9648787995382839
rf2 : 0.9666179299730666
rf3 : 0.9683570604078492
rf4 : 0.9666179299730665
rf5 : 0.9666179299730666

今回は、最も性能の良い三番目のモデルよりも、わずかながら Voting した結果の方が性能が良くなっている。 これは、各モデルの推論結果を平均することで、最終的なモデルの識別境界がなめらかになる作用が期待できるためと考えられる。

Soft Voting と Hard Voting

Voting と一口に言っても、推論結果の出し方には Soft Voting と Hard Voting という二つのやり方がある。 分かりやすいのは Hard Voting で、これは単純に各モデルの意見を多数決で決めるというもの。 もうひとつの Soft Voting は、それぞれのモデルの出した推論結果の確率を平均するというもの。 そこで、続いては、それぞれの手法について詳しく見ていくことにする。

Hard Voting

まずは Hard Voting から見ていく。

次のサンプルコードでは、結果を分かりやすいようにするために scikit-learn のインターフェースを備えたダミーの分類器を書いた。 この分類器は、インスタンスを初期化したときに指定された値をそのまま返すだけの分類器になっている。 つまり、fit() メソッドでは何も学習しない。 この分類器を三つ使って、これまたダミーの学習データに対して Hard Voting してみよう。 scikit-learn の VotingClassifier はデフォルトで Soft Voting なので、Hard Voting するときは明示的に指定する必要がある。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

from sklearn.base import BaseEstimator
from sklearn.ensemble import VotingClassifier


class EchoBinaryClassifier(BaseEstimator):
    """インスタンス化したときに指定した値をオウム返しする二値分類器"""

    def __init__(self, answer_proba):
        """
        :param answer_proba: オウム返しする値 (0 ~ 1)
        """
        self.answer_proba = answer_proba

    def fit(self, X, y=None):
        # 何も学習しない
        return self

    def predict(self, X, y=None):
        # 指定された値が 0.5 以上なら 1 を、0.5 未満なら 0 を推論結果として返す
        return [1 if self.answer_proba >= 0.5 else 0 for _ in X]

    def predict_proba(self, X, y=None):
        # 指定された値を推論結果の確率としてそのまま返す
        return [(float(1 - self.answer_proba), float(self.answer_proba)) for _ in X]


def main():
    # ダミーの入力 (何も学習・推論しないため)
    dummy = [0, 0, 1]

    estimators = [
        ('ebc1', EchoBinaryClassifier(1.0)),
        ('ebc2', EchoBinaryClassifier(1.0)),
        ('ebc3', EchoBinaryClassifier(0.0)),
    ]

    # Hard voting する
    voting = VotingClassifier(estimators, voting='hard')
    voting.fit(dummy, dummy)

    # voting の結果を表示する
    y_pred = voting.predict(dummy)
    print('predict:', y_pred)

    # Hard voting は単純な多数決なので確率 (probability) は出せない
    # y_pred_proba = voting.predict_proba(dummy)
    # print(y_pred_proba)


if __name__ == '__main__':
    main()

上記のサンプルコードにおいて三つの分類器は、正反対の推論結果を返すことになる。 具体的には、1 を返すものが二つ、0 を返すものが一つある。 Voting による最終的な推論結果はどうなるだろうか。

上記を実行してみよう。

$ python hard.py      
predict: [1 1 1]

全て 1 と判定された。 これは多数派の判定結果として 1 が二つあるためだ。

一応、もうちょっと際どい確率でも試してみよう。 今度は、それぞれのモデルが 0.51, 0.51, 0.0 を返すようになっている。 もし、確率で平均したなら (0.51 + 0.51 + 0.0) / 3 = 0.34 < 0.5 となって 0 と判定されるはずだ。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

from sklearn.base import BaseEstimator
from sklearn.ensemble import VotingClassifier


class EchoBinaryClassifier(BaseEstimator):
    """インスタンス化したときに指定した値をオウム返しする二値分類器"""

    def __init__(self, answer_proba):
        """
        :param answer_proba: オウム返しする値 (0 ~ 1)
        """
        self.answer_proba = answer_proba

    def fit(self, X, y=None):
        # 何も学習しない
        return self

    def predict(self, X, y=None):
        # 指定された値が 0.5 以上なら 1 を、0.5 未満なら 0 を推論結果として返す
        return [1 if self.answer_proba >= 0.5 else 0 for _ in X]

    def predict_proba(self, X, y=None):
        # 指定された値を推論結果の確率としてそのまま返す
        return [(float(1 - self.answer_proba), float(self.answer_proba)) for _ in X]


def main():
    # ダミーの入力 (何も学習・推論しないため)
    dummy = [0, 0, 1]

    estimators = [
        ('ebc1', EchoBinaryClassifier(0.51)),
        ('ebc2', EchoBinaryClassifier(0.51)),
        ('ebc3', EchoBinaryClassifier(0.00)),
    ]

    # Hard voting する
    voting = VotingClassifier(estimators, voting='hard')
    voting.fit(dummy, dummy)

    # voting の結果を表示する
    y_pred = voting.predict(dummy)
    print('predict:', y_pred)

    # Hard voting は単純な多数決なので確率 (probability) は出せない
    # y_pred_proba = voting.predict_proba(dummy)
    # print(y_pred_proba)


if __name__ == '__main__':
    main()

上記を実行してみよう。

$ python hard2.py 
predict: [1 1 1]

これは、やはり多数派として 1 があるため。

Soft Voting

続いては、先ほどのサンプルコードをほとんどそのまま流用して手法だけ Soft Voting にしてみよう。 Soft Voting では確率の平均を取るため、今度は (0.51 + 0.51 + 0.0) / 3 = 0.34 < 0.5 となって 0 に判定されるはず。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

from sklearn.base import BaseEstimator
from sklearn.ensemble import VotingClassifier


class EchoBinaryClassifier(BaseEstimator):
    """インスタンス化したときに指定した値をオウム返しする二値分類器"""

    def __init__(self, answer_proba):
        """
        :param answer_proba: オウム返しする値 (0 ~ 1)
        """
        self.answer_proba = answer_proba

    def fit(self, X, y=None):
        # 何も学習しない
        return self

    def predict(self, X, y=None):
        # 指定された値が 0.5 以上なら 1 を、0.5 未満なら 0 を推論結果として返す
        return [1 if self.answer_proba >= 0.5 else 0 for _ in X]

    def predict_proba(self, X, y=None):
        # 指定された値を推論結果の確率としてそのまま返す
        return [(float(1 - self.answer_proba), float(self.answer_proba)) for _ in X]


def main():
    # ダミーの入力 (何も学習・推論しないため)
    dummy = [0, 0, 1]

    estimators = [
        ('ebc1', EchoBinaryClassifier(0.51)),
        ('ebc2', EchoBinaryClassifier(0.51)),
        ('ebc3', EchoBinaryClassifier(0.00)),
    ]

    # Soft voting する
    voting = VotingClassifier(estimators, voting='soft')
    voting.fit(dummy, dummy)

    # voting の結果を表示する
    y_pred = voting.predict(dummy)
    print('predict:', y_pred)

    # Soft voting は確率の平均を出す
    y_pred_proba = voting.predict_proba(dummy)
    print('proba:', y_pred_proba)


if __name__ == '__main__':
    main()

上記を実行してみよう。

$ python soft.py         
predict: [0 0 0]
proba: [[0.66 0.34]
 [0.66 0.34]
 [0.66 0.34]]

無事、今度は判定結果が 0 になることが確認できた。

めでたしめでたし。

統計的学習の基礎 ―データマイニング・推論・予測―

統計的学習の基礎 ―データマイニング・推論・予測―

  • 作者: Trevor Hastie,Robert Tibshirani,Jerome Friedman,杉山将,井手剛,神嶌敏弘,栗田多喜夫,前田英作,井尻善久,岩田具治,金森敬文,兼村厚範,烏山昌幸,河原吉伸,木村昭悟,小西嘉典,酒井智弥,鈴木大慈,竹内一郎,玉木徹,出口大輔,冨岡亮太,波部斉,前田新一,持橋大地,山田誠
  • 出版社/メーカー: 共立出版
  • 発売日: 2014/06/25
  • メディア: 単行本
  • この商品を含むブログ (6件) を見る