読者です 読者をやめる 読者になる 読者になる

Keras で変分自己符号化器(VAE)を学習したい

以下の記事の続きです。Kerasブログの自己符号化器チュートリアルをやるだけです。
Keras で自己符号化器を学習したい - クッキーの日記



Kerasブログの自己符号化器チュートリアルBuilding Autoencoders in Keras)の最後、Variational autoencoder(変分自己符号化器;VAE)をやります。VAE についてのチュートリアル上の説明は簡単なものなので、以下では自分で言葉を補っています。そのため、不正確な記述があるかもしれません。

変分自己符号化器(VAE)って何

そのデータが生成するメカニズムに仮定をおいているとき(そのデータの生成モデルを仮定しているとき)、モデルのパラメータの最適化をするのに VAE を用いることができます。今回は、「それぞれの手書き数字には、その手書き数字に対応する隠れ変数の確率分布があって、その分布からの実現値を変換することによって手書き数字ができている」と仮定しています。さらに、その隠れ変数の次元は2次元であって(  \in {\mathbb R}^2 )、隠れ変数の確率分布は2次元正規分布 \mu, \, \Sigma によってのみ決まる )と仮定しています。この分布からの実現値が f: {\mathbb R}^2 \to {\mathbb R}^{784} なる写像によって 784 ピクセルに変換されて目の前に手書き数字として具現化したのだと考えているわけです。

f:id:cookie-box:20170513202623p:plain:w360
なので、今回訓練するのは以下のようなモデルになります。
後から気付いたのですが、分散ベクトルの次元が2であることからわかるように、第1変数と第2変数が独立な2次元正規分布を考えているようです。したがって、図のように分布は斜めに傾きません。が、面倒なので描き直しません。

f:id:cookie-box:20170513200541p:plain:w720

ここまでの自己符号化器では、入力と出力の交差エントロピーを損失関数としてモデルを訓練していました。VAE でも同様に訓練してもよいのですが、今回は入力と出力の交差エントロピーに、隠れ変数空間の事前分布と事後分布のKL情報量も加えたものを損失関数とします。つまり、隠れ変数の確率分布が大幅に更新されることに制約を課すわけですが、こうすることで、隠れ変数をきれいに( well-formed ? )モデリングでき、過学習も防げるそうです。

  • このような特殊な損失関数を実現するために、訓練時には自分で定義したレイヤーを最後にかぶせて、そのレイヤーで入出力の交差エントロピーと事前事後分布のKL情報量を損失関数に加えています(スクリプトは記事の一番最後)。そして、compile() では損失関数を指定していません( loss=None )。Keras の仕様をきちんと確認していないのですが、Layerクラスを継承して自分で定義したレイヤーでは損失関数を付加するということができ、それより手前の層に誤差逆伝播するのだと思います。たぶん。
実行結果

今回、上図の VAE を MNIST の 60000 の訓練用データで学習しました(バッチサイズ:100、エポック数:10)。
テストデータについて、隠れ変数の確率分布の平均をプロットすると以下のようになりました。同じ数字のデータを同じ色でプロットしています。左上の濃い紫のかたまりが "0" です。左下のもう少し薄い紫のかたまりが "1" です。右下の黄緑色のかたまりは "7" です。同じ数字が隠れ変数空間上でかたまる傾向が確認できます。ただ、Kerasブログの元記事とは数字のかたまり方の形や配置が結構異なっています。

   f:id:cookie-box:20170513223659p:plain:w550

また、隠れ変数空間から均等にサンプリングした点がどんな「手書き数字」を生成するか(どうデコードされるか)というのも確かめることができます。以下のようになりました。実行したスクリプトの関係で上のプロットと上下が反転していますが、上のプロットと対応するように左上の方が "1"、右上の方が "7" に見えるのが確認できます。下の方は "0" に見えます。右側の真ん中あたりが "9" に見えますが、ちょうど上の図でも右側の真ん中あたりに "9" (黄色)が見えます。
f:id:cookie-box:20170513223749p:plain:w770

スクリプト

補助関数の定義とクラスの定義を冒頭にもってきているのと日本語のコメントを加えている以外は以下にあるスクリプトと同じです。ただし、2番目のプロットはブログ記事のスクリプトと記述が違ったのでブログ記事の方に合わせています。
keras/variational_autoencoder.py at master · fchollet/keras · GitHub

