NIPS2017論文読みメモ: Inverse Reward Design(その1)

お正月ですがNIPS2017論文読み会に参加するので論文を読みたいと思います。今回読むのは以下です。

Dylan Hadfield-Menell, Smitha Milli, Pieter Abbeel, Stuart Russell and Anca Dragan. Inverse Reward Design. arXiv: 1711:02827, 2017. https://arxiv.org/abs/1711.02827
※ 以下、キャラクターが会話します。それぞれの原作とは関係ありません。論文内容の解釈誤りは本ブログ筆者に帰属します。
次回: その2
f:id:cookie-box:20180101155919p:plain:w60

強化学習エージェントを学習させるときの報酬関数って実はちゃんとわからないから、「報酬関数は『真の報酬関数』のある観測結果に過ぎない」という前提で学習させようという話みたいだねー。共著者に連なっている Russel や Abbeel は「逆強化学習」の定式化やその解法である「見習い学習」を提唱した人だにゃー。「これからの強化学習」の2.3節に載ってるね。

f:id:cookie-box:20180101155951p:plain:w60

強化学習の報酬の話ですか。Sutton の強化学習本には、「報酬を最大化することでエージェントが我々の目的を達成してくれるように報酬を与える必要がある」とありました。チェスをプレイするエージェントは勝負に勝ったときのみに報酬を得るべきであって、敵の駒を取ったときなどに報酬を与えると勝ちにつながらない駒取りばかりするおかしなエージェントになりかねないとか…。

f:id:cookie-box:20180101155919p:plain:w60

そーそー。そーいう報酬関数の設計ミスによる悪影響をこの論文では「(未特定の報酬による)ネガティブサイドエフェクト」と「報酬ハッキング」の2つ挙げているね。前者は考慮漏れにより望まないふるまいが引き起こされること、後者は報酬自体が望まないふるまいを引き起こすことっぽいかなー。いま瑞希ちゃんが言ってくれたのはまさに「報酬ハッキング」の例だねー。報酬ハッキングとして本文に挙げられている例は、

  • 「ゴミを吸引すること」に報酬を与えられたお掃除ロボが、ゴミをもっと吸引するために一度吸引したゴミを外に出してしまった。
  • 標的を撃ち落としながらゲームを周回するボートレースゲームで、標的を撃ち落とすことに報酬を与えたら、標的をずっと撃ち続けてコースを周回してくれなくなった。

f:id:cookie-box:20180101155951p:plain:w60

…なぜお掃除ロボが自らゴミを外に出せるような設計になっているのでしょうか。…そんな機能要らないぞ。

f:id:cookie-box:20180101155919p:plain:w60

…きっとロボで掃除したい場所がすごいごみだらけで、満杯になったごみをごみ箱に捨てるって行動も学習させたかったんじゃない? だから自分でゴミを外に出す機能も付けておいたんじゃないかなー。話を戻すと、もう1つの報酬による悪影響である「ネガティブサイドエフェクト」の例は、宝探しロボットに草むらを避けてほしくて草むらを進むことにマイナスの報酬を与えたら、草むらを避けて溶岩に突っ込んじゃったって(論文に挿絵が載ってるね)。溶岩に出くわすとわかっていたら溶岩には草むらよりもっと大きなマイナスの報酬を与えていたけど、想定していなかったってことだね。

f:id:cookie-box:20180101155951p:plain:w60

なるほど。でも、溶岩に出くわすレベルで想定外な事態は、チェスのエージェントには起こりえないと思います。ルールは、決まっているから。そもそも、「勝ったときに報酬を得る」というトリビアルな報酬デザインができるので、ネガティブサイドエフェクトや報酬ハッキングに悩まされる余地はありません。

f:id:cookie-box:20180101155919p:plain:w60

それはそーだよねー。でも、「これからの強化学習」の2.3節にあるように、一般に「目標状態や終端状態にだけ定義された報酬によって学習することは難しいことは多い」んだ。それは、状態空間が広い場合、まず初期方策からはじめて目標の終端状態にたどり着くのに時間がかかるのと、たどり着けてもだから序盤どう行動するべきかまでわかるにはまた時間がかかるんだよね。それに、もし終端状態に報酬を与えるだけでじゅうぶん学習できるだけの状態数だったとしても、一般的なタスクの終端状態は「勝ち or 負け」のようにシンプルじゃないよね。「まあまあよい終わり方」とか「もっとよい終わり方」とかあって、それらをどう評価するかはやっぱりトリビアルじゃないと思う。

f:id:cookie-box:20180101155951p:plain:w60

そうですね。一般的には報酬をどうデザインするべきかはトリビアルでなく、考慮漏れによるネガティブサイドエフェクトや、報酬の与えどころの誤りによる報酬ハッキングが起きかねないというのは理解できます。それで、この論文はどのようにしてこの問題に対処するというのでしょうか。

f:id:cookie-box:20180101155919p:plain:w60

