LoginSignup
11

More than 5 years have passed since last update.

DCGANで手書き文字を生成するチュートリアル(メモ)

Posted at

tensorflow->チュートリアル->Generative modelsのDCGANをやったので要約。

tf.kerasとeager executionを使って、google colabで実行している。
理論的なことにはほとんど触れない。

元記事はTensorFlow Authorsらによる。

Copyright 2018 The TensorFlow Authors.
Licensed under the Apache License, Version 2.0 (the "License").

GANとは

生成モデルを作るフレームワーク。
データ生成するGenerator(以下G)と、訓練データまたは生成されたデータから、データが来た確率を推定するDiscriminator(以下D)を敵対的に同時訓練する。Dが本物か偽物か識別できなくなるまで、Gは少しずつデータ生成がうまくなる。

ここではMNISTでこのプロセスを示す。
50epoch訓練したGの生成した画像を時系列で示す。時間が経つごとに訓練画像と見分けがつかなくなる。

GANについてはMITのIntro to Deep Learningコースをおすすめする。ディープ生成モデルについてもレクチャーがある。(スライドもリンクしてあったがリンク切れ)

#imageioは訓練のgif画像のため 
!pip install imageio

import Tensorflow, eager executionの有効化

import tensorflow as tf
tf.enable_eager_execution()

import glob
import imageio
import matplotlib.pyplot as plt
import numpy as np
import os
import PIL
import time

from IPython import display

データセットロード

GとDはMNISTで訓練する。GはMNISTに似た画像を生成する。


(train_images, train_labels), (_, _) = tf.keras.datasets.mnist.load_data()

train_images = train_images.reshape(train_images.shape[0], 28, 28, 1).astype('float32')
train_images = (train_images - 127.5) / 127.5 # 画像は正規化して 値を[-1, 1]に収める

BUFFER_SIZE = 60000
BATCH_SIZE = 256

バッチの作成、データセットのシャッフルににtf.dataを使う

train_dataset = tf.data.Dataset.from_tensor_slices(train_images).shuffle(BUFFER_SIZE).batch(BATCH_SIZE)

モデルを作る

tf.kerasSequential APIを使う。

Generator

GはDを騙せるような画像を作る。アーキテクチャはConv2DTranspose(Upsampling)レイヤを使う。
ほしい画像サイズである28x28x1にするために全結合からスタートして2回upsampleする。幅・高さは増加させ、奥行きは減少させていく。活性化関数にLeaky ReLUを使うが、最後のみtanhにする。

def make_generator_model():
    model = tf.keras.Sequential()
    model.add(tf.keras.layers.Dense(7*7*256, use_bias=False, input_shape=(100,)))
    model.add(tf.keras.layers.BatchNormalization())
    model.add(tf.keras.layers.LeakyReLU())

    model.add(tf.keras.layers.Reshape((7, 7, 256)))
    assert model.output_shape == (None, 7, 7, 256) # Note: None は(指定しない意味で、自動的に)batch sizeの値になる

    model.add(tf.keras.layers.Conv2DTranspose(128, (5, 5), strides=(1, 1), padding='same', use_bias=False))
    assert model.output_shape == (None, 7, 7, 128)  
    model.add(tf.keras.layers.BatchNormalization())
    model.add(tf.keras.layers.LeakyReLU())

    model.add(tf.keras.layers.Conv2DTranspose(64, (5, 5), strides=(2, 2), padding='same', use_bias=False))
    assert model.output_shape == (None, 14, 14, 64)    
    model.add(tf.keras.layers.BatchNormalization())
    model.add(tf.keras.layers.LeakyReLU())

    model.add(tf.keras.layers.Conv2DTranspose(1, (5, 5), strides=(2, 2), padding='same', use_bias=False, activation='tanh'))
    assert model.output_shape == (None, 28, 28, 1)

    return model

Discriminator

画像が本物かどうか判別する。通常のCNNベース分類器に似ている。