# -*- coding: utf-8 -*-
import numpy
import matplotlib.pyplot as plt
from scipy.stats import norm
import sys

from keras.layers import Input, Dense, Lambda, Layer
from keras.models import Model
from keras import backend, metrics
from keras.datasets import mnist

# 2次元正規分布から1点サンプリングする補助関数です
def sampling(args):
  z_mean, z_log_var = args
  epsilon = backend.random_normal(shape=(batch_size, latent_dim), mean=0., stddev=epsilon_std)
  return z_mean + backend.exp(z_log_var / 2) * epsilon

# Keras の Layer クラスを継承してオリジナルの損失関数を付加するレイヤーをつくります
class CustomVariationalLayer(Layer):
  def __init__(self, **kwargs):
    self.is_placeholder = True
    super(CustomVariationalLayer, self).__init__(**kwargs)

  def vae_loss(self, x, x_decoded_mean): # オリジナルの損失関数
    # 入力と出力の交差エントロピー
    xent_loss = original_dim * metrics.binary_crossentropy(x, x_decoded_mean) 
    # 事前分布と事後分布のKL情報量
    kl_loss = - 0.5 * backend.sum(1 + z_log_var - backend.square(z_mean) - backend.exp(z_log_var), axis=-1)
    return backend.mean(xent_loss + kl_loss)

  def call(self, inputs):
    x = inputs[0]
    x_decoded_mean = inputs[1]
    loss = self.vae_loss(x, x_decoded_mean)
    self.add_loss(loss, inputs=inputs) # オリジナルの損失関数を付加
    return x # この自作レイヤーの出力を一応定義しておきますが、今回この出力は全く使いません

if __name__ == "__main__":
  batch_size = 100
  original_dim = 784
  latent_dim = 2
  intermediate_dim = 256
  epochs = 10
  epsilon_std = 1.0

  #============================================================
  # 変分自己符号化器を構築します

  # エンコーダ
  x = Input(batch_shape=(batch_size, original_dim))
  h = Dense(intermediate_dim, activation='relu')(x)
  z_mean = Dense(latent_dim)(h)
  z_log_var = Dense(latent_dim)(h)
  z = Lambda(sampling, output_shape=(latent_dim,))([z_mean, z_log_var])

  # デコーダ
  decoder_h = Dense(intermediate_dim, activation='relu')
  decoder_mean = Dense(original_dim, activation='sigmoid')
  h_decoded = decoder_h(z)
  x_decoded_mean = decoder_mean(h_decoded)

  # カスタマイズした損失関数を付加する訓練用レイヤー
  y = CustomVariationalLayer()([x, x_decoded_mean])
  vae = Model(x, y)
  vae.compile(optimizer='rmsprop', loss=None)

  #============================================================
  # モデルを訓練します

  (x_train, y_train), (x_test, y_test) = mnist.load_data()
  x_train = x_train.astype('float32') / 255.
  x_test = x_test.astype('float32') / 255.
  x_train = x_train.reshape((len(x_train), numpy.prod(x_train.shape[1:])))
  x_test = x_test.reshape((len(x_test), numpy.prod(x_test.shape[1:])))
  vae.fit(x_train, shuffle=True, epochs=epochs, batch_size=batch_size,
          validation_data=(x_test, x_test))

  #============================================================
  # 結果を表示します

  # (1) 隠れ変数空間のプロット(エンコードした状態のプロット)
  encoder = Model(x, z_mean) # エンコーダのみ分離
  x_test_encoded = encoder.predict(x_test, batch_size=batch_size)
  plt.figure(figsize=(6, 6))
  plt.scatter(x_test_encoded[:, 0], x_test_encoded[:, 1], c=y_test)
  plt.colorbar()
  plt.show()

  # (2) 隠れ変数空間からサンプリングした点がどんな手書き数字を生成するか(どうデコードされるか)をプロット
  decoder_input = Input(shape=(latent_dim,))
  _h_decoded = decoder_h(decoder_input)
  _x_decoded_mean = decoder_mean(_h_decoded)
  generator = Model(decoder_input, _x_decoded_mean) # デコーダのみ分離

  n = 15  # 15x15 個の手書き数字をプロットする
  digit_size = 28
  figure = numpy.zeros((digit_size * n, digit_size * n))
  grid_x = numpy.linspace(-15, 15, n)
  grid_y = numpy.linspace(-15, 15, n)

  for i, yi in enumerate(grid_x):
    for j, xi in enumerate(grid_y):
      z_sample = numpy.array([[xi, yi]])
      x_decoded = generator.predict(z_sample)
      digit = x_decoded[0].reshape(digit_size, digit_size)
      figure[i * digit_size: (i + 1) * digit_size,
             j * digit_size: (j + 1) * digit_size] = digit

  plt.figure(figsize=(10, 10))
  plt.imshow(figure)
  plt.show()