それはねー、もう真の報酬がわからないことを認めるんだ。ヒトがデザインできるのはあくまでプロキシ報酬(代理報酬)で、真の報酬は推定しなければならない。マルコフ決定過程、プロキシ報酬関数、報酬関数候補の集合を所与として真の報酬関数を推定する問題をIRD: Inverse Reward Design(逆報酬デザイン)と定義するよ。

f:id:cookie-box:20180101155951p:plain:w60

待ってください。真の報酬がわからないというのはいいです。それはむしろ自然だと思います。でも、真の報酬がわからないのに真の報酬を推定するということができるのでしょうか。

f:id:cookie-box:20180101155919p:plain:w60

プロキシ報酬がある程度真の報酬に近いとか、エピソードが選択される確率が「最大エントロピー」の分布にしたがうとかの仮定は置くけど、事後分布を効果的に近似することができるみたいだよー。ここから先は式を追っていった方がいいかにゃー。

つづく

Keras で GAN の練習(その2)

前回の記事で GAN を動かしてみたのですが、実装があやしいのでまた別の記事を参考にしてみます。

参考文献

以下の記事を参考にします。やることは前回と同じで手書き数字の模造です。
MNIST Generative Adversarial Model in Keras

  • この記事ではディスクリミネータのことをアドバーサリアルモデルといっている。
    • 昨日の記事ではジェネレータ+ディスクリミネータのことをアドバーサリアルモデルといっていた。
  • 記事の初っ端から「freeze the weights in the adversarial part of the network, and train the generative network weights」、つまりディスクリミネータ部分のネットワークの重みを固定してジェネレータを訓練するといっているので、昨日の記事で気になった部分はこちらの記事では大丈夫そうです。
  • こちらの記事は Sequential モデルではなくて functional API をつかっています。
  • ディスクリミネータの出力層がの次元が2になっています。つまり、本物なら [0, 1] 、模造品なら [1, 0] を目標出力とします。
    • 2クラス分類では1次元にすることが多いと思っていました。どっちでもいいと思いますが。
    • ただ、2クラス分類で出力層を2次元にする場合、判定を誤ったときに「正しいクラスであると考えた度合いが小さかった」のか、「正しいクラスであると考えた度合いは大きかったが、それ以上に誤ったクラスだと考えた度合いが大きかった」のかの区別は付くと思います(最終出力がソフトマックスされる前をみれば)。それをみたい機会があるのかはわかりませんが。
実行結果

ジェネレータとディスクリミネータをがっちゃんこした状態が以下です。

_________________________________________________________________
_________________________________________________________________
Layer (type)                 Output Shape              Param #
=================================================================
input_3 (InputLayer)         (None, 100)               0
_________________________________________________________________
model_1 (Model)              (None, 28, 28, 1)         4341801
_________________________________________________________________
model_2 (Model)              (None, 2)                 9707266
=================================================================
Total params: 14,049,067
Trainable params: 13,970,367
Non-trainable params: 78,700
_________________________________________________________________

ただ肝心の訓練が上手くいっていないので、API はこのままに前回の記事のモデル構造を適用してみたいと思います。

スクリプト

元の記事は Keras のバージョンが手元より古いので、一部サポートされなくなってしまっていた機能がありました(BatchNormalization の mode=2)。

TypeError: The `mode` argument of `BatchNormalization` no longer exists. `mode=1`  and  `mode=2` are no longer supported.

Normalization Layers - Keras 1.2.2 Documentation
元の記事に比べて色々関数にくくり出しています。また、TensorFlow バックエンドの Keras 2.0 で動くように全体的に変更してあります。

# -*- coding: utf-8 -*-
import numpy as np
from keras.models import Model
from keras.layers import Input
from keras.layers.core import Reshape, Dense, Dropout, Activation, Flatten
from keras.layers.convolutional import Conv2D, MaxPooling2D, ZeroPadding2D, UpSampling2D
from keras.layers.advanced_activations import LeakyReLU
from keras.layers.normalization import *
from keras.activations import *
from keras.optimizers import *
from keras.datasets import mnist
import matplotlib.pyplot as plt
from tqdm import tqdm

# ---------- ネットワークの訓練可能オンオフを制御する ----------
def make_trainable(net, trainable=False):
  net.trainable = trainable
  for l in net.layers:
    l.trainable = trainable

# ---------- ジェネレータの生成 ----------
def create_generator(opt):
  g_input = Input(shape=[100])
  H = Dense(14*14*200, init='glorot_normal')(g_input)
  H = BatchNormalization()(H)
  H = Activation('relu')(H)
  H = Reshape([14, 14, 200])(H)
  H = UpSampling2D(size=(2, 2))(H)
  H = Conv2D(100, (3, 3), padding='same', init='glorot_uniform')(H)
  H = BatchNormalization()(H)
  H = Activation('relu')(H)
  H = Conv2D(50, (3, 3), padding='same', init='glorot_uniform')(H)
  H = BatchNormalization()(H)
  H = Activation('relu')(H)
  H = Conv2D(1, (1, 1), padding='same', init='glorot_uniform')(H)
  g_V = Activation('sigmoid')(H)
  generator = Model(g_input, g_V)
  generator.compile(loss='binary_crossentropy', optimizer=opt)
  generator.summary()
  return generator

