CS231n/課題3/敵対的生成ネットワーク(PyTorch)

今回は、Stanford University/CS231n/Assignment3/Generative Adversarial Networks(GANs/敵対的生成ネットワーク)パイトーチ編をやる。

今までCS231Nで探究してきたニューラルネットワークの全アプリは、入力を受け取ってラベル付きの出力を生成するように訓練される識別モデルだった。これは、単純な画像カテゴリー分類から文生成(未だ分類問題として表現され、我々のラベルは語彙空間にあり、我々は複数単語ラベルをキャプチャーするためのリカレンスを学んだ。)まで多岐にわたる。このノートブックでは、レパートリーを広げてニューラルネットワークを用いて生成モデルを構築する。特に、一連の訓練画像に似た新たな画像を生成するモデルの構築法を学習する。

What is a GAN?

2014年、Goodfellow et alは、Generative Adversarial Networks(GANs/敵対的生成ネットワー)と呼ばれる生成モデルを訓練するための手法をプレゼンした。GANで、2種類のニューラルネットワークを構築する。最初のネットワークは、識別器(discriminator)と呼ばれる従来型の分類ネットワークだ。この識別器に画像を入力して、リアル(訓練セットに属する)もしくはフェイク(訓練セットに存在しない)であるかを分類させて訓練する。生成器(generator)と呼ばれるもう一方のネットワークは、入力として不規則ノイズを受け、それをニューラルネットを用いて変換して画像を生成する。生成器の目的は、それが生成した画像がリアルであると識別器を欺くことである。

生成器($G$)が識別器($D$)を欺こうとし、識別器は正確に本物と偽物を分類しようとするこのやりとりを以下のようなミニマックスゲームとして考えることができる。
$$\underset{G}{\text{minimize}}\; \underset{D}{\text{maximize}}\; \mathbb{E}_{x \sim p_\text{data}}\left[\log D(x)\right] + \mathbb{E}_{z \sim p(z)}\left[\log \left(1-D(G(z))\right)\right]$$ $x \sim p_\text{data}$が入力データのサンプル、$z \sim p(z)$が不規則ノイズサンプル、$G(z)$がニューラルネットワーク生成器$G$を使って生成された画像、$D$が識別器の出力で入力がリアルである確率を特定する。Goodfellow等は、このミニマックスゲームを解析し、それがいかにして、訓練データ分布と$G$の生成サンプル間のJensen-Shannon divergence(ジェンセン・シャノン・ダイバージェンス)を極小化することと関連しているのかを明らかにしている。

このミニマックスゲームを最適化するために、$G$の目的への勾配降下ステップと$D$の目的への勾配上昇ステップを繰り返す。

  1. 識別器が正しい選択をする確率を最小化するのに生成器($G$)を更新する。
  2. 識別器が正しい選択をする確率を最大化するのに識別器($D$)を更新する。

こういった更新は分析には有効である一方で、実際には上手く機能しない。その代わりとして、生成器を更新する際に別の目的を使用して、識別機が誤った選択をする確率を最大化する。この僅かな修正が、識別機の信頼度が高い時に生成器勾配が消失する問題を軽減するのに役立つ。これは、ほとんどのGAN研究論文で使われている標準的なアップデートで、グッドフェロー等による原論文の中でも使われていた。

この宿題では、以下のアプデを交互に行う。

  1. 識別器が生成画像に対して不正確な選択をする確率を最大化するように生成器($G$)をアプデする。$$\underset{G}{\text{maximize}}\; \mathbb{E}_{z \sim p(z)}\left[\log D(G(z))\right]$$
  2. 識別器が真・偽画像に対して正しい選択をする確率を最大化するように識別器($D$)をアプデする。$$\underset{D}{\text{maximize}}\; \mathbb{E}_{x \sim p_\text{data}}\left[\log D(x)\right] + \mathbb{E}_{z \sim p(z)}\left[\log \left(1-D(G(z))\right)\right]$$

What else is there?

2014年以降、GANsは多くの大規模研修会や何百もの新論文により、大人気の研究分野へと急速に発展している。生成モデルへの他のアプローチと比べ、それらは頻繁に最上級のサンプルを作り出すが、訓練するのが最も困難で細心の注意が必要なモデルになっている(モデルを機能させるのに役立つ17のハック集を含むこのgithub repoを参照のこと)。GAN訓練の安定性と堅牢性を向上することはオープンな研究課題になっており、毎日のように新たな研究論文が発表されている。GANの最新のチュートリアルに関してはここを参照。目的関数(objective function)をWasserstein distanc(ワッサースタイン距離)に変えてモデルアーキテクチャを越えてはるかに安定した結果をもたらす、いくつかのもっと最近の興味深い研究も存在する(WGAN, WGAN-GP)。

