プレトレーニングを用いた少量ラベルデータ下における画像識別精度の向上

# はじめに

&nbsp;　機械学習プロジェクトで大量にデータはあるがラベル付けがされていないという状況が多々あると思います。ラベル付けを最小限に抑えることは人的、時間的コストの削減に貢献します。本記事では、オートエンコーダーによる事前学習を用いることで、少ないラベルデータでもMNIST画像識別精度の向上を図ることができたのでご紹介させていただきます。また、TensorFlowで作成したモデルの下位層だけ再利用する方法で苦しんだので、私の方法を参考にしていただければ幸いです！
&nbsp;　プレトレーニングを用いたMNIST画像識別器において、1000個のラベル付きデータを10epoch学習させるだけで、テストデータにおける正解率が90％を超えました。プレトレーニングにはMNISTデータのノイズ除去を行うオートエンコーダーを作成しました。オートエンコーダーの下位層だけを再利用し、MNIST画像識別モデルを構築しました。MNIST画像識別モデルの訓練データ(ラベル付き)の数を100-60000まで変化させ、正解率のデータ数依存性を確認しました。最後にプレトレーニングを行わなかった場合の結果と比較し、プレトレーニングの有用性を示します！


# 前提知識

&nbsp; 本記事のメイントピックである「オートエンコーダー」と「プレトレーニング」を簡単に説明し、検証手順を図解しながら説明します。

## オートエンコーダー
&nbsp; オートエンコーダーは「次元削除」、「特徴抽出」、「プレトレーニング」「データ生成」のタスクを遂行する教師無し機械学習です。オートエンコーダーのニューラルネットワーク構成を下図に示します。隠れ層を中心とした左右対称の構造をしており、中心の層が最もニューロン数が少ないのが特徴です。オートエンコーダーはニューロン数が減少していく「エンコーダー」部分と、ニューロン数が増大していく「デコーダー」部分に分けられます。オートエンコーダーに入力データを加え、出力で入力データを再現するように学習を行うことで、中間層が少ない次元で入力データを表現する内部表現を獲得します。つまり「エンコーダー」部で入力を内部表現に変換し、「デコーダー」部で内部表現を元のデータに逆変換します。この一連のプロセスを経る事で内部表現がデータの特徴を効率的に捉え、オートエンコーダーは次元削除といったタスクをこなす事ができます。

◾️オートエンコーダーの概念図
<img width="600" alt="オートエンコーダー_概念図.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/393993/276e00f8-2c08-e9e3-7ff1-54a43e30ff4d.png">

## プレトレーニング
&nbsp;  プレトレーニング（事前学習）は、複雑なタスクを遂行するニューラルネットワークに対して、それと同様なタスクを実行する学習済みニューラルネットワークの一部を再利用し、少ない学習データで高精度のモデルを構築する手法です。プレトレーニングを行うためにオートエンコーダーの仕組みを利用する事が可能です。上述した様に、オートエンコーダーは入力データの特徴を捉える事が可能であるため、はじめにオートエンコーダーを学習させ、その一部を再利用する事で本題の識別器を効率的に学習させる事が可能です。

## 検証の手順
&nbsp; 本記事ではMNIST画像のノイズ除去オートエンコーダーを学習させ、下位層を再利用したMNIST画像識別器を作成します。ノイズ除去オートエンコーダーは入力からノイズを除去するタスクを遂行します。学習の方法は、入力にノイズを混入させ、出力でノイズを除去した画像を再構築する様に学習を行います(下図参照)。オートエンコーダーの隠れ層1, 隠れ層2, 隠れ層3のニューロン数はそれぞれ50, 20, 50, MNIST画像識別器の出力層のニューロン数は10としました。ニューラルネットワークの設定を表にまとめています。

◾️学習の条件

|項目|値  |
|---|---|
|隠れ層1 ニューロン数|50  |
|隠れ層2 ニューロン数 |20  |
|隠れ層3 ニューロン数 |50  |
|出力層  ニューロン数 |10  |
|最適化手法 |Adam |
|学習率  |0.001  |
|活性化関数  |ELU  |
|ノイズ  |平均:0, 分散:1のガウスノイズ  |