# ---------- ディスクリミネータの生成 ----------
def create_discriminator(shp, dropout_rate, dopt):
  d_input = Input(shape=shp)
  H = Conv2D(256, (5, 5), subsample=(2, 2), padding='same', activation='relu')(d_input)
  H = LeakyReLU(0.2)(H)
  H = Dropout(dropout_rate)(H)
  H = Conv2D(512, (5, 5), subsample=(2, 2), padding='same', activation='relu')(H)
  H = LeakyReLU(0.2)(H)
  H = Dropout(dropout_rate)(H)
  H = Flatten()(H)
  H = Dense(256)(H)
  H = LeakyReLU(0.2)(H)
  H = Dropout(dropout_rate)(H)
  d_V = Dense(2, activation='softmax')(H)
  discriminator = Model(d_input,d_V)
  discriminator.compile(loss='categorical_crossentropy', optimizer=dopt)
  discriminator.summary()
  return discriminator

# ---------- アドバーサリアルモデルの生成 ----------
def create_adversarial_model(generator, discriminator, opt):
  gan_input = Input(shape=[100])
  H = generator(gan_input)
  gan_V = discriminator(H)
  GAN = Model(gan_input, gan_V)
  GAN.compile(loss='categorical_crossentropy', optimizer=opt)
  GAN.summary()
  return GAN

# ---------- ディスクリミネータを1バッチ分トレーニングする ----------
def train_discriminator_1batch(discriminator, X_train, batch_size):
  image_batch = X_train[np.random.randint(0, X_train.shape[0], size=batch_size),:,:,:]
  noise_gen = np.random.uniform(0, 1, size=[batch_size, 100])
  generated_images = generator.predict(noise_gen)
  
  X = np.concatenate((image_batch, generated_images))
  y = np.zeros([2*batch_size, 2])
  y[:batch_size, 1] = 1 # 本物データのときのディスクリミネータの期待出力は y=[0, 1]
  y[batch_size:, 0] = 1 # 模造データのときのディスクリミネータの期待出力は y=[1, 0]
  
  make_trainable(discriminator, True)
  d_loss = discriminator.train_on_batch(X, y)
  return d_loss

# ---------- アドバーサリアルモデルを1バッチ分トレーニングする ----------
def train_GAN_1batch(discriminator, GAN, X_train, batch_size):
  noise_tr = np.random.uniform(0, 1, size=[batch_size, 100])
  y = np.zeros([batch_size, 2])
  y[:, 1] = 1 # 本物データと判断してほしいので期待出力は y=[0, 1]
  
  make_trainable(discriminator, False) # ディスクリミネータの重み係数更新は忘れずにオフ
  g_loss = GAN.train_on_batch(noise_tr, y)
  return g_loss

# ---------- 損失をプロットする ----------
def plot_loss(losses, filename='loss.png'):
  plt.figure(figsize=(10,8))
  plt.plot(losses["d"], label='discriminitive loss')
  plt.plot(losses["g"], label='generative loss')
  plt.legend()
  plt.savefig(filename)
  plt.close('all')

# ---------- ジェネレータが出力する模造データをプロットする ----------
def plot_gen(noise, generator, filename='result.png'):
  generated_images = generator.predict(noise)
  plt.figure(figsize=(10,10))
  for i in range(generated_images.shape[0]):
    plt.subplot(4, 4, i+1)
    img = generated_images[i,:,:,:]
    img = np.reshape(img, [28, 28])
    plt.imshow(img, cmap='gray')
    plt.axis('off')
  plt.tight_layout()
  plt.savefig(filename)
  plt.close('all')

# ---------- ネットワークをトレーニングする(メイン) ----------
def train_GAN(generator, discriminator, GAN, X_train, batch_size=32, steps=50):
  # ディスクリミネータの事前学習
  for i in range(5):
    train_discriminator_1batch(discriminator, X_train, batch_size=10)
  
  losses = {"d":[], "g":[]} # 損失の記録用
  noise_for_plot = np.random.uniform(0, 1, size=[16, 100]) # 途中経過出力用のノイズ
  for i in tqdm(range(steps)):
    # ディスクリミネータの学習
    d_loss = train_discriminator_1batch(discriminator, X_train, batch_size)
    losses["d"].append(d_loss)
    
    # アドバーサリアルモデルの学習
    g_loss = train_GAN_1batch(discriminator, GAN, X_train, batch_size)
    losses["g"].append(g_loss)
    
    # プロット
    if i%25 == 25-1:
      plot_loss(losses)
      plot_gen(noise_for_plot, generator, "result_%d.png" % i)