def make_discriminator_model():
    model = tf.keras.Sequential()
    #出力チャネル数、カーネルサイズ、ストライド、パディング
    model.add(tf.keras.layers.Conv2D(64, (5, 5), strides=(2, 2), padding='same'))
    model.add(tf.keras.layers.LeakyReLU())
    model.add(tf.keras.layers.Dropout(0.3))

    model.add(tf.keras.layers.Conv2D(128, (5, 5), strides=(2, 2), padding='same'))
    model.add(tf.keras.layers.LeakyReLU())
    model.add(tf.keras.layers.Dropout(0.3))

    model.add(tf.keras.layers.Flatten())
    model.add(tf.keras.layers.Dense(1))

    return model

generator = make_generator_model()
discriminator = make_discriminator_model()

誤差関数とoptimizerの定義

Generator

Gの誤差関数は生成画像とarray of onesのシグモイドクロスエントロピー。
tf.ones_like()は引数のテンソルと同じshape, typeで中身が全て1のテンソルを返す。


def generator_loss(generated_output):
    return tf.losses.sigmoid_cross_entropy(tf.ones_like(generated_output), generated_output)

Discriminator

Dの誤差関数は2つ引数を取る。一つは本物画像、もう一つは生成画像。計算は次の通り。

  1. 真の画像とarray of onesとのシグモイドクロスエントロピーを計算する。これはreal_lossと呼ぶ。
  2. 生成画像とarray of zerosのシグモイドクロスエントロピーを計算する。これはgenerated_lossと呼ぶ。
  3. generated_lossreal_lossの和であるtotal_lossを計算する。
# 引数のreal_output, generated_outputはいずれもdiscriminatorを通した後の値。
# generatorの生成したoutputではない。一瞬びっくりした。
def discriminator_loss(real_output, generated_output):
    # [1,1,...,1] with real output since it is true and we want our generated examples to look like it
    real_loss = tf.losses.sigmoid_cross_entropy(multi_class_labels=tf.ones_like(real_output), logits=real_output)

    # [0,0,...,0] with generated images since they are fake
    generated_loss = tf.losses.sigmoid_cross_entropy(multi_class_labels=tf.zeros_like(generated_output), logits=generated_output)

    total_loss = real_loss + generated_loss

    return total_loss

2つのネットワークを別々に訓練するのでGとDのオプティマイザは別物。

generator_optimizer = tf.train.AdamOptimizer(1e-4)
discriminator_optimizer = tf.train.AdamOptimizer(1e-4)

Checkpoints (Object-based saving)
こちらによるとObject-based savingは従来のname based(変数名だけでcheckpointに保存される値を管理する)とは異なり、オブジェクトのdependency graphをcheckpointに一緒に保存し、プログラムの変更や複数checkpointの使用にロバストになるらしい。

checkpoint_dir = './training_checkpoints'
checkpoint_prefix = os.path.join(checkpoint_dir, "ckpt")
checkpoint = tf.train.Checkpoint(generator_optimizer=generator_optimizer,
                                 discriminator_optimizer=discriminator_optimizer,
                                 generator=generator,
                                 discriminator=discriminator)

訓練の設定

パラメータの定義

EPOCHS = 50
noise_dim = 100
num_examples_to_generate = 16

#訓練の経過を見るための固定入力ランダムベクトル
random_vector_for_generation = tf.random_normal([num_examples_to_generate,
                                                 noise_dim])

訓練方法

Gにはランダムベクトルを入力する。MNISTっぽくなるようにする。DはMNISTの本物と、Gの生成画像を入力。