◾️検証手順の図解
![検証手順_図解.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/393993/9cab52d7-2ac6-b179-9dca-4c518117e08b.png)



## 動作環境
　主な動作環境の情報は以下です

|項目|値  |
|---|---|
|macOS Mojave| 10.14.6|
|tensorflow| 1.13.1|
|numpy| 1.15.4|
|keras| 2.3.0|


# プレトレーニングを目的としたオートエンコーダーの学習

&nbsp; MNIST画像に対するノイズ除去オートエンコーダーの構築・学習・性能確認を行います。kerasを用いてロードしたMNIST画像を1次元に展開し、パーセプトロンで扱える形状に変換します。ノイズ除去オートエンコーダーはTensorFLowで構築します。全てのMNIST画像(ラベルは用いない)でノイズ除去オートエンコーダーを学習させモデルを保存します。保存したモデルをロードし、ノイズ除去タスクを学習できているかを定性的に確認します。

## MNISTデータの準備
kerasを用いてMNISTデータをロードします

```python
# kerasでMNISTデータをロード
from keras.datasets import mnist
(x_train, y_train), (x_test, y_test) = mnist.load_data()
```
データを一次元に展開すると同時にピクセル値が0-1になる様に正規化します

```python
# MNISTデータを正規化
X_train = x_train.reshape(x_train.shape[0], -1) / 255.
X_test = x_test.reshape(x_test.shape[0], -1) / 255.

# モデルの入力にするために[サンプル数, 1]に配列の形状を変形
Y_train = y_train.reshape(-1, 1)
Y_test = y_test.reshape(-1, 1)
```

## オートエンコーダーモデルの構築
続いてオートエンコーダーのモデルを作成します。
ハイパーパラメーターを定義します

```python
## ハイパーパラメーター ##
# ニューロン数
n_inputs = 28 * 28
n_hidden1 = 50
n_hidden2 = 20
n_hidden3 = n_hidden1
n_outputs = n_inputs

# 学習率
learning_rate = 0.001
```

TensorFLowのグラフを初期化しオートエンコーダーモデルを構築します

```python
## オートエンコーダーモデルの構築 ##
import tensorflow as tf

# グラフの初期化
tf.reset_default_graph()
X = tf.placeholder(dtype=tf.float32, shape=[None, n_inputs], name='X')
X_target = tf.placeholder(dtype=tf.float32, shape=[None, n_inputs], name='X_target')

# ニューラルネットワークの定義の構築 #
activation = tf.nn.elu
hidden1 = tf.layers.dense(X, n_hidden1, activation=activation, name='hidden1')
hidden2 = tf.layers.dense(hidden1, n_hidden2, activation=activation, name='hidden2')
hidden3 = tf.layers.dense(hidden2, n_hidden3, activation=activation, name='hidden3')
outputs_denoise = tf.layers.dense(hidden3, n_outputs, activation=None, name='outputs_denoise'))
```

損失関数、オプティマイザを定義します。損失関数には入力と出力のMSEを用い「再構築誤差」の指標とします

```python
## 学習方法の設定 ##
# 損失関数: 再構築誤差
## 学習方法の設定 ##
# 損失関数: 再構築誤差
reconstruction_loss = tf.reduce_mean(tf.square(X_target - outputs_denoise), name='loss')

# オプティマイザ
optimizer = tf.train.AdamOptimizer(learning_rate)
training_op = optimizer.minimize(reconstruction_loss)

# 保存
saver = tf.train.Saver()
```
構築したモデルをTensorBoardで見ると以下の様に見えます
◾️ TensorBoardによるオートエンコーダーモデルの可視化
<img width="415" alt="オートエンコーダー_TensorBoard.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/393993/72490933-b408-74de-756b-6de03505d0d3.png">


