超言理論

特に益もない日記である

PybrainでLSTM言語モデル動かしてみた

昔、自分が全然Neural Networkとかわからなかった頃、Pybrainというライブラリに興味を持ってNNLM(Neural Network Language Model)を作ろうと挑戦したことがあった。
ma13.hateblo.jp

結果だけ言うと全く出来てなかったわけなんだけど、ちょっとリベンジしようかと思ってほぼ同じ環境でNNLM(実際はRNNLM, LSTMLM)を作ってみた。
コードは以下Gistにある。

gist0eccd8d989ac11ef74a5

適当に書いたのでちょっと微妙なんだけど*1、簡単に解説する。

まず、学習するデータ(単語分割済み)を読みだして、sklearnのCountVectorizerのfittingに使う。

train_word = []
with open(filename,'r') as f:
    for line in f:
        for w in line.split():
            train_word.append(w)
train_word.append('<s>')
train_word.append('</s>')

vectorizer = CountVectorizer(analyzer=dummy, dtype=numpy.float64)
vectorizer.fit(train_word)
print('Vocab : '+str(len(vectorizer.vocabulary_)))
      
vocab_list = []
for k,v in sorted(vectorizer.vocabulary_.items(), key=lambda x:x[1] ):
    vocab_list.append(k)

ぶっちゃけたことを言うと、昔書いたコードがうまく動かなかったすべての原因はここにあって*2、sklearnのCountVectorizerは全語彙を.vocabulary_という辞書に入れて持っているんだけど、これ実は辞書の中身(key, value)が(単語, 単語の総数)とか、(単語のindex番号, 単語)とかではなく、(単語, 単語のindex番号)になっていて、つまるところ以下のようになっている。

vectorizer.vocabulary_.items()
{('a', 3), ('is', 2), ('this', 0), ('pen', 4), ('.', 5), ('I', 1)}

なので、CountVectorizerの出力と単語の対応をとる場合はvalueでsortした上でkeyを取り出す必要がある。正直頭おかしいと思うし、何のためにこうなっているのかはちょっと想像がつかなかった。これは、おそらくだけど、入力を一発でindexに置き換えるためにこういう構造になっているだろう。
つまり、vectorizer.vocabulary_[word]=indexのようにone-hot-vectorならどこを1にすればいいのかすぐにわかる、便利だね!!!!!!(白目)*3

そして、fittingしたvectorizerを使って単語のベクトル(one-hot vector)を作成

voc_size = len(vectorizer.vocabulary_)

ds = SequentialDataSet( voc_size, voc_size )
with open(filename,'r') as f:
    for line in f:
        ds.newSequence()
        # insert <s>
        p = numpy.array(vectorizer.transform(['<s>']).todense()).reshape(-1)
        for w in line.split():
            n = numpy.array(vectorizer.transform([w]).todense()).reshape(-1)
            ds.addSample(p, n)
            p = n
        # append </s>
        ds.addSample(p, numpy.array(vectorizer.transform(['</s>']).todense()).reshape(-1))

文頭文末にはそれを示す記号を入れる。

ネットワークの構築

net = RecurrentNetwork()
net.addInputModule(LinearLayer(voc_size, name='in'))
net.addModule(TanhLayer(2 ** _compsize, name='comp'))
net.addModule(LSTMLayer(2 ** _hiddensize, name='lstm'))
net.addOutputModule(SoftmaxLayer(voc_size, name='out'))

net.addConnection(FullConnection(net['in'], net['comp'], name='in_to_comp'))
net.addConnection(FullConnection(net['comp'], net['lstm'], name='comp_to_lstm'))
net.addConnection(FullConnection(net['lstm'], net['out'], name='lstm_to_out'))

net.addRecurrentConnection(FullConnection(net['lstm'], net['lstm'], name='recurrent_lstm'))
net.sortModules()

LSTMを使ってみたかったのでPybrainのLSTMLayerを利用する。
LSTMLayerに入れる前にTanhLayerで適当に入力の単語ベクトルを圧縮して、それから入力。

学習する。

ind = [ i for i in range(0, ds.getNumSequences()) ]
random.shuffle(ind)
batch_size = int(len(ind) / batches)
for batch_count in range(0, batches ):
    print(str(batch_count + 1)+' / '+ str(batches))
    batch = SequentialDataSet( voc_size, voc_size )
    for i in ind[batch_size * batch_count : min(len(ind), (batch_size * (batch_count + 1)))]:
        batch.newSequence()
        for _a,_b in ds.getSequenceIterator(i):
            batch.addSample(_a, _b)

    trainer = RPropMinusTrainer(net, dataset=batch, )
    trainer.train()

シャッフルしたデータから適当な個数に切り分けて、それぞれTrainerに入れる。
もっときれいに書けると思うしいちいちデータを作っているのがかなりダサい。

そして学習が終わったらテストで生成をしてみる。

