DCGANでアニメキャラクターを作ろうとして失敗した話

#概要
初投稿です。kosakae256と申すものです。

kerasでのDCGAN実装にあたっての失敗談を簡潔にまとめてみました。
本投稿ではGenerator内で**resnet**を使用しています。keras、DCGAN、resnetの実装がこれまでなかったので、ぜひ参考にしてください。
わかりにくい解説ですが、何卒よろしくお願いします。
本実装に関しては@taku-buntu さんの記事が非常に参考になりました。なんなら私のサイトなんていらないくらいわかりやすい解説です。ぜひこちらもご覧ください。https://qiita.com/taku-buntu/items/0093a68bfae0b0ff879d

#前提
- GANについてかじったことがある程度の知識
- DCGANによるMNISTの手書き文字生成をやったことがある
- kerasによるFunctionalAPIの用法及び、CNNの発展知識
- googlecolabの使い方を知っている
- 感覚でクッソ読みづらいコードを読み進めれる人
- resnetが目当てであれば、resnetの基本知識

#環境
- GoogleColaboratoryの実行環境

#画像生成のGANとは
敵対的生成ネットワークと呼ばれる、教師なし学習です。
名前の通り、二つのネットワークが競い合って成長していくネットワークのことを指します。
画像を生成する__Generator__、Generatorが作り出した画像と本物の画像判別する__Discriminator__の二つのネットワークを競い合わせます。
__Generator__は、__Discriminator__をだますように成長していき、やがてDiscriminatorが判別できないほど本物の画像に近い画像を生成できるようになります。
![is20tech001zu004-1.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1128128/eb455964-71eb-7f9f-0da5-dda7ed379db1.jpeg)
この一連の動作は、「偽札を作る犯罪者」と「偽札を見破る警察」にたとえられます。
ノイズzをうまく操作することで、Generatorは__本物の画像を複数個合成した画像__を作ることも可能です。今回の実装ではノイズzの操作をしていないため省略します。
(正直に言うと、完全に理解していないためです)

#実装
ノイズzからアニメ絵を生成する実装例を見ていきましょう。
アニメ絵のデータセットは以下から拝借しました。計12万枚となります
https://www.kaggle.com/soumikrakshit/anime-faces
https://www.kaggle.com/scribbless/another-anime-face-dataset

予め、128x128の画像に整形しています。
まずは下準備から

Colabで実装しているため、システム上にGoogleDriveをマウントするためのコード

```python
from google.colab import drive
drive.mount('/content/drive')
```
<br>

コピーとかなんやかんややる

```
!cp "/content/drive/My Drive/animeface.zip" .
```
```
!unzip "animeface.zip"
```
<br>

X_trainに128x128のアニメキャラクターの画像データ12万枚を入れ込む。メモリ不足に注意すべし。

```python
import cv2
import numpy as np

#アニメキャラの画像をX_trainに入れる
X_train = []
for i in range(1,120000):
    print(i)
    
    img = cv2.imread(f'./animeface/animeface.{str(i)}.jpg')
    X_train.append(img)
    
X_train = np.array(X_train)
```
<br>

メインコード
Generatorのネットワークには劣化版resnetを用いている。何故劣化版を用いているかは後述する。

