rnnlmを作ってみる その1 定義とか

深層学習による自然言語処理 の本によれば、

"言語モデル(Language model: LM)あるいは確率的言語モデル (probabilistic language model)とは、人間が扱う自然言語で書かれた文や文書が生成される確率をモデル化したものです"

となっています。

例えば、"今日はいい天気ですね"という文はよくある表現ですが、"今日はいい確率モデルですね"とは文脈によっては言うことは無いとは言えませんが最初の例よりは生成されにくいでしょうし、"カレーライスがいい確率モデルですね"などはわけがわからないですね。

このように、"文の自然さ"とか"文がよくありそうか"みたいなものはそれぞれ異なるのでそういったものを"文や文書が生成される確率"として表すのが言語モデルなんだと思います。

この場合、 P(今日はいい天気ですね) > P(今日はいい確率モデルですね) > P(レーライスがいい確率モデルですね) みたいな感じになるのだと思います。

{ \displaystyle {\bf y_t} } を単語とし、 { \displaystyle {\bf Y = \left( y_0, y_1, y_2, ... , y_T, y_{T+1} \right) } } が文を表すものとします。

{ \displaystyle {\bf y_t} } はone-hot ベクトルで表されているとします。

また、{ \displaystyle {\bf y_0} } { \displaystyle {\bf y_{T+1}} } はそれぞれ文頭と文末を表す擬似的な単語(BOSとEOS)とします。

再帰ニューラルネットワークを利用した言語モデル(recurrent neural network language model; rnnlm)は下記の様に表せます。

{ \displaystyle P_{rnnlm} ({\bf Y}) = \prod_{t=1}^{T+1} P(  {\bf y}_t | {\bf Y}_{\lbrack 0, t-1 \rbrack }) }

ここで { \displaystyle {\bf Y}_{\lbrack 0, t-1 \rbrack }   }{ \displaystyle {\bf Y = \left( y_0, y_1, y_2, ... , y_{t-1} \right) } } を表すものとします。

{ \displaystyle  P(  {\bf y}_t | {\bf Y}_{\lbrack 0, t-1 \rbrack }) } の計算は下記で行うとのことです。 (1層、活性化関数はtanhとした場合)

埋め込みベクトルの取得

{ \displaystyle \overline{{\bf y}}_t = {\bf E y}_{t-1} }

隠れ層の計算

{ \displaystyle {\bf h}_t = \tanh \left( {\bf W}^{(l)}  \begin{bmatrix}   \overline{{\bf y}}_t  \\  {\bf h}_{t-1}  \end{bmatrix}  + {\bf b}^{(l)}  \right)       }

出力層の計算

{ \displaystyle {\bf o}_t = {\bf W}^{(o)} {\bf h}_t + {\bf b}^{(o)}   }

確率化

{ \displaystyle {\bf p}_t = softmax ({\bf o}_t) }

確率の抽出

{ \displaystyle P(  {\bf y}_t | {\bf Y}_{\lbrack 0, t-1 \rbrack }) = {\bf p}_t \cdot {\bf y}_t  }

単語を入力するたびに、 { \displaystyle P(  {\bf y}_t | {\bf Y}_{\lbrack 0, t-1 \rbrack })   } を計算していき、その結果をすべてかけ合わせたものが { \displaystyle P_{rnnlm} ({\bf Y}) } となります

この出力からなんらかの誤差関数を評価してそれを最小化するようにEとかWを調整していくことになるのですが、それは次回書きます。

juman++を利用して分かち書き

ひとまずの目標がrnnlmを作ってみることなので、その準備として日本語の文書を渡してそれを形態素ごとに区切って出力するやつを作っておきたいです。

環境を汚したくないのでdocker内にjuman++をインストールします

FROM ubuntu:17.10

RUN apt-get update
RUN apt-get install -y git wget build-essential libboost-all-dev

RUN wget http://lotus.kuee.kyoto-u.ac.jp/nl-resource/jumanpp/jumanpp-1.02.tar.xz
RUN tar Jxf jumanpp-1.02.tar.xz
RUN cd jumanpp-1.02 && ./configure && make && make install