## オートエンコーダーモデルの学習
&nbsp; 構築したオートエンコーダーモデルの学習を行います
まずは準備として学習パラメター、モデルの保存先、データからバッチを取得する関数を定義します。

```python
import datetime

＃ 学習パラメータ
epoch_num=10
batch_size=100
# モデルの保存作
now = datetime.datetime.now().strftime("%Y-%m-%d-%H:%M")
ckpt_path = './{}/denoise_auto_encoder.ckpt'.format(now) # 学習済みモデルの保存ディレクトリ名は現在時刻を使用する

# MNISTデータからbatchを取得する関数
def fetch_batch(X, y, batch_size):
    num_data = X.shape[0]
    random_index = np.random.permutation(num_data)
    for batch_idx in np.array_split(random_index, batch_size):
        X_batch, y_batch = X[batch_idx, :], y[batch_idx]
        yield X_batch, y_batch
```
学習を行います

```python
# 初期化変数の宣言
init = tf.global_variables_initializer()
with tf.Session() as sess:
    init.run()
    # エポックでループ
    for epoch in range(epoch_num):
        # バッチを取得し学習を行う
        for X_batch, y_batch in fetch_batch(X_train, y_train, batch_size):
            X_batch_noise = X_batch + np.random.normal(size=(X_batch.shape))# ノイズを混入
            sess.run(training_op, feed_dict={X:X_batch_noise, X_target:X_batch})
        #　最後のバッチにおける再構築誤差を計算 
        loss_val_last_batch = reconstruction_loss.eval(feed_dict={X:X_batch, X_target:X_batch})
        print("Epoch: {0} \tLast batch MSE: {1}".format(epoch, loss_val_last_batch))
    # テストデータにおける再構築誤差を計算
    X_test_noise = X_test + np.random.normal(size=(X_test.shape))# ノイズを混入
    loss_val_test = reconstruction_loss.eval(feed_dict={X:X_test_noise, X_target:X_test})
    print("===\tTest MSE: {0}===".format(loss_val_test))
    # モデルの保存
    tf.train.Saver().save(sess, ckpt_path)
```
コンソールの出力を以下に示します。再構築誤差(本記事ではMSE)が学習するたびに低くなり、テストデータにおけるMSE約0.04に落ち着きました。

```console
Epoch: 0 	Last batch MSE: 0.05711618438363075
Epoch: 1 	Last batch MSE: 0.0470886304974556
Epoch: 2 	Last batch MSE: 0.040134407579898834
Epoch: 3 	Last batch MSE: 0.0365045927464962
Epoch: 4 	Last batch MSE: 0.032733481377363205
Epoch: 5 	Last batch MSE: 0.03418581560254097
Epoch: 6 	Last batch MSE: 0.03256369009613991
Epoch: 7 	Last batch MSE: 0.03150740638375282
Epoch: 8 	Last batch MSE: 0.03078094869852066
Epoch: 9 	Last batch MSE: 0.02976345084607601
===	Test MSE: 0.03738117963075638===
```

## ノイズ除去モデルの性能を定性的に確認
&nbsp; 続いてオートエンコーダーがノイズを除去タスクを学習できているかを確認します。
(コードの詳細はgithubを参照ください)
ランダムに100個のデータを取得し、ノイズを加え、オートエンコーダーに入力した時の出力を確認します。
以下に 1.ランダムに抽出した100個のMNIST画像 2.ノイズ混入画像 3.ノイズ除去を行なった画像を示します。
ノイズ除去後の画像を見ると、ノイズがあるという感じはなくなっています。
しかし、常にうっすら'0'の痕跡がある様に見えます。

◾️1.ランダムに抽出した100個のMNIST画像
![元データ.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/393993/0244b84f-61b2-7df3-98a3-b253a3456dff.png)


◾️2.ノイズ混入画像
![ノイズ混入データ.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/393993/d6381d3c-d3b4-b026-7337-48e6db217266.png)

◾️3.ノイズ除去後画像
![ノイズ除去データ.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/393993/e6a28fe4-e471-1d59-30d6-a8929200035c.png)




