確率的文法を用いた文章の「調整」(その1)


モーフィングした文章は、文法的におかしい。


理由は明確で、名詞、動詞、形容詞のみを、前後のつながりを無視して置換しているからである。つまり、そもそも文法を考慮していない。


文法とは、単語のつながりの規則である。


「カトちゃん」という単語が出現したら、次の単語は「ペ」である確率が高い。

「ケンちゃん」である確率もそこそこ高い。「しむらー、うしろ、うしろ」はちょっと低い。


「次に出現する単語」は、そこまでの単語列に基づいた確率変数になっている。

これを確率的文法という(らしい)。


文章生成システムの多くは、確率的文法に基づいて、文章を作成している。


ただし、文法と意味は別である。日本語の文法としては正しいが、意味は通じない文章というのは存在する。


しかし今回の「文法的におかしいテキストモーフィングの出力結果を、文法的に修正したい」という目的であれば、確率的文法が利用できるのではないかと考えた。


そしてどうせやるなら、ニューラルネットワークの勉強をかねてChainerで実装してみますかね、というのが、この先のしばらくのテーマである。


で、だ。


僕のスタンスは、RNNとかは道具として使おう、理屈はあとから、なので、まずは、Chainer本(Chainerによる実践深層学習)のサンプルコードをもとにしながら、LSTMを用いてテキストを学習して言語モデルを構築するところをやってみることにした。


Chainer本はニューラルネットの入門書としてもよいし、参考文献も沢山列挙してあり、よい本だと思う。


言語モデルは時系列データであり、時系列データの学習には、LSTMを用いるのがよい、らしい。


サンプルコードは、日本語の処理を考慮していなかったことと、辞書を作成するために、全テキストをオンメモリで読んでいて、実用的ではなかったので、細かな調整を加えた。


まず、辞書の作成。


def create_vocab(filename):

global vocab, word_count

f = open(filename)

for l in f:

l0 = l.decode('utf-8').replace('\n', u' <eos> ').replace(u" ","")

words = l0.split()

for i, word in enumerate(words):

word_count += 1

if word not in vocab:

vocab[word] = len(vocab)

vocab_inv[vocab[word]] = word


f.close()

f = open("vocab.dump", "w")

pickle.dump(vocab, f)

f.close()

f = open("vocab_inv.dump", "w")

pickle.dump(vocab_inv, f)

f.close()

return


正引きと逆引の辞書を作り、pickle化して保存している。インデックスが整数だが、まあ32bit環境であっても問題ないだろう。


次、データセットである学習元テキストの読み込みとインデックス列化。


def create_dataset(filename):

global vocab, word_count

f = open(filename)


dataset = np.ndarray((word_count,), dtype=np.int32)

i = 0


for l in f:

l0 = l.decode('utf-8').replace('\n', u' <eos> ').replace(u" ","")

words = l0.split()

for word in words:

dataset[i] = vocab[word]

i += 1


return dataset


読み込むファイルは同じである。想定として、

・読み込むテキストはUTF-8である。改行コードは\n。

・分かち書き(単語ごとに半角空白が入っている)されている。これはmecabで前処理しておく。

・一行につき、一文が格納されている。


LSTM本体であるが、以下のようにしてある。学習の部分は、Chainer本のlstm2.pyのコード、ほぼそのままである。推論の処理がサンプルコードにはなかったので、predict_oneとpredictというメソッドを追加した。



class NueLSTM(chainer.Chain):

def __init__(self, v, k):

super(NueLSTM, self).__init__(

embed = L.EmbedID(v, k),

H = L.LSTM(k, k),

W = L.Linear(k, v),

)

def __call__(self, s):

global eos_id

accum_loss = None

v, k = self.embed.W.data.shape

self.H.reset_state()

for i in range(len(s)):

next_w_id = eos_id if (i == len(s) - 1) else s[i+1]

tx = Variable(np.array([next_w_id], dtype=np.int32))

x_k = self.embed(Variable(np.array([s[i]], dtype=np.int32)))

y = self.H(x_k)

loss = F.softmax_cross_entropy(self.W(y), tx)