自己符号化器でイラストが再現できるかな

参考文献
  1. Building Autoencoders in Keras の Convolutional autoencoder のモデル
  2. かわいいフリー素材集 いらすとや
手順
  • いらすとや(参考文献2)のカレー、ハンバーガー、ラーメンの画像を保存します。
  • 画像を読み込んで 64×64 にリサイズします。
  • 以下の自己符号器を学習します(参考文献1)。次元は少し異なります。エンコード後の shape は (8, 8, 8) です。

f:id:cookie-box:20170507222625p:plain:w720

結果

上段が元画像(リサイズで解像度が落ちています)、下段がエンコード→デコード後の画像です。
エンコード→デコード後はぼやけていますがカレー、ハンバーガー、ラーメンが再現できていると思います。

f:id:cookie-box:20170508220651p:plain:w480

2017-05-09 追記
学習した自己符号器でいらすとやの別のいらすとが再現できるかもテストしてみます。
いらすとやの別のハンバーガー、ラーメンをテストします(右側の2枚)。
→ 訓練用の3枚の再現に特化しすぎていて全然駄目でした。
f:id:cookie-box:20170509061643p:plain:w800

スクリプト
# -*- coding: utf-8 -*-
import numpy
import cv2
import matplotlib.pyplot as plt
import sys

from keras.layers import Input, Dense, Conv2D, MaxPooling2D, UpSampling2D
from keras.models import Model

if __name__ == "__main__":
  #============================================================
  # 画像を読み込みます
  image_files = ['/Users/cookie/Downloads/food_curryruce.png',
                 '/Users/cookie/Downloads/food_hamburger.png',
                 '/Users/cookie/Downloads/food_ramen_iekei.png']
  n_image = len(image_files)
  images = []
  size = (64, 64)
  for image_file in image_files:
    image = cv2.imread(image_file)
    image = cv2.resize(image, size)
    # plt.imshow(cv2.cvtColor(image, cv2.COLOR_BGR2RGB))
    # plt.show()
    image = image.astype('float32') / 255.
    images.append(image)

  images = numpy.array(images)

  #============================================================
  # 自己符号化器を構築します
  input_img = Input(shape=(64, 64, 3))
  x = Conv2D(16, 3, 3, activation='relu', border_mode='same')(input_img)
  x = MaxPooling2D((2, 2), border_mode='same')(x)
  x = Conv2D(8, 3, 3, activation='relu', border_mode='same')(x)
  x = MaxPooling2D((2, 2), border_mode='same')(x)
  x = Conv2D(8, 3, 3, activation='relu', border_mode='same')(x)
  encoded = MaxPooling2D((2, 2), border_mode='same')(x)

  x = Conv2D(8, 3, 3, activation='relu', border_mode='same')(encoded)
  x = UpSampling2D((2, 2))(x)
  x = Conv2D(8, 3, 3, activation='relu', border_mode='same')(x)
  x = UpSampling2D((2, 2))(x)
  x = Conv2D(16, 3, 3, activation='relu', border_mode='same')(x)
  x = UpSampling2D((2, 2))(x)
  decoded = Conv2D(3, 3, 3, activation='sigmoid', border_mode='same')(x)

  autoencoder = Model(input_img, decoded)
  print(autoencoder.summary())

  autoencoder.compile(optimizer='adadelta', loss='binary_crossentropy')

  autoencoder.fit(images, images, nb_epoch=10000, batch_size=3)

  #============================================================
  # 結果を表示します
  decoded_images = autoencoder.predict(images)

  for i in range(n_image):
    # 元画像の表示
    ax = plt.subplot(2, n_image, i+1)
    plt.imshow(cv2.cvtColor(images[i], cv2.COLOR_BGR2RGB))
    ax.get_xaxis().set_visible(False)
    ax.get_yaxis().set_visible(False)

    # エンコード→デコードした画像の表示
    ax = plt.subplot(2, n_image, i+1+n_image)
    plt.imshow(cv2.cvtColor(decoded_images[i], cv2.COLOR_BGR2RGB))
    ax.get_xaxis().set_visible(False)
    ax.get_yaxis().set_visible(False)

  plt.show()

