Kerasでの学習とEagerModeでの学習の比較

# 概要
TensorFlowに導入されているEager Modeについて、プログラムの書き方を理解するため、Kerasとの書き方の比較をしてみました。また、学習結果の差がどうなるかを検証しました。

# 環境
TensorFlow v1.10
Windows10

# Eager Modeの起動
まずは、TensorFlowをEagerModeに切り替えます。KerasはEagerModeにしておいても動くようです。

```python
import tensorflow as tf
tf.enable_eager_execution()
tfe = tf.contrib.eager

print("TensorFlow version: {}".format(tf.VERSION))  # TensorFlowのバージョン確認
print("Eager execution: {}".format(tf.executing_eagerly()))  # Eager Modeになっていることの確認
```

# データの準備
APIの検証をしたいだけなので、学習時間の短いIrisデータを使います。Irisデータの場合、scikit-learnのAPIを使うのが便利。

```python
from sklearn import datasets, preprocessing, model_selection
iris = datasets.load_iris()

all_x = preprocessing.scale(iris.data).astype('float32')  # データのスケーリングと標準化
all_y = iris.target

# 訓練データと検証データに分割（8割（120サンプル）を訓練データに）
train_x, val_x, train_y, val_y = model_selection.train_test_split(all_x, all_y, train_size=0.8)
```

# モデルの定義
学習にはシンプルな全結合ニューラルネットワークを使います。

```python
from tensorflow import keras
def get_model():
    model = tf.keras.Sequential([
        tf.keras.layers.Dense(10, activation=tf.nn.relu, input_shape=(4,)),
        tf.keras.layers.Dense(10, activation=tf.nn.relu),
        tf.keras.layers.Dense(3, activation=tf.nn.softmax)
    ])
    return model
```

# 最適化条件
なんでも良いですが、シンプルにSDGを使うことにします。エポック数、バッチサイズもここで定義しておきます。

```python
def get_optimizer():
    return tf.train.GradientDescentOptimizer(learning_rate=0.01)

batch_size = 32
num_epochs = 1000

```

# 学習
まずは、Kerasでの学習をしてみます。出力結果は、後ほど、Eager Modeとの比較で示します。

```python
def keras_training():
    model = get_model()
    optimizer = get_optimizer()
    model.compile(loss=keras.losses.sparse_categorical_crossentropy,
                  optimizer=optimizer,
                  metrics=['accuracy'])

    hist = model.fit(train_x, train_y, epochs=num_epochs, batch_size=batch_size, validation_data=(val_x, val_y))
    return hist

keras_hist = keras_training()

```


# Eager Modeでの学習
主な相違点は以下の3点です、それぞれについて説明します。

- データはDatasetクラスで扱う
- モデル・損失関数の定義
- 学習ループを自分で記述する

## Dataの準備
Eager Modeの学習のためには、TensorFlowのDataset形式である必要があるようです。Datasetはいろいろな作り方がありますが、Numpyの配列からは`from_tensor_slices`を使って、以下のように変換できます。

```python
from tensorflow.data import Dataset
train_dataset = Dataset.from_tensor_slices((train_x, train_y)).batch(batch_size)
val_dataset = Dataset.from_tensor_slices((val_x, val_y)).batch(batch_size)
```

## モデル・損失関数・勾配の定義
Kerasの標準的な損失関数はsoftmaxの出力を入力として、算出されますが、TensorFlowの場合は、softmaxを通さない出力（Logits）を入力し、損失関数の中でsoftmaxを算出するのが標準のようです。

したがって、モデルとしてはsoftmaxを通さないモデルを定義します。

```python
def get_model_wo_softmax():
    model = tf.keras.Sequential([
        tf.keras.layers.Dense(10, activation=tf.nn.relu, input_shape=(4,)),
        tf.keras.layers.Dense(10, activation=tf.nn.relu),
        tf.keras.layers.Dense(3)
    ])
    return model


def loss(model, x, y):
    logits = model(x)
    return tf.losses.sparse_softmax_cross_entropy(labels=y, logits=logits)


def grad(model, inputs, targets):
    with tf.GradientTape() as tape:
        loss_value = loss(model, inputs, targets)
    return loss_value, tape.gradient(loss_value, model.trainable_variables)
```

## 学習の仕方
学習ループをfor文を使って自分で書きます。今回のように、単純なニューラルネットを最適化するだけだと、あまりメリットはないでしょうが、より複雑な手順で学習したい時にはメリットがありそうです。