GANだけが生成モデルを訓練する方法ではない。生成モデリングへの他のアプローチに関しては、深層学習本深層生成モデルの章を参照。生成モデルとしてニューラルネットワークを訓練するもう一つの人気の方法が変分オートエンコーダだ(Variational Autoencoders(VAE)はここここで共発見された)。変分自己符号化器(VAE)は、深層生成モデルを訓練するために、ニューラルネットワークをvariational inference(変分推論)と組み合わせている。このようなモデルは、はるかに安定していて訓練しやすい傾向があるが、今のところはGANと同等の綺麗なサンプルは生成していない。

期待される画像のいくつかの見本(若干違うかもしれない)

alt text

Setup

import torch
import torch.nn as nn
from torch.nn import init
from torch.autograd import Variable
import torchvision
import torchvision.transforms as T
import torch.optim as optim
from torch.utils.data import DataLoader
from torch.utils.data import sampler
import torchvision.datasets as dset
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.gridspec as gridspec

plt.rcParams['figure.figsize'] = (18, 12) # set default size of plots
plt.rcParams["font.size"] = "16"
plt.rcParams['image.interpolation'] = 'nearest'
plt.rcParams['image.cmap'] = 'gray'

def show_images(images):
    images = np.reshape(images, [images.shape[0], -1])  # images reshape to (batch_size, D)
    sqrtn = int(np.ceil(np.sqrt(images.shape[0])))
    sqrtimg = int(np.ceil(np.sqrt(images.shape[1])))

    fig = plt.figure(figsize=(sqrtn*2, sqrtn*2))
    gs = gridspec.GridSpec(sqrtn, sqrtn)
    gs.update(wspace=0.05, hspace=0.05)

    for i, img in enumerate(images):
        ax = plt.subplot(gs[i])
        plt.axis('off')
        ax.set_xticklabels([])
        ax.set_yticklabels([])
        ax.set_aspect('equal')
        plt.imshow(img.reshape([sqrtimg,sqrtimg]))
    return 

def preprocess_img(x):
    return 2 * x - 1.0

def deprocess_img(x):
    return (x + 1.0) / 2.0

def rel_error(x,y):
    return np.max(np.abs(x - y) / (np.maximum(1e-8, np.abs(x) + np.abs(y))))

def count_params(model):
    """Count the number of parameters in the current TensorFlow graph """
    param_count = np.sum([np.prod(p.size()) for p in model.parameters()])
    return param_count

answers = np.load('gan-checks-tf.npz')

%matplotlib inline

Dataset

GANでハイパーパラメータを取り扱うのは非常に難しく、多くの訓練エポックも必要となる。今回のエクササイズをGPU無しで取っ付きやすくするために、6万訓練画像と1万テスト画像を持つMNISTデータセットを使って作業する。各画像は黒い背景に白い(0から9の)数字が中心にある画像を含んでいる。これは、畳み込みニューラルネットワークを訓練するのに使われる最初のデータセットの一つで非常に簡単であるため、標準的なCNNモデルなら余裕で正確度99%を超える。

コードを簡素化する目的で、MNISTデータセットをダウンロードしてロードしてくれるPyTorch MNIST wrapperを使用する。このインターフェースに関するさらなる情報はドキュメンテーションを参照されたし。デフォルトパラメーターは訓練標本のうち5000を取得し、それらを検証データセットに据える。データはMNISTデータと呼ばれるフォルダーに保存される。

class ChunkSampler(sampler.Sampler):
    """Samples elements sequentially from some offset. 
    Arguments:
        num_samples: # of desired datapoints
        start: offset where we should start selecting from
    """
    def __init__(self, num_samples, start=0):
        self.num_samples = num_samples
        self.start = start

    def __iter__(self):
        return iter(range(self.start, self.start + self.num_samples))

    def __len__(self):
        return self.num_samples

NUM_TRAIN = 50000
NUM_VAL = 5000

NOISE_DIM = 96
batch_size = 128

mnist_train = dset.MNIST('./cs231n/datasets/MNIST_data', train=True, download=True,
                           transform=T.ToTensor())
loader_train = DataLoader(mnist_train, batch_size=batch_size,
                          sampler=ChunkSampler(NUM_TRAIN, 0))

mnist_val = dset.MNIST('./cs231n/datasets/MNIST_data', train=True, download=True,
                           transform=T.ToTensor())
loader_val = DataLoader(mnist_val, batch_size=batch_size,
                        sampler=ChunkSampler(NUM_VAL, NUM_TRAIN))