Keras で自己符号化器を学習したい

Kerasブログの自己符号化器のチュートリアルをやります。
Building Autoencoders in Keras


このチュートリアルではMNISTの手書き数字のデータを例に色々な種類の自己符号化器を示しています。スクリプトは全て記事内に示されているので割愛します。上記の記事内でのモデルは Sequential() で生成したインスタンスに層を add していくのではなく、Model (functional API) で組み立てています。 この方法だと自己符号化器(エンコーダ + デコーダ)全体を学習して後からエンコーダ(デコーダ)部分のみ利用するというのが容易にできました。

以下はチュートリアル内で紹介されているモデルの理解のためのお絵描きです(この記事はお絵描きだけです)。

モデル1: 単純な自己符号化器

f:id:cookie-box:20170507160835p:plain:w600
このモデルの自分の環境での実行結果は以下のようになりました。
f:id:cookie-box:20170507160620p:plain:w770

モデル2: エンコーダがスパースな自己符号化器

f:id:cookie-box:20170507181112p:plain:w600
エンコーダの重みベクトルについてL1正則化項をペナルティにしてスパースにします。

モデル3: 多層な自己符号化器

f:id:cookie-box:20170507182856p:plain:w600

モデル4: 畳み込み自己符号化器

f:id:cookie-box:20170507222625p:plain:w800

モデル5: ノイズ除去モデル

f:id:cookie-box:20170507223517p:plain:w480

モデル6: 系列データに対する自己符号化器

f:id:cookie-box:20170507224843p:plain:w480
系列データに対する自己符号化器はこのように実現するらしいですが実例はありませんでした。

モデル7: Variational autoencoder(VAE)→ 別の記事

VAE についてのお絵描きと実行結果は以下の記事に書きました。
Keras で変分自己符号化器(VAE)を学習したい - クッキーの日記

雑記: 交差エントロピーって何

機械学習をしているとよく交差エントロピーを最小化させられると思います。
でも冷静に考えると交差エントロピーが何かよくわかりませんでした。むしろエントロピーがわかりませんでした。
以下の記事を読み、もっと無理がない例で短くまとめたかったのですが、やはり例に無理があり、長くなってしまいました。
参考文献

以下はこれら記事の劣化アレンジです。



A国、B国、C国があります。
A国では、一日の天気は25%ずつの確率で晴れ、曇り、雨、雪になります。B国では、晴れになる確率が50%、曇りになる確率が25%、雨か雪になる確率が12.5%ずつです。C国では天気は晴れにしかなりません。この3国の天気の確率関数をグラフに表すと以下のようになります。
f:id:cookie-box:20170506183749p:plain:w550

または、それぞれの天気になる確率を横向きの積み上げ棒グラフで表すと以下のようになります。
f:id:cookie-box:20170506184007p:plain:w250

あるとき、A国の気象庁に勤めるアリスは、A国内の大学から「過去の天気のデータをなるべくたくさんの日数ほしい」と申請を受けました。A国の法律で、気象庁からの情報提供は1枚のフロッピーディスク(死語)によって行うことになっていましたが、この架空の世界では記録媒体技術が残念だったのでフロッピーディスクの容量が1KB(8000ビット)しかありませんでした。しかも経費削減のため1件の情報提供にディスクは1枚しかつかえませんでした。そこで、アリスは容量を最大限に活用するためそれぞれの天気を「晴:00、曇:01、雨:10、雪:11」にエンコードし、1日の天気の情報を2ビットで表現することで、4000日分のデータを提供しました(8000 ÷ 2 = 4000)。

