(この記事を書いていたのはだいぶ前なのですが、そのまま公開します)
ディープラーニングの二大トレンドといえば識別と生成と言っても過言では無いと思います(思っています)。
最近巷を賑わす、人工知能が自分で考えて何かを創造したと謳われているものは、ほぼ間違いなく Generative Adversarial Networks (GAN) の派生品でしょう。
[1406.2661] Generative Adversarial Networks
自分もそろそろ GAN が使えるようになっておかないとなと思っていたところ、PFNさんが GAN のライブラリを公開したのでちょっと使ってみたブログです。
理論
初めて GAN の原理を聞いた時は感動を覚えたものですが、簡単に言ってしまえば、いわゆる人工知能 (N) が創造者と評論家に分かれてお互いをいかに出し抜くかを戦う (A) と、もはや人間には見分けのつかない創造物を生み出してしまう (G) というもの。それが GAN です。
それだけ聞くと夢のような技術ではあるんですが、当然その代償にハンドリングがとてつもなく難しいという欠点があります。評論家の審美眼が上がりすぎると創造者の創作意欲は無くなり、逆に創造者のクリエイティビティが上がりすぎると評論家の審美眼は機能しなくなってしまうという。当たり前といえば当たり前な話なんですが、違うとすれば人間ではなくコンピューター、ひいては数学の話ということです。
WGAN
ところが最近 GAN 界隈でブレイクスルーがあり、先述の数学的な困難を克服するために開発されたのが Wasserstein GAN (WGAN) です。
創造という過程は、現実が表現しているものを模倣する、つまり現実と創造の距離を近づける問題と考えることが出来ます。その距離尺度に Wasserstein 距離 (or Earth Mover’s Distance : EMD) を用いることで GAN の学習を数学的にめちゃくちゃ安定させたらしいっす(ぶっちゃけ元論文読んでも自分にはよく分かんないっす…)。
ここで 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 を実現したとのこと。
ただ使うだけだとただ使うだけなのですが、自分がやりたかったのは、犬を描いてと言うと、何かしら自分の思う犬を描いてくれるという機能を実装してみようかと。
簡単に実現するとしたら 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.) のそのままで、
Generator の loss が、適当な係数 、生成カテゴリの softmax cross entropy 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 を生成してみましたが、安定しませんでした…。すぐ発狂するので、僕の方まで発狂しそうでした。
学習率やらしっかり調整しようと思ったら、二階微分を搭載した Chainer ver.3 が公開されてしまい、やる気をなくしました(笑)
Released #Chainer v3.0.0 RC! Many functions are supported for differentiable backprop (a.k.a. grad of grads) https://t.co/VQwsUSxZUd
— Chainer (@ChainerOfficial) 2017年9月12日
ここで諦めるわけにはいかないので、ver3.0 の二階微分を使いながら、Cramer GAN でも使ってみよう思い立ったので、下書きに放置していたこの記事を投下したのでした。

- 作者: 高根正昭
- 出版社/メーカー: 講談社
- 発売日: 1979/09/18
- メディア: 新書
- 購入: 49人 クリック: 368回
- この商品を含むブログ (58件) を見る