これは

docker build . -t jumanpp

みたいな感じでイメージを作成できます

イメージを作成したら

docker run -d -it --name jumanpp-container jumanpp

みたいな感じでコンテナを起動しておきます

すると

$ echo "吾輩は猫である。" | docker exec -i jumanpp-container jumanpp

吾輩 わがはい 吾輩 名詞 6 普通名詞 1 * 0 * 0 "代表表記:我が輩/わがはい カテゴリ:人"
は は は 助詞 9 副助詞 2 * 0 * 0 NIL
猫 ねこ 猫 名詞 6 普通名詞 1 * 0 * 0 "代表表記:猫/ねこ 漢字読み:訓 カテゴリ:動物"
である である だ 判定詞 4 * 0 判定詞 25 デアル列基本形 15 NIL
。 。 。 特殊 1 句点 1 * 0 * 0 NIL
EOS

みたいな感じで実行できます。


やりたいのは分かち書きなので、適当にシェルを書きます

#!/bin/bash

for line in $(cat -)
do
    echo "$line" | docker exec -i jumanpp-container jumanpp | while read output
    do
      if [ "${output}" != "EOS" ]; then
          word=$(echo ${output} | cut -d " " -f 1)
          if [ "${word}" != "@" ]; then
              result="${result} ${word}"
          fi
      else
          echo $result
      fi
    done
done

これをwakachi.shとか名前をつけて保存しておきます。

吾輩は猫である。
名前はまだ無い。
どこで生まれたかとんと見当がつかぬ。

みたいなファイルをテストのために用意しました

$ cat test.txt | ./wakachi.sh 
吾輩 は 猫 である 。
名前 は まだ 無い 。
どこ で 生まれた か とんと 見当 が つか ぬ 。

こんな感じで出力できました。

batch, epochについて学ぶ

1回のパラメータ更新のために利用するデータの数をbatch_size,
データを何周するかをepochと呼ぶみたいです

前回は簡単のためbatch_size = 1, epochs = 1 としましたが、現実的には両方1というのはあまりないと思うので前回のプログラムを修正してbatch_sizeとepochsに対応できるようにします。

dynamic_rnnのinputは [batch_size, max_len, input_size]なshapeなので、batch_size = 5, max_len = 3, input_size = 1なら

xs = [ [[1], [2], [3]],
       [[4], [5], [6]],
       [[7], [8], [9]],
       [[0], [0], [0]],
       [[1], [1], [1]] ] 

な感じで渡してやる必要があります

また、outputは [batch_size, max_len, cell.output_size] なshapeなので今回は上記xsと同じような形になります

output = [ [[1], [4], [7]],
           [[2], [5], [8]],
           [[3], [6], [9]],
           [[9], [9], [9]],
           [[8], [8], [8]] ] 

このとき、[ [1], [4], [7] ] は xsの [ [1], [2], [3] ] に対応していて、xsの1を入力したときの出力が1, 2を入力したときの出力が4, 3を入力したときの出力が7 となっているっぽいです。

今回の例では最後の入力を与えたときの出力がほしいので、outputの右端の列をもらうために、output[:, -1, 0]としています

まとめると、下記な感じになります。

import random
import tensorflow as tf
sess = tf.Session()

def generate_train_data(n):
    train_x = []
    train_y = []
    for i in range(n):
        xs = [random.random() * 10 for j in range(max_len)]
        ys = sum(xs)
        train_x.append(xs)
        train_y.append(ys)
    return train_x, train_y

size = 1
rnn_cell = tf.nn.rnn_cell.BasicRNNCell(num_units=size, activation=tf.nn.leaky_relu)

n_batch = 5
epochs = 4
max_len = 3
data_size = 1000

x = tf.placeholder(tf.float32, shape=[n_batch, max_len, size])

output, state = tf.nn.dynamic_rnn(rnn_cell, x, dtype=tf.float32)