imgs = loader_train.__iter__().next()[0].view(batch_size, 784).numpy().squeeze()
show_images(imgs)
Downloading http://yann.lecun.com/exdb/mnist/train-images-idx3-ubyte.gz
Downloading http://yann.lecun.com/exdb/mnist/train-labels-idx1-ubyte.gz
Downloading http://yann.lecun.com/exdb/mnist/t10k-images-idx3-ubyte.gz
Downloading http://yann.lecun.com/exdb/mnist/t10k-labels-idx1-ubyte.gz
Processing...
Done!

Random Noise

shape [batch_size, dim]を持つ-1~1の均一ノイズを生成する。

Hint: torch.randを使用する。

def sample_noise(batch_size, dim):
    """
    Generate a PyTorch Tensor of uniform random noise.

    Input:
    - batch_size: Integer giving the batch size of noise to generate.
    - dim: Integer giving the dimension of noise to generate.
    
    Output:
    - A PyTorch Tensor of shape (batch_size, dim) containing uniform
      random noise in the range (-1, 1).
    """
    return torch.Tensor(batch_size, dim).uniform_(-1, 1)

ノイズが正しい形と型であるかを確かめる。

def test_sample_noise():
    batch_size = 3
    dim = 4
    torch.manual_seed(231)
    z = sample_noise(batch_size, dim)
    np_z = z.cpu().numpy()
    assert np_z.shape == (batch_size, dim)
    assert torch.is_tensor(z)
    assert np.all(np_z >= -1.0) and np.all(np_z <= 1.0)
    assert np.any(np_z < 0.0) and np.any(np_z > 0.0)
    print('All tests passed!')
    
test_sample_noise()
All tests passed!

Flatten

前回のノートブックのFlatten演算を思い起こす。今回は、畳み込み生成器を実装する場合に使いたくなるかもしれないUnflattenも提供する。PyTorchの同型デフォルトの代わりにXavier initializationを使用するweight initializerを提供する(なおかつそれをコールする)。

class Flatten(nn.Module):
    def forward(self, x):
        N, C, H, W = x.size() # read in N, C, H, W
        return x.view(N, -1)  # "flatten" the C * H * W values into a single vector per image
    
class Unflatten(nn.Module):
    """
    An Unflatten module receives an input of shape (N, C*H*W) and reshapes it
    to produce an output of shape (N, C, H, W).
    """
    def __init__(self, N=-1, C=128, H=7, W=7):
        super(Unflatten, self).__init__()
        self.N = N
        self.C = C
        self.H = H
        self.W = W
    def forward(self, x):
        return x.view(self.N, self.C, self.H, self.W)

def initialize_weights(m):
    if isinstance(m, nn.Linear) or isinstance(m, nn.ConvTranspose2d):
        init.xavier_uniform(m.weight.data)

CPU / GPU

デフォルトでは、全てのコードはCPUで実行される。この宿題に関してはGPUは必要ないが、モデルの訓練を加速するのには役立つ。GPUでコードを実行したいなら、下のセルのdtype変数を変更する。

#dtype = torch.FloatTensor
dtype = torch.cuda.FloatTensor ## UNCOMMENT THIS LINE IF YOU'RE ON A GPU!

Discriminator

初めの一歩は識別器の構築。下の関数にnn.Sequential constructorの一部として仕様を書き込む。全ての全結合層はバイアス項を含んでいなければならない。

Architecture:

  • サイズが784~256の全結合層
  • LeakyReLU with alpha 0.01
  • サイズが256~256の全結合層
  • LeakyReLU with alpha 0.01
  • サイズが256~1の全結合層

Leaky ReLU nonlinearity(非線形性)が、ある固定定数$\alpha$に対する$f(x) = \max(\alpha x, x)$を算出することを思い出す。上の仕様のLeakyReLU非線形性に対しては$\alpha=0.01$を設定する。

識別器の出力はshape [batch_size, 1]であり、各batch_size入力が実画像のスコアに対応する実数を含んでいなければならない。

def discriminator():
    """
    Build and return a PyTorch model implementing the architecture above.
    """
    model = nn.Sequential(Flatten(),
                          nn.Linear(784, 256),
                          nn.LeakyReLU(negative_slope=0.01, inplace=True),
                          nn.Linear(256, 256),
                          nn.LeakyReLU(negative_slope=0.01, inplace=True),
                          nn.Linear(256, 1))
    return model

識別器のパラメーター数が正しいことを確認するためにテストする。

def test_discriminator(true_count=267009):
    model = discriminator()
    cur_count = count_params(model)
    if cur_count != true_count:
        print('Incorrect number of parameters in discriminator. Check your achitecture.')
    else:
        print('Correct number of parameters in discriminator.')     