```python
#128*128*3の、アニメキャラクターの顔の画像を生成する例

from __future__ import print_function, division

from keras.datasets import mnist
from keras.layers import Input, Dense, Reshape, Flatten, Dropout,Reshape,Add
from keras.layers import BatchNormalization, Activation, ZeroPadding2D,MaxPooling2D,GlobalAveragePooling2D
from keras.layers.advanced_activations import LeakyReLU
from keras.layers.convolutional import UpSampling2D, Conv2D
from keras.models import Sequential, Model,model_from_json
from keras.optimizers import Adam
import cv2


import matplotlib.pyplot as plt

import sys

import numpy as np
import random

from keras.models import Sequential, Model,model_from_json

from tensorflow.keras.utils import plot_model

class DCGAN():
    def __init__(self):
        # Input shape
        self.input_img_rows = 128
        self.input_img_cols = 128
        self.input_channels = 3
        self.input_img_shape = (self.input_img_rows, self.input_img_cols, self.input_channels,)

        # チューニング対象
        self.latent_dim = 100
        dis_optimizer = Adam(0.0002, 0.5)  
        gen_optimizer = Adam(0.0002, 0.5) 

        # 2値分類のための識別モデルを作成
        self.discriminator = self.build_discriminator()
        self.discriminator.compile(loss='binary_crossentropy',
            optimizer=dis_optimizer,
            metrics=['accuracy'])

        # Build the generator
        
        self.generator = self.build_generator()
        plot_model(
          self.generator,
          show_shapes=True,
        )
        z = Input(shape = self.latent_dim)
        img = self.generator(z)

        # discriminatorのパラメータ固定
        self.discriminator.trainable = False
        valid = self.discriminator(img)

        self.combined = Model(z, valid)#並列結合
        self.combined.compile(loss='binary_crossentropy', optimizer=gen_optimizer)

        self.value=0

    def build_generator(self):
        k=0.5 #成長率(笑)
        self.filter=256

        # ノイズ
        input = Input(shape=self.latent_dim)

        # 排出画像 128 * 128 * 3
        x = Dense(self.filter * 32 * 32)(input)
        x = Reshape((32 , 32 , self.filter))(x)
        
        x = Conv2D(self.filter,(3,3),padding="same")(x)
        x = BatchNormalization(momentum=0.8)(x)
        x = UpSampling2D((2,2))(x) # 32x32 → 64x64
        self.filter*=k
        x = Conv2D(self.filter,(3,3),padding="same")(x)
      
        x = self.resblock(x)
        
        x = Activation("relu")(x)
        x = BatchNormalization(momentum=0.8)(x)
        x = UpSampling2D((2,2))(x) # 64x64 → 128x128
        self.filter*=k
        x = Conv2D(self.filter,(3,3),padding="same")(x)

        x = self.resblock(x)

        x = Activation("relu")(x)
        x = BatchNormalization()(x)
        x = Conv2D(self.filter , kernel_size=3, padding="same")(x)
        x = Activation("relu")(x)
        x = BatchNormalization()(x)
        x = Conv2D(self.input_channels, kernel_size=3, padding="same")(x) # 128x128x3になる

        output = Activation('tanh')(x) #-1.0 ～ 1.0
        model = Model(input,output)

        model.summary()
        return model

    def resblock(self,x):
        x2 = Conv2D(self.filter,(3,3),padding="same")(x)
        x2 = Activation("relu")(x2)
        x2 = BatchNormalization(momentum=0.8)(x2)

        x = Conv2D(self.filter,(1,1))(x)
        x = Add()([x2,x])
        return x

    def build_discriminator(self):

        model = Sequential()
        
        model.add(Conv2D(256, kernel_size=3, strides=2, input_shape=self.input_img_shape, padding="same"))
        model.add(LeakyReLU(alpha=0.2))
        model.add(Dropout(0.25))
        
        model.add(Conv2D(128, kernel_size=3, strides=2,padding="same"))
        model.add(ZeroPadding2D(padding=((0, 1), (0, 1))))
        model.add(LeakyReLU(alpha=0.2))
        model.add(Dropout(0.25))
        
        model.add(Flatten())
        model.add(Dense(1, activation='sigmoid'))

        model.summary()

        img = Input(shape=self.input_img_shape)
        validity = model(img)

        return Model(img, validity)

    def train(self, epochs, batch_size=128, save_interval=5000):
        print("実行1")
        batch_size = int(batch_size/2)

        valid = np.ones((batch_size, 1))
        fake = np.zeros((batch_size, 1))

        for epoch in range(self.value,epochs + self.value):
            

            # 本物の画像をbatchの数だけランダムで持ってきます
            idx = np.random.randint(0, len(X_train), batch_size)
            true_imgs = (X_train[idx].astype(np.float16) - 127.5) / 127.5

            # batchの数だけ平均0,分散1の正規分布のノイズを生成し、generatorに画像を生成させる
            noise = np.random.normal(0, 1, (batch_size, self.latent_dim))
            gen_imgs = self.generator.predict(noise)

            # 本物の画像とフェイク画像を識別機に学習させます。
            d_loss_real = self.discriminator.train_on_batch(true_imgs, valid)
            d_loss_fake = self.discriminator.train_on_batch(gen_imgs, fake)
            d_loss = 0.5 * np.add(d_loss_real, d_loss_fake)

            # 誤差を伝搬させて、generatorを学習させます
            g_loss = self.combined.train_on_batch(noise, valid)
            noise = np.random.normal(0, 1, (batch_size, self.latent_dim))

            # エポック数表示
            print ("%d [D loss: %f, acc.: %.2f%%] [G loss: %f]" % (epoch, d_loss[0], 100*d_loss[1], g_loss))

            if epoch % 50 == 0:
                showjpg = np.array((gen_imgs[0] * 0.5 + 0.5) * 255, np.int32) #0~255(人間に見やすく)
                cv2.imwrite(f'/content/drive/MyDrive/GANstudy/animegenerate/resimages9/image_{str(epoch)}.jpg', showjpg)

            # モデルと学習結果を保存する
            if epoch % save_interval == save_interval-1:
                print("save_model")
                save_path = "/content/drive/MyDrive/GANstudy/animegenerate/resmodels9" 
                model_json_str = self.combined.to_json()
                open(save_path + f'/combined{epoch+1}.json', 'w').write(model_json_str)
                self.combined.save_weights(save_path + f'/combined{epoch+1}_weights.h5')

                save_path = "/content/drive/MyDrive/GANstudy/animegenerate/resmodels9" 
                model_json_str = self.generator.to_json()
                open(save_path + f'/generator{epoch+1}.json', 'w').write(model_json_str)
                self.generator.save_weights(save_path + f'/generator{epoch+1}_weights.h5')

                save_path = "/content/drive/MyDrive/GANstudy/animegenerate/resmodels9" 
                model_json_str = self.discriminator.to_json()
                open(save_path + f'/discriminator{epoch+1}.json', 'w').write(model_json_str)
                self.discriminator.save_weights(save_path + f'/discriminator{epoch+1}_weights.h5')


if __name__ == '__main__':
    dcgan = DCGAN()
    dcgan.train(epochs=1000000, batch_size=32, save_interval=5000)
```
####各関数の説明
**init**
パラメータの調整をする
generatorとdiscriminatorのモデルを生成