同じころ、B国の気象庁に勤めるボブもB国内の大学から同じ申請を受けました。B国も情報提供に係る制約はA国と同じです。ボブは、最初はアリスのように「晴:00、曇:01、雨:10、雪:11」というエンコードで情報を提供しようとしましたが、B国ではそれぞれの天気の確率に偏りがあることに気付きました。ボブは「晴:0、曇:10、雨:110、雪:111」と、確率が大きい晴れに短いビット数を割り振り、その代わり確率が小さい雨と雪に長いビット数を割り振ることで、1日の天気の情報の平均ビット数を 0.5*1 + 0.25*2 + 0.125*3 + 0.125*3 = 1.75 ビットにしました。こうすることで、8000ビットのディスクで4500日以上分のデータを提供しました(8000 ÷ 1.75 > 4500)。
f:id:cookie-box:20170506213818p:plain:w660
なお、C国の気象庁に勤めるクリスのもとには申請はありませんでした。C国の人々は申請するまでもなく過去の天気が晴れでしかないことを知っていました。いわば、C国のある1日の天気の情報を伝えるのに必要なビット数は0ビットでした。

アリスよりボブが同じ容量のディスクでよりたくさんの日数の情報を提供できたのは、ボブの方が優れたエンコードをしたからではありません。何ならクリスはディスクをつかうまでもなく無限の日数の情報を提供できています。1回の試行の結果(ある1日の天気)を伝えるのに必要な平均ビット数の最小値は確率分布によってのみ決まります。つまり、確率  P(\omega) で起きるできごとに長さ  \log (1/P(\omega)) のコードを割り当てるのが最適になります(証明略)。この最適なエンコードのとき、「晴、曇、雨、雪」を伝えるのに要するそれぞれのビット数  \log (1/P(\omega))情報量(選択情報量)、その期待値(1回の試行の結果を伝えるのに要する平均ビット数)をエントロピー(平均情報量)とよびます(以下)。
 \displaystyle H(P) = E_P \left[ \log \frac{1}{P(\omega)} \right] = \sum_{\omega \in \Omega} P(\omega) \log \frac{1}{P(\omega)}

A国、B国、C国それぞれの、「1日の天気を調べる」という試行のエントロピーは以下です。
 \begin{cases} H(P_A)=E_{P_A}\left[ \log_2(1/P_A(\omega)) \right]=0.25 \log_2 (1/0.25)+0.25 \log_2 (1/0.25)+0.25 \log_2 (1/0.25)+0.25 \log_2 (1/0.25)=2 \\ H(P_B)=E_{P_B}\left[ \log_2(1/P_B(\omega)) \right]=0.5 \log_2 (1/0.5)+0.25 \log_2 (1/0.25)+0.125 \log_2 (1/0.125)+0.125 \log_2 (1/0.125)=1.75  \\ H(P_C)=E_{P_C}\left[ \log_2(1/P_C(\omega)) \right]=1 \log_2 (1/1) =0\end{cases}

情報量を「その試行の結果を知らされたときの価値」、エントロピーを「1回の試行の結果の結果を知らされたときの価値の期待値」と解釈する人もあります。あるできごとを知らされたときの価値は、そのできごとが珍しいほど高くなります。A国ではどの天気になる確率も等しいので、どの天気だと知らされても等しく2の価値があります。B国では晴れになる確率が高いので、晴れだと知らされたときの価値は1しかありませんが、雨や雪になる確率は低いので、雨や雪だと知らされたときの価値は3あります。また、C国で晴れだと知らされても価値は0です。この価値の平均としてのエントロピーは不確実さが大きいほど(確率分布がばらつくほど)大きくなります。天気の不確実さは A国 > B国 > C国 の順になっているといえるでしょう。

ところであるとき、B国の大学でA国の天気のデータが必要になりました。B国の大学の人はボブ式のエンコードに慣れていたので、A国のアリスに、B国のボブ式のエンコードでA国の天気のデータを送るよう依頼しました。アリスは言われた通りにしましたが、A国の天気をボブ式にエンコードすると、1日あたりの平均ビット長が 1*0.25 + 2*0.25 + 3*0.25 + 3*0.25 = 2.25 になってしまうことに気付きました。この世界の残念なフロッピーディスクでは3600日分のデータも提供することができませんでした(8000 ÷ 2.25 < 3600)。

同じころボブもA国の大学の人にアリス式のエンコードでB国の天気データを提供するよう要請されました。アリス式のエンコードはどの天気にも2ビットをあてがうので、B国の天気をエンコードしても平均ビット長が2ビットになりました。ボブは、「自分のエンコードなら、この国の天気を1日あたり1.75ビットに圧縮できるのになあ」と思いました。
f:id:cookie-box:20170507075306p:plain:w220