def train_step(images):
   # 正規分布に従う入力ベクトルを作る
      noise = tf.random_normal([BATCH_SIZE, noise_dim])

      with tf.GradientTape() as gen_tape, tf.GradientTape() as disc_tape:
        generated_images = generator(noise, training=True)

        real_output = discriminator(images, training=True)
        generated_output = discriminator(generated_images, training=True)

        gen_loss = generator_loss(generated_output)
        disc_loss = discriminator_loss(real_output, generated_output)

      gradients_of_generator = gen_tape.gradient(gen_loss, generator.variables)
      gradients_of_discriminator = disc_tape.gradient(disc_loss, discriminator.variables)

      generator_optimizer.apply_gradients(zip(gradients_of_generator, generator.variables))
      discriminator_optimizer.apply_gradients(zip(gradients_of_discriminator, discriminator.variables))

ColabのTesla K80 1台で1epochあたり30秒だったらしい。(2018/10時点で)

Eager executionはグラフ定義に比べて最適化できないので遅くなる場合がある。tf.contrib.eager.defunでグラフにコンパイルすると最大20秒/epochくらい速くなる、とのこと。

train_step = tf.contrib.eager.defun(train_step)
def train(dataset, epochs):  
  for epoch in range(epochs):
    start = time.time()

    for images in dataset:
      train_step(images)

    display.clear_output(wait=True)
    generate_and_save_images(generator,
                               epoch + 1,
                               random_vector_for_generation)

    # saving (checkpoint) the model every 15 epochs
    if (epoch + 1) % 15 == 0:
      checkpoint.save(file_prefix = checkpoint_prefix)

    print ('Time taken for epoch {} is {} sec'.format(epoch + 1,
                                                      time.time()-start))
  # generating after the final epoch
  display.clear_output(wait=True)
  generate_and_save_images(generator,
                           epochs,
                           random_vector_for_generation)

Generate and save images関数

def generate_and_save_images(model, epoch, test_input):
  # 推定時にbatchnormは訓練しないので、Falseにする
  predictions = model(test_input, training=False)

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

  for i in range(predictions.shape[0]):
      plt.subplot(4, 4, i+1)
      plt.imshow(predictions[i, :, :, 0] * 127.5 + 127.5, cmap='gray')
      plt.axis('off')

  plt.savefig('image_at_epoch_{:04d}.png'.format(epoch))
  plt.show()

訓練する

上で定義したtrain()関数で開始。
GANの訓練は扱いづらい。GとDが互いに拮抗するようにして、どちらかが強くなりすぎないように。
訓練の最初は生成イメージがノイズっぽいが、訓練が進むとどんどんリアルになる。50epochぐらいでMNISTの数字っぽくなる。

%%time
train(train_dataset, EPOCHS)

%%timeはjupyterでセルの計測時間を測るセルマジック

最新のcheckpointをリストアする

checkpoint.restore(tf.train.latest_checkpoint(checkpoint_dir))

生成画像

訓練が終わったら画像生成しよう!plotして完成だ!

# 単体画像
def display_image(epoch_no):
  return PIL.Image.open('image_at_epoch_{:04d}.png'.format(epoch_no))
display_image(EPOCHS)

保存された画像全部のGIFを生成する

訓練中に保存された全ての画像を使ってアニメーションGIFを作る。imageioを使う。

with imageio.get_writer('dcgan.gif', mode='I') as writer:
  filenames = glob.glob('image*.png')
  filenames = sorted(filenames)
  last = -1
  for i,filename in enumerate(filenames):
    frame = 2*(i**0.5)
    if round(frame) > round(last):
      last = frame
    else:
      continue
    image = imageio.imread(filename)
    writer.append_data(image)
  image = imageio.imread(filename)
  writer.append_data(image)

# this is a hack to display the gif inside the notebook
os.system('cp dcgan.gif dcgan.gif.png')

表示

display.Image(filename="dcgan.gif.png")

アニメーションGIFをダウンロードする

from google.colab import files
files.download('dcgan.gif')

GANについて学ぶ

次のステップとして別のデータセットで実験したいかもしれない。例えば Large-scale Celeb Faces Attributes (CelebA)

GANについては、

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
11