# ========================= メイン処理 =========================
if __name__ == '__main__':
  # データ読み込みとプレ処理
  img_rows, img_cols = 28, 28
  (X_train, y_train), (X_test, y_test) = mnist.load_data()
  X_train = X_train[np.where(y_train == 1)] # 「1」のみにしぼる
  X_train = X_train.reshape(X_train.shape[0], img_rows, img_cols, 1).astype('float32')
  X_train /= 255.0
  shp = X_train.shape[1:]
  
  # 訓練設定
  dropout_rate = 0.25
  dopt = Adam(lr=1e-3)
  opt = Adam(lr=1e-4)
  
  # ネットワークの生成
  generator = create_generator(opt)
  discriminator = create_discriminator(shp, dropout_rate, dopt)
  GAN = create_adversarial_model(generator, discriminator, opt)
  
  # ネットワークの訓練
  train_GAN(generator, discriminator, GAN, X_train)
その他

tqdm という進捗バーを表示してくれるパッケージをはじめて知りました。
Pythonで進捗バーを表示する(tqdm) - naritoブログ

Keras で GAN の練習

今週強化学習アーキテクチャ勉強会で GAN の話を聴いてきたので(勉強会自体は GAN ではなくて GAN の手法の強化学習への応用が主題ですが)、GAN を手元で動かしてみたいと思います。

参考文献

「keras gan example」と検索すると色々出てきますが、以下の記事を参考にしたいと思います。今回書いたスクリプトはほぼ以下の記事と同じです(ただし、訓練ルールがおかしい可能性があります;後述)。
GAN by Example using Keras on Tensorflow Backend – Towards Data Science

GAN(Generative Adversarial Networks)って何

※ これは参考文献の記述というより自分で適当に書いています。

  • 何かの模造品が出てくるジェネレータです。例えば「日本人の顔っぽい画像」を生成します。もちろん、このジェネレータをつくるには日本人の顔の画像のサンプルデータセットを用意して、どんな画像を出せば日本人の顔っぽいのかを訓練する必要があります。
  • このジェネレータを訓練する過程で、模造品と本物を識別するディスクリミネータというのを一緒に合わせて訓練するのがミソです。
    • ジェネレータは乱数から模造データへの写像です。
    • ディスクリミネータはデータ(模造でも本物でも)から本物らしい確率 0~1 への写像です。
    • つまり、ジェネレータ とディスクリミネータをがっちゃんこすると乱数から本物らしい確率 0~1 への写像です(以下、がっちゃんこしたのをアドバーサリアルモデルとよびます)。
    • 訓練は以下の繰り返しです。
      • まず n 個の乱数を用意して、ジェネレータに入れて n 個の模造データを生成します。
      • 次にディスクリミネータを訓練します; n 個の模造データと n 個の本物データを受け取って、模造品を入れたら0、本物を入れたら1が出てくるように訓練します。
      • 次にアドバーサリアルモデルを訓練します; また新しく n 個の乱数を用意して、どの乱数を入れても1が出てくるように訓練します。

※ ただし、アドバーサリアルモデルの訓練のときディスクリミネータ部分のネットワークの重みは固定しておかないとディスクリミネータまで更新されてしまいます。参考の記事のスクリプトはディスクリミネータが2回更新されているようにみえるので確認中です。

今回やること
  • MNIST から訓練して模造手書き数字を生成します。
    • ジェネレータは100次元の乱数から模造手書き数字を生成します。最後の活性化を除き活性化の直前に必ず Batch Normalization します(通常の訓練データからの訓練と違って、入力が一様乱数だから入念な安定化が必要なのですかね?)。
    • ディスクリミネータは普通の手書き数字分類と違って、maxプーリングしません。maxプーリングは手書き数字の「4」が少しずれたものでも「4」と識別てきるようにする効果がありますが、今回ディスクリミネータに求められるのはそういう能力じゃないからということなのですかね?
実行結果

ディスクリミネータ → アドバーサリアルモデルの順に何か訓練は進んでいるようです(?)。

0: [D loss: 0.693790, acc: 0.447266]  [A loss: 2.802749, acc: 0.000000]
1: [D loss: 0.605273, acc: 0.958984]  [A loss: 5.334227, acc: 0.000000]
2: [D loss: 0.443096, acc: 0.882812]  [A loss: 1.039907, acc: 0.058594]
3: [D loss: 1.566417, acc: 0.500000]  [A loss: 14.592390, acc: 0.000000]
4: [D loss: 0.264856, acc: 0.890625]  [A loss: 0.048600, acc: 1.000000]
5: [D loss: 0.341633, acc: 0.830078]  [A loss: 7.195857, acc: 0.000000]
6: [D loss: 0.095739, acc: 0.990234]  [A loss: 0.227625, acc: 0.953125]
7: [D loss: 0.088694, acc: 1.000000]  [A loss: 0.125637, acc: 0.992188]
8: [D loss: 0.090487, acc: 0.992188]  [A loss: 0.058483, acc: 1.000000]

