【多クラス分類問題】LightGBMにてパラメータチューニングの有無で精度を比較

#1.目的
MNISTデータ(0から9までの手書き数字)を分類するモデル(多クラス分類モデル)をLightGBMを用いて実装する。
その際にハイパーパラメータをチューニングしないモデルと、チューニングしたモデルの精度を比較する。
チューニングはOptunaを用いて行う。その使い方等をメモ程度に置いておく。

#2.MNISTとは
MNISTはMixed National Institute of Standards and Technology databaseの略で、手書き数字画像60,000枚とテスト画像10,000枚を集めた、画像データセット。
0～9の手書き数字が教師ラベルとして各画像に与えられている。
詳しくは[こちら](https://atmarkit.itmedia.co.jp/ait/articles/2001/22/news012.html)
今回はKerasで公開されているMNISTを使う。

#3.使用ライブラリ

```Python
import lightgbm as lgb
import pandas as pd
import numpy as np
from tensorflow.keras.datasets import mnist
from sklearn.model_selection import train_test_split
#評価指標は以下４つ
from sklearn.metrics import accuracy_score
from sklearn.metrics import recall_score
from sklearn.metrics import precision_score
from sklearn.metrics import f1_score
```

#4.モデル作成(チューニング無しVer)
コードは以下の通り。

```Python
# Kerasに付属の手書き数字画像データをダウンロード
(X_train, y_train), (X_test, y_test) = mnist.load_data()

# training set を学習データ（X_train, y_train）と検証データ（X_valid, y_valid）に8:2で分割する
X_train,X_valid,y_train,y_valid = train_test_split(X_train, y_train, test_size=0.2, random_state=0)

# 各データを1次元に変換
X_train = X_train.reshape(-1,784)
X_valid = X_valid.reshape(-1,784)
X_test = X_test.reshape(-1,784)

#正規化
X_train = X_train.astype('float32')
X_valid = X_valid.astype('float32')
X_test = X_test.astype('float32')
X_train /= 255
X_valid /= 255
X_test /= 255

# 訓練・検証データの設定
train_data = lgb.Dataset(X_train, label=y_train)
eval_data = lgb.Dataset(X_valid, label=y_valid, reference=train_data)

#パラメータ設定
params = {
    'task': 'train',  #トレーニング用
    'boosting_type': 'gbdt', #勾配ブースティング決定木
    'objective': 'multiclass', #目的：多値分類
    'num_class': 10, #分類クラス数
    'metric': 'multi_logloss' #評価指標は多クラスのLog損失
}
#モデル作成
gbm = lgb.train(
    params,
    train_data,
    valid_sets=eval_data,
    num_boost_round=100,
    early_stopping_rounds=10
)
```

こうして出来たモデルを用いて、testデータの予測を行う。
そのコードは以下。

```Python
#予測
preds = gbm.predict(X_test, num_iteration=gbm.best_iteration)

y_pred = []
for x in preds:
    y_pred.append(np.argmax(x))
```
この予測y_predと教師データy_testを用いて評価指標を計算し、出力する。
ここでは説明を省くが、各評価指標については[こちら](https://analysis-navi.com/?p=553)でわかりやすく解説されている。

```Python
#正解率など評価指標の計算
print('正解率(accuracy_score):{}'.format(accuracy_score(y_test, y_pred)))
#適合率、再現率、F1値はマクロ平均を取る
print('再現率(recall_score):{}'.format(recall_score(y_test, y_pred, average='macro')))
print('適合率(precision_score):{}'.format(precision_score(y_test, y_pred, average='macro')))
print('F1値(f1_score):{}'.format(f1_score(y_test, y_pred, average='macro')))

```

出力結果は以下の通りである。

```Python
正解率(accuracy_score):0.9766
再現率(recall_score):0.9764353695446533
適合率(precision_score):0.9764619714676158
F1値(f1_score):0.9764381593889576
```

ハイパーパラメータをチューニングしなくても、正解率が97.6%とかなり高い数字になっている。
また、モデルの作成もわずか数分で終わったのでlightGBMの凄さが分かる。

ちなみに、上の4つの指標はclassification_reportを使うことで以下のように一気に計算できることが分かった。0から9の各数字についての指標も出してくれる。

```python
from sklearn.metrics import classification_report
print(classification_report(y_test, y_pred, digits=5)) #digitsは表示桁数
```
```Python:実行結果
              precision    recall  f1-score   support

           0    0.97778   0.98776   0.98274       980
           1    0.99120   0.99207   0.99163      1135
           2    0.97282   0.97093   0.97187      1032
           3    0.96961   0.97921   0.97438      1010
           4    0.97955   0.97556   0.97755       982
           5    0.97860   0.97422   0.97640       892
           6    0.98115   0.97808   0.97961       958
           7    0.97835   0.96693   0.97260      1028
           8    0.96735   0.97331   0.97032       974
           9    0.96822   0.96630   0.96726      1009

    accuracy                        0.97660     10000
   macro avg    0.97646   0.97644   0.97644     10000
weighted avg    0.97661   0.97660   0.97660     10000

```

#5.モデル作成 (チューニング有り)
次に、ハイパーパラメータをチューニングしてモデルを作る。
Optunaを用いてチューニングをするため、以下をインポート。

```Python
# LightGBM Optunaを使ってハイパーパラメタチューニング
import optuna.integration.lightgbm as lgb_o
```
あとはパラメータ設定までは同じコードになる。

```Python
# Kerasに付属の手書き数字画像データをダウンロード
(X_train, y_train), (X_test, y_test) = mnist.load_data()

# training set を学習データ（X_train, y_train）と検証データ（X_valid, y_valid）に8:2で分割する
X_train,X_valid,y_train,y_valid = train_test_split(X_train, y_train, test_size=0.2, random_state=0)

# 各データを1次元に変換
X_train = X_train.reshape(-1,784)
X_valid = X_valid.reshape(-1,784)
X_test = X_test.reshape(-1,784)

#正規化
X_train = X_train.astype('float32')
X_valid = X_valid.astype('float32')
X_test = X_test.astype('float32')
X_train /= 255
X_valid /= 255
X_test /= 255

# 訓練・検証データの設定
train_data = lgb_o.Dataset(X_train, label=y_train)
eval_data = lgb_o.Dataset(X_valid, label=y_valid, reference=train_data)

# パラメータ設定
params = {
    'task': 'train', 
    'boosting_type': 'gbdt',
    'objective': 'multiclass',
    'num_class': 10,
    'metric': 'multi_logloss'
}
```

Optunaでパラメータチューニングをする。
チューニングされたパラメータをbest_paramsとして表示させてみる。
この実行はかなり時間がかかるため注意。(特徴量が784列分もあるためだろうか。。？)

```Python
best_params= {}

#モデル作成
gbm = lgb_o.train(
    params,
    train_data,
    valid_sets=[train_data, eval_data], #ここがチューニングしない場合と違う
    num_boost_round=100,
    early_stopping_rounds=10
)

best_params = gbm.params
best_params
```

チューニングされたパラメータが次のようになった。

```Python:実行結果
{'task': 'train',
 'boosting_type': 'gbdt',
 'objective': 'multiclass',
 'num_class': 10,
 'metric': 'multi_logloss',
 'feature_pre_filter': False,
 'lambda_l1': 1.4206763386954986e-08,
 'lambda_l2': 0.0004970847920575475,
 'num_leaves': 37,
 'feature_fraction': 0.784,
 'bagging_fraction': 0.9001098370804291,
 'bagging_freq': 3,
 'min_child_samples': 20,
 'num_iterations': 100,
 'early_stopping_round': 10}
```


先と同じように、このモデルを用いて予測してみる。

```Python
#予測
preds = gbm.predict(X_test, num_iteration=gbm.best_iteration)

y_pred = []
for x in preds:
    y_pred.append(np.argmax(x))

#正解率など評価指標の計算
print('正解率(accuracy_score):{}'.format(accuracy_score(y_test, y_pred)))
#適合率、再現率、F1値はマクロ平均を取る
print('再現率(recall_score):{}'.format(recall_score(y_test, y_pred, average='macro')))
print('適合率(precision_score):{}'.format(precision_score(y_test, y_pred, average='macro')))
print('F1値(f1_score):{}'.format(f1_score(y_test, y_pred, average='macro')))
```
チューニング後のモデルでの評価指数が以下となる。

```Python:チューニング後
正解率(accuracy_score):0.978
再現率(recall_score):0.9778036992589507
適合率(precision_score):0.9778601989002492
F1値(f1_score):0.9778154759497815
```

チューニング前と比べてみる

```Python:チューニング前
正解率(accuracy_score):0.9766
再現率(recall_score):0.9764353695446533
適合率(precision_score):0.9764619714676158
F1値(f1_score):0.9764381593889576
```

いずれも0.0014ほど上昇していることが分かる。
が、チューニングせずともかなり良い精度で分類できていたので、あまりその恩恵を感じられなかった。。
次は違うデータセットでも試してみたい。