ベイズ最適化シリーズ（2）　－アンサンブル学習（Voting）の最適化－

[前回](https://qiita.com/shinmura0/items/2b54ab0117727ce007fd)は、ベイズ最適化の可視化を行いました。
今回は、アンサンブル学習（Voting）にベイズ最適化を適用します。

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

![voting.png](https://qiita-image-store.s3.amazonaws.com/0/264781/409dda07-3ce5-c0a8-0f5c-dfc02628d008.png)

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

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

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

#各分類器の用意
使うデータはお馴染みのMNISTです。
学習用と評価用のデータ数は以下のとおりです。
・学習データ：1000個
・評価データ：300個

```py
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近傍法を学習させます。

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

XGBoostを学習させます。

```py

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

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

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

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

```py
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)
```

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

```py
#それぞれの分類精度
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を行ってみます。

```py
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をベイズ最適化で
探索します。[前回](https://qiita.com/shinmura0/items/2b54ab0117727ce007fd)と同じように、最適化する関数と重み係数の幅を定義します。

```py
#ベイズ最適化
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)}]
```

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

```py
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](https://qiita-image-store.s3.amazonaws.com/0/264781/7afa6dc7-4b5f-dc0d-213c-ea3e34ea1440.png)

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

```py
x_train_n = x_train + np.random.normal(0, 0.1, (len(x_train), 784))
```
ベイズ最適化のコードは以下のように代えて、再度ベイズ最適化を行います。

```py
score = -Voting(x_train_n,y_train,k,kk,kkk)
```

```
Weighted Voting
0.877
```

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

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

探索履歴をグラフにしてみます。
![figure_2.png](https://qiita-image-store.s3.amazonaws.com/0/264781/b9b297d6-e7c2-f4de-e239-da74e484cd51.png)

全体をグラフでまとめてみました。
![figure_1__.png](https://qiita-image-store.s3.amazonaws.com/0/264781/99e5b276-9f87-7379-d05e-953de18c9475.png)

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

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

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

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

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

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

[次回](https://qiita.com/shinmura0/items/b2ddd8dfc16a321ec14c)は、今度こそXGBoostのハイパーパラメータ探索にベイズ最適化を適用します。

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

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