41ステップ目からなんかディスクリミネータが識別しづらくなっているようです。41ステップ目や43ステップ目は D の acc が 0.5 で A の acc が 1.0 なのでディスクリミネータが全てのデータを本物と判定してしまっているようです。

36: [D loss: 0.008699, acc: 0.998047]  [A loss: 0.000055, acc: 1.000000]
37: [D loss: 0.009811, acc: 0.998047]  [A loss: 0.000191, acc: 1.000000]
38: [D loss: 0.012975, acc: 0.998047]  [A loss: 0.000129, acc: 1.000000]
39: [D loss: 0.010878, acc: 0.998047]  [A loss: 0.016210, acc: 0.992188]
40: [D loss: 0.130817, acc: 0.957031]  [A loss: 16.118101, acc: 0.000000]
41: [D loss: 5.751718, acc: 0.500000]  [A loss: 0.000000, acc: 1.000000]
42: [D loss: 1.771331, acc: 0.587891]  [A loss: 4.267523, acc: 0.250000]
43: [D loss: 7.854210, acc: 0.500000]  [A loss: 0.000000, acc: 1.000000]
44: [D loss: 7.210688, acc: 0.500000]  [A loss: 4.953451, acc: 0.253906]
45: [D loss: 6.669549, acc: 0.500000]  [A loss: 11.922536, acc: 0.015625]

50ステップ後のジェネレータ出力は以下です。なんかもにょもにょしていて全然手書き数字ではないです。

f:id:cookie-box:20171216161856p:plain:w380

参考文献には1000ステップくらい回して手書き数字っぽい出力を学習できている例が載っていますが、このまま学習を続けるとよくなるのでしょうか。いま作業しているマシンには GPU 積んでいないのでやるなら一晩かけてみないとよくわかりません。
しかし、GPU がなくても、もうちょっと学習が上手くいっているのかどうか知りたいものです。そこで、以下の強硬手段をとります。

  • 数字が10種類もあるのがよくない。ここはもう数字の「1」のみに絞る。棒くらい学んでほしい。
  • ディスクリミネータが4回も畳み込んでいる。畳み込みすぎ。時間がかかるので最後の畳込みを削る。

このようにして実行してみると着実に数字の「1」への道を歩んでいるように見えます。よかった。

25ステップ
f:id:cookie-box:20171216210234p:plain:w210
50ステップ
f:id:cookie-box:20171216210259p:plain:w210
75ステップ
f:id:cookie-box:20171216210320p:plain:w210
100ステップ
f:id:cookie-box:20171216210609p:plain:w210
125ステップ
f:id:cookie-box:20171216210648p:plain:w210
150ステップ
f:id:cookie-box:20171216210801p:plain:w210
175ステップ
f:id:cookie-box:20171216211527p:plain:w210
200ステップ
f:id:cookie-box:20171216211539p:plain:w210
225ステップ
f:id:cookie-box:20171216211550p:plain:w210

スクリプト

変数名と処理の順序を一部変更している以外参考文献のコードと同じです。以下注意書きです。

  • 参考文献の記事は後半以降 model という言葉を「ネットワーク構造 + 訓練ルール(損失関数と勾配法)」という意味合いでつかっているようです。スクリプトでもネットワーク構造のみ(self.D)とネットワーク構造+訓練ルール(self.DM)を別のメンバとして持っています。
    • 通常の訓練データの識別や回帰ではこれをわざわざ分けないですが、GAN の訓練では「ディスクリミネータ構造 + ディスクリミネータの訓練ルール」による訓練と「ジェネレータ構造 + ディスクリミネータ構造 + アドバーサリアルモデルの訓練ルール」による訓練の2種類の訓練をしなければならないので、構造は構造単体で持っておかなければならないのですね。
  • Windows などでユーザ名が日本語になっているなどすると(変えたいのですが…)一時ファイルのパスに日本語が交じりデータのダウンロードに失敗します。環境変数 TMP, TEMP を日本語のないパスに変更すると解決します(import - windows10環境でtensorflowを動かしたい(69477)|teratail)。
  • MNIST_DCGAN クラスに1種類の数字に絞るかどうかのコメントアウトがあるので適宜変更してください。さらに計算量を削りたい人は、上でやったようにディスクリミネータの最後の Conv2D と Dropout を削るとか、バッチサイズを小さくするとかするといいと思います。
# -*- coding: utf-8 -*-
import numpy as np
from tensorflow.examples.tutorials.mnist import input_data
from keras.layers import Conv2D, LeakyReLU, Dense, Flatten, Dropout
from keras.layers import BatchNormalization, Activation, Conv2DTranspose, UpSampling2D, Reshape
from keras.models import Sequential
from keras.optimizers import Adam, RMSprop
import matplotlib.pyplot as plt