ある確率分布に最適化された方式で別の確率分布をエンコードしたときの平均ビット長を交差エントロピーとよびます。
 \displaystyle H(P, Q) = E_P \left[ \log \frac{1}{Q(\omega)} \right] = \sum_{\omega \in \Omega} P(\omega) \log \frac{1}{Q(\omega)}

 \begin{cases} H(P_A, P_B)=E_{P_A}\left[ \log_2(1/P_B(\omega)) \right]=0.25 \log_2 (1/0.5)+0.25 \log_2 (1/0.25)+0.25 \log_2 (1/0.125)+0.25 \log_2 (1/0.125)=2.25 \\ H(P_B, P_A)=E_{P_B}\left[ \log_2(1/P_A(\omega)) \right]=0.5 \log_2 (1/0.25)+0.25 \log_2 (1/0.25)+0.125 \log_2 (1/0.25)+0.125 \log_2 (1/0.25)=2 \end{cases}

これはエンコードを誤ったようなイメージです。エンコードを誤ると、めずらしいできごとに小さなビット長を割り振ってしまう影響よりも、めずらしくないできごとに大きなビット長を割り振ってしまう影響の方が必ず大きくなり、必要な平均ビット長が大きくなってしまいます( H(P_A, P_B) > H(P_A, P_A) = H(P_A) )。つまり、ビット長の無駄が生じます。

H(P_A, P_B) は「B国の情報の価値尺度でA国の情報を受け取ったときの平均的な価値」と解釈することもできます。B国ではめずらしい雨や雪が、A国では比較的めずらしくないので、その分A国の天気をA国の尺度で受け取るよりも、A国の天気をB国の尺度で受け取った方が価値が上がってしまいます。

このビット長の無駄/価値の誤差 H(P_A, P_B) - H(P_A) = D_{\rm KL}(P_A \, || \, P_B)カルバック・ライブラー情報量とよびます。
 \displaystyle D_{\rm KL}(P \, || \, Q) =H(P,Q)-H(P)= E_P \left[ \log \frac{1}{Q(\omega)} - \log \frac{1}{P(\omega)} \right] = E_P \left[\log \frac{P(\omega)}{Q(\omega)} \right] = \sum_{\omega \in \Omega} P(\omega) \log \frac{P(\omega)}{Q(\omega)}

 \begin{cases} D_{\rm KL}(P_A \, || \, P_B)=E_{P_A}\left[ \log_2(1/P_B(\omega)) - \log_2(1/P_A(\omega)) \right]=0.25 (1-2)+0.25 (2-2)+0.25 (3-2)+0.25 (3-2)=0.25 \\ D_{\rm KL}(P_B \, || \, P_A)=E_{P_B}\left[ \log_2(1/P_A(\omega)) - \log_2(1/P_B(\omega)) \right]=0.5 (2-1)+0.25 (2-2)+0.125 (2-3)+0.125 (2-3)=0.25 \end{cases}

ここではたまたま D_{\rm KL}(P_A \, || \, P_B)=D_{\rm KL}(P_B \, || \, P_A) になっていますが、一般に、KL情報量は確率分布の交換について非対称です。「A国の天気をB国方式でエンコードしたときのビット長の無駄」と「A国の天気をB国方式でエンコードしたときのビット長の無駄」は一般には等しくなりません。例えば、天気が晴れにしかならないC国では、晴れ以外の天気は無限大のコード長をもつことになるので、C国の方式で他の国の天気をエンコードした交差エントロピーも、カルバック・ライブラー情報量も無限大になります。しかし、他の国の方式でC国の天気をエンコードした場合は無限大にはなりません。
 \begin{cases} H(P_A, P_C)=E_{P_A}\left[ \log_2(1/P_C(\omega)) \right]=+\infty \\ H(P_C, P_A)=E_{P_C}\left[ \log_2(1/P_A(\omega)) \right]=2 \end{cases}

 \begin{cases} D_{\rm KL}(P_A \, || \, P_C)=H(P_A, P_C)-H(P_A)=+\infty \\ D_{\rm KL}(P_C \, || \, P_A)=H(P_C, P_A)-H(P_C)=2 \end{cases}

 \begin{cases} H(P_B, P_C)=E_{P_B}\left[ \log_2(1/P_C(\omega)) \right]=+\infty \\ H(P_C, P_B)=E_{P_C}\left[ \log_2(1/P_B(\omega)) \right]=1 \end{cases}

 \begin{cases} D_{\rm KL}(P_B \, || \, P_C)=H(P_B, P_C)-H(P_B)=+\infty \\ D_{\rm KL}(P_C \, || \, P_B)=H(P_C, P_B)-H(P_C)=1 \end{cases}

