鴨川η

not δ

オタク機械学習勉強会#0のLTで使ったSCRNについて

更新

Keras v2に対応したコードに書き直しました.本記事のコードがKeras v2で動く保証はできません.

はじめに

2016年06月05日にオタク機械学習勉強会#0があり,LT枠で参加しました.機械学習が盛んなのか,わかりませんが,約100人の参加者がいて,小さい研究会よりも人が多く,胃を痛めながら資料を作成しました.

発表スライドはこちらで公開してあります.

自分のLTの内容

多くの方が画像を使われる中,一切画像を使わない発表としてSimpleRNNを基にした言語モデルについてざっくりとした説明を城ヶ崎美嘉というキャラのセリフを使って紹介しました.

ちなみになぜ城ヶ崎美嘉なのかというとSHIROBAKOというアニメの安原絵麻というキャラの声優が同じだからとセリフデータがありそうだったからです.

城ヶ崎美嘉ネタは実は今回で3つ目でして,過去に

に取り組んでました.今回の差分としては

  • セリフ数を増やしたこと
  • RNNLMの構造をより単純なものに変更したこと

です.

さてなぜ単純化したかですが,

  1. 発表2日前にLSTM2層重ねたRNNLMを書いてGPUマシンで回して寝る
  2. 起きたらGPUマシンにログインできず,作業ができないしデータも取り出せない
  3. やる気が無くなったので前の実験結果でお茶を濁すことに
  4. YoutubeでTomas Mikolov氏のThe Roadmap towards Machine Intelligenceを見てたら「LSTM使うより速いし性能もそこそこあるよみたいなこと」を偶然見つける
  5. その論文Learning Longer Memory in Recurrent Neural Networksを読む
  6. Kerasでモデルを書く(一部フォロワーさんに教えてもらった)
  7. CPUでも回せる程度の速度になった(勉強会開始の半日前)

みたいな経過がありました. パラメータがあれなのかデータが少ないのか SimpleRNN 単体でもそこそこ上手くいくことにあとで気づきました.

本題

コードはjupyter notebookで公開しています. スライドの結果を出した時から一部修正を加えたので,セリフが異なっていますのでご注意を.

RNNのユニットにSimpleRNN(全結合層で再帰しているもの)を用いた場合,時間方向に展開すると多層パーセプトロンと同じように誤差逆伝播法が使えます. しかし系列長が長くなるとその分層が深くなってしまうので,勾配消失問題が起こり,学習がうまく行われません.

そこでRNNのユニットにゲートを持ったようなLSTMを使うことで比較的長い依存関係を記憶できます.とはいうものの,LSTMは通常のSimpleRNNと比較すると最適化するパラメータが多く,学習に時間がかかります.そこでMikolov et.alが提案したのが, Structurally Constrained Recurrent Network (SCRN) です(word2vecの人).

スクリーンショット 2016-06-05 11.37.07.png 論文より転載

SimpleRNNが左で,右がSCRNです. の層は,SimpleRNNと同じ全結合の再帰層ですが,それに加えて という よりさらに低次元な再帰層を持ちます. ですが,学習しない場合0.95のような固定値の係数,学習する場合は を要素にもつ対角行列です.こうすることで勾配消失問題を防ごうとしているのがこのネットワークです.

実装

例によってKerasで書きます.

実装上で問題になるのは です. この再帰層は重みやバイアスがなく,だけもつことから独自にクラスを定義します.

class ContextRNN(Recurrent):
    def __init__(self,
                 init='zero',
                 alpha=0.95,
                 **kwargs):
        self.init = initializations.get(init)
        self.alpha = alpha

        super(ContextRNN, self).__init__(**kwargs)

    def build(self, input_shape):
        self.input_spec = [InputSpec(shape=input_shape)]
        if self.stateful:
            self.reset_states()
        else:
            self.states = [None]
        input_dim = input_shape[2]
        self.input_dim = input_dim
        self.output_dim = input_dim

    def reset_states(self):
        assert self.stateful, 'Layer must be stateful.'
        input_shape = self.input_spec[0].shape
        if not input_shape[0]:
            raise Exception('If a RNN is stateful, a complete ' +
                            'input_shape must be provided (including batch size).')
        if hasattr(self, 'states'):
            K.set_value(self.states[0],
                        np.zeros((input_shape[0], self.output_dim)))
        else:
            self.states = [K.zeros((input_shape[0], self.output_dim))]

    def step(self, x, states):
        prev_output = states[0]
        output = (1.-self.alpha)*x + (self.alpha)*prev_output
        return output, [output]

    def get_config(self):
        config = {'output_dim': self.output_dim,
                  'alpha': self.alpha
                  }
        base_config = super(ContextRNN, self).get_config()
        return dict(list(base_config.items()) + list(config.items()))

ここではSimpleRNN をベースにしました.

加えて は全時系列を使い,は最終時刻のみ使います. KerasのRecurrent Layersは全時系列か最終時刻しか返さないので,両方使う場合は全時系列を返した結果に Labmda を使います.

def get_RNN_last(x):
    return x[:,-1,:]

def get_RNN_last_shape(input_shape):
    assert len(input_shape) == 3
    return (None, input_shape[-1])

nb_hidden = 100
nb_context = 40
m = 100
w = Input(shape=(maxlen,), dtype='int32', name='x')

#slow
b = Embedding(output_dim=nb_context, input_dim=V, input_length=maxlen, name='Bx')(w)
s = ContextRNN(return_sequences=True, name="s")(b)
ps = TimeDistributed(Dense(m), name="Ps")(s)

s = Lambda(lambda x: get_RNN_last(x), output_shape=get_RNN_last_shape, name="s_T")(s)
v = Dense(V, name="Vs")(s)

# fast
x = Embedding(output_dim=m, input_dim=V, input_length=maxlen, name='Ax')(w)
x = merge([x, ps], mode='sum')

x = SimpleRNN(nb_hidden, activation="sigmoid", return_sequences=False, name="h")(x)
x = Dense(V)(x)
x = merge([x,v], mode='sum')
y = Activation("softmax", name='y')(x)

model = Model(input=w, output=y)

可視化するとこんな感じです. スクリーンショット 2016-06-05 20.32.52.png

実行結果や生成されたセリフはnotebookを参考ください.