# プレトレーニングを用いたMNIST画像識別器の学習

&nbsp; 3章で作成したノイズ除去オートエンコーダーの下位層のみ再利用し、プレトレーニングを用いたMNIST画像識別器の構築・学習を行います。作成済みモデルをロードし、下位層に画像識別用の出力層を新たに付け加えます。学習データサイズを100-60000の間で変化させ、MNIST画像識別器を10epochで学習させます。最後に学習データサイズにおける正解率の推移を示します。

## オートエンコーダーモデルを再利用したMNIST画像識別モデルの構築
&nbsp; 保存したオートエンコーダーモデルを再利用するために、TensorFlowグラフ内の変数名を取得します。モデルが正常に保存できていれば、そのディレクトリに'ファイル名.meta'というファイルが存在するはずです。これを読み出すことで欲しい層の変数を取得します。

```python
# グラフメタファイルのパス
ckpt_meta = ckpt_path + '.meta'

# グラフをリセット
tf.reset_default_graph()
saver_new = tf.train.import_meta_graph(ckpt_meta)

# メタ情報の表示
for op in tf.get_default_graph().get_operations():
    print(op.name)
```
コンソールの出力の一部は以下です。モデル構築で指定したname属性の値である'X', 'hidden1', 'hidden2'が正しく反映されているが分かります。今回は隠れ層1と2の活性化関数の出力まで利用します。(自分で作ったモデルなら変数名を調べるまでもないですが、ネットで拾った時には確認が必要かと思います。)

```console
X
hidden1/kernel/Initializer/random_uniform/shape
hidden1/kernel/Initializer/random_uniform/min
hidden1/kernel/Initializer/random_uniform/max
〜中略〜
hidden1/MatMul
hidden1/BiasAdd
hidden1/Elu
hidden2/kernel/Initializer/random_uniform/shape
hidden2/kernel/Initializer/random_uniform/min
hidden2/kernel/Initializer/random_uniform/max
〜中略〜
hidden2/MatMul
hidden2/BiasAdd
hidden2/Elu
〜略〜
```
&nbsp; 再利用する層の名前が判明したのでMNIST画像を識別するモデルを構築します。

```py
## ハイパーパラメータ
# Graphのリセット
tf.reset_default_graph()

n_outputs = 10 # 出力層の数
learning_rate = 0.001 # 学習率

## モデル構築
# モデル再利用のためのSaver
resotre_saver = tf.train.import_meta_graph(ckpt_meta)
# 入力の定義 (Xは再利用する)
X = tf.get_default_graph().get_tensor_by_name("X:0") ### <- 変数の再利用!!!
y = tf.placeholder(dtype=tf.int32, shape=[None])

# hidden2までを再利用
hidden1 = tf.get_default_graph().get_tensor_by_name("hidden1/Elu:0")### <- 変数の再利用!!!
hidden2 = tf.get_default_graph().get_tensor_by_name("hidden2/Elu:0")### <- 変数の再利用!!!
# 出力を新規に追加
logits = tf.layers.dense(hidden2, n_outputs, activation=None, name='logits')

## 学習の設定
# 損出関数
with tf.name_scope("loss"):
    xentropy = tf.nn.sparse_softmax_cross_entropy_with_logits(labels=y, logits=logits)
    loss = tf.reduce_mean(xentropy, name='loss')

# オプティマイザ
with tf.name_scope("train"):
    new_optimizer = tf.train.AdamOptimizer(learning_rate, name='Adam_clf')
    training_op_clf = new_optimizer.minimize(loss)

# 精度評価
correct = tf.nn.in_top_k(logits, y, 1)
accuracy = tf.reduce_mean(tf.cast(correct, tf.float32), name='accuracy')
```
コードの解説です。
[1] tf.train.import_meta_graph(ckpt_meta)
➡︎ '.meta'のファイルで定義されるグラフをインポートします。これを実行しないと保存したモデルを再利用できません。