test_discriminator()
Correct number of parameters in discriminator.

Generator

次に生成器ネットワークをビルドする。

  • Fully connected layer from noise_dim to 1024
  • ReLU
  • Fully connected layer with size 1024
  • ReLU
  • Fully connected layer with size 784
  • TanH (画像が[-1,1]になるようにクリップする)
def generator(noise_dim=NOISE_DIM):
    """
    Build and return a PyTorch model implementing the architecture above.
    """
    model = nn.Sequential(nn.Linear(noise_dim, 1024),
                          nn.ReLU(inplace=True),
                          nn.Linear(1024, 1024),
                          nn.ReLU(inplace=True),
                          nn.Linear(1024, 784),
                          nn.Tanh())
    return model

生成器のパラメーター数が正しいことを確認するためにテストする。

def test_generator(true_count=1858320):
    model = generator(4)
    cur_count = count_params(model)
    if cur_count != true_count:
        print('Incorrect number of parameters in generator. Check your achitecture.')
    else:
        print('Correct number of parameters in generator.')

test_generator()
Correct number of parameters in generator.

GAN Loss

生成器/識別器損失を算出する。生成器損失$$\ell_G = -\mathbb{E}_{z \sim p(z)}\left[\log D(G(z))\right]$$識別器損失$$ \ell_D = -\mathbb{E}_{x \sim p_\text{data}}\left[\log D(x)\right] – \mathbb{E}_{z \sim p(z)}\left[\log \left(1-D(G(z))\right)\right]$$生成器/識別器損失が最小化していくに連れて、これらは前出の式からネゲートされることに留意する。

HINTS: 識別器のlogit output(ロジット出力)に与えられる正解ラベルのlog probability(対数確率)算出に必要なbinary cross entropy lossを算出するのに下で定義されているbce_loss関数を使うべき。スコアを$s\in\mathbb{R}$、ラベルを$y\in\{0, 1\}$と仮定すると、バイナリ交差エントロピー損失は$$ bce(s, y) = y * \log(s) + (1 – y) * \log(1 – s) $$

この公式のnaive implementation(ナイーブ実装)は、数値的に不安定になる可能性があるので、下に数値的に安定した実装を用意してある。

真・偽に対応するラベルを算出し、それらのサイズを割り出すのにlogit arguments(ロジット引数)を使用する必要がある。global dtype variable(グローバルdtype変数)を使用して正しいデータ型にこれらのラベルを確実にキャストする。例えば、
true_labels = Variable(torch.ones(size)).type(dtype)

期待値を算出する代わりにミニバッチの要素を平均するので、合計するのではなく平均することで損失を組み合わせることを忘れないこと。

def bce_loss(input, target):
    """
    Numerically stable version of the binary cross-entropy loss function.

    As per https://github.com/pytorch/pytorch/issues/751
    See the TensorFlow docs for a derivation of this formula:
    https://www.tensorflow.org/api_docs/python/tf/nn/sigmoid_cross_entropy_with_logits

    Inputs:
    - input: PyTorch Variable of shape (N, ) giving scores.
    - target: PyTorch Variable of shape (N,) containing 0 and 1 giving targets.

    Returns:
    - A PyTorch Variable containing the mean BCE loss over the minibatch of input data.
    """
    neg_abs = - input.abs()
    loss = input.clamp(min=0) - input * target + (1 + neg_abs.exp()).log()
    return loss.mean()
def discriminator_loss(logits_real, logits_fake):
    """
    Computes the discriminator loss described above.
    
    Inputs:
    - logits_real: PyTorch Variable of shape (N,) giving scores for the real data.
    - logits_fake: PyTorch Variable of shape (N,) giving scores for the fake data.
    
    Returns:
    - loss: PyTorch Variable containing (scalar) the loss for the discriminator.
    """
    N, _ = logits_real.size()
    loss = (bce_loss(logits_real, Variable(torch.ones(N)).type(dtype)) +
            bce_loss(logits_fake, Variable(torch.zeros(N)).type(dtype)))
    return loss

def generator_loss(logits_fake):
    """
    Computes the generator loss described above.

    Inputs:
    - logits_fake: PyTorch Variable of shape (N,) giving scores for the fake data.
    
    Returns:
    - loss: PyTorch Variable containing the (scalar) loss for the generator.
    """
    N, _ = logits_fake.size()
    loss = bce_loss(logits_fake, Variable(torch.ones(N)).type(dtype))
    return loss

生成器/識別器損失をテストする。エラーは1e-7未満になる必要がある。

def test_discriminator_loss(logits_real, logits_fake, d_loss_true):
    d_loss = discriminator_loss(Variable(torch.Tensor(logits_real)).type(dtype),
                                Variable(torch.Tensor(logits_fake)).type(dtype)).data.cpu().numpy()
    print("Maximum error in d_loss: %g"%rel_error(d_loss_true, d_loss))