accum_loss = loss if accum_loss is None else accum_loss + loss

return accum_loss


def predict_one(self, word, argmax=False):

x_k = self.embed(Variable(np.array([word], dtype=np.int32)))

y = self.H(x_k)

output = F.softmax(self.W(y))

if argmax:

ids = np.argmax(output.data, axis=1)

else:

ids = [np.random.choice(np.arange(output.data.shape[1]), p=output.data[0])]

return ids[0]


def predict(self, word, argmax=False):

x_k = self.embed(Variable(np.array([word], dtype=np.int32)))

y = self.H(x_k)

result = F.softmax(self.W(y))

ids_sorted = np.argsort(result.data, axis=1)

ret_vals=[]

max = len(ids_sorted[0]) - 1

for i in range(max):

ret_vals.append(ids_sorted[0][max-i])

return ret_vals


学習のコードは以下になる。


demb = 100

model = NueLSTM(len(vocab), demb)

optimizer = optimizers.Adam()

optimizer.setup(model)


for epoch in range(500):

s = []

for pos in range(len(train_data)):

id = train_data[pos]

s.append(id)

if (id == eos_id):

model.zerograds()

loss = model(s)

loss.backward()

if (len(s) > 29):

loss.unchain_backward() # truncate

optimizer.update()

s = []

if (pos % 100 == 0):

print pos, "/", len(train_data)," finished"

if (epoch % 10 == 0):

outfile = "lstm-lesson-" + str(epoch) + ".model"

serializers.save_npz(outfile, model)



コードをいじりつつ、ちょこちょことサンプルテキストを学習させたりはしたのだが、まず、この状態で「走れメロス」を学習させた結果を以下に示す。


・語彙数は 1378

・単語数(データセットの数)は 6789

・テキスト全体を500回学習させるのに要した時間は、5時間50分。

・シングルスレッドで動作しており、学習中の使用メモリは600MB程度だった。


これを元に、「メロス」という単語から始まる文章を生成してみる。

最初のパターンは、もっとも有力な候補のみをつかう場合である。


結果: メロスは、いまは、ほとんど全裸体であった。


この文章は、本文中にそのまま出現しており、まあ、そうだねという結果である。

激怒するよりも全裸のほうが重要なんだというのは、ちょっと面白い。


次のパターンは、候補の単語のうち、先頭の2つのどちらかをランダムに選んでみる。

どのくらいの確度かは考慮せずに、完全にランダムにした。ただし、次の候補が文末の場合は、そこで終わるようにした。


結果: メロスは腕に出発をして、まちは賑やかで様子を怪しくわいだが。


もういっちょう。


結果: メロスは、いまはも、ほとんど、あの方が死刑をも撃ちでもに此のした頃、私は君をと、それから、花婿の市を行くだろう。


そうですか。


うーん。


そうですか。


難しいものだ。しかし、学習しているテキストが少ないし、word2vecの時に使ったデータを学習させれば、もっとよい結果が出るのではないかというのが次の課題である。


実は一回読み込ませてみたのだが、一晩動かしてもループの一周が終わらなかった。

この先はCPUだけではつらい領域のようだ。


じゃあ、そこそこのGPUを買おうかと思ったが、その前にCUDA対応にしてどの程度性能が変わるのかを調べてみてもよいだろうと考えた。


次のテーマは、AWS上にGPU対応のインスタンスを作り、そこでの性能評価である。


先は長い。

  • Twitterで共有
  • Facebookで共有
  • はてなブックマークでブックマーク

作者を応援しよう!

ハートをクリックで、簡単に応援の気持ちを伝えられます。(ログインが必要です)

応援したユーザー

応援すると応援コメントも書けます

新規登録で充実の読書を

マイページ
読書の状況から作品を自動で分類して簡単に管理できる
小説の未読話数がひと目でわかり前回の続きから読める
フォローしたユーザーの活動を追える
通知
小説の更新や作者の新作の情報を受け取れる
閲覧履歴
以前読んだ小説が一覧で見つけやすい
新規ユーザー登録無料

アカウントをお持ちの方はログイン

カクヨムで可能な読書体験をくわしく知る