[2] X = tf.get_default_graph().get_tensor_by_name("X:0")
[3] hidden1 = tf.get_default_graph().get_tensor_by_name("hidden1/Elu:0")
[4] hidden2 = tf.get_default_graph().get_tensor_by_name("hidden2/Elu:0")
➡︎ 上記に様にして変数を名前指定(by name)で取得可能です。 
指定する名前の中の ':0' はあまり意味はわかりませんが必要です。。。誰かわかる人教えてください。。。
加えて、hidden2まで完全に再利用することになるので、再利用する層は[4]の宣言だけで十分かもしれないです。。。

&nbsp; 以上の様にして構築したオートエンコーダーを再利用したMNIST画像識別モデルのネットワーク構成は以下に示します。オートエンコーダーの隠れ層の出力の先にMNIST画像識別用のそうが付加されていることがわかります。

◾️ オートエンコーダーを再利用して構築したMNIST画像識別モデルのネットワーク構成
<img width="677" alt="MNIST識別モデル_tensorboard.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/393993/9400e479-a34e-34d7-41c6-8c8a855dbc0a.png">


## プレトレーニングを用いたMNIST画像識別モデルの学習
&nbsp; 学習データサイズを100-60000で変化させ、MNIST画像識別器を10epochで学習させます。各学習データサイズでの学習終了後にテストデータ(データ数10000)を用いて正解率を算出し配列に格納します。以下がコードです。
※trainClfの詳細は以下を参照くださいhttps://github.com/Sampeipei/qiita/blob/master/AutoEncoder_Pretraining_for_MNIST.ipynb


```python
## テストデータの大きさに対する精度を格納する
# 検証用データのインデックス
data_size_array = [100, 300, 500, 1000, 2500, 5000, 10000, 30000, 60000]

# 結果格納用配列
acc_list = []

for data_size in data_size_array:
    print("Data size: {}".format(data_size))
    X_train_extracted, y_train_extracted = X_train[:data_size], y_train[:data_size]
    # 学習を実行しテストデータの精度を返す
    # trainClfの詳細はgithubを参照ください
    acc_val_test = trainClf(X_train_extracted, y_train_extracted,
                            X_test, y_test, training_op_clf, 
                            restore_ckpt_path=ckpt_path, epoch=10)
    acc_list.append(acc_val_test)
```

学習データ数に対するMNIST画像データのテストデータ識別精度を以下に示します。
データサイズが100でも69%のテストデータ正解率が出ています。また、データサイズが5000でテストデータ正解率が90%を超え、全て(データ数60000)を利用すると正解率が最大の94%でした。次の章ではプレトレーニングの効果を検証するためにプレトレーニングを用いない場合の結果を比較します。

![精度のデータサイズ依存性.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/393993/a04b0a74-d459-c6a4-02e3-8b88a9ea368e.png)


# プレトレーニングの効果検証

プレトレーニングの効果を示すためプレトレーニング無しのモデルと、4章で作成したプレトレーニング有りのモデルの正解率を比較します。

## プレトレーニングを用いないMNIST画像識別モデルの構築
&nbsp; プレトレーニングを行わない通常のMNIST画像識別器を生成します。モデルの構成は今までと同じです。以下がコードです。

```python
tf.reset_default_graph()
## ハイパーパラメータ
n_inputs = 28 * 28
n_hidden1 = 50
n_hidden2 = 20
n_outputs = 10
learning_rate = 0.001

## モデル構築
# 入力
X = tf.placeholder(tf.float32, shape=[None, n_inputs])
y = tf.placeholder(tf.int32, shape=[None])
# ネットワークの構築
hidden1 = tf.layers.dense(X, n_hidden1, activation=tf.nn.elu, name='hidden1_no_pretrain')
hidden2 = tf.layers.dense(hidden1, n_hidden2, activation=tf.nn.elu, name='hidden2_no_pretrain')
logits = tf.layers.dense(hidden2, n_outputs, activation=None, name='logits_no_pretrain')

## 学習の設定
# 損失関数
xentropy = tf.nn.sparse_softmax_cross_entropy_with_logits(logits=logits, labels=y)
loss = tf.reduce_mean(xentropy, name='loss_no_pretrain')
# オプティマイザ
optimizer = tf.train.AdadeltaOptimizer(learning_rate, name='Adam_no_pretrain')
training_op_no_pretrain = optimizer.minimize(loss)
# 精度評価
correct = tf.nn.in_top_k(logits, y, 1)
accuracy = tf.reduce_mean(tf.cast(correct, tf.float32), name='accuracy')
# 初期化と保存
saver_no_pretrain = tf.train.Saver()
```