test_discriminator_loss(answers['logits_real'], answers['logits_fake'],
                        answers['d_loss_true'])
Maximum error in d_loss: 3.97058e-09
def test_generator_loss(logits_fake, g_loss_true):
    g_loss = generator_loss(Variable(torch.Tensor(logits_fake)).type(dtype)).data.cpu().numpy()
    print("Maximum error in g_loss: %g"%rel_error(g_loss_true, g_loss))

test_generator_loss(answers['logits_fake'], answers['g_loss_true'])
Maximum error in g_loss: 3.4188e-08

Optimizing our loss

学習率=1e-3、beta1=0.5、beta2=0.999の所与のモデルに対してoptim.Adam optimizerを返す関数を作る。今後このノートブックで使われる生成器と識別器用オプティマイザを構築するのにこの関数を使用することになる。

def get_optimizer(model):
    """
    Construct and return an Adam optimizer for the model with learning rate 1e-3,
    beta1=0.5, and beta2=0.999.
    
    Input:
    - model: A PyTorch model that we want to optimize.
    
    Returns:
    - An Adam optimizer for the model with the desired hyperparameters.
    """
    optimizer = optim.Adam(model.parameters(), lr=1e-3, betas=(0., 0.999))
    return optimizer

Training a GAN!

main training loopはこっちで用意してある。この関数を変える必要はないが、それに一通りじっくり目を通して完璧に理解することを強く奨励する。

def run_a_gan(D, G, D_solver, G_solver, discriminator_loss, generator_loss, show_every=250, 
              batch_size=128, noise_size=96, num_epochs=10):
    """
    Train a GAN!
    
    Inputs:
    - D, G: PyTorch models for the discriminator and generator
    - D_solver, G_solver: torch.optim Optimizers to use for training the
      discriminator and generator.
    - discriminator_loss, generator_loss: Functions to use for computing the generator and
      discriminator loss, respectively.
    - show_every: Show samples after every show_every iterations.
    - batch_size: Batch size to use for training.
    - noise_size: Dimension of the noise to use as input to the generator.
    - num_epochs: Number of epochs over the training dataset to use for training.
    """
    iter_count = 0
    for epoch in range(num_epochs):
        for x, _ in loader_train:
            if len(x) != batch_size:
                continue
            D_solver.zero_grad()
            real_data = Variable(x).type(dtype)
            logits_real = D(2* (real_data - 0.5)).type(dtype)

            g_fake_seed = Variable(sample_noise(batch_size, noise_size)).type(dtype)
            fake_images = G(g_fake_seed).detach()
            logits_fake = D(fake_images.view(batch_size, 1, 28, 28))

            d_total_error = discriminator_loss(logits_real, logits_fake)
            d_total_error.backward()        
            D_solver.step()

            G_solver.zero_grad()
            g_fake_seed = Variable(sample_noise(batch_size, noise_size)).type(dtype)
            fake_images = G(g_fake_seed)

            gen_logits_fake = D(fake_images.view(batch_size, 1, 28, 28))
            g_error = generator_loss(gen_logits_fake)
            g_error.backward()
            G_solver.step()

            if (iter_count % show_every == 0):
                print('Iter: {}, D: {:.4}, G:{:.4}'.format(iter_count,d_total_error.data[0],g_error.data[0]))
                imgs_numpy = fake_images.data.cpu().numpy()
                show_images(imgs_numpy[0:16])
                plt.show()
                print()
            iter_count += 1

まぁ、そんなに難しくはなかったはず。反復が数百程度だと黒い背景だけが見え、1000に近付くとぼやけた形、3000を超えると約半分はくっきり鮮明なまともな形に見えるはず。

# Make the discriminator
D = discriminator().type(dtype)

# Make the generator
G = generator().type(dtype)

# Use the function you wrote earlier to get optimizers for the Discriminator and the Generator
D_solver = get_optimizer(D)
G_solver = get_optimizer(G)
# Run it!
import time

