Skip to content

Chainer-GAN-lib の WGAN-GP で Auxiliary Classifier してみたが失敗した (という思い出)

(この記事を書いていたのはだいぶ前なのですが、そのまま公開します)

ディープラーニングの二大トレンドといえば識別と生成と言っても過言では無いと思います(思っています)。
最近巷を賑わす、人工知能が自分で考えて何かを創造したと謳われているものは、ほぼ間違いなく Generative Adversarial Networks (GAN) の派生品でしょう。

[1406.2661] Generative Adversarial Networks

自分もそろそろ GAN が使えるようになっておかないとなと思っていたところ、PFNさんが GAN のライブラリを公開したのでちょっと使ってみたブログです。

理論

初めて GAN の原理を聞いた時は感動を覚えたものですが、簡単に言ってしまえば、いわゆる人工知能 (N) が創造者と評論家に分かれてお互いをいかに出し抜くかを戦う (A) と、もはや人間には見分けのつかない創造物を生み出してしまう (G) というもの。それが GAN です。

elix-tech.github.io

それだけ聞くと夢のような技術ではあるんですが、当然その代償にハンドリングがとてつもなく難しいという欠点があります。評論家の審美眼が上がりすぎると創造者の創作意欲は無くなり、逆に創造者のクリエイティビティが上がりすぎると評論家の審美眼は機能しなくなってしまうという。当たり前といえば当たり前な話なんですが、違うとすれば人間ではなくコンピューター、ひいては数学の話ということです。

WGAN

ところが最近 GAN 界隈でブレイクスルーがあり、先述の数学的な困難を克服するために開発されたのが Wasserstein GAN (WGAN) です。

[1701.07875] Wasserstein GAN

musyoku.github.io

創造という過程は、現実が表現しているものを模倣する、つまり現実と創造の距離を近づける問題と考えることが出来ます。その距離尺度に Wasserstein 距離 (or Earth Mover’s Distance : EMD) を用いることで GAN の学習を数学的にめちゃくちゃ安定させたらしいっす(ぶっちゃけ元論文読んでも自分にはよく分かんないっす…)。

aidiary.hatenablog.com

www.slideshare.net

ここで WGAN において非常に重要な問題が、モデル間を Wasserstein 距離で測るために、リプシッツ連続を担保している必要があるとのこと。元論文では無理やり補正していたためうまく収束しなかったらしい。

そこで、WGAN を形成するディープラーニングの学習において、勾配そのものにリプシッツ連続を保つようにペナルティを与えてしまうのが WGAN-GP (gradient penalty) です。

[1704.00028] Improved Training of Wasserstein GANs

WGAN-GP

実は自分も Chainer を使って WGAN-GP をやってみようとしたのですが、二階微分を扱って、勾配そのものにペナルティを与える機能が(記事執筆時点で)まだなく一度は諦めました。

ところが、最新の GAN 技術を Chainer で実装した Chainer-GAN-lib が公開され、よく見てみると Chainer ver.2 の上で無理やり WGAN-GP を実現したとのこと。

preferredresearch.jp

ただ使うだけだとただ使うだけなのですが、自分がやりたかったのは、犬を描いてと言うと、何かしら自分の思う犬を描いてくれるという機能を実装してみようかと。

簡単に実現するとしたら Auxiliary Classifier GAN (ACGAN) が思いついたので、AC-WGAN-GP のようなものを Chainer で作ってしまえばいいのでは無いかと。

[1610.09585] Conditional Image Synthesis With Auxiliary Classifier GANs

先に言い訳をしておくと、全然収束しなくて泣きました。ここからは創造者を Generator(G)、評論家を Discriminator (D:WGANの文脈ではCriticと呼ばれますが) と言い直しておきます。ACGAN では D に、G が作った偽物か本物かの真贋識別と、それが何の画像かのカテゴリ識別がついてるんですが、どうもこのままだとうまくいきませんでした… (Chainer ver.2 の限界でしょうか…)。