KL情報量は2つの分布の間の距離のようなものと表現されることがあります。KL情報量は非対称なので、D_{\rm KL}(P \, || \, Q) P から見た  Q の近さといった方がいいのかもしれません。

ところで、機械学習では、分類器を学習するときに、正解の分布 P と分類器の予測分布 Q の交差エントロピー H(P, Q) を損失関数とすることがよくあります。交差エントロピーQ=P のときに最小となるので、分類器を学習して QP に近づくよう改善していく作業は、正解の分布に合わせてエンコードを最適化しようとする作業に似ています。P が固定ならば、H(P, Q) の最小化は  D_{\rm KL}(P \, || \, Q) の最小化に他なりません。また、学習の初期にC国のような分布からはじめてしまうと、正解の分布から見た距離がどうしようもなく遠くなってしまうことが予想されます(ちょうどA国の分布から見たC国の分布の交差エントロピーが無限大であるように)。

分布と分布がどれだけずれているかの尺度は何も交差エントロピーだけでなく、2乗誤差なども考えられます。2乗誤差も交差エントロピーも、小さくなるようにパラメータを更新し続ければ分類器が改善していくことが期待されます。交差エントロピーの利点は、分布がまるで期待外れのとき(誤差が大きいとき)学習の速度が速い点です。2乗誤差は、誤差が大きいときに必ずしも学習が速くなりません。例えばシグモイド関数やソフトマックス関数で活性化された分類器の出力があるクラスにかなり偏ってしまっているとき、それが期待とはほど遠い状態であるにもかかわらず、2乗誤差の勾配はとてもなだらかになってしまいます。この状態でパラメータを少し動かしたところで、分布はほとんど変わらず、2乗誤差はほぼ変わらないからです。交差エントロピーの場合、分類器の出力が期待せずかなり偏ってしまった場合、真の分布から見た交差エントロピーがそれこそ無限大の勢いで大きくなるので(A国の分布から見たC国の分布の交差エントロピーのように)、勾配も大きくなります。この期待外れの時に勾配が大きいという望ましい性質により、交差エントロピーは広く採用されています。

LSTMでどのキャラクターのセリフか判別する

Qiita に投稿しました。
qiita.com

Keras の LSTM で「数学ガール」に出てくるセリフを誰のセリフか判別しました。
数学ガール/フェルマーの最終定理 (数学ガールシリーズ 2) | 結城 浩 |本 | 通販 | Amazon
モデルの中身は以下の記事(映画レビューのポジティブ/ネガティブ分類)です。
Sequence Classification with LSTM Recurrent Neural Networks in Python with Keras - Machine Learning Mastery

所感

  • 今回のモデルでセリフの話者を正しく判定できなかったのは以下です(セリフは「数学ガール」からの引用です)。
    A:僕、B:ミルカさん、C:テトラちゃん、D:ユーリ
    f:id:cookie-box:20170504220718p:plain:w700
    • 「あたし」が出てきたらテトラちゃんのセリフだとわかってほしいような。
  • モデルの構造もハイパーパラメータも自由度がありすぎてどうするといいのかよくわかりませんでした。
    • ただ、「数学ガール」に出てくるセリフの話者を判別するのに必要な特徴量ベクトルの次元(LSTMの出力の次元)は映画レビューと違って100もない気がします(?)。例えば人間的な感覚では「女っぽさ」「先生っぽさ」「後輩っぽさ」の3次元くらいで判別できそう。「先生っぽさ」だけ強ければ「僕」、「女っぽさ」と「後輩っぽさ」が強ければ「テトラちゃん」とか…。ただ、LSTMの出力の次元をそこまで極端に減らすとかえって精度が悪くなりました。
    • 元モデルは英語の単語列に対するもので、今回は日本語の形態素列に対するものですが、畳み込みのフィルターサイズやマックスプーリングのプールサイズは変更するとかえって悪くなるようでした。
  • セリフの話者が判別できると何がうれしいの? → 特にないです。
  • 全キャラクターのセリフが100個以上収集できるまで、数学ガールを読み返しながらタイプしていたのですが、テトラちゃんが一番無口でボトルネックでした…。いや、テトラちゃんが無口というより、他の3人がしゃべるしゃべる…。