start_time = time.time()
run_a_gan(D, G, D_solver, G_solver, discriminator_loss, generator_loss)
end_time = time.time()
print("Elapsed time was %g seconds" % (end_time - start_time))
/root/.pyenv/versions/miniconda3-4.3.30/envs/caffe2/lib/python3.6/site-packages/ipykernel_launcher.py:44: UserWarning: invalid index of a 0-dim tensor. This will be an error in PyTorch 0.5. Use tensor.item() to convert a 0-dim tensor to a Python number
Iter: 0, D: 1.328, G:0.7206
Iter: 250, D: 1.306, G:0.9452
Iter: 500, D: 1.284, G:0.6205
Iter: 750, D: 1.372, G:0.7986
Iter: 1000, D: 1.317, G:0.8151
Iter: 1250, D: 1.263, G:0.7967
Iter: 1500, D: 1.366, G:0.7751
Iter: 1750, D: 1.344, G:0.7602
Iter: 2000, D: 1.391, G:0.7866
Iter: 2250, D: 1.376, G:0.7057
Iter: 2500, D: 1.325, G:0.7211
Iter: 2750, D: 1.348, G:0.7354
Iter: 3000, D: 1.402, G:0.7339
Iter: 3250, D: 1.352, G:0.7355
Iter: 3500, D: 1.371, G:0.7138
Iter: 3750, D: 1.364, G:0.7229
Elapsed time was 42.5012 seconds

Least Squares GAN

今度は、原型GANに代わる新型でより安定したLeast Squares GAN(最小二乗GAN)に注目する。この部分に関しては、損失関数を変えてモデルを再訓練するだけでいい。論文中の式(9)を以下の生成器損失と識別器損失を使って実装する。$$\ell_G = \frac{1}{2}\mathbb{E}_{z \sim p(z)}\left[\left(D(G(z))-1\right)^2\right]$$$$ \ell_D = \frac{1}{2}\mathbb{E}_{x \sim p_\text{data}}\left[\left(D(x)-1\right)^2\right] + \frac{1}{2}\mathbb{E}_{z \sim p(z)}\left[ \left(D(G(z))\right)^2\right]$$

HINTS: 期待値を算出する代わりにミニバッチの要素を平均するので、合計するのではなく平均することで損失を組み合わせることを忘れないこと。$D(x)$と$D(G(z))に代入する場合、識別器の直接出力(score_realとscore_fake)を使う。

def ls_discriminator_loss(scores_real, scores_fake):
    """
    Compute the Least-Squares GAN loss for the discriminator.
    
    Inputs:
    - scores_real: PyTorch Variable of shape (N,) giving scores for the real data.
    - scores_fake: PyTorch Variable of shape (N,) giving scores for the fake data.
    
    Outputs:
    - loss: A PyTorch Variable containing the loss.
    """
    N, _ = scores_real.size()
    loss_real = 0.5*torch.mean(torch.pow(scores_real-Variable(torch.ones(N)).type(dtype), 2))
    loss_fake = 0.5*torch.mean(torch.pow(scores_fake, 2))
    loss = loss_real + loss_fake
    return loss

def ls_generator_loss(scores_fake):
    """
    Computes the Least-Squares GAN loss for the generator.
    
    Inputs:
    - scores_fake: PyTorch Variable of shape (N,) giving scores for the fake data.
    
    Outputs:
    - loss: A PyTorch Variable containing the loss.
    """
    N, _ = scores_fake.size()
    loss = 0.5*torch.mean(torch.pow(scores_fake-Variable(torch.ones(N)).type(dtype), 2))
    return loss

新しい損失関数でGANを実行する前に、それをチェックする。

def test_lsgan_loss(score_real, score_fake, d_loss_true, g_loss_true):
    d_loss = ls_discriminator_loss(Variable(torch.Tensor(score_real)).type(dtype),
                                   Variable(torch.Tensor(score_fake)).type(dtype)).data.cpu().numpy()
    g_loss = ls_generator_loss(Variable(torch.Tensor(score_fake)).type(dtype)).data.cpu().numpy()
    print("Maximum error in d_loss: %g"%rel_error(d_loss_true, d_loss))
    print("Maximum error in g_loss: %g"%rel_error(g_loss_true, g_loss))

test_lsgan_loss(answers['logits_real'], answers['logits_fake'],
                answers['d_loss_lsgan_true'], answers['g_loss_lsgan_true'])
Maximum error in d_loss: 1.53171e-08
Maximum error in g_loss: 1.48703e-07
D_LS = discriminator().type(dtype)
G_LS = generator().type(dtype)

D_LS_solver = get_optimizer(D_LS)
G_LS_solver = get_optimizer(G_LS)

