今回は機械学習におけるアンサンブル学習の一種として 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件) を見る
スマートPythonプログラミング: Pythonのより良い書き方を学ぶ
- 作者: もみじあめ
- 発売日: 2016/03/12
- メディア: Kindle版
- この商品を含むブログ (1件) を見る