今回は普通の GAN のように D に真贋判定をさせて、カテゴリ識別には別に Classifier ( C ) を用意しました (登場人物増えたら原型無いやん…)。さらに全部 Wasserstein 距離で測ろうと思ったらこれまたうまくいかず、C は単純に softmax cross-entropy を使わざるを得ないという散々な出来に…。作ったはいいもののただの失敗作のキメラになったのですが、このまま捨てるのももったいないので公開することにしました。

Discriminator の loss は、WGAN-GP (Gulrajani et al.) のそのままで、


\displaystyle{
L_d = \mathbb{E}_{x' \sim \mathbb{G}} [D (x')]
- \mathbb{E} _{x \sim \mathbb{R}} [D (x)]
+ \lambda \mathbb{E} _{\hat{x}} [( \|\nabla D (\hat{x} )\| -1 )^{2} ]
}

Generator の loss が、適当な係数  \epsilon \sim 100、生成カテゴリの softmax cross entropy E’ (正しく書き下すのめんどくさい…)とかとしておいて、


\displaystyle{
L_g = -\mathbb{E}_{x' \sim \mathbb{G}} [D (x')] + \epsilon E'
}

この時点で全然ダメだ。案の定失敗しましたが…
もう記事を書く元気もなくなったので、以下適当です…。

実装

Chainer GAN lib の WGAN のソースコードをそのまま使わせてもらうとして、common.net を継承して Ganerator をちょっといじる。

import chainer
import chainer.functions as F
import chainer.links as L
from chainer import cuda

import common.net as cn