y = tf.placeholder(tf.float32, shape=[n_batch])

loss = tf.reduce_mean(tf.square(y - output[:, -1, 0]))

optimizer = tf.train.GradientDescentOptimizer(0.0001)
train_step = optimizer.minimize(loss)

init = tf.global_variables_initializer()
sess.run(init)
losses = []
n_batches = int(data_size/n_batch)
x_train, y_train = generate_train_data(data_size)
for epoch in range(epochs):
    for i in range(n_batches):
        min_ix = i * n_batch
        max_ix = (i+1) * n_batch
        xs = x_train[min_ix:max_ix]
        # shape [batch_size, max_len, input_size]
        # xs = [ [[1], [2], [3]],
        #        [[4], [5], [6]],
        #        [[7], [8], [9]],
        #        [[0], [0], [0]],
        #        [[1], [1], [1]] ] なイメージ
        xs = [ [[xxx] for xxx in xx] for xx in xs]
        ys = y_train[min_ix:max_ix]
        sess.run(train_step, feed_dict={x: xs, y: ys})
        if i % 100 == 0:
            losses.append(sess.run(loss, feed_dict={x: xs, y: ys}))

print('vars : ', sess.run(rnn_cell.variables[0])) # RNNのウェイトを表示してみる
print('losses : ', losses)


結果は

vars :  [[ 1.0746733 ]
 [ 0.92348057]]
losses :  [138.32841, 0.81912673, 0.57693189, 0.40475434, 0.29114446, 0.1994734, 0.14868297, 0.098647617]

な感じでした

dynamic_rnn を使ってみる

dynamic_rnn は前回のinference相当の事をやってくれるらしいです。

In [47]: def inference(x, max_len):
    ...:     state = initial_state
    ...:     for i in range(max_len):
    ...:         (output, state) = rnn_cell(x[i], state)
    ...:     return tf.squeeze(output)

あと、前回reluを使いましたが変数の値がマイナスになった途端にoutputが0になっちゃうのでleaky reluとかを使ったほうが良さそうでした。

その辺を踏まえて、前回の足し合わせをdynamic_rnnを使って書き直してみました。

import random
import tensorflow as tf
sess = tf.Session()

size = 1
rnn_cell = tf.nn.rnn_cell.BasicRNNCell(num_units=size, activation=tf.nn.leaky_relu)

n_batch = 1
max_len = 3

x = tf.placeholder(tf.float32, shape=[n_batch, max_len, size])

output, state = tf.nn.dynamic_rnn(rnn_cell, x, dtype=tf.float32)

y = tf.placeholder(tf.float32, shape=[])

loss = tf.reduce_mean(tf.square(y - output[-1, -1, -1]))

optimizer = tf.train.GradientDescentOptimizer(0.0001)
train_step = optimizer.minimize(loss)

init = tf.global_variables_initializer()
sess.run(init)
losses = []
for i in range(3000):
    xs = [random.random() * 10 for j in range(max_len)]
    yy = sum(xs)
    # TODO batch_size, max_timeに対応する
    xs = [ [ [xx] for xx in xs] ] # shape 1, 3, 1
    sess.run(train_step, feed_dict={x: xs, y: yy})
    if i % 100 == 0:
        losses.append(sess.run(loss, feed_dict={x: xs, y: yy}))

print('vars : ', sess.run(rnn_cell.variables[0])) # RNNのウェイトを表示してみる
print('losses : ', losses)

動かしてみた結果がこちらです

vars :  [[ 0.94803673]
 [ 1.00511789]]
losses :  [189.65527, 120.48457, 402.87241, 0.10081632, 0.23049662, 0.54468751, 0.041665234, 0.57337528, 0.0074442979, 0.010118874, 0.097668171, 0.047499236, 0.22022037, 0.11357728, 0.14063072, 0.081412017, 0.15039197, 0.30661905, 0.010041318, 0.30713466, 0.2246805, 0.12985943, 0.013962295, 0.0031880976, 0.0033751372, 0.35504347, 0.043138403, 0.034578215, 0.17706399, 0.043236308]


