Help us understand the problem. What is going on with this article?

ベイズ最適化シリーズ(2) -アンサンブル学習(Voting)の最適化-

More than 1 year has passed since last update.

前回は、ベイズ最適化の可視化を行いました。
今回は、アンサンブル学習(Voting)にベイズ最適化を適用します。

Votingとは

アンサンブル学習といえばStackingが有名ですが、Votingは各分類器で多数決を
とって決める手法です。詳しくは参考資料のリンクを見て下さい。
図にすると以下のようになります。

voting.png

ここでは、K近傍法、XGBoost、ランダムフォレスト、ニューラルネットの4つを
使っています。予測ラベルの決定は単純な多数決ではなく、それぞれの分類器に
重み係数kを掛けて投票させます。

この重み係数をベイズ最適化で求めるのが、本稿のテーマです。
式にすると、以下のようになります。

KNN + k_1XGB + k_2RF + k_3NN

ここでは、K近傍法の係数を1に固定しています。重み付きの投票で一番多かった
投票がラベルとして出力されます。
分類精度が最も高くなるように、k1,k2,k3をベイズ最適化で求めます。

各分類器の用意

使うデータはお馴染みのMNISTです。
学習用と評価用のデータ数は以下のとおりです。
・学習データ:1000個
・評価データ:300個

import numpy as np
import pandas as pd

from keras.datasets import mnist

#MNIST データロード
(X_train, y_train), (X_test, y_test) = mnist.load_data()
X_train = X_train.reshape(60000, 784)
X_test = X_test.reshape(10000, 784)

x_train = X_train.astype('float32')
x_test = X_test.astype('float32')
x_train /= 255
x_test /= 255

x_train = x_train[:1000]
x_test = x_test[:300]
y_train = y_train[:1000]
y_test = y_test[:300]

まず、k近傍法を学習させます。

from sklearn import neighbors
#KNN
knn = neighbors.KNeighborsClassifier(5, weights = 'distance')
knn.fit(x_train,y_train)

XGBoostを学習させます。

import xgboost as xgb
#XGBoost
Xgb = xgb.XGBClassifier()
Xgb.fit(x_train,y_train)

ランダムフォレストを学習させます。

from sklearn.ensemble import RandomForestClassifier
#Random Forest
RF=RandomForestClassifier()
RF.fit(x_train,y_train)

最後にkerasを使って、ニューラルネットを学習させます。

from keras.utils import to_categorical
from keras.models import Sequential
from keras.layers import Dense, Activation
#NN
model = Sequential()
model.add(Dense(30, input_dim=x_train.shape[1]))
model.add(Activation('relu'))
model.add(Dense(10))
model.add(Activation('softmax'))

model.compile(loss='categorical_crossentropy',
              optimizer='adam',
              metrics=['accuracy'])

Y_train = to_categorical(y_train, 10)

hist = model.fit(x_train, Y_train,
                 epochs=30, batch_size=100,verbose=0)

各分類器の精度を出してみます。

#それぞれの分類精度
from sklearn.metrics import accuracy_score

knn_predict = knn.predict(x_test)
print("KNN")
print(accuracy_score(y_test, knn_predict))

xgb_predict = Xgb.predict(x_test)
print("XGBoost")
print(accuracy_score(y_test, xgb_predict))

RF_predict = RF.predict(x_test)
print("Random Forest")
print(accuracy_score(y_test, RF_predict))

NN_predict = model.predict_classes(x_test, batch_size=1)
print("NN")
print(accuracy_score(y_test, NN_predict))
KNN
0.85

XGBoost
0.873

Random Forest
0.75

NN
0.863

意外にもXGBoostが一番でした。画像系に強いNNは少し届かず。
アンサンブルすることにより、XGBの87.3%をどれだけ高めることができるのかが、
焦点になってきます。

アンサンブル学習

重み無しでVotingを行ってみます。

def Voting(x_voting,y_voting,k1=1,k2=1,k3=1):
    knn_predict = knn.predict(x_voting)    
    xgb_predict = Xgb.predict(x_voting)    
    RF_predict = RF.predict(x_voting)    
    NN_predict = model.predict_classes(x_voting,batch_size=1)    

    knn_onehot = to_categorical(knn_predict, 10)
    xgb_onehot = to_categorical(xgb_predict, 10)
    RF_onehot = to_categorical(RF_predict, 10)
    NN_onehot = to_categorical(NN_predict, 10)

    voting_result = []
    for i in range(len(x_voting)):
        voting = np.argmax(knn_onehot[i] + k1*xgb_onehot[i] +
                           k2*RF_onehot[i] + k3*NN_onehot[i])
        voting_result.append(voting)
    result = accuracy_score(y_voting, voting_result)
    print(result)
    return result