class MTGenerator(cn.DCGANGenerator):
    def __init__(self, n_hidden=128, n_category=10, bottom_width=4, ch=512, wscale=0.02,
                 z_distribution="uniform", hidden_activation=F.leaky_relu, output_activation=F.tanh, use_bn=True):
        super(cn.DCGANGenerator, self).__init__()
        self.n_hidden = n_hidden
        self.n_category = n_category
        self.ch = ch
        self.bottom_width = bottom_width
        self.z_distribution = z_distribution
        self.hidden_activation = hidden_activation
        self.output_activation = output_activation
        self.use_bn = use_bn
        
        with self.init_scope():
            w = chainer.initializers.Normal(wscale)
            self.l0 = L.Linear(self.n_hidden+self.n_category, bottom_width * bottom_width * ch,
                               initialW=w)
            self.dc1 = L.Deconvolution2D(ch, ch // 2, 4, 2, 1, initialW=w)
            self.dc2 = L.Deconvolution2D(ch // 2, ch // 4, 4, 2, 1, initialW=w)
            self.dc3 = L.Deconvolution2D(ch // 4, ch // 8, 4, 2, 1, initialW=w)
            self.dc4 = L.Deconvolution2D(ch // 8, 3, 3, 1, 1, initialW=w)
            if self.use_bn:
                self.bn1 = L.BatchNormalization(ch // 2)
                self.bn2 = L.BatchNormalization(ch // 4)
                self.bn3 = L.BatchNormalization(ch // 8)

    def make_hidden(self, batchsize, t, test=False):
        xp = cuda.cupy
        if test: xp.random.seed(0)
        
        if self.z_distribution == "normal":
            x = xp.random.randn(batchsize, self.n_hidden, 1, 1).astype(xp.float32)
        elif self.z_distribution == "uniform":
            x = xp.random.uniform(-1, 1, (batchsize, self.n_hidden, 1, 1)).astype(xp.float32)
        else:
            raise Exception("unknown z distribution: %s" % self.z_distribution)
            
        c = xp.array([[1 if i==t[j] else -1 for i in range(self.n_category)] for j in range(batchsize)])
        c = c.reshape(batchsize, 10, 1, 1).astype(xp.float32)
        x = F.concat((x,c), axis=1)
        return x

    def __call__(self, z):
        if not self.use_bn:
            h = F.reshape(self.hidden_activation(self.l0(z)), 
                          (len(z), self.ch, self.bottom_width, self.bottom_width))
            h = self.hidden_activation(self.dc1(h))
            h = self.hidden_activation(self.dc2(h))
            h = self.hidden_activation(self.dc3(h))
            x = self.output_activation(self.dc4(h))
        else:
            h = F.reshape(self.hidden_activation(self.l0(z)), 
                          (len(z), self.ch, self.bottom_width, self.bottom_width))
            h = self.hidden_activation(self.bn1(self.dc1(h)))
            h = self.hidden_activation(self.bn2(self.dc2(h)))
            h = self.hidden_activation(self.bn3(self.dc3(h)))
            x = self.output_activation(self.dc4(h))
        return x

あとは Updeter を AC でちょっと書き直し。

class Updater():
    def __init__(self, gen, opt_gen, 
                 dis, opt_dis, 
                 cls, opt_cls,
                 n_category=10, gpu=-1):
        self.gen, self.opt_gen = gen, opt_gen
        self.dis, self.opt_dis = dis, opt_dis
        self.cls, self.opt_cls = cls, opt_cls
        self.gpu = gpu
        self.n_category = n_category
        self.xp = np if gpu < 0 else chainer.cuda.cupy
        self.lam = 10.0
        self.epsilon = 100.0
        
    def update(self, x, t):
        xp = self.xp
        batchsize = x.shape[0]
        
        x_real = chainer.Variable(xp.asarray(x))
        y_real = self.dis(x_real)
        y_real_l = self.cls(x_real)
        
        loss_cls = F.softmax_cross_entropy(y_real_l, t)
        
        self.cls.cleargrads()
        loss_cls.backward()
        self.opt_cls.update()
        
        # generator
        z = self.gen.make_hidden(batchsize, t)
        x_fake = self.gen(z)
        y_fake = self.dis(x_fake)
        y_fake_l = self.cls(x_fake)
        
        loss_gen = F.sum(-y_fake) / batchsize
        loss_gen += self.epsilon * F.softmax_cross_entropy(y_fake_l, t)
        
        self.gen.cleargrads()
        loss_gen.backward()
        self.opt_gen.update()
        
        # discriminator
        x_fake.unchain_backward()
        eps = xp.random.uniform(0, 1, size=batchsize).astype("f")[:, None, None, None]
        x_mid = eps * x_real + (1.0 - eps) * x_fake
        
        x_mid_v = chainer.Variable(x_mid.data)
        y_mid = self.dis(x_mid_v)
        dydx = self.dis.differentiable_backward(xp.ones_like(y_mid.data))
        dydx = F.sqrt(F.sum(dydx ** 2, axis=(1, 2, 3)))
        loss_gp = self.lam * F.mean_squared_error(dydx, xp.ones_like(dydx.data))
        
        loss_dis = F.sum(-y_real) / batchsize
        loss_dis += F.sum(y_fake) / batchsize
        
        self.dis.cleargrads()
        loss_dis.backward()
        loss_gp.backward()
        self.opt_dis.update()
        
        return loss_gen, loss_dis, loss_cls, loss_gp

あとがき

CIFAR-10 を生成してみましたが、安定しませんでした…。すぐ発狂するので、僕の方まで発狂しそうでした。

f:id:mocchitam:20171004151059p:plain

学習率やらしっかり調整しようと思ったら、二階微分を搭載した Chainer ver.3 が公開されてしまい、やる気をなくしました(笑)

ここで諦めるわけにはいかないので、ver3.0 の二階微分を使いながら、Cramer GAN でも使ってみよう思い立ったので、下書きに放置していたこの記事を投下したのでした。

github.com

創造の方法学 (講談社現代新書)

創造の方法学 (講談社現代新書)

    コメントを残す

    メールアドレスが公開されることはありません。 が付いている欄は必須項目です

    %d人のブロガーが「いいね」をつけました。