日本語プログラミング言語「なでしこ3」で自然言語処理#2(文生成)

前回:日本語プログラミング言語「なでしこ3」で自然言語処理(言語モデル)

前回、せっかくバイグラム言語モデルを作ったので、文生成をしてみようと思う。
バイグラム言語モデルとは、ある単語の次にどんな単語がどれくらいの割合で出現するか、をモデル化したものだった。
つまり、ある単語が与えられたときに、次の単語を確率的に選択することができる。

たとえば「太郎」という単語があるとき、次に出現しやすい単語として「は」が確率的に選ばれるとする。今度は「は」の次に出現しやすい単語として「人間」が確率的に選ばれる。これを繰り返していくと「太郎 は りんご が 好き だ 。」という文が生成できる…かもしれない。

これを作ってみる。

確率分布からサンプリングする関数

なでしこ3には現状、numpyのchoiceみたいな確率分布を指定して値をひとつサンプリングするような関数はない。たぶん。
なので自分で作る。前回紹介しそびれた、なでしこの関数についてもここで触れられるので丁度良い。

●(確率辞書から)ランダム選択とは
 ((10^15の乱数)/10^15)をポイントに代入
 累積は0
 確率辞書を反復
  累積 = 累積 + 対象
  もし(ポイント<累積)なら
   対象キーを戻す
  ここまで
 ここまで
ここまで

●印を使うと、なでしこの関数が書ける。関数名の前に(Aの)という感じに書くと引数を指定できる。
助詞は色々使えるので、今回みたいに(Aから)みたいに書くこともできる。良い。

今回の引数は確率辞書という名前の変数で、単語がkey、出現確率がvalueとなるようなハッシュ。
ここでは面倒なのでチェックしていないが、この確率辞書に含まれる単語の確率の合計は当然1にならないといけない。

なでしこにはfloatの乱数生成がたぶんないので、適当にintの乱数を割り算して0~1の乱数を作る。
intとfloatの区別はないので、お気持ちでコーディングできる。好き。

生成された乱数の値を超えるまで、辞書に含まれるkeyとvalueを反復して確率を累積していく。
ハッシュの反復なので、単語が「対象キー」、対応する確率が「対象」という変数に代入されている。
乱数の値を超えた時点での対象キー、すなわち単語をreturnすれば確率分布からサンプリングしたことになる。

一応、以下のテスト用辞書から100万回サンプリングしてみる。

テスト辞書 = {「あ」:0.1,「い」:0.2, 「う」:0.3,「え」:0.35,「お」:0.05}
カウント辞書 = {「あ」:0, 「い」:0, 「う」:0, 「え」:0, 「お」:0}

1000000回
 テスト辞書からランダム選択して単語に代入
 カウント辞書@単語 = カウント辞書@単語+1
ここまで
カウント辞書を表示
// { 'あ': 99337, 'い': 200014, 'う': 299793, 'え': 350685, 'お': 50171 }

サンプリングの結果を見ると、まあちゃんと確率分布からサンプリングできてることがわかる。

バイグラム言語モデルで文生成

順番に単語を確率分布からサンプリングするプログラムを書けばいい。
確率分布には、前回作成したバイグラム辞書を使う。
単純には、前回作ったコードの下に以下のプログラムを貼り付ければ動く。

バイグラム言語モデルで単語の確率分布を得るためには、直前の単語を与える必要がある。
文の一単語目は直前の単語がないため、これをどうやって選ぶかが問題になる。
ここで、前回特に説明もせず導入した特殊トークンの<BOS>と<EOS>が生きてくる。
<BOS>は文頭、<EOS>は文末を表す特殊トークンだった。
つまり、<BOS>を一単語目として与えることで、バイグラム言語モデルで実質的な最初の単語をサンプリングすることができる。

また、選択された単語が<EOS>だった場合は文が終わることを表すので、そこで文生成を止めればよい。
確率的なサンプリングは再帰的に行い、始まりと終わりが分かったので、あとはプログラムにする。

生成文は[「<BOS>」]
文最大長は30

(生成文の要素数)<文最大長の間
 生成文@((生成文の要素数)-1)を前単語に代入
 単語 = (バイグラム辞書@前単語)からランダム選択
 生成文に単語を配列追加
 もし単語が「<EOS>」なら
  抜ける
 ここまで
ここまで
生成文を「 」で配列結合して表示

サンプリングされた単語は、配列に追加していく。
最後にこの配列をスペースで結合し、文として表示される。

上では<EOS>が出るまで再帰的にサンプリングを続けると説明したが、確率的に無限ループに陥ってしまうので、適当な長さで生成を止める。
今回は英語なので、30単語くらいを限度にしておく。

なでしこで反復や繰り返しを抜ける(breakする)ときは、そのまま「抜ける」と記述すればよい。
「抜ける」に関する記述は、現在時点でなでしこ3のリファレンスにはないが、なでしこv1の機能を踏襲してるので普通に使える。

以上のプログラムで文を10回ほど生成してみる。

<BOS> without waiting till she <EOS>
<BOS> left-hand bit. Her first thing I know is, look of them, I mean 'purpose'?" said nothing. <EOS>
<BOS> it belongs to it!" <EOS>
<BOS> "But what she tried the <EOS>
<BOS> [Illustration: _The Mock Turtle went on, you talking in a porpoise." <EOS>
<BOS> "What else had got back <EOS>
<BOS> "Nor I," said the top with the <EOS>
<BOS> "I daresay you mean you did," said the dance?" <EOS>
<BOS> eagerly, for it at the left-hand bit. <EOS>
<BOS> time <EOS>

局所的に英語っぽい何かが生成された。
まともなのは三つ目のit belongs to it!くらいだけど、まあまあそれっぽい。
そもそもバイグラム辞書を作ったコーパスがアリスの小説がぶっ飛んでるし、こんなもんか、というかんじ。
ピリオドやカンマのまじめな前処理もしてないし、近々なでしこでまじめに前処理するってネタでまた記事を書こうと思う。

ちなみにこれを応用すると、しゅうまいくんとかTLから学習するフランちゃんbotみたいなのが作れる。

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です