自前のデータでDCGANをやってみる

#GANとは
**GAN**(*Generative Adversarial Networks*)とは、訓練データを学習し、
それらに似た新しいデータを生成するモデルのことで、**生成モデル**と呼ばれるネットワークの一種です。
**DCGAN**(*Deep Convolutional GAN*)とは、
CNN（*Convolutional Neural Network*）を使ったGANのことです。

#GANの仕組み
GANでは**generator**(画像生成器)と**discriminator**(識別器)という２つのネットワークを使用します。
generatorではノイズから正解データに似た画像を生成し、
discriminator(識別器)を通して、それらが訓練データか、generatorによって生成されたデータかを識別します。
![図1.png](https://qiita-image-store.s3.amazonaws.com/0/265258/154e59f6-a78b-b055-49c8-3ea6c85c78a6.png)

discriminatorでは訓練データのラベルが「1」、生成したデータのラベルが「0」と識別されるよう学習していきますが、
generatorでは、生成したデータをdiscriminatorに通したときに、
出力が「1」に近づく(**生成データが訓練データと判断される**)ように学習していきます。
最終的にdiscriminatorの正答率が「50％」になれば、訓練データと見分けがつかないデータを生成できていることになります。

この関係は、よく**紙幣の偽造者と警察の関係**に例えられます。
偽造者はなるべく本物の紙幣に似ている紙幣を造ろうとし、警察はそれらを本物の紙幣と見分けようとします。
次第に警察の能力が上がり、本物と偽造紙幣をうまく見分けられるようになれば、
偽造者は、更に本物に近い偽造紙幣を造るようになります。
最終的に偽造者は、本物と区別が付かない偽造紙幣を製造できるようになる…ということですね。

#実際にやってみる
今回の訓練データはこちらです。
<img width="557" alt="スクリーンショット 2018-12-03 9.58.09.png" src="https://qiita-image-store.s3.amazonaws.com/0/265258/d6dc79d6-7ef1-5bef-6801-4dcb84962fc8.png">
また、実装に関しては[こちらの記事](https://elix-tech.github.io/ja/2017/02/06/gan.html)を参考にさせていただきました。
以下、今回実装したコードになります。

①まずは必要なライブラリをインポートします。

```python
from keras.models import Sequential
from keras.layers import Dense, Activation, Reshape
from keras.layers.normalization import BatchNormalization
from keras.layers.convolutional import UpSampling2D, Conv2D
from keras.layers.advanced_activations import LeakyReLU
from keras.layers import Flatten, Dropout
from keras.preprocessing.image import img_to_array, load_img
from keras.optimizers import Adam
import math
import numpy as np
import os
from PIL import Image
```
②generatorとdiscriminatorを作成します。

```python:generator
def generator_model():
    model = Sequential()
    model.add(Dense(input_dim=100, units=1024))
    model.add(BatchNormalization())
    model.add(Activation('relu'))
    model.add(Dense(32 * 32 * 128))
    model.add(BatchNormalization())
    model.add(Activation('relu'))
    model.add(Reshape((32, 32, 128), input_shape=(32 * 32 * 128,)))
    model.add(UpSampling2D((2, 2)))
    model.add(Conv2D(64, (5, 5), padding="same"))
    model.add(BatchNormalization())
    model.add(Activation('relu'))
    model.add(UpSampling2D((2, 2)))
    model.add(Conv2D(3, (5, 5), padding="same"))
    model.add(Activation('tanh'))
    return model
```
generatorでは2x2のアップサンプリング(転置畳み込み)を2回行っているので、最終的な画像サイズは４倍になることに注意が必要です。
今回、訓練データの画像サイズは128×128のため、128÷4=32を最初のノードに設定しています。

```python:discriminator
def discriminator_model():
    model = Sequential()
    model.add(Conv2D(64, (5,5), strides=(2, 2), input_shape=(128, 128, 3), padding="same"))
    model.add(LeakyReLU(0.2))
    model.add(Conv2D(128, (5,5), strides=(2, 2)))
    model.add(LeakyReLU(0.2))
    model.add(Flatten())
    model.add(Dense(256))
    model.add(LeakyReLU(0.2))
    model.add(Dropout(0.5))
    model.add(Dense(1))
    model.add(Activation('sigmoid'))
    return model
```
`input_shape`のみ対応する画像サイズに変更する必要があります。
また、出力層を除いて活性化関数に[Leaky ReLU](http://www.thothchildren.com/chapter/59b93f7575704408bd4300f2)を使用しているのも特徴です。

③生成画像を並べて表示するための関数を用意します。(※必須ではありません。)

```python
def combine_images(generated_images):
    total = generated_images.shape[0]
    cols = int(math.sqrt(total))
    rows = math.ceil(float(total)/cols)
    width, height, ch= generated_images.shape[1:]
    output_shape = (
        height * rows,
        width * cols,
        ch
    )
    combined_image = np.zeros(output_shape)

    for index, image in enumerate(generated_images):
        i = int(index/cols)
        j = index % cols
        combined_image[width*i:width*(i+1), height*j:height*(j+1)] = image[:, :, :]
    return combined_image
```
④最後に学習部分を作成します。

```python
def train():
    # 訓練データ読み込み
    img_list = os.listdir(TRAIN_IMAGE_PATH)
    X_train = []
    for img in img_list:
        img = img_to_array(load_img(TRAIN_IMAGE_PATH+img, target_size=(128,128,3)))
        # -1から1の範囲に正規化
        img = (img.astype(np.float32) - 127.5)/127.5
        X_train.append(img)
    # 4Dテンソルに変換(データの個数, 128, 128, 3)
    X_train = np.array(X_train)

    # generatorとdiscriminatorを作成
    discriminator = discriminator_model()
    d_opt = Adam(lr=1e-5, beta_1=0.1)
    discriminator.compile(loss='binary_crossentropy', optimizer=d_opt)
    # discriminatorの重みを固定(dcganの中のみ)
    discriminator.trainable = False
    generator = generator_model()

    dcgan = Sequential([generator, discriminator])
    g_opt = Adam(lr=2e-4, beta_1=0.5)
    dcgan.compile(loss='binary_crossentropy', optimizer=g_opt)

    num_batches = int(X_train.shape[0] / BATCH_SIZE)
    print('Number of batches:', num_batches)
    for epoch in range(NUM_EPOCH):

        for index in range(num_batches):
            noise = np.array([np.random.uniform(-1, 1, 100) for _ in range(BATCH_SIZE)])
            image_batch = X_train[index*BATCH_SIZE:(index+1)*BATCH_SIZE]
            generated_images = generator.predict(noise, verbose=0, batch_size=BATCH_SIZE)

            # 生成画像を出力
            if (index+1) % (num_batches) == 0:
                image = combine_images(generated_images)
                image = image*127.5 + 127.5
                if not os.path.exists(GENERATED_IMAGE_PATH):
                    os.mkdir(GENERATED_IMAGE_PATH)
                Image.fromarray(image.astype(np.uint8))\
                    .save(GENERATED_IMAGE_PATH+"%04d_%04d.png" % (epoch, index))

            # discriminatorを更新
            X = np.concatenate((image_batch, generated_images))
            # 訓練データのラベルが1、生成画像のラベルが0になるよう学習する
            y = [1]*BATCH_SIZE + [0]*BATCH_SIZE
            d_loss = discriminator.train_on_batch(X, y)

            # generator更新
            noise = np.array([np.random.uniform(-1, 1, 100) for _ in range(BATCH_SIZE)])
            # 生成画像をdiscriminatorにいれたときに
            # 出力が1に近くなる(訓練画像と識別される確率が高くなる)ように学習する
            g_loss = dcgan.train_on_batch(noise, [1]*BATCH_SIZE)

            print("epoch: %d, batch: %d, g_loss: %f, d_loss: %f" % (epoch, index, g_loss, d_loss))

        generator.save_weights('generator.h5')
        discriminator.save_weights('discriminator.h5')
```
これで実装は完了です。
画像のパスやバッチサイズなどは自由に設定してください。

#結果
epoch = 1
![0000_0004.png](https://qiita-image-store.s3.amazonaws.com/0/265258/57e5838e-0488-fd13-4dbc-cac2edf93d3c.png)
epoch = 100
![0100_0004.png](https://qiita-image-store.s3.amazonaws.com/0/265258/7a16fd78-3179-2c73-0445-4c0da81c48fc.png)
epoch = 200
![0200_0004.png](https://qiita-image-store.s3.amazonaws.com/0/265258/3695c427-90ba-d134-b9ce-bd79e38ae7c4.png)
epoch = 600
![0600_0004.png](https://qiita-image-store.s3.amazonaws.com/0/265258/aea4292e-55a2-10b6-abad-ca8e39abcbce.png)
epoch = 1000
![0999_0004.png](https://qiita-image-store.s3.amazonaws.com/0/265258/098fd4d8-ba85-f43f-6e15-77d64d6b77ef.png)
思いのほか猫っぽくなりましたね。

#まとめ
今回初めてDCGANを触ってみました。
DCGANを任意の画像でやろうとすると処理が重たいので、CPU環境ではMNISTなどで試すのがオススメです。
細かい理論についてはまだ詰め切れていない部分があるので、またの機会に。

#参考リンク
[はじめてのGAN](https://elix-tech.github.io/ja/2017/02/06/gan.html)
[GANについて概念から実装まで　～DCGANによるキルミーベイベー生成～](https://qiita.com/taku-buntu/items/0093a68bfae0b0ff879d)
[GAN(論文)](https://arxiv.org/abs/1406.2661)
[DCGAN(論文)](https://arxiv.org/abs/1511.06434)