start_time = time.time()
run_a_gan(D_LS, G_LS, D_LS_solver, G_LS_solver, ls_discriminator_loss, ls_generator_loss)
end_time = time.time()
print("Elapsed time was %g seconds" % (end_time - start_time))
/root/.pyenv/versions/miniconda3-4.3.30/envs/caffe2/lib/python3.6/site-packages/ipykernel_launcher.py:44: UserWarning: invalid index of a 0-dim tensor. This will be an error in PyTorch 0.5. Use tensor.item() to convert a 0-dim tensor to a Python number
Iter: 0, D: 0.6062, G:0.5067
Iter: 250, D: 0.2445, G:0.2824
Iter: 500, D: 0.07292, G:0.2907
Iter: 750, D: 0.008764, G:0.393
Iter: 1000, D: 0.001229, G:0.4924
Iter: 1250, D: 0.001887, G:0.5065
Iter: 1500, D: 0.001865, G:0.4929
Iter: 1750, D: 0.0007944, G:0.5272
Iter: 2000, D: 0.00098, G:0.5258
Iter: 2250, D: 0.0002816, G:0.4952
Iter: 2500, D: 0.000904, G:0.5104
Iter: 2750, D: 0.000851, G:0.5028
Iter: 3000, D: 0.001762, G:0.5048
Iter: 3250, D: 0.0009604, G:0.5045
Iter: 3500, D: 0.0005986, G:0.502
Iter: 3750, D: 0.001287, G:0.5031
Elapsed time was 40.2926 seconds

訓練結果が明らかにおかしいのでやり直す。

D_LS = discriminator().type(dtype)
G_LS = generator().type(dtype)

D_LS_solver = get_optimizer(D_LS)
G_LS_solver = get_optimizer(G_LS)

start_time = time.time()
run_a_gan(D_LS, G_LS, D_LS_solver, G_LS_solver, ls_discriminator_loss, ls_generator_loss)
end_time = time.time()
print("Elapsed time was %g seconds" % (end_time - start_time))
/root/.pyenv/versions/miniconda3-4.3.30/envs/caffe2/lib/python3.6/site-packages/ipykernel_launcher.py:44: UserWarning: invalid index of a 0-dim tensor. This will be an error in PyTorch 0.5. Use tensor.item() to convert a 0-dim tensor to a Python number
Iter: 0, D: 0.6714, G:0.4465
Iter: 250, D: 0.1945, G:0.3013
Iter: 500, D: 0.1384, G:0.3636
Iter: 750, D: 0.1882, G:0.4945
Iter: 1000, D: 0.1871, G:0.2755
Iter: 1250, D: 0.1648, G:0.2384
Iter: 1500, D: 0.2, G:0.1784
Iter: 1750, D: 0.2161, G:0.1567
Iter: 2000, D: 0.2416, G:0.1397
Iter: 2250, D: 0.239, G:0.1527
Iter: 2500, D: 0.2531, G:0.1498
Iter: 2750, D: 0.2499, G:0.1243
Iter: 3000, D: 0.2324, G:0.1296
Iter: 3250, D: 0.2408, G:0.1345
Iter: 3500, D: 0.2422, G:0.138
Iter: 3750, D: 0.2433, G:0.1334
Elapsed time was 40.7089 seconds

Deeply Convolutional GANs

ノートブックの前半部分では、Ian GoodfellowのオリジナルGANのほぼ直接コピーを実装した。しかし、このネットワーク仕様では、real spatial reasoning(実空間推論)ができない。それには畳み込み層が実装されていないので、基本的に、”sharp edges”のようなものについて推論することができない。従って、この部分では、識別機と生成器として畳み込みネットワークを利用する、DCGANの着想の一部を導入する。

Discriminator

TensorFlow MNIST classificationチュートリアルにヒントを得た、かなり急速にMNISTデータセットで99%を超える正確度を得られる識別器を使う。

  • Reshape into image tensor (Use Unflatten!)
  • 32 Filters, 5×5, Stride 1, Leaky ReLU(alpha=0.01)
  • Max Pool 2×2, Stride 2
  • 64 Filters, 5×5, Stride 1, Leaky ReLU(alpha=0.01)
  • Max Pool 2×2, Stride 2
  • Flatten
  • Fully Connected size 4 x 4 x 64, Leaky ReLU(alpha=0.01)
  • Fully Connected size 1
def build_dc_classifier():
    """
    Build and return a PyTorch model for the DCGAN discriminator implementing
    the architecture above.
    """
    return nn.Sequential(
        ###########################
        ######### TO DO ###########
        ###########################
        Unflatten(batch_size, 1, 28, 28),
        nn.Conv2d(1, 32, kernel_size=5, stride=1),
        nn.LeakyReLU(negative_slope=0.01, inplace=True),
        nn.MaxPool2d(2, stride=2),
        nn.Conv2d(32, 64, kernel_size=5, stride=1),
        nn.LeakyReLU(negative_slope=0.01, inplace=True),
        nn.MaxPool2d(2, stride=2),
        Flatten(),
        nn.Linear(1024, 1024),
        nn.LeakyReLU(negative_slope=0.01, inplace=True),
        nn.Linear(1024, 1))

