CoLabでAutoEncoder


これまで、[KerasでAutoEncoder](https://qiita.com/fukuit/items/2f8bdbd36979fff96b07)とか、[KerasでAutoEncoderその2](https://qiita.com/fukuit/items/1a9760821b1166aba90c#_reference-f6540439b5bd44b4e49a)とかの記事を書いてきたんだけれど、これをGoogle Colaboratoryで動かしてみようか、というココロミについて。

Google Colaboratoryで動かせば、GPUが使えるのが大きい。

## 目的

- 手書きの数字の「1」を学習する
- 手書きの「1以外の数字」が入力された際に、その画像が「1」とは何やら違う、ということを示す

まずは、[KerasでAutoEncoderその2](https://qiita.com/fukuit/items/1a9760821b1166aba90c#_reference-f6540439b5bd44b4e49a)で書いたネタを、tensorflowのKerasで動作するようにするのがゴール。

## Colab上で実装する

Colabで```pip install -U Keras```しようかと思ったけれど、```tf.keras```を使うことにする。

まずは、Colabの「ファイル」メニューで「Python3の新しいノートブック」を選択して、ノートブックを作成する。後は、Jupyter Notebook上での作業と本質的には変わらない。

### 宣言部

```python
import tensorflow as tf
import tensorflow.keras.backend as K
```

### MNISTのダウンロードと前処理

```python
mnist = tf.keras.datasets.mnist
(x_train, y_train), (x_test, y_test) = mnist.load_data()
image_size = x_train.shape[1]

import numpy as np

x_train = x_train.astype('float32')/255.
x_test = x_test.astype('float32')/255.
x_train = np.reshape(x_train, (len(x_train), image_size, image_size, 1))
x_test = np.reshape(x_test, (len(x_test), image_size, image_size, 1))
```

上述した目的に従い、「1」のみを学習させたいので、```x_train```の中身を「1」だけにしておく。

```python
x_train = x_train[y_train==1]
```

学習時のバリデーション用に、```x_train```を更に分割して、```x_valid```を作っておく。
学習時のバリデーションに```x_test```を使えば良いという考え方もあるけれど、自分は```x_test```は学習後の評価時に使うもんなんじゃないか？と思っている。

```python
from sklearn.model_selection import train_test_split
x_train, x_valid, y_train, y_valid = train_test_split(x_train, y_train, test_size=0.175)
```

### よく使う変数の設定

この後よく使う変数を設定しておく。

```python
input_shape = (image_size, image_size, 1)
batch_size = 64
kernel_size = 3
filters = 16
latent_dim = 2
epochs = 50
```

### encoder、潜在変数z、decoderを定義する

encoderを[Kerasのsample](https://github.com/keras-team/keras/blob/master/examples/variational_autoencoder_deconv.py)にあった書き方で。

```python
inputs = tf.keras.layers.Input(shape=input_shape, name='encoder_input')
x = inputs
for i in range(2):
  filters *= 2
  x = tf.keras.layers.Conv2D(filters=filters,
                             kernel_size=kernel_size,
                             activation='relu',
                             strides=2,
                             padding='same')(x)
shape = K.int_shape(x)
x = tf.keras.layers.Flatten()(x)
x = tf.keras.layers.Dense(16, activation='relu')(x)
```

潜在変数```z```と、そのための```sampling()```の定義。

```python
def sampling(args):
  z_mean, z_log_var = args
  batch = K.shape(z_mean)[0]
  dim = K.int_shape(z_mean)[1]
  epsilon = K.random_normal(shape=(batch, dim))
  return z_mean + K.exp(0.5 * z_log_var) * epsilon
```

```python
z_mean = tf.keras.layers.Dense(latent_dim, name='z_mean')(x)
z_log_var = tf.keras.layers.Dense(latent_dim, name='z_log_var')(x)
z = tf.keras.layers.Lambda(sampling, name='z')([z_mean, z_log_var])
```

いよいよ、```encoder```を定義する。

```python
encoder = tf.keras.models.Model(inputs, [z_mean, z_log_var, z], name='encoder')
```

ここで、```encoder.summary()```すると、次のようになる。

```
__________________________________________________________________________________________________
Layer (type)                    Output Shape         Param #     Connected to                     
==================================================================================================
encoder_input (InputLayer)      (None, 28, 28, 1)    0                                            
__________________________________________________________________________________________________
conv2d (Conv2D)                 (None, 14, 14, 32)   320         encoder_input[0][0]              
__________________________________________________________________________________________________
conv2d_1 (Conv2D)               (None, 7, 7, 64)     18496       conv2d[0][0]                     
__________________________________________________________________________________________________
flatten (Flatten)               (None, 3136)         0           conv2d_1[0][0]                   
__________________________________________________________________________________________________
dense (Dense)                   (None, 16)           50192       flatten[0][0]                    
__________________________________________________________________________________________________
z_mean (Dense)                  (None, 2)            34          dense[0][0]                      
__________________________________________________________________________________________________
z_log_var (Dense)               (None, 2)            34          dense[0][0]                      
__________________________________________________________________________________________________
z (Lambda)                      (None, 2)            0           z_mean[0][0]                     
                                                                 z_log_var[0][0]                  
==================================================================================================
Total params: 69,076
Trainable params: 69,076
Non-trainable params: 0
______________________________________________________________________________________________
```

引き続き、```decoder```を定義する。

```python
latent_inputs = tf.keras.layers.Input(shape=(latent_dim,), name='z_sampling')
x = tf.keras.layers.Dense(shape[1]*shape[2]*shape[3], activation='relu')(latent_inputs)
x = tf.keras.layers.Reshape((shape[1], shape[2], shape[3]))(x)
```

```python
for i in range(2):
  x = tf.keras.layers.Conv2DTranspose(filters=filters,
                                      kernel_size=kernel_size,
                                      activation='relu',
                                      strides=2,
                                      padding='same')(x)
  filters //= 2
outputs = tf.keras.layers.Conv2DTranspose(filters=1,
                                          kernel_size=kernel_size,
                                          activation='sigmoid',
                                          padding='same',
                                          name='decoder_output')(x)
decoder = tf.keras.models.Model(latent_inputs, outputs, name='decoder')
```

ここで、```decoder.summary()```すると、次のようになる。

```
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
z_sampling (InputLayer)      (None, 2)                 0         
_________________________________________________________________
dense_1 (Dense)              (None, 3136)              9408      
_________________________________________________________________
reshape (Reshape)            (None, 7, 7, 64)          0         
_________________________________________________________________
conv2d_transpose (Conv2DTran (None, 14, 14, 64)        36928     
_________________________________________________________________
conv2d_transpose_1 (Conv2DTr (None, 28, 28, 32)        18464     
_________________________________________________________________
decoder_output (Conv2DTransp (None, 28, 28, 1)         289       
=================================================================
Total params: 65,089
Trainable params: 65,089
Non-trainable params: 0
_________________________________________________________________
```

これで、```encoder```と```decoder```が逆のネットワーク構造になっていることが確認できた。

以上を踏まえて、Variational Autoencoderモデルを次のように定義する。

```python
outputs = decoder(encoder(inputs)[2])
autoencoder = tf.keras.models.Model(inputs, outputs, name='vae')
```

### loss関数の定義

loss関数を、[Kerasのsample](https://github.com/keras-team/keras/blob/master/examples/variational_autoencoder_deconv.py)に記載されていたように定義する。

```python
rx_loss = tf.keras.losses.binary_crossentropy(K.flatten(inputs), K.flatten(outputs))
rx_loss *= image_size*image_size
kl_loss = 1+z_log_var-K.square(z_mean)-K.exp(z_log_var)
kl_loss = K.sum(kl_loss, axis=-1)
kl_loss *= -0.5
vae_loss = K.mean(rx_loss+kl_loss)

autoencoder.add_loss(vae_loss)
```

### 最適化関数の設定

最適化はAdamで行う。なんとなく、学習率だけ、デフォルトより小さい値にしてみる。

```python
from tensorflow.keras.optimizers import Adam
adam = Adam(lr=0.0001)

autoencoder.compile(optimizer=adam)
```

### 学習の実施

実行に要した時間を表示するようにしておく。

```python
import time
start_time = time.time()

autoencoder.fit(x_train,
                epochs=epochs,
                batch_size=batch_size,
                validation_data = (x_valid, None))

print('{:.2f}[sec]'.format(time.time()-start_time))
```

50 epochsを実行してみた結果、79.28秒だった。GPU速い。自宅のMacBook Pro (Mid2012)だと、10分弱くらいかかる。Colab上でも666.18秒かかった。

### 学習結果の確認

x_testの先頭10データを入力してみる。入力値から得られた特徴量を「1」のモノだと思って復元を試みるので、入力した画像と復元した画像との間で差異が小さければ「1」だろうし、大きければ「1以外の数値」である。

潜在変数```z```が今回2次元なので、その分布を図示して、学習した「1」の分布の重心等代表値と、入力した画像から求めたzとの距離を数値化するだけでも良いかもしれない。

```python
from matplotlib.pyplot as plt
%matplotlib inline
import cv2
```

```python
n = 10
decoded_imgs = autoencoder.predict(x_test[:n])

plt.figure(figsize=(10, 4))

for i in range(n):+1: 
    # original_image
    orig_img = x_test[i].reshape(image_size, image_size)

    # reconstructed_image
    reconst_img = decoded_imgs[i].reshape(image_size, image_size)

    # diff image
    diff_img = ((orig_img - reconst_img)+2)/4
    diff_img = (diff_img*255).astype(np.uint8)
    orig_img = (orig_img*255).astype(np.uint8)
    reconst_img = (reconst_img*255).astype(np.uint8)
    diff_img_color = cv2.applyColorMap(diff_img, cv2.COLORMAP_JET)

    # display original
    ax = plt.subplot(3, n,  i + 1)
    plt.imshow(orig_img, cmap=plt.cm.gray)
    ax.get_xaxis().set_visible(False)
    ax.get_yaxis().set_visible(False)

    # display reconstruction
    ax = plt.subplot(3, n, i + n + 1)
    plt.imshow(reconst_img, cmap=plt.cm.gray)
    ax.get_xaxis().set_visible(False)
    ax.get_yaxis().set_visible(False)

    # display diff
    ax = plt.subplot(3, n, i + n*2 + 1)
    plt.imshow(diff_img, cmap=plt.cm.jet)
    ax.get_xaxis().set_visible(False)
    ax.get_yaxis().set_visible(False)
plt.show()
```

![20181008.png](https://qiita-image-store.s3.amazonaws.com/0/15553/f1422b93-58ad-b065-b8f6-7ad3fbdd2f2d.png "20181008.png")

上段が、入力画像で、中段がautoencoderを使って、上段の画像を基に生成した画像で、下段が上段と中段の差分である。

「1」以外が入力されたときに、無理やり「1」を復元しようとして破綻していることが可視化された。

----

というワケで、[KerasでAutoEncoderその2](https://qiita.com/fukuit/items/1a9760821b1166aba90c#_reference-f6540439b5bd44b4e49a)そのまんまなんだけれど、CoLabでGPUを使ってみたかったというハナシ。