class DCGAN:
  def __init__(self, img_rows=28, img_cols=28, channel=1):
    self.img_rows = img_rows
    self.img_cols = img_cols
    self.channel = channel
    self.D = None   # ディスクリミネータ(のネットワーク構造だけ)
    self.G = None   # ジェネレータ(のネットワーク構造だけ)
    self.DM = None  # ディスクリミネータモデル(ディスクリミネータ + 訓練ルール)
    self.AM = None  # アドバーサリアルモデル(ジェネレータ + ディスクリミネータ + 訓練ルール)

  def discriminator_network(self): # ディスクリミネータ: 畳み込み x 4回
    if self.D:
      return self.D
    self.D = Sequential()
    depth = 64
    dropout = 0.4
    input_shape = (self.img_rows, self.img_cols, self.channel)
    self.D.add(Conv2D(depth*1, 5, strides=2, padding='same', activation=LeakyReLU(alpha=0.2),
                      input_shape=input_shape))                 # 28 x 28 x 1 --> 14 x 14 x 64
    self.D.add(Dropout(dropout))
    self.D.add(Conv2D(depth*2, 5, strides=2, padding='same',
                      activation=LeakyReLU(alpha=0.2)))         # 14 x 14 x 64 --> 7 x 7 x 128
    self.D.add(Dropout(dropout))
    self.D.add(Conv2D(depth*4, 5, strides=2, padding='same', 
                      activation=LeakyReLU(alpha=0.2)))         # 7 x 7 x 128 --> 4 x 4 x 256
    self.D.add(Dropout(dropout))
    self.D.add(Conv2D(depth*8, 5, strides=1, padding='same', 
                      activation=LeakyReLU(alpha=0.2)))         # 4 x 4 x 256 --> 4 x 4 x 512
    self.D.add(Dropout(dropout))
    self.D.add(Flatten())                                       # 4 x 4 x 512 --> 8192
    self.D.add(Dense(1, activation='sigmoid'))                  # 8192 --> 1
    return self.D

  def generator_network(self): # ジェネレータ: 逆畳み込み x 4回
    if self.G:
      return self.G
    self.G = Sequential()
    dropout = 0.4
    depth = 64 + 64 + 64 + 64
    dim = 7
    self.G.add(Dense(dim*dim*depth, input_dim=100))             # 100 --> 12544
    self.G.add(BatchNormalization(momentum=0.9))
    self.G.add(Activation('relu'))
    self.G.add(Reshape((dim, dim, depth)))                       # 12544 --> 7 x 7 x 256
    self.G.add(Dropout(dropout))
    self.G.add(UpSampling2D())                                   # 7 x 7 x 256 --> 14 x 14 x 256
    self.G.add(Conv2DTranspose(int(depth/2), 5, padding='same')) # 14 x 14 x 256 --> 14 x 14 x 128
    self.G.add(BatchNormalization(momentum=0.9))
    self.G.add(Activation('relu'))
    self.G.add(UpSampling2D())                                   # 14 x 14 x 128 --> 28 x 28 x 128
    self.G.add(Conv2DTranspose(int(depth/4), 5, padding='same')) # 28 x 28 x 128 --> 28 x 28 x 64
    self.G.add(BatchNormalization(momentum=0.9))
    self.G.add(Activation('relu'))
    self.G.add(Conv2DTranspose(int(depth/8), 5, padding='same')) # 28 x 28 x 64 --> 28 x 28 x 32
    self.G.add(BatchNormalization(momentum=0.9))
    self.G.add(Activation('relu'))
    self.G.add(Conv2DTranspose(1, 5, padding='same', 
               activation='sigmoid'))                            # 28 x 28 x 32 --> 28 x 28 x 1
    return self.G

  def discriminator_model(self):
    if self.DM:
      return self.DM
    optimizer = RMSprop(lr=0.0002, decay=6e-8)
    self.DM = Sequential()
    self.DM.add(self.discriminator_network())
    self.DM.compile(loss='binary_crossentropy', optimizer=optimizer, metrics=['accuracy'])
    return self.DM

  def adversarial_model(self):
    if self.AM:
      return self.AM
    optimizer = RMSprop(lr=0.0001, decay=3e-8)
    self.AM = Sequential()
    self.AM.add(self.generator_network())
    self.AM.add(self.discriminator_network())
    self.AM.compile(loss='binary_crossentropy', optimizer=optimizer, metrics=['accuracy'])
    return self.AM