一応想定した通り、variablesは(1, 1)あたりになってlossもなんとなく減っているみたいです

tensorflow で rnn を動かしてみる

tensorflow の rnn のチュートリアルはちょっと複雑で何をやってるのかパッとはわからないので、簡単なやつを作って動かしてみたい

そもそもRNNとは?

出力をまた入力に使うらしい。よくこんな図を見るけど具体的に何が起きているのか?

深層学習による自然言語処理 という本だと、

ということなので、1層で入力も出力も2個のときは、下図っぽい感じになるはず。

任意の長さの入力 (今回は入力素子が2個なので長さ4なら [ [3,2], [4,4], [1,0], [9,9] ] みたいな感じ) について、
最後の入力(上の例では [9,9])が渡されたときの出力(にsoftmaxとかかけたもの)で誤差関数の値を計算して、それを最小化するようにWを調整していくことで学習していく。
(必要なら各入力に対する出力を保存しておいてそれらで誤差関数を計算してもいいみたい)

入力の素子数が1つで活性化関数がreluとかなら、Wを全部1にしてbを全部0にすれば任意の長さの正の数の和が計算できそうなので、それを実装してみようと思う。

実装してみる


とりあえず、セルというものが必要らしいのでこれを作成する。
今回は1層だけだけどL層に重ねたりするときは、各層でWとかが存在するので、それぞれの層をひとまとめで表すものがセルなんだと思う。
今回は BasicRNNCell を利用する。

In [1]: import tensorflow as tf

In [2]: sess = tf.Session()

In [3]: size = 1

In [4]: rnn_cell = tf.nn.rnn_cell.BasicRNNCell(num_units=size, activation=tf.nn.relu)

BasicRNNCell は上で図示した感じの最も単純なネットワークのセル。
出力の素子数(num_units)と、利用する活性化関数(activation)を指定できる。

dynamic_rnn を使うとこれより先の処理を自分で書かなくてもよくなるっぽいけど、何が起こっているのか知りたいのでまずはdynamic_rnnを使わずに実装してみる。

各層の出力を入力として使うんだけど、一番最初の入力を渡すときは各層の出力(初期状態)は無いので何らかの値を指定しないと計算ができない。

セルの設定ごとに必要な初期状態のデータの形は変わるけどセル自体に初期状態のデータを出力する機能があるので、これを利用する

In [5]: n_batch = 1

In [6]: initial_state = rnn_cell.zero_state(n_batch, tf.float32)

In [7]: type(initial_state)
Out[7]: tensorflow.python.framework.ops.Tensor


ここまで来ると、任意の入力に対して出力を計算できるようになる

In [32]: x = tf.placeholder(tf.float32, shape=[None, n_batch, size])

In [47]: def inference(x, max_len):
    ...:     state = initial_state
    ...:     for i in range(max_len):
    ...:         (output, state) = rnn_cell(x[i], state)
    ...:     return tf.squeeze(output)
    ...: 

ほんとはxの長さでループを回したいんだけど上手いやり方がわからなかったので、とりあえず長さ固定でやることにする...

次に、誤差の計算をする

In [82]: max_len = 3

In [83]: output = inference(x, max_len)

In [84]: y = tf.placeholder(tf.float32, shape=[])    

In [85]: loss = tf.reduce_mean(tf.square(y - output))

誤差関数を最小化するための、optimizerを作る

In [86]: optimizer = tf.train.GradientDescentOptimizer(0.01)

In [88]: train_step = optimizer.minimize(loss)
In [89]: xs = [ [[[1]],[[2]],[[3]]],
    ...:        [[[2]],[[2]],[[2]]],
    ...:        [[[5]],[[4]],[[3]]],
    ...:        [[[7]],[[7]],[[7]]],
    ...:        [[[9]],[[8]],[[7]]] ]
    ...:        

In [95]: ys = [6, 6, 12, 21, 24]

In [96]: init = tf.global_variables_initializer()

In [97]: sess.run(init)

