変分オートエンコーダで猫人間を作ってみた

# こんなのが作れました

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/319477/caf59229-ae4b-6288-2ece-5ddfb22843e1.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/319477/60aace4e-934a-624e-6bc2-449ddd1de138.png)

# 説明

基本的には次のサイトを参考にしました。

http://cedro3.com/ai/keras-vae-celeba/

ただしこの参考サイトではソースコードがリンク切れしていたので、
https://github.com/awslabs/keras-apache-mxnet/blob/master/examples/variational_autoencoder_deconv.py で代用しました。

また、ソースコードは、スタンドアロンKerasを使って書かれているなど、
古い流儀に従って書かれていたものだったので、「独自の若干のアレンジ」を加えました。
最終的に、次のようなソースコードになりました。

```python:vae.py
# 新しい環境でplot_modelしたい人は https://qiita.com/hrs1985/items/7d923e9465985d6b8968を参考にpydotplusとgraphvizを。


import tensorflow as tf

# GPUをフルに使うと死ぬので、必要に応じて使うように設定変更

config = tf.compat.v1.ConfigProto() # 変更箇所
config.gpu_options.allow_growth = True # 変更箇所
sess = tf.compat.v1.Session(config=config) # 変更箇所


keras = tf.keras # 変更箇所
from keras.layers import Dense, Input
from keras.layers import Conv2D, Flatten, Lambda
from keras.layers import Reshape, Conv2DTranspose
from keras.models import Model
from keras.datasets import mnist
from keras.losses import mse, binary_crossentropy
from keras.utils import plot_model, np_utils # 変更箇所
K = tf # 変更箇所
from sklearn.model_selection import train_test_split  # 変更箇所
K.flatten = lambda inputs: K.reshape(inputs, [-1]) # 変更箇所

import numpy as np
import matplotlib.pyplot as plt
import argparse
import os


#if K.backend() == 'mxnet': # 変更箇所
#    raise NotImplementedError("MXNet Backend: Cannot auto infer input shapes.") # 変更箇所


# reparameterization trick
# instead of sampling from Q(z|X), sample eps = N(0,I)
# then z = z_mean + sqrt(var)*eps
def sampling(args):
    """Reparameterization trick by sampling fr an isotropic unit Gaussian.
    # Arguments:
        args (tensor): mean and log of variance of Q(z|X)
    # Returns:
        z (tensor): sampled latent vector
    """

    z_mean, z_log_var = args
    batch = K.shape(z_mean)[0]
    dim = K.shape(z_mean)[1] # 変更箇所
    # by default, random_normal has mean=0 and std=1.0
    epsilon = K.random.normal(shape=(batch, dim)) # 変更箇所
    return z_mean + K.exp(0.5 * z_log_var) * epsilon


def plot_results(models,
                 data,
                 batch_size=128,
                 model_name="vae_mnist"):
    """Plots labels and MNIST digits as function of 2-dim latent vector
    # Arguments:
        models (tuple): encoder and decoder models
        data (tuple): test data and label
        batch_size (int): prediction batch size
        model_name (string): which model is using this function
    """

    encoder, decoder = models
    x_test, y_test = data
    os.makedirs(model_name, exist_ok=True)

    filename = os.path.join(model_name, "vae_mean.png")
    # display a 2D plot of the digit classes in the latent space
    z_mean, _, _ = encoder.predict(x_test,
                                   batch_size=batch_size)
    plt.figure(figsize=(12, 10))
    plt.scatter(z_mean[:, 0], z_mean[:, 1]) # 変更箇所
    # plt.colorbar() # 変更箇所
    plt.xlabel("z[0]")
    plt.ylabel("z[1]")
    plt.savefig(filename)
    # plt.show() # 変更箇所

    filename = os.path.join(model_name, "digits_over_latent.png")
    # display a 30x30 2D manifold of digits
    n = 15 # 変更箇所
    digit_size = 64 # 変更箇所
    figure = np.zeros((digit_size * n, digit_size * n, 3)) # 変更箇所
    # linearly spaced coordinates corresponding to the 2D plot
    # of digit classes in the latent space
    grid_x = np.linspace(-4, 4, n)
    grid_y = np.linspace(-4, 4, n)[::-1]

    for i, yi in enumerate(grid_y):
        for j, xi in enumerate(grid_x):
            z_sample = np.array([[xi, yi]])
            x_decoded = decoder.predict(z_sample)
            digit = x_decoded[0].reshape(digit_size, digit_size, 3) # 変更箇所
            plt.imshow(digit,cmap='Greys_r')   # 変更箇所
            plt.savefig(str(i)+'@'+str(j)+'fig.png')   # 変更箇所  
            figure[i * digit_size: (i + 1) * digit_size,
                   j * digit_size: (j + 1) * digit_size] = digit

    plt.figure(figsize=(10, 10))
    start_range = digit_size // 2
    end_range = n * digit_size + start_range + 1
    pixel_range = np.arange(start_range, end_range, digit_size)
    sample_range_x = np.round(grid_x, 1)
    sample_range_y = np.round(grid_y, 1)
    plt.xticks(pixel_range, sample_range_x)
    plt.yticks(pixel_range, sample_range_y)
    plt.xlabel("z[0]")
    plt.ylabel("z[1]")
    plt.imshow(figure, cmap='Greys_r')
    plt.savefig(filename)
    plt.show()

""" # 変更箇所 ここから
# MNIST dataset
(x_train, y_train), (x_test, y_test) = mnist.load_data()

image_size = x_train.shape[1]
x_train = np.reshape(x_train, [-1, image_size, image_size, 1])
x_test = np.reshape(x_test, [-1, image_size, image_size, 1])
x_train = x_train.astype('float32') / 255
x_test = x_test.astype('float32') / 255

"""
x = np.load("人や猫の顔データ.npy")

x = x.astype('float32')
x = x/ 255.0
y = np.zeros(len(x))
y = np_utils.to_categorical(y, 1) 
 
x_train, x_test, y_train, y_test = train_test_split(x, y, test_size=0.2, random_state=111)
original_dim = 12288  
image_size = 64   
# 変更箇所ここまで

# network parameters
input_shape = (image_size, image_size, 3) # 変更箇所
batch_size = 128
kernel_size = 3
filters = 32 # 変更箇所
latent_dim = 2
epochs = 50 # 変更箇所

# VAE model = encoder + decoder
# build encoder model
inputs = Input(shape=input_shape, name='encoder_input')
x = inputs
for i in range(3): # 変更箇所
    filters *= 2
    x = Conv2D(filters=filters,
               kernel_size=kernel_size,
               activation='relu',
               strides=2,
               padding='same')(x)

# shape info needed to build decoder model
shape = x.shape # 変更箇所

# generate latent vector Q(z|X)
x = Flatten()(x)
x = Dense(16, activation='relu')(x)
z_mean = Dense(latent_dim, name='z_mean')(x)
z_log_var = Dense(latent_dim, name='z_log_var')(x)

# use reparameterization trick to push the sampling out as input
# note that "output_shape" isn't necessary with the TensorFlow backend
z = Lambda(sampling, output_shape=(latent_dim,), name='z')([z_mean, z_log_var])

# instantiate encoder model
encoder = Model(inputs, [z_mean, z_log_var, z], name='encoder')
encoder.summary()
plot_model(encoder, to_file='vae_cnn_encoder.png', show_shapes=True)

# build decoder model
latent_inputs = Input(shape=(latent_dim,), name='z_sampling')
x = Dense(shape[1] * shape[2] * shape[3], activation='relu')(latent_inputs)
x = Reshape((shape[1], shape[2], shape[3]))(x)

for i in range(3): # 変更箇所
    x = Conv2DTranspose(filters=filters,
                        kernel_size=kernel_size,
                        activation='relu',
                        strides=2,
                        padding='same')(x)
    filters //= 2

outputs = Conv2DTranspose(filters=3, # 変更箇所
                          kernel_size=kernel_size,
                          activation='sigmoid',
                          padding='same',
                          name='decoder_output')(x)

# instantiate decoder model
decoder = Model(latent_inputs, outputs, name='decoder')
decoder.summary()
plot_model(decoder, to_file='vae_cnn_decoder.png', show_shapes=True)

# instantiate VAE model
outputs = decoder(Lambda(lambda x:x[2])(encoder(inputs))) # 変更箇所
vae = Model(inputs, outputs, name='vae')

vae.summary()
plot_model(vae, to_file='vae_cnn.png', show_shapes=True)

if __name__ == '__main__':
    parser = argparse.ArgumentParser()
    help_ = "Load h5 model trained weights"
    parser.add_argument("-w", "--weights", help=help_)
    help_ = "Use mse loss instead of binary cross entropy (default)"
    parser.add_argument("-m", "--mse", help=help_, action='store_true')
    args = parser.parse_args()
    models = (encoder, decoder)
    data = (x_test, y_test)

    # VAE loss = mse_loss or xent_loss + kl_loss
    if args.mse:
        reconstruction_loss = mse(K.flatten(inputs), K.flatten(outputs))
    else:
        reconstruction_loss = binary_crossentropy(K.flatten(inputs),
                                                  K.flatten(outputs))

    reconstruction_loss *= image_size * image_size
    kl_loss = 1 + z_log_var - K.square(z_mean) - K.exp(z_log_var)
    kl_loss = K.math.reduce_sum(kl_loss, axis=-1) # 変更箇所
    kl_loss *= -0.5
    vae_loss = K.math.reduce_mean(reconstruction_loss + kl_loss) # 変更箇所
    vae.add_loss(vae_loss)
    vae.compile(optimizer='rmsprop')


    if args.weights:
        vae.load_weights(args.weights)
    else:
        # train the autoencoder
        vae.fit(x_train,
                epochs=epochs,
                batch_size=batch_size,
                validation_data=(x_test, None))
        vae.save_weights('vae_cnn_mnist.h5')

    plot_results(models, data, batch_size=batch_size, model_name="vae_cnn")
```