class MNIST_DCGAN(object):
  def __init__(self):
    self.img_rows = 28
    self.img_cols = 28
    self.channel = 1

    # データ読み込み
    # データをしぼらない場合
    #self.x_train = input_data.read_data_sets("mnist", one_hot=True).train.images
    # データをしぼる場合
    mnist = input_data.read_data_sets("mnist", one_hot=False)
    images = mnist.train.images
    labels = mnist.train.labels
    images = images[np.where(labels == 1)] # 「1」だけにしぼる場合
    
    self.x_train = images
    self.x_train = self.x_train.reshape(-1, self.img_rows, self.img_cols, 1).astype(np.float32)
    self.DCGAN = DCGAN()
    self.discriminator_model =  self.DCGAN.discriminator_model()
    self.adversarial_model = self.DCGAN.adversarial_model()
    self.generator_network = self.DCGAN.generator_network()

  def train(self, train_steps=2000, batch_size=256, save_interval=0):
    # 学習の途中でジェネレータ出力を吐き出す場合、それ用の乱数を確保しておく
    noise_input = None
    if save_interval > 0:
      noise_input = np.random.uniform(-1.0, 1.0, size=[16, 100])
    
    # GAN の訓練
    for i in range(train_steps):
      # (1) batch_size 個の乱数から batch_size 個の模造データ作成
      noise = np.random.uniform(-1.0, 1.0, size=[batch_size, 100])
      images_fake = self.generator_network.predict(noise)
      # (2) batch_size 個の本物データと batch_size 個の模造データでディスクリミネータを訓練
      images_train = self.x_train[np.random.randint(0, self.x_train.shape[0], size=batch_size), :, :, :]
      x = np.concatenate((images_train, images_fake))
      y = np.ones([2*batch_size, 1])
      y[batch_size:, :] = 0
      d_loss = self.discriminator_model.train_on_batch(x, y)
      # (3) batch_size 個の乱数でアドバーサリアルモデル(ジェネレータ+ディスクリミネータ)を訓練
      y = np.ones([batch_size, 1])
      noise = np.random.uniform(-1.0, 1.0, size=[batch_size, 100])
      a_loss = self.adversarial_model.train_on_batch(noise, y)
      
      log_mesg = "%d: [D loss: %f, acc: %f]" % (i, d_loss[0], d_loss[1])
      log_mesg = "%s  [A loss: %f, acc: %f]" % (log_mesg, a_loss[0], a_loss[1])
      print(log_mesg)
      
      if save_interval > 0:
        if (i+1) % save_interval == 0:
          self.plot_images(samples=noise_input.shape[0], noise=noise_input, step=(i+1))

  def plot_images(self, fake=True, samples=16, noise=None, step=0):
    filename = 'mnist.png'
    if fake:
      if noise is None:
        noise = np.random.uniform(-1.0, 1.0, size=[samples, 100])
      else:
        filename = "mnist_%d.png" % step
      images = self.generator_network.predict(noise)
    else:
      i = np.random.randint(0, self.x_train.shape[0], samples)
      images = self.x_train[i, :, :, :]
    plt.figure(figsize=(10,10))
    for i in range(images.shape[0]):
      plt.subplot(4, 4, i+1)
      image = images[i, :, :, :]
      image = np.reshape(image, [self.img_rows, self.img_cols])
      plt.imshow(image, cmap='gray')
      plt.axis('off')
    plt.tight_layout()
    plt.savefig(filename)
    plt.close('all')

if __name__ == '__main__':
  mnist_dcgan = MNIST_DCGAN()
  mnist_dcgan.train(train_steps=10000, batch_size=256, save_interval=25)
  mnist_dcgan.plot_images(fake=True)
  mnist_dcgan.plot_images(fake=False)

2017-12-11 週の日記

12/11(月)~12/17(日)の日記を書きます。

参加したイベント

第10回 強化学習アーキテクチャ勉強会 - connpass(12/12)
  • スライドがアップされていました: GAN(と強化学習との関係)
  • 生成モデルって何だったっけ; 生成モデルは日本人の顔が出てくる箱。逆にこの箱をつくるには日本人の顔のデータが要る。生成モデルをつくるときに、「特徴量をニューラルネットに入れて出てくるものを取り出す」というやり方でつくろうとするのが VAE や GAN。ただし両者はこの箱の学習の仕方が異なる。
    • VAEは入力と出力の差が小さくなるようにエンコーダ+デコーダを学習して、デコーダに適当な値を入力すると模造データが出てくる。
      • ところでVAEは特徴量がガウス分布であることまで求めるのでしたっけ。
    • GANでは、まずジェネレータ  G が乱数から模造データを生成して、ディスクリミネータ  D がそれを模造データかどうか純正データか識別する。それを受けてジェネレータはディスクリミネータに純正データと識別してもらえるように模造データを純粋データに近づける。ディスクリミネータはそれでも識別できるようにする。その繰り返し。
      • MCMC のようだと思った; MCMC でいう標本分布=模造データの分布で、MCMCでいう真の分布=純正データの分布。
  • JSダイバージェンス
  • 課題:  D が未熟だと、 G は同じような模造データばかりつくってしまう。なので、 D の性能を十分上げておきたいが、といって完璧にしてしまうと、 G はネットワークの重みをどの方向に更新しても損失が変わらなくなってしまい、学習できなくなる。
  • 純正データは純正データ空間のとても局所的なところにしか存在しないので、 G がもともと上手い模造データをつくらないとほぼ交わらないというのも MCMC に似ている。それを解決するのがワッサースタイン距離。
  • GAN がよいかどうかの評価は、GANをつかう目的に照らし合わせてだと思った(GANそのものの研究についての論文ではもちろんGANだけで評価することが必要ですが)。
  • アクター・クリティックについて昔作成したスライド(この前頁が REINFORCE):
  • 強化学習でいう「期待収益をよくすること」と GAN にとっての「純正データっぽい模造データを出すこと」はどうアナロジーなのか。強化学習での方策の期待収益を見極める=GANの真贋を見極めるで、よい方策を求める=精巧な模造品をつくるで、訊こうと思ったけどまあいいや。