In [106]: for i in range(len(ys)):
     ...:     sess.run(train_step, feed_dict={x: xs[i], y: ys[i]})
     ...:     


やったー 学習させることに成功しました。

一応結果を確認してみます。

In [111]: print(sess.run(rnn_cell.variables[0])) # RNNのウェイトを表示してみる
[[-4.94734144]
 [-8.36993599]]

In [112]: loss.eval(session=sess, feed_dict={x:[[[1]],[[1]],[[1]]], y:3})
Out[112]: 9.0

学習データが大分少ないので全然誤差がでかいですが、とりあえず動かすことができたのでここまで

tensorflowの初歩の初歩

tensorのshape/dimension

一般的には m行n列の行列は m×n行列 と表記すると思うけど

m = tf.Variable([2,3])

とかしたときは2行3列なのか??とかちゃんとわかってないので確認したい

constantで確認する

In [12]: c1 = tf.constant([[1,2,3], [2,3,4]])

In [13]: c2 = tf.constant([[2,2], [3,3], [4,4]])

In [14]: sess.run(tf.matmul(c1, c2))
Out[14]: 
array([[20, 20],
       [29, 29]], dtype=int32)

In [15]: sess.run(tf.matmul(c2, c1))
Out[15]: 
array([[ 6, 10, 14],
       [ 9, 15, 21],
       [12, 20, 28]], dtype=int32)

In [16]: c1.shape
Out[16]: TensorShape([Dimension(2), Dimension(3)])

In [17]: c2.shape
Out[17]: TensorShape([Dimension(3), Dimension(2)])

ということで、constantでは shape(2, 3) は 2行3列 の行列を表すっぽい

では、縦ベクトル、横ベクトルはどうか?

In [18]: v1 = tf.constant([1,2,3])

In [19]: v2 = tf.constant([[2],[2],[2]])

In [21]: sess.run(tf.matmul(v1, v2))

...(略)
~/dev/tf-train/tf-env/lib/python3.6/site-packages/tensorflow/python/framework/common_shapes.py in _call_cpp_shape_fn_impl(op, input_tensors_needed, input_tensors_as_shapes_needed, require_shape_fn)
    689       missing_shape_fn = True
    690     else:
--> 691       raise ValueError(err.message)
    692 
    693   if missing_shape_fn:

ValueError: Shape must be rank 2 but is rank 1 for 'MatMul_2' (op: 'MatMul') with input shapes: [3], [3,1].

... rank が 2じゃなきゃいけないらしい

In [23]: v1.shape
Out[23]: TensorShape([Dimension(3)])

In [24]: v2.shape
Out[24]: TensorShape([Dimension(3), Dimension(1)])

https://www.tensorflow.org/programmers_guide/tensors

rank は 0がscalar, 1がvector, 2がmatrix らしい

逆に、縦ベクトルも横ベクトルも[1,2,3]みたいに書くべきなのか?

In [25]: v1 = tf.constant([1,2,3])

In [26]: v2 = tf.constant([2,2,2])

In [27]: sess.run(tf.matmul(v1, v2))
...(略)

~/dev/tf-train/tf-env/lib/python3.6/site-packages/tensorflow/python/framework/common_shapes.py in _call_cpp_shape_fn_impl(op, input_tensors_needed, input_tensors_as_shapes_needed, require_shape_fn)
    689       missing_shape_fn = True
    690     else:
--> 691       raise ValueError(err.message)
    692 
    693   if missing_shape_fn:

ValueError: Shape must be rank 2 but is rank 1 for 'MatMul_6' (op: 'MatMul') with input shapes: [3], [3].

やっぱりだめ 内積を計算したければ、ベクトルであっても1×n と n×1 の行列で表さなければならないみたい

In [31]: v1 = tf.constant([[1,2,3]])

In [32]: v2 = tf.constant([[2],[2],[2]])

In [33]: sess.run(tf.matmul(v1, v2))
Out[33]: array([[12]], dtype=int32)

In [34]: v1.shape
Out[34]: TensorShape([Dimension(1), Dimension(3)])

