DCGANやってみた

年末にパソコンを自作してグラボが使えるようになったのでCPUだけだときつそうだったGANを試してみることにした。
丁寧に解説してるサイトはいくらでもあるので適当に説明する。


パクった参考にしたサイト
elix-tech.github.io

GAN(Generative Adversarial Network)

生成器(generator)と識別器(discriminator)の二つのモデルを同時に学習させて、お互いがお互いに勝てるように競争してくイメージらしい。ガンとギャンどっちなんでしょうか。
DCGAN(Deep Convolutional GAN)はその名の通りCNNを取り入れたGANのことで、様々なテクニックで学習がうまくいくように工夫している。

論文より
例えば一般的にCNNで使われるプーリングの代わりにDCGANのdiscriminatorでは代わりにストライド2の畳み込み使ったり、batch normalization使ったり、Leaky ReLU使ったり。これらの工夫は一長一短らしいからちゃんと論文読まないと。

ソースコード

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

# 生成モデル
def generator_model():
    model = Sequential()
    # 入力は100次元のノイズ
    model.add(Dense(1024,input_dim=100))
    model.add(BatchNormalization())
    model.add(Activation('relu'))
    # あとで(128,7,7)にreshapeするため
    model.add(Dense(128*7*7))
    model.add(BatchNormalization())
    model.add(Activation('relu'))
    model.add(Reshape((128,7,7),input_shape=(128*7*7,)))
    # UpSamplingで画像を2倍に拡大
    model.add(UpSampling2D((2,2)))
    model.add(Conv2D(64,(5,5),padding='same'))
    model.add(BatchNormalization())
    model.add(Activation('relu'))
    # UpSamplingで画像を2倍に拡大
    model.add(UpSampling2D((2,2)))
    model.add(Conv2D(1,(5,5),padding='same'))
    model.add(Activation('tanh'))
    # 二回UpSamplingを行うことにより最終的に28*28の画像になる
    return model

from keras.layers.advanced_activations import LeakyReLU
from keras.layers import Flatten,Dropout

# 識別モデルCNN)
def discriminator_model():
    model = Sequential()
    # プーリングの代わりにストライド2の畳み込みを行う
    model.add(Conv2D(
        64,(5,5),strides=(2,2),padding='same',input_shape=(1,28,28)
    ))
    # 活性化関数はLeakyReLUを使用
    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

import math
import numpy as np 

# 生成画像表示用の関数
def combine_images(generated_images):
    total = generated_images.shape[0]
    cols = int(math.sqrt(total))
    rows = math.ceil(float(total)/cols)
    width,height = generated_images.shape[2:]
    combined_image = np.zeros((height*rows,width*cols),dtype=generated_images.dtype)
    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[0,:,:]
    return combined_image

import os
from keras.datasets import mnist
from keras.optimizers import Adam
from PIL import Image

BATCH_SIZE = 32
NUM_EPOCH = 20
GENERATED_IMAGE_PATH = 'keras_dcgan_generated_images/'

def train():
    # 訓練データのみ必要になる
    (X_train, _), (_, _) = mnist.load_data()
    # -1~1の範囲にする
    X_train = (X_train.astype(np.float32) - 127.5)/127.5
    # おそらくRGBとかなら第二引数は3になる
    X_train = X_train.reshape(X_train.shape[0], 1, X_train.shape[1], X_train.shape[2])

    discriminator = discriminator_model()
    d_opt = Adam(lr=1e-5, beta_1=0.1)
    discriminator.compile(loss='binary_crossentropy', optimizer=d_opt)
    # generatorの学習時はdiscriminatorの学習は行わない
    discriminator.trainable = False
    generator = generator_model()
    # 生成モデルの訓練はdiscriminatorも用いて行う
    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)

            if index % 500 == 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))

            X = np.concatenate((image_batch, generated_images))
            y = [1]*BATCH_SIZE + [0]*BATCH_SIZE
            d_loss = discriminator.train_on_batch(X, y)

            noise = np.array([np.random.uniform(-1, 1, 100) for _ in range(BATCH_SIZE)])
            # generatorの学習時にはラベルはすべて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')

if __name__ == "__main__":
    train()

ほとんど写経でConvolution2DをConv2Dにしたりコメントをつけ足したりした。discriminatorへの入力を-1から1にしたのはLeakyReLUの特性を生かすためなのかな。

f:id:busongames:20190116231434p:plain f:id:busongames:20190116231501p:plain f:id:busongames:20190116232212p:plain
左から0エポック、1エポック、20エポック
学習終了時にはしっかり数字画像が生成できている。

おまけでfashion-mnistとひらがなデータセットを使ってやってみた。fashion-mnistはTシャツのみを取り出し、ひらがなの方はグレースケールに変換したりしてから学習を始めた。
f:id:busongames:20190117003944p:plain f:id:busongames:20190117003958p:plain
Tシャツの方は結構わかりやすい。ひらがなは50文字以上もあるから難しいかな。