Speee もくもく会(12/16)

また昼食をいただきました。ありがとうございました。

mockmock.dev #142 - connpass(12/17)

遅刻してしまいました。ありがとうございました。

2017-12-04 週の日記

最近雑記ばかりで雑すぎるので週次の日記にしようと思います。12/4(月)~12/10(日)の日記にしようと思いますがついでなのでその前の週のことも一緒に書きます。

読んだ記事

GitHub - NVIDIA/sentiment-discovery: Unsupervised Language Modeling at scale for robust sentiment classification
  • テキストを単語列ではなく文字列として学習してセンチメント分類しても上手くいくという話(辞書を作成しないから Unsupervised?)。
  • 単語列ではなく文字列として扱うことで、capitalized/uncaptilized の取り扱いを明示的に与えなくていいし、ミススペルにも対応できるし、未知語にも対応できる。
  • でも英語は26文字しかないけど日本語は平仮名、片仮名、漢字がたくさんあるけど上手くいくのだろうか。
Interpretable Machine Learning
  • 機械学習モデルを解釈可能にするという話。GitHub Pages で作成してあるようなので自分もいつかこういうのを書きたいなあ。
  • model-agnostics: モデルに依存しない(という意味でいいのですよね?)
[1712.02029] AdaBatch: Adaptive Batch Sizes for Training Deep Neural Networks
  • SGDによる深層ニューラルネットの最適化は学習率とバッチサイズを慎重に選ぶ必要がある。バッチサイズが小さい方が少ないエポック数で収束するけどバッチサイズが大きい方がたくさん並列計算できるので計算効率がいい。なのでエポックを経るごとにバッチサイズを大きくしていくことで、小さい固定バッチサイズ並のエポック数で大きい固定バッチサイズ並の計算効率を達成しましたという話(?)。

参加したイベント

第9回 強化学習アーキテクチャ勉強会 - connpass(11/28)
  • この勉強会の中で、紹介された高次行動を抽象化する解法だとベルマン最適方程式の解より高い報酬が得られるのかという質問があって、得られるのではないかというやり取りがあったのですが、それを聞いていて、報酬が高くなるのではなくてよりよい解にたどり着く確率とか収束の速さがよくなるのではないかなと思ったんですが、オンライン学習なら確かによくなるのかなと思いました(オンライン学習の解ってどこなんですけど)。よくわかりません。
  • オプションのサブゴールは所与ということだったのですが、サブゴールもタスクから決める論文もあるとかおっしゃっていたのですが、もっとちゃんと訊けばよかったです。
Gunosyデータマイニング研究会 #132 - connpass(11/29)
  • 分散未知の場合の共役事前分布がなんでウィシャート分布じゃなくて逆ガンマ分布なのかと思ったのですが、共分散行列を  \sigma^2 I に決め打っているからで、でも共分散行列が  \sigma^2 I って何だろう、予測対象変数が各要素が独立の確率値とかだったらそんな状況もあるのだろうかと思いました。
Speee もくもく会 #31 - connpass(12/2)
  • 朝食も昼食もご馳走になってしまいました…なんかすみません…。
朝もくもく会@神田橋 - connpass(12/7)
  • もくもく会を主催してみたのですが誰も来ませんでした。逆に来たらびっくりするんでそれはいいんですけど、1時間というのはどうにも短いと思いました。30分単位で借りられれば30分延ばしたいんですが。もっとアクセスがよさそうな神田駅前にも貸し会議室を見つけましたが、神田駅に寄るのが面倒なので次もここでやります。
mockmock.dev #141 - connpass(12/10)
  • Slack 上のもくもく会に参加させていただきました。実は冒頭50分昼食作って食べてました。すみません…。

その他

来週やりたいこと

  • GitHub Pages をせっかくつくったので、週の日記にはその週に読んだことを、GitHub Pages にはその蓄積を配置したいですが、フォルダ以下に記事群を置いたら PythonPerl で各記事にリンクを張った HTML を生成するようにしたい(そんなことをやっているので文献読みが進みません)。