print("Normal Voting")
_ = Voting(x_test,y_test)
Normal Voting
0.873

精度は、XGB単独と変わらずでした。

アンサンブル学習の最適化

ここからが本題です。分類精度が良くなるように重み係数kをベイズ最適化で
探索します。前回と同じように、最適化する関数と重み係数の幅を定義します。

#ベイズ最適化
import GPy
import GPyOpt
import matplotlib.pyplot as plt

def f(x):
    k = x[:,0]
    kk = x[:,1]
    kkk = x[:,2]
    score = -Voting(x_train,y_train,k,kk,kkk)
    print(-score)
    return score

bounds = [{'name': 'k', 'type': 'continuous', 'domain': (0.5,1.5)},
          {'name': 'kk', 'type': 'continuous', 'domain': (0.5,1.5)},
          {'name': 'kkk', 'type': 'continuous', 'domain': (0.5,1.5)}]

あとは、学習データの予測結果を関数に渡して最適化させます。

myBopt = GPyOpt.methods.BayesianOptimization(f=f, domain=bounds)
myBopt.run_optimization(max_iter=10)

#結果出力
print("best [k1 k2 k3] =")
print(myBopt.x_opt)
print("Weighted Voting")
k_best = myBopt.x_opt
_ = Voting(x_test,y_test,k_best[0],k_best[1],k_best[2])

#探索履歴描画
result_z=-myBopt.Y

plt.figure()
plt.plot(result_z)
plt.xlabel("epochs")
plt.ylabel("accuracy")
plt.show()
Weighted Voting
0.87

あれ? 重み無しよりも精度が下がってしまいました。

これは、「学習データの予測結果」を使って重み係数を最適化すると
簡単に精度100%が出てしまい、それ以降の探索を止めてしまっている
可能性があります。学習データでは、各分類器が優秀な分類結果を出して
いるため、いいかげんな重み係数を使っても精度100%が出てしまうわけです。
探索履歴をグラフ化するとこうなります。

figure_1.png

従って、「学習データではない他のデータ」の予測結果を使って精度100%が
出にくい状況で、ベイズ最適化を行う必要があります。ここでは、学習データに
ノイズを加えたデータを用意しました。

x_train_n = x_train + np.random.normal(0, 0.1, (len(x_train), 784))

ベイズ最適化のコードは以下のように代えて、再度ベイズ最適化を行います。

score = -Voting(x_train_n,y_train,k,kk,kkk)
Weighted Voting
0.877

見事、重み無しのVotingよりも良い結果が出ました。

※今回は学習データにノイズを加えたものを用意しました。一見、「評価データ」を
 使えば良いじゃん!と思うかもしれませんが、「評価データ」に手を出しては
 いけません。評価データに手を出すとテストでカンニングするのと同じになります。
 評価データは、最後の一回に評価用で使います。

探索履歴をグラフにしてみます。
figure_2.png

全体をグラフでまとめてみました。
figure_1__.png

重み付きのVotingが一番良い成績となりました。

コツ

すんなりと最適化ができたように思えますが、実は2、3日ハマっていました。

以下にコツを示しますが、あくまでもここで試したコツです。使うデータと
データの個数とアンサンブルの組み合わせで、変わってくる可能性があります。

・前述したように、最適化する関数では「学習データの予測結果」を使ってはいけません。
 精度が100%で飽和してしまうためです。本稿のように、ノイズを加えた学習データを
 用意するか、もしくは学習データを分割して「各分類器用の学習データ」と
 「ベイズ最適化用の学習データ」を用意すると良いかもしれません。

・ベイズ最適化のイタレーションは、あまり大きくとらない方が良いです
 大きくとると、ベイズ最適化用の学習データに過学習を起こす原因と
 なります。ニューラルネットでepochsを大きくとるのと同じ現象です。

・重み係数の上限値は、あまり大きくとらない方が良いです。
 重み係数が大きすぎると、これも過学習を起こす原因となります。

次回は、今度こそXGBoostのハイパーパラメータ探索にベイズ最適化を適用します。

参考資料

・アンサンブル学習(Stacked generalization)のサンプルプログラムと実行例
https://qiita.com/TomHortons/items/2a05b72be180eb83a204

・KAGGLE ENSEMBLING GUIDE
https://mlwave.com/kaggle-ensembling-guide/

shinmura0
自己紹介はツイッターをご覧ください。 https://twitter.com/shinmura0
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away