generation_max = 1000
output_candidates = [(['<s>'], 0.0)]
for _x in range(0, generation_max):
    output_seq = None
    output_per = float('inf')
    output_no = -1
    for i, (s, p) in enumerate(output_candidates):
        if output_per >= p:
            output_seq = s
            output_per = p
            output_no = i
    print(' '.join(output_seq) +' / '+ str(output_per) + ' ('+str(len(output_candidates))+')', end='\r', flush=True)

    if output_seq[-1] == '</s>' or len(output_seq ) > 20:
        print(' '.join(output_seq) +' / '+ str(output_per) + ' ('+str(len(output_candidates))+')', end='\n\n', flush=True)
        break

    output_candidates.remove((output_seq, output_per,))

    net.reset()
    ts = UnsupervisedDataSet(voc_size, )
    for w in output_seq:
        n = numpy.array(vectorizer.transform([w]).todense()[0]).reshape(-1)
        ts.addSample(n)

    for s, w in zip(net.activateOnDataset(ts)[-1], vocab_list):
        if s > 0.0:
            output_candidates.append((output_seq + [w], output_per - math.log(s),))

初期状態"<s>"から、次の単語の確率を推定して、文末に追加し、文と対数尤度をペアにして候補として記憶。
候補から最も対数尤度が高いものを選択して、次の単語の確率を推定して文末に追加し、また候補に戻す。これを文末または既定の長さになるまで繰り返す。
普通にある状態から最大の単語確率のものを選んでそのまま次の単語を推定してもよかったんだけど、全体で確率最大化したほうが生成される文の品質が良くなるかなと思ってこっちにしてみた。
当然のことなんだけど、文の候補数は1ループで語彙数分だけ増えるので、大規模なデータでこれを使うと非常にメモリを食う。


で、動かしてみるとこんな感じになる。

18 / 32
Error:6.45327155815e-05
Test generation :
<s> こんにちは 。 声 かけ て くれる の を 待っ て たん だ 。 </s> / -11.548804912609333 (107339)

19 / 32
Error:6.28088535697e-05
Test generation :
<s> こんばんは ー た 。 気軽 </s> / -2.1222976358696273 (46003)

20 / 32
Error:6.39956006963e-05
Test generation :
<s> こんにちは 。 声 かけ て くれる の を 待っ て たん だ 。 </s> / -10.342886978783383 (107339)

21 / 32
Error:6.44167821664e-05
Test generation :
<s> こんばんは ー 。 </s> / -2.719341666660525 (30669)

22 / 32
Error:6.32813259365e-05
Test generation :
<s> こんにちは 。 声 かけ て くれる ん です か ? </s> / -7.535082186820195 (84338)

23 / 32
Error:6.21185037777e-05
Test generation :
<s> こんばんは ー です ね </s> / -3.2706245155460785 (38336)

24 / 32
Error:6.40324274094e-05
Test generation :
<s> こんにちは 。 気軽 に 声 かけ て くれる ん です か ? </s> / -8.480708678997797 (99672)

25 / 32
Error:6.25260848365e-05
Test generation :
<s> こんばんは 。 </s> / -1.8480408894751301 (23002)

26 / 32
Error:6.38205037144e-05
Test generation :
<s> こんにちは 。 気軽 に 声 かけ て くれる の を 待っ て たん だ 。 </s> / -9.814649153298937 (122673)

27 / 32
Error:6.23060400758e-05
Test generation :
<s> こんばんは ー 。 ちょうど 退屈 し て た ん だ 。 </s> / -9.159558817603198 (92005)

これは対話破たん検出タスク*4の発話文をあつめて学習した。
発話文なので、口語で、かつ複数対話あるので挨拶や対話開始時のよくある発話を学習している。精度はそこそこって感じ。
コーパスのサイズは全文集めても千文単位で、このくらいのサイズなら別に問題なく学習が終わるんだけど、これより大きいサイズになると馬鹿みたいに時間がかかるうえに、精度もそれほどでない。
原因は既に分かっていて、PybrainがGPUに対応していないとか、数値計算/学習データあたりが大規模なデータで動くように対応してないとかそのへんの問題だと思われる*5
Pybrainは(CPUで)簡単にNeural Networkを動かせる、ってところで止めておいて、これ以上大規模/複雑なネットワークを学習するならchainerとかを使った方がよさげ。
そのへんのことを考えて、このプログラムはこれ以上手を付けないで、PybrainでRNNLMのリベンジ終了、ということにしておく。

今後の課題はchainerのお勉強です。

オンライン機械学習 (機械学習プロフェッショナルシリーズ)

オンライン機械学習 (機械学習プロフェッショナルシリーズ)

*1:キレイに書き直してまとめようと思ったんだけど諸事情でやめました、理由はあとで。

*2:つまり原因を解明するのに2年かかったわけだ!

*3:なんでvectorizer.transform()というメソッドが存在するのか…

*4:対話破綻検出チャレンジ

*5:これが先述の"諸事情"


Copyright © 2012-2016 Masahiro MIZUKAMI All Rights Reserved.