In [35]: v2.shape
Out[35]: TensorShape([Dimension(3), Dimension(1)])
shape part2

触りだしたらやっぱり理解があやふやだったので、再確認

shape [1,2,3] と shape [3,2,1] はどう違うのか?

左からi番目は外側からi番目の[]に入ってる要素の数、みたいに考えれば良さそう

In [12]: tf.constant([  [ [1,1,1], [2,2,2] ] ])
Out[12]: <tf.Tensor 'Const_4:0' shape=(1, 2, 3) dtype=int32>

shape [1, ...] なので 一番外側のに入っている要素は1つ

-> [ [...] ]

shape [1, 2, ...] なので 次のに入っている要素は2つ

-> [ [ [...], [...] ] ]

shape [1, 2, 3] なので 次の[]に入っている要素は3つ

-> [ [ [1,1,1], [2,2,2] ] ]

みたいな

逆に shape [3,2,1] なら

[ [...], [...], [...] ]

  • > [ [ [...], [...] ], [ [...], [...] ], [ [...], [...] ] ]
  • > [ [ [1], [2] ], [ [5], [6] ], [ [9], [0] ] ]

みたいな


あと c[1, :, 1] みたいな ':' をつかった操作もあんまり自信がないので確認

In [26]: c1 = tf.constant([[ [1,3,5], [2,4,6] ]])

In [27]: c2 = tf.constant([ [[1], [2]], [[5], [6]], [[9], [0]] ])

c1[0, :, 1] だと [ [3], [4] ]

c2[:, 1, :] だと [ [2], [6], [0] ]

c2[:, 1, 0] だと [2, 6, 0]

となるみたい

In [28]: sess.run(c1[0,:,1])
Out[28]: array([3, 4], dtype=int32)

In [29]: sess.run(c2[:,1,:])
Out[29]: 
array([[2],
       [6],
       [0]], dtype=int32)

In [30]: sess.run(c2[:,1,0])
Out[30]: array([2, 6, 0], dtype=int32)
scope

サンプルとか見ると

with tf.variable_scope("conv1"):
    ....

とかあるけどこれは何???

https://www.tensorflow.org/programmers_guide/variables

を読むと、同じ名前でget_variableを複数回呼ぶと変数を再利用するのか新規に作るのか判別できないけど、variable_scopeで区切っておけば別のvariable_scope内の変数は再利用しない、みたいなことらしい

In [37]: v1 = tf.get_variable('the_var', [2,3])

In [38]: v2 = tf.get_variable('the_var', [2,3])
... 略
ValueError: Variable the_var already exists, disallowed. Did you mean to set reuse=True or reuse=tf.AUTO_REUSE in VarScope? Originally defined at:

  File "<ipython-input-37-ce1950cb36a3>", line 1, in <module>
    v1 = tf.get_variable('the_var', [2,3])
  File "/home/shuhei/dev/tf-train/tf-env/lib/python3.6/site-packages/IPython/core/interactiveshell.py", line 2910, in run_code
    exec(code_obj, self.user_global_ns, self.user_ns)
  File "/home/shuhei/dev/tf-train/tf-env/lib/python3.6/site-packages/IPython/core/interactiveshell.py", line 2850, in run_ast_nodes
    if self.run_code(code, result):

やはり同名の変数を get_value しようとすると "Variable the_var already exists,"のエラーが起こるらしい
(reuse=Trueとかすれば再利用できるらしい)

これを、variable_scopeで囲っておくと別のスコープで定義された変数は無視して新しく作ってくれるらしい

In [39]: with tf.variable_scope('scope1'):
    ...:     v1 = tf.get_variable('scoped_var', [1,2])
    ...:     

In [40]: with tf.variable_scope('scope2'):
    ...:     v2 = tf.get_variable('scoped_var', [1,2])
    ...:     

In [41]: v1.name
Out[41]: 'scope1/scoped_var:0'

In [42]: v2.name
Out[42]: 'scope2/scoped_var:0'