# 実験手順

1. [人間の顔自動生成サイト](https://thispersondoesnotexist.com/)と[猫の顔自動生成サイト](https://thiscatdoesnotexist.com)から10000枚ずつ人や猫の顔写真を集めてくる。
2. 画像をそれぞれ3次元numpy配列にする。shapeは(高さ, 幅, チャンネル数)=(64,64,3)だ。
3. 手順2の3次元numpy配列たちを集めた4次元numpy配列を作る。
4. 4次元numpy配列を`人や猫の顔データ.npy`って名前で好きなフォルダに保存する
5. 先ほど示した`vae.py`を、手順4と同じフォルダに保存する。
6. そのフォルダをカレントディレクトリとしてコマンドプロンプトを開き、`python vae.py`してしばらく待つ！

# 感想
vaeは潜在変数が確率変数で「広がり」を持つため、モーフィングが可能。
[参考サイト](http://cedro3.com/ai/keras-vae-celeba/)でも女性の顔から男性の顔への滑らかな変換が実現していた。その特徴として
- 女性の顔から男性の顔へ「だんだん」移り変わっていく
- 移り変わっていく間のどの「中性的」画像についても、人間の顔として自然な顔画像だ。

今回の実験はこのモーフィングの性質を利用したもの。
人の顔から猫の顔へ「だんだん」移り変わっていくモーフィングを実現すれば、
「人と猫の中間」っぽい「自然な」画像ができるだろう。
では、そんな「猫人間として自然な画像」とはいったいどんなものなのだろうか、と気になってこの実験を行った。

みなさんも、是非記事に示したソースをコピペして実験してみてほしい。
猫人間に限らず、「ロボットと人間の中間的な画像」を作って『不気味の谷』で遊ぶのもいいだろう