2017年12月20日

物語自動生成プログラム『ジェネジェネちゃん』の作り方 基礎編

前回の記事 ラノベ1万冊分を自動生成してみても、人工知能は産まれない

昨日公開しましたラノベ1万冊分の出力サンプルの元となる
物語自動生成プログラム『ジェネジェネちゃん』の技術的な
解説をしていきたいと思います。

といっても、人工無脳とそこまで変わらないですが、
いくつかの細かなアイディアが同じような研究をおこなっている
方々の参考になれば幸いです。


■データ収集と文データの保存

まず、インターネット上にある小説テキストをクローリングして
収集します。

次にそのテキストを文単位に分割します。終端は、「。」(句点)や
「!」「?」になりますが、

「ば、バカな?!」

など、複数個連続する場合もあるので、注意が必要です。

また、

「まあ、そうですよね」


のように、カギ括弧のセリフ終端には「。」が付かないので
(たまに付いている文章もありますが)、そこも注意です。

それと、その文が「地の文」か「セリフ内」かのフラグも
文と一緒に保存しておきます。


■文の解析

次に、1文ずつ形態素解析をおこないます。形態素解析とは、
単語やそれよりも小さい形態素に文を分割し、各形態素ごとの
品詞を判別する処理です。

ここでは、

MeCab
http://taku910.github.io/mecab/

という、オープンソース形態素解析エンジンを利用します。

たとえば、

織田は杯を持ち上げた


という文を解析させると、

織田 名詞,固有名詞,人名,姓,*,*,織田,オダ,オダ
は 助詞,係助詞,*,*,*,*,は,ハ,ワ
杯 名詞,一般,*,*,*,*,杯,ハイ,ハイ
を 助詞,格助詞,一般,*,*,*,を,ヲ,ヲ
持ち上げ 動詞,自立,*,*,一段,連用形,持ち上げる,モチアゲ,モチアゲ
た 助動詞,*,*,*,特殊・タ,基本形,た,タ,タ


と、なります。

物語自動生成プログラム『ジェネジェネちゃん』では、
この左から2列目の品詞情報に着目します。この品詞だけを
順番に取り出すと、

名詞,助詞,名詞,助詞,動詞,助動詞


と、なります。ただ、「織田」などの人名は、後々の処理で
特別に扱いたいので、ここでは「姓」に置き換えます。結果、

姓,助詞,名詞,助詞,動詞,助動詞


と、なります。これをここでは「品詞列」と名付けます。

そして、この情報を元の文データと合わせてワンセットとします。

文=「織田は杯を持ち上げた」
品詞列=「姓,助詞,名詞,助詞,動詞,助動詞」


このような感じです。これをすべての文に対して処理します。


■連続する文の関連性

ある文と、すぐ直後に続く文との関連性を考えてみたいと
思います。

たとえば、

織田は杯を持ち上げた。
「では、飲むか」


このような連続する文があったとして、両方を先ほどの
形態素解析による品詞列も合わせて表すと、

織田は杯を持ち上げた。(姓,助詞,名詞,助詞,動詞,助動詞)
「では、飲むか」(接続詞,記号,動詞,助詞)


に、なります。

次にこれとは別の文章を解析してみると、

羽柴は箸を持った。(姓,助詞,名詞,助詞,動詞,助動詞)
「まずは、食うか」(接続詞,記号,動詞,助詞)


と、先ほどと同じ品詞列になりました。

ここで一つの仮説を立てます。同じ品詞列が連続するのであれば、
文章の流れというかノリが似ているのではないか、と。試しに、
2番目の文を入れ替えてみます。

織田は杯を持ち上げた。
「まずは、食うか」

羽柴は箸を持った。
「では、飲むか」


そこまで違和感はないですね。

……はい、かなり強引な仮説だと思いますよね。まあでも、
完全にランダムに別の文を選択して文同士を連結させるよりかは、
多少のノリが生み出せます。

たとえば、

信長が攻撃を仕掛ける。
ザンッ!
「ギャアア」
秀吉が倒れた。
「おのれぇ!」
家康が突進する。


というような、短い文の連続で擬音が多い文章と、

信長は秀吉を睨み付けながら、刀に手をかけた。
家康はこれから起こるであろう惨事を本能的に察知した。
響き渡る断末魔を聞き終わる前に、獣と化した体が動いていた。
自らが咆吼を上げていることにも気づかずに。


というダラダラ長い文章とがランダムで混ぜ合わされると、
文体のリズムのようなものが失われてしまいます。


■実装

話が長くなったので、さっさと実装の話に移りたいと思います。

形態素解析を終えて「文」と「品詞列」がワンセットになったものが
出来ていると思いますが、そこから「品詞列」をキーとして
文のリストを作成します。

文=std::string
品詞列=std::string
文リスト=std::vector<文>
品詞列/文リスト辞書=std::map<品詞列, 文リスト>


みたいな感じです。C++っぽく書くと。

そして、品詞列と次に来る品詞列との連結性を表す辞書を作成します。

品詞列リスト=std::set<品詞列>
品詞列/品詞列リスト辞書=std::map<品詞列, 品詞列リスト>


あとは、

1.ランダムに品詞列を選択する
2.品詞列/文リスト辞書からランダムに文を選択して出力する
3.品詞列/品詞列リスト辞書からランダムに次の品詞列を選択する
4.2に戻る


を、実行すると無限に文章が出力されていきます。

ただ、元の小説データが少なかったり、品詞列に偏りがあると、
元の小説がそのまま出力されてしまう場合がありますので、
元データのインデックスなどを参照して、任意回数連続する
ようなら、上記の1~3番のランダム選択をやり直すなどの
処理が必要です。

また、品詞列の遷移が短期間で循環したり、終端に到達した
場合も同様です。


■まとめ

まあ、いわゆる単純マルコフ連鎖なのですけど、形態素や単語や
品詞や文章をそのまま状態として利用せず、文を一段抽象化した
品詞列をキーとしているところが『ジェネジェネちゃん』のミソです。

ただ、これだけでは、まだまだ文章がランダム的なので、
「戦闘パート」「日常パート」などを生成する方法や、
その他の細かな調整テクニックを加味していくのですが、
その辺はまた後日書きたいと思います。


posted by 妹尾雄大 at 21:37| Comment(0) | 物語自動生成 | このブログの読者になる | 更新情報をチェックする
この記事へのコメント
コメントを書く
コチラをクリックしてください