「小説家になろう」の小説を自動生成するマンになろう

この記事は eeic (東京大学工学部電気電子・電子情報工学科)その2 Advent Calendar 2018 - Qiita の13日目の記事です。

 

1. なにをしたいの?

本記事は、小説を書くことができない人間がなんとかして自力(?)で小説を生み出すために試行錯誤した記録です。

リカレントニューラルネットワークの一種である多層LSTMを用いて、「小説家になろう」に投稿された小説の「言語モデル」を学習し、学習したモデルをもとに実際に小説を自動生成します。

「御託はいいから生成された文章を見せろ」という人はこの記事の10章に飛ぶか、https://ncode.syosetu.com/n7444fc/ を見てください。

 

2. 「小説家になろう」とは

https://syosetu.com/

誰でも無料で小説を投稿、閲覧することができるWEBサイトです。

独自の文化を形成しており、主に異世界転生、異世界転移ものの小説が好まれる傾向にあるようです。

異世界転生と異世界転移の違いについては ガイドライン「異世界転生」「異世界転移」のキーワード設定に関して を参照してください。なお、この記事には一切関係がありません。)

 

3. 言語モデルとは

言語モデルについてざっくりと説明すると、なんらかの前提を置いた際の単語の出現確率分布です。言語モデルの中でもよく用いられるN-gramモデルは、単語の出現確率を直前のN-1単語のみに依存するとして、N-1階マルコフ過程で近似します。

例えば、語彙として「私」「犬」「猫」「好き」「は」「が」「な」「だ」の8単語のみをもち、以下の3つの文以外は存在しない(文として認めない)「ホマボガベ語」という架空言語を考えます。

  • 私は猫が好きだ
  • 私は猫だ
  • 私は犬が好きな猫だ

「ホマボガベ語」ではこれらの3つの文が等確率で話されるとします。

2-gramモデルでは、単語の出現確率は直前の一単語のみに依存すると仮定するので、「ホマボガベ語」を2-gramモデルで表すと、

  • 「私」の直後には100%の確率で「は」が出現する
  • 「は」の直後には2/3の確率で「猫」が、1/3の確率で「犬」が出現する
  • 「好き」の直後には1/2の確率で「だ」が、1/2の確率で「な」が出現する
  • 「(文頭)」の直後には100%の確率で「私」が出現する
  • 「だ」の直後には100%の確率で「(文末)」が出現する

などが分かります。

しかし2-gramモデルは直前の一単語しか見ていないため、このモデルにしたがって一単語ずつ順に文章を自動生成させると、

  • 私は猫が好きな猫が好きだ

といったように、ホマボガベ語としては無効な文章が生成されてしまうことがあります。そこでより正確な文章を得るために、4-gramを求めてみます。4-gramでは直前3単語の組み合わせによって次の単語の出現確率をモデル化します。

  • 「(文頭)」「私」「は」の直後には2/3の確率で「猫」が、1/3の確率で「犬」が出現する
  • 「犬」「が」「好き」の直後には100%の確率で「な」が出現する
  • 「猫」「が」「好き」の直後には100%の確率で「だ」が出現する

などが分かります。ここで求めた確率分布をもとに文章を自動生成することで、ホマボガベ語を完全再現することができます。このように、基本的にはN-gramのNを大きくしていくことで、母集団の真の確率分布に近いモデルを得ることができるようになります。

今回は「小説家になろう」に投稿された小説を教師データにしてこのモデルを学習することで、「小説家になろう」の小説における単語出現確率分布を求め、その確率分布からサンプリングすることで小説を自動生成しようという趣旨になります。

 

4. N-gram言語モデルの欠点

前述の通り、N-gramモデルのNを大きくすることで母集団(または教師データ、今回の場合は小説家になろうに投稿された小説)の確率分布に近づけることができるのですが、大きく2つの問題があります。

・テーブルサイズが非常に大きくなる

ホマボガベ語では語彙数が8だったので大したことはありませんでしたが、実際には何万という語彙数を持つ言語を学習することになります。語彙数をVと置くと、N-gramでは前提となるN-1単語(Vの(N-1)乗)と、その時出力され得るV単語それぞれの確率のテーブルを持つことになり、本質的にはVの N乗のメモリを食います。もちろん学習データに出現する組み合わせのみをハッシュ化して持つことで大幅にデータ数を削減することができるのですが、テーブルの計算、保持が大変であることは間違いないです。