```python
def eager_training():
    model = get_model_wo_softmax()
    optimizer = get_optimizer()
    global_step = tf.train.get_or_create_global_step()

    # 損失関数の値・精度を残しておくためのリスト
    train_loss_list = []
    train_accuracy_list = []
    val_loss_list = []
    val_accuracy_list = []

    for epoch in range(num_epochs):
        epoch_loss_avg = tfe.metrics.Mean()
        epoch_accuracy = tfe.metrics.Accuracy()

        # 学習のループ
        for b, (x, y) in enumerate(train_dataset):
            loss_value, grads = grad(model, x, y)
            optimizer.apply_gradients(zip(grads, model.variables),
                                      global_step)

            epoch_loss_avg(loss_value)  # add current batch loss
            epoch_accuracy(tf.argmax(model(x), axis=1, output_type=tf.int32), y)

        train_loss_list.append(epoch_loss_avg.result())
        train_accuracy_list.append(epoch_accuracy.result())

        # 検証のループ
        epoch_test_loss_avg = tfe.metrics.Mean()
        epoch_test_accuracy = tfe.metrics.Accuracy()
        for b, (x, y) in enumerate(val_dataset):
            epoch_test_loss_avg(loss(model, x, y))
            epoch_test_accuracy(tf.argmax(model(x), axis=1, output_type=tf.int32), y)

        val_loss_list.append(epoch_test_loss_avg.result())
        val_accuracy_list.append(epoch_test_accuracy.result())

    return train_loss_list, train_accuracy_list, val_loss_list, val_accuracy_list


train_loss_results, train_accuracy_results, val_loss_results, val_accuracy_results = eager_training()
``` 

# KerasとEager Modeの結果比較
気になるのは、ちゃんと結果が一致するのかというところです。そのために比較をしてみます。

## 学習曲線の比較
学習曲線を比較すると、およそ一致することがわかります。

```python
import matplotlib.pyplot as plt
import numpy as np
x = range(num_epochs)

plt.plot(x, train_accuracy_results, label="eager_acc")
plt.plot(x, keras_hist.history['acc'], label="keras_acc")
plt.title("accuracy")
plt.legend(loc='best')
plt.show()

plt.plot(x, val_accuracy_results, label="eager_val_acc")
plt.plot(x, keras_hist.history['val_acc'], label="keras_val_acc")
plt.title("val_accuracy")
plt.legend(loc='best')
plt.show()

plt.plot(x, train_loss_results, label="eager_loss")
plt.plot(x, keras_hist.history['loss'], label="keras_loss")
plt.title("loss")
plt.legend(loc='best')
plt.show()

plt.plot(x, val_loss_results, label="eager_val_loss")
plt.plot(x, keras_hist.history['val_loss'], label="keras_loss")
plt.title("val_loss")
plt.legend(loc='best')
plt.show()
```

### 訓練データでの損失関数の値（横軸Epoch数）
![1000_loss.png](https://qiita-image-store.s3.amazonaws.com/0/78508/9d787a2a-0e71-9d49-7383-6c43919d508a.png)

### 検証データでの損失関数の値（横軸Epoch数）
![1000_val_loss.png](https://qiita-image-store.s3.amazonaws.com/0/78508/6db31783-c655-be1d-b6fd-8da795b50e69.png)

### 訓練データに対する精度（横軸Epoch数）
![1000_acc.png](https://qiita-image-store.s3.amazonaws.com/0/78508/33d0207a-c978-a711-212f-b70d3e3c782e.png)

### 検証データに対する精度（横軸Epoch数）
![1000_val_acc.png](https://qiita-image-store.s3.amazonaws.com/0/78508/9471e1ba-e348-78da-78e3-2dbe155568a1.png)


## 損失関数の差分
乱数が毎回違うためか、結果が完全には一致をしないので、KerasとEager Modeの差分を20回取って、プロットしてみます。序盤では差が出ることがわかりますが、Epochを進めると、結果が一致してくることがわかります。（もう少しEpoch増やしてみたほうがよかったかもしれません。）

```python
val_loss_difference = []
loss_difference = []
for i in range(20):
    keras_hist = keras_training()
    train_loss_results, train_accuracy_results, val_loss_results, val_accuracy_results = eager_training()
    loss_difference.append(np.array(train_loss_results) - np.array(keras_hist.history['loss']))
    val_loss_difference.append(np.array(val_loss_results) - np.array(keras_hist.history['val_loss']))


for item in loss_difference:
    plt.plot(item)

plt.title("Loss Difference(Eager - Keras)")
plt.show()

for item in val_loss_difference:
    plt.plot(item)

plt.title("Val Loss Difference(Eager - Keras)")
plt.show()
```


### 訓練データに対する損失関数値の差（横軸Epoch数）
![1000_loss_diff.png](https://qiita-image-store.s3.amazonaws.com/0/78508/7b8a0128-9cae-1bba-67cc-1069bff75b7e.png)

### 検証データに対する損失関数値の差（横軸Epoch数）
![1000_val_loss_diff.png](https://qiita-image-store.s3.amazonaws.com/0/78508/70d70973-c4b4-6951-0053-aa989ac957de.png)

## 損失関数の差分（中間層のユニット数：30の場合）
結果が安定することを期待して、中間層のユニット数を10から30に増加させると、1000Epoch学習後のばらつきは小さくなっているように見えます。（縦軸のスケールが違うことに注意。検証データのほうはあまり変わらないかも。）

### 訓練データに対する損失関数値の差（横軸Epoch数）
![int30_loss_diff.png](https://qiita-image-store.s3.amazonaws.com/0/78508/0c1a22c6-0715-bde5-adc6-a8cad322c3f6.png)

### 検証データに対する損失関数値の差（横軸Epoch数）
![int30_val_loss_diff.png](https://qiita-image-store.s3.amazonaws.com/0/78508/eb144676-b2be-38cf-758f-90a91d296c44.png)