**build_generator**
劣化版resnetを用いたモデルを生成する

**build_discriminator**
discriminatorのモデルを生成する

**train**
実行

#工夫
- Generatorにはreluを、DiscriminatorにはLeakyreluを用いること
- generatorに与えるノイズには正規分布を用いる。generatorの表現力が上がるらしい。

```python
# batchの数だけ平均0,分散1の正規分布のノイズを生成し、generatorに画像を生成させる
            noise = np.random.normal(0, 1, (batch_size, self.latent_dim))
            gen_imgs = self.generator.predict(noise)
```
- モデルを定期的に保存、とりあえず保存しとけばいろいろできる。

```python
if epoch % save_interval == save_interval-1:
    print("save_model")
    save_path = "/content/drive/MyDrive/GANstudy/animegenerate/resmodels9" 
    model_json_str = self.combined.to_json()
    open(save_path + f'/combined{epoch+1}.json', 'w').write(model_json_str)
    self.combined.save_weights(save_path + f'/combined{epoch+1}_weights.h5')
    #下に続く...
```

#結果
学習時のデータを適当にとってきた
左から順に250, 2000, 10000, 20000, 170000 epochとなっている
![image_250.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1128128/cb233b53-342a-f586-7e76-19ca268f8793.jpeg)![image_2000.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1128128/fcf274b2-dfc6-aec3-fedc-f8dd9c15bdf6.jpeg)![image_8500.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1128128/71f93bef-bbf4-b409-695b-3855ffc2b35b.jpeg)![image_18750.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1128128/72845b69-ac08-d67c-a0bd-b7d9cf0207c9.jpeg)![image_172000.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1128128/f527c385-49fd-73a1-011f-955c4209e296.jpeg)

epochが増えるにつれて高精度な画像が生成されて行っているのが見て取れる。
ただ、5万epochを超えたころから目が3つになる現象が多発しはじめる。
そこから進化はせず、学習終了ということになった。

##何故こうなったのか
結論から言うと、シンプルなDCGANでは様々な問題によって、128x128の画像を作り出すことはできないみたいです。先人がこれまで挑戦してきたらしいですが、64x64が限界なんだとか。人にギリギリ見えるレベルの画像をつくれただけだいぶ奮闘できているらしい。
原因は、**Generator側の力不足**
DCGANの学習には、GeneratorとDiscriminatorのパワーバランスを保つ必要があります。二つのネットワークは**だましだまされを繰り返して強くなる**ので、片方が強すぎると学習が進まなくなります。
基本的にはDiscriminatorのほうが強くなります。そのため、Discriminatorは**手加減**してあげないといけません。
ですが、Generatorは強化しすぎると学習速度が遅くなりすぎるため、シンプルなGANを強くするすべはないでしょう。

#劣化版resnetについての解説
resnetについてよくわからない人は飛ばしてください。
GANの学習において重要なのは、Generatorの精度と成長速度です。
シンプルなresnetは学習速度が遅すぎます。かといってresnetを使用しないモデルは精度が低すぎます。そこで、私が提案したのが劣化版resnetというわけです。

```python
def resblock(self,x):
        x2 = Conv2D(self.filter,(3,3),padding="same")(x)
        x2 = Activation("relu")(x2)
        x2 = BatchNormalization(momentum=0.8)(x2)

        x = Conv2D(self.filter,(1,1))(x)
        x = Add()([x2,x])
        return x
```
速度、精度を天秤にかけた結果、このような形になりました。

#まとめ
**シンプルなDCGAN**には限界があります。
ですが、一度128x128xRGBの画像生成に挑戦してみるのはいいかもしれません。
勉強にはなります。また、なぜ無理なのかも感覚で分かってきます。
MNIST手書き文字で満足せず、本当のGANに挑戦してほしいと願っています。
ちなみに、シンプルなDCGANには限界がありますが、**PGGAN**なら**1024x1024**の画像生成も可能ですよ。ぜひ調べてみてください。