data = Variable(loader_train.__iter__().next()[0]).type(dtype)
b = build_dc_classifier().type(dtype)
out = b(data)
print(out.size())
torch.Size([128, 1])

サニティーチェックとして分類器のパラメーター数をチェックする。

def test_dc_classifer(true_count=1102721):
    model = build_dc_classifier()
    cur_count = count_params(model)
    if cur_count != true_count:
        print('Incorrect number of parameters in generator. Check your achitecture.')
    else:
        print('Correct number of parameters in generator.')

test_dc_classifer()
Correct number of parameters in generator.

Generator

生成器に対しては、InfoGAN paperからの仕様を正確にコピーする。Appendix C.1 MNISTとtf.nn.conv2d_transposeを参照されたし。常にGANモードで訓練をすること。

  • Fully connected of size 1024, ReLU
  • BatchNorm
  • Fully connected of size 7 x 7 x 128, ReLU
  • BatchNorm
  • Reshape into Image Tensor
  • 64 conv2d^T filters of 4×4, stride 2, ‘same’ padding, ReLU
  • BatchNorm
  • 1 conv2d^T filter of 4×4, stride 2, ‘same’ padding, TanH
  • Should have a 28x28x1 image, reshape back into 784 vector
def build_dc_generator(noise_dim=NOISE_DIM):
    """
    Build and return a PyTorch model implementing the DCGAN generator using
    the architecture described above.
    """
    return nn.Sequential(
        ###########################
        ######### TO DO ###########
        ###########################
        nn.Linear(noise_dim, 1024),
        nn.ReLU(inplace=True),
        nn.BatchNorm1d(1024),
        nn.Linear(1024, 7*7*128),
        nn.ReLU(inplace=True),
        nn.BatchNorm1d(7*7*128),
        Unflatten(batch_size, 128, 7, 7),
        nn.ConvTranspose2d(128, 64, kernel_size=4, stride=2, padding=1),
        nn.ReLU(inplace=True),
        nn.BatchNorm2d(64),
        nn.ConvTranspose2d(64, 1, kernel_size=4, stride=2, padding=1),
        nn.Tanh(),
        Flatten())

test_g_gan = build_dc_generator().type(dtype)
test_g_gan.apply(initialize_weights)

fake_seed = Variable(torch.randn(batch_size, NOISE_DIM)).type(dtype)
fake_images = test_g_gan.forward(fake_seed)
fake_images.size()
/root/.pyenv/versions/miniconda3-4.3.30/envs/caffe2/lib/python3.6/site-packages/ipykernel_launcher.py:22: UserWarning: nn.init.xavier_uniform is now deprecated in favor of nn.init.xavier_uniform_.
torch.Size([128, 784])

サニティーチェックとして生成器のパラメーター数をチェックする。

def test_dc_generator(true_count=6580801):
    model = build_dc_generator(4)
    cur_count = count_params(model)
    if cur_count != true_count:
        print('Incorrect number of parameters in generator. Check your achitecture.')
    else:
        print('Correct number of parameters in generator.')

test_dc_generator()
Correct number of parameters in generator.
D_DC = build_dc_classifier().type(dtype) 
D_DC.apply(initialize_weights)
G_DC = build_dc_generator().type(dtype)
G_DC.apply(initialize_weights)

D_DC_solver = get_optimizer(D_DC)
G_DC_solver = get_optimizer(G_DC)

start_time = time.time()
run_a_gan(D_DC, G_DC, D_DC_solver, G_DC_solver, discriminator_loss, generator_loss, num_epochs=5)
end_time = time.time()
print("Elapsed time was %g seconds" % (end_time - start_time))
/root/.pyenv/versions/miniconda3-4.3.30/envs/caffe2/lib/python3.6/site-packages/ipykernel_launcher.py:22: UserWarning: nn.init.xavier_uniform is now deprecated in favor of nn.init.xavier_uniform_.
/root/.pyenv/versions/miniconda3-4.3.30/envs/caffe2/lib/python3.6/site-packages/ipykernel_launcher.py:44: UserWarning: invalid index of a 0-dim tensor. This will be an error in PyTorch 0.5. Use tensor.item() to convert a 0-dim tensor to a Python number
Iter: 0, D: 1.47, G:0.5066
Iter: 250, D: 1.377, G:0.8461
Iter: 500, D: 1.294, G:0.6266
Iter: 750, D: 1.299, G:0.782
Iter: 1000, D: 1.301, G:0.7577
Iter: 1250, D: 1.357, G:0.7486
Iter: 1500, D: 1.258, G:1.079
Iter: 1750, D: 1.247, G:0.7959
Elapsed time was 52.7298 seconds
参考サイトhttps://github.com/