## プレトレーニングの効果を評価
&nbsp; プレトレーニングありの場合と同様に、学習データサイズを100-60000で変化させてMNIST画像識別器を10epochで学習させます。

```python
## テストデータの大きさに対する精度を格納する
# 検証用データのインデックス
data_size_array = [100, 300, 500, 1000, 2500, 5000, 10000, 30000, 60000]

# 結果格納用配列
acc_list_no_pretrain = []

for data_size in data_size_array:
    print("Data size: {}".format(data_size))
    X_train_extracted, y_train_extracted = X_train[:data_size], y_train[:data_size]
    acc_val_test = trainClf(X_train_extracted, y_train_extracted,
                            X_test, y_test, training_op_no_pretrain, epoch=10)
    acc_list_no_pretrain.append(acc_val_test)
```
以下にプレトレーニング有無による正解率の違いを示します。黒線がプレトレーニングあ、青線がプレトレーニングなしの結果です。プレトレーニングなしの場合は60000全てのデータを用いても、10epochでは正解率が13%程度しか得られないということがわかりました。ノイズ除去オートエンコーダーによるプレトレーニングはとても効果がありそうです！！！
![プレトレーニングの効果検証.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/393993/b059bce4-c6da-1df2-b378-20b6cbccc88c.png)

## プレトレーニングのメリット・デメリット
&nbsp; 個人的に思う、プレトレーニングのメリット・デメリットは以下です
**メリット**
　- Pretrainingを行うことで100のデータでも70%近い正解率を出すことができた(Pretrainingなしだと正解率10%程度とほぼ当てずっぽの様子)
　- Epoch数が10でもMnistデータ5000個で90%以上の正解率を出せた
　- 少ないラベル付きデータにおいても高い精度のモデルを作成可能

**デメリット**
　-プレトレーニングの時点で60000のデータセットを使用しているため、何も学習していないモデルと比較するのは不公平かも。。。

# まとめ
&nbsp; 大量データはあるがラベル付けがされていないという状況において効率的な学習を行う方法を検証しました。本記事では、オートエンコーダーによるプレトレーニング（事前学習）を用いることで、少ないラベルデータでも正解率が90％を超えるMNIST画像識別精度構築したのでご紹介させていただきました。また、TensorFlowで作成したモデルの下位層だけ再利用する方法で苦しんだので、私の方法を参考にしていただければ幸いです！
&nbsp; プレトレーニングを用いたMNIST画像識別器において、1000個のラベル付きデータを10epoch学習させるだけで、テストデータにおける正解率が90％を超えました。プレトレーニングにはMNISTデータのノイズ除去を行うオートエンコーダーを作成しました。オートエンコーダーの下位層だけを再利用し、MNIST画像識別モデルを構築しました。MNIST画像識別モデルの訓練データ(ラベル付き)の数を100-60000まで変化させ、正解率のデータ数依存性を確認しました。プレトレーニングを用いることで、100個の訓練データで約70%、5000個のデータで90%を超えるテストデータ正解率を出すことができました。


# 参考資料 
◾️ scikit-learnとTensorFlowによる実践機械学習
https://www.oreilly.co.jp/books/9784873118345/
![参考図書.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/393993/7425c21e-8f69-4e7e-ef8f-609962e98500.jpeg)

◾️本記事のコードは以下を参照ください
https://github.com/Sampeipei/qiita/blob/master/AutoEncoder_Pretraining_for_MNIST.ipynb