・学習データにある組み合わせしか出力できない

「学習データの確率分布を再現することが目的だ」などといっておきながら矛盾するようですが、当然のことながら完璧に学習をしてしまうと学習データに存在する組み合わせしか出力できなくなってしまいます。盗作待ったなしです。ある程度は「元の言語っぽさ」をだしつつも、新しい文章を出力してくれると嬉しいですね。この点に関してはNが小さめのN-gramは優秀で、ホマボガベ語2-gramの例で見たように、途中で切り替わる(ねじれる)ことで学習データには存在しない文章を出力することがあります。とはいえN-gramではNの長さを最初にチューニングすることしかできないため、言語における「重要度」を元に学習させることはできません。例えばかぎ括弧の対応など、「絶対に外してはいけない部分」を学ぶことができないため、やはり言語としては違和感が拭えないものになってしまいます。

 

5. リカレントニューラルネットワークによる言語モデル

今回はリカレントニューラルネットワーク(RNN)による言語モデルを使用しました。RNNによる言語モデルN-gramモデルの派生とみることができますが、上記で挙げたN-gramモデルの欠点をある程度克服することができます。基本的には「Nが非常に大きなN-gramモデルの近似関数をニューラルネットワークで学習する」ものです。

ニューラルネットワークで学習をするために、まず単語にIDを割り振ります。3章の「ホマボガベ語」の例をもう一度使うと、「私」「犬」「猫」「好き」「は」「が」「な」「だ」「(文頭)」「(文末)」の10単語に順に0~9のIDを振ったとしましょう。その後、例えば「私は犬が好きな猫だ」という文(学習データ)から、

  • 「(文頭)」「私」「は」「犬」の直後に「が」が来る

という学習を行うことを考えます。

f:id:nus_miz:20181213055739p:plain

リカレントニューラルネットワークによる言語モデル

リカレントニューラルネットワークを用いた言語モデルの概略図を示しました。白い長方形に囲まれた部分(θ_{\mathrm{xxx}})が学習対象のパラメータで、赤い長方形が今回の学習データにおける入力、青い長方形は計算途中で出現するベクトル、黄色い長方形は定数(零ベクトル)です。

まず、入力された単語をベクトル表現に落とし込むことから始めます。最初にID8番(文頭)という入力が来るため、それに対応する単語ベクトルw_1θ_{\mathrm{words}}を用いて計算します。

  • 基本的にはθ_{\mathrm{words}}は単語ベクトルが語彙数分だけ並んだ配列であることが多く、入力されたIDに対応するベクトルをw_iとして出力するだけです。

次にw_1と零ベクトルおよびθ_{\mathrm{rnn}}を用いて何らかの演算を行うことで、h_1を求めます。

  • 過度な一般化により「何らかの演算」などという雑な日本語になっていますが、大抵は行列演算やベクトルの足し算、またベクトルの各要素に非線形スカラ関数を適用するといった操作の組み合わせになります。

次にID0番(私)に対応するw_2と今計算したh_1から、同様の操作でh_2を計算します。これを複数回繰り返してh_4を求めたのち、それとθ_{\mathrm{out}}から何らかの演算により10次元のベクトルp_4を求めます。このp_4が単語出現確率分布に当たります。「(文頭)」「私」「は」「犬」という入力の直後において、「私」(ID0)が出現する確率はp_4の0次元目の値に対応し、「犬」(ID1)が出現する確率はp_4の1次元目の値に対応し、「猫」(ID2)が出現する確率は2次元目の値に対応する、といったように、p_4の各次元の値が各IDの単語の出現確率を表すようになっています。

  • p_4の各次元の値は0以上1以下であり、かつ合計は1になるようになっています。

今回の場合、教師データでは「が」(ID5)が出現しているため、p_4の5次元目の要素が大きくなり、かつそれ以外の要素が小さくなるように各種パラメタθ_{\mathrm{xxx}}を更新します。学習初期はでたらめな確率分布p_4が出力されますが、学習が進むにつれ、今回と同様の入力があった場合にはp_4の5次元目が限りなく1に近く、それ以外の要素が限りなく0に近いような出力が出せるようになることでしょう。

なお、θ_{\mathrm{rnn}}θ_{\mathrm{words}}は計算過程において複数回出現しますが、すべて同じものを使用します。また、w_ih_{i-1}からh_iを求める操作も毎回同様になるようにします。このようにパラメタを共有することで、可変長の入力に対応し、全体のパラメタ数を抑え、学習も進みやすいようにしたのがリカレントニューラルネットワークです。

ちなみにホマボガベ語の教師データには「(文頭)」「私」「は」の直後に「犬」(ID1)が来るものと「猫」(ID2)が来るものが1:2の割合で含まれています。この際、出力確率分布を示すベクトルpの1次元目を大きくしようとする学習と2次元目を大きくしようとする学習が1:2の割合で起こることによって、最終的にpの1次元目が1/3、2次元目が2/3に限りなく近づいたところで収束します。

  • というか、そうなるようにパラメタの更新方法を工夫します。

 

また、「私は猫だ」という教師データからは

  • 「(文頭)」の直後に「私」が来る
  • 「(文頭)」「私」の直後に「は」が来る
  • 「(文頭)」「私」「は」の直後に「猫」が来る
  • 「(文頭)」「私」「は」「猫」の直後に「だ」が来る
  • 「(文頭)」「私」「は」「猫」「だ」の直後に「(文末)」が来る

 という5つの学習を行うことができます。これらは以下に示すように同時に学習することができます。

f:id:nus_miz:20181213064353p:plain

言語モデルの学習

図中にh_iが2回ずつ出てきますが、同じ添え字のhは同じものを表しています。このようにp_1p_5を一気に計算した後、p_1については0次元目を大きく、p_2については4次元目を大きく、p_3については2次元目を大きく、といった学習を行うことで、効率的な計算が可能となります。

 

リカレント ニューラルネットワークを用いた言語モデルでは、すべての単語の組み合わせについてテーブルを保持するのではなく、各単語のベクトル表現や、それを用いた確率モデル計算用のパラメータを学習するだけなので、組み合わせ爆発が起こることはありません。またニューラルネットワークの表現力にもよりますが、過学習しない限りは、学習データの傾向をつかみつつも学習データに含まれない出力を出すことができます。

 

6. 実験設定

小説家になろうの累計ランキングのうち、「異世界転移」または「異世界転生」タグを含み、短編ではないものを抽出し、上位1000作品を学習データとして用いました。

形態素解析エンジンのMeCabを用いて文章を単語に分割しました。

  • この際MeCabにより得られた品詞データを用い、同様の文字列の単語でも品詞が異なるものは別の単語として扱いました。
  • 単語を出現頻度順にソートし、上位75000単語に含まれなかった単語は未知語として扱い、各品詞ごとに一つの単語にまとめました。

前書きや後書きを含まない本文中に現れる複数の文頭から800単語を切り出し、学習データとして用いました。

  • 学習データ数はおよそ6000000となりました。これには開始位置が違うだけで、部分的には同じ内容を含むデータ対が存在していることに注意してください。

 リカレントニューラルネットワークの一種である4層スキップコネクション付きLSTMを用いて言語モデルの学習を行いました。

 

7. 固有名詞の処理

各作品に出てくる固有名詞は、作品をまたいでの学習が難しいだけでなく、生成した文章に脈絡なく様々な固有名詞が出現してしまうとまとまりの無さが目立ってしまいます。そこで今回の実験では、各作品の固有名詞を「男性主人公」「女性主人公」「男性名」「女性名」「苗字」「国名」「種族名」等に分類したうえで、その作品内での出現頻度順に並べ、例えば「男性名出現頻度4位」といった単語に置き換えて学習させました。

  • 各分類に置いて、一定の順位以下はすべて一単語にまとめました。

なおこの分類はある程度の作品については手作業でタグ付けした後、そのデータを用いて、タグを自動分類するような学習を行い、残りの作品のタグ付けを行いました。

  • タグ付作業中は非常に強い虚無感を感じました。心を無にして固有名詞の分類を当てる作業の繰り返し…

生成された文章における固有名詞タグを、予め作成した固有名詞テーブルに従って置き換え直すことで、文章内で出てくる固有名詞にある程度の一貫性をもたせることができました。

 

8. 未知語に対する処理

生成された文章における未知語を「未知語」のように表示するわけにはいきません。そこでデータセットの前処理段階において、未知語に分類されてしまった単語の出現回数を数えておきます。そして、自動生成された文章中の未知語タグを、学習データセットに出てきた(が、未知語になってしまった)単語のうちどれか一つにランダムに置き換えました。

  • この際置き換え元の未知語タグと同様の品詞の単語を選ぶようにします。
  • またランダムに選ぶ際に、学習データセットに出てきた回数の分布に従って単語を選択するようにします。出現回数100の単語は出現回数50の単語よりも2倍選ばれやすくするということですね。

 

9. 実装上の工夫

今回は学習データセットの系列帳が800と非常に長いため、愚直に実装すると計算グラフの構築に非常に多くのメモリを必要とするため、バッチサイズを大きくすることができず、学習効率が落ちてしまいます。

それを解決するための工夫が今回一番書きたかったことのはずなのですが、疲れてしまったので別の記事に分離してあとで書くことにしました。ごめんなさい。いずれここにリンクを貼る予定です。

 

10. 実験結果

https://ncode.syosetu.com/n7444fc/

上記リンクから自動生成された文章を読むことができます。学習したモデルに従っておよそ2000単語を出力した後、末尾の中途半端な文を切って投稿しています。それ以外は基本的に手付かずで、モデルの出力そのまま、素材の味を楽しむことができます。基本的には最近の投稿の方が学習が進んだモデルを使っているのですが、あまり違いは感じられないかもしれません。

以下個人的に好きなポイントです。

 

#002において、

“――絶ハンマーてんそくおういおうせい”――

【炎斬】(ありったけ)

【光剣・変】片手斧

【剣】剣

【剣】オリハルコン

 突然パラメータ表示(のようなもの)が出てきます「【剣】剣」は何が言いたかったのでしょうか。

 

#022の冒頭

「俺もそう思った。いつまで経っても嫌がられるにはちょっと時間がかかるんだよ……。でも教科書で言うなら、10分は365時間っていうのは凄いだろ?でも10年後にはお前は更に難しくなる」

なにか深いことを言っていそうな感じがしますが、まるで意味がわかりません。

 

#054は全編通して登場人物紹介のようななにかを行っています。

 

#064内のセリフ

「いえ、ただ、魔力の注入をされている場合だと一度無理をすれば魔力を吸収していくのが普通です。そこに魔力を込めると魔力は枯渇するので、魔力に対する同調量も極端に抑えます。そして、魔力であれば魔力に比例して魔力も使えるわけですし、魔力は一定魔力へ変換して魔力を循環することで得られる魔力値を飛躍的に上昇させます」

魔力のゲシュタルト崩壊

 

11. 関連研究

AIによる小説の自動生成というトピックだと、「きまぐれ人工知能プロジェクト作家ですのよ」の「コンピュータが小説を書く日」などが有名です。非常に興味深い研究ですが、何らかの形でプロットを人為的に与える必要があり、私のように小説が全く書けない人間には厳しい設定となっています。

今回の実験に近いものは、一時期話題になったBotnik Studiosによる、「ハリー・ポッターと山盛りの灰のようにみえるものの肖像」でしょうか。しかしこれも編集者による文章の取捨選択により最終的な成果物を得ています。

小説家になろうに限っても、小説の自動生成にまつわる制作物はいくつか見つけることができます。

が、脈絡のある長文を出すことはやはり難しいようです。

 

11. おわりに

なんだかんだ多少の文脈が捉えられているなぁと感じられる文章が得られて楽しかったです。そして自動生成された文章からも溢れ出る「小説家になろう」感。

強い人には退屈な記事だったかもしれませんが、この記事を読んで「自然言語処理って意外と面白いかもしれない」と思ってくれる人が一人でも増えたら幸いです。

 

参考文献