SeqGANを用いてテキスト(小説のあらすじ)の生成をする

  • 12
    いいね
  • 0
    コメント

SeqGANについて

画像におけるGenerative Adversarial Networks(GANs)の進歩はめざましく、今もどんどん新しい手法が次々と提案されています。しかしながら、GANsを直接テキストのような系列データに適用しても、文法的な正しさを保った結果を得るのは困難です。

SeqGANは、その課題の克服を目指した生成モデルの一つです。

関連研究

GANs以外の生成モデルである、VAEを自然言語処理に適用した手法もあります。

また、画像認識方面では教師データにノイズをのせるなどして見かけ上の学習データを増やすデータ拡張が広く行われていますが、自然言語処理においても同様の試みがなされています。

小方孝先生が、統合物語生成システム (Integrated Narrative Generation
System: INGS)を研究されています。こちらはあらすじレベルではなく、小説そのものを生成するものです。

GANs

既存のGANsは、画像のような連続量・かつ決まったサイズの出力を想定しています。一般的なビットマップのピクセル情報は色の濃淡をたとえば256階調で表現します。この辺りが連続的あることが重要です。

一方で自然言語処理を行う時には、多くの場合Bag-of-Words(BOW)で単語を表現します。ある単語に対し一意のIDをふるので、隣り合うID間には何の意味もありません。この点が決定的に違うため、GANsをそのまま自然言語処理に適用するのは困難です。

SeqGAN

GANsを系列データに応用する工夫をしたものがSeqGANです。Discriminator自体は単純な真贋判定をするだけのネットワークで既存のGANsと同じですが、GeneratorはRNN生成モデルをベースとしています。

さらに、Generatorに強化学習を適用するという工夫があります。RNNに対しある単語(の分散表現)を入力し、次に生成すべき単語の生成確率の学習と、文章全体(最後の出力が文末を意味するマークに至るまでの出力系列)が適切な出力であるかどうかをDiscriminatorで判断しつつ、学習に反映させます。次の単語を予測する局所的な判断と、文章全体が適切かどうかの判断をトータルで行うという行動は、将棋やチェスのようなゲームの手筋の学習と非常に似ている、というわけです。これにより、文法的に破綻する方向への学習が抑制されます。

事前学習でGeneratorは学習元のデータをそのまま出力する方向で学習させます。ただしあんまりたくさん学習はさせません。Discriminatorは学習元のデータがpositiveに、事前学習させたGeneratorの出力がnegativeに分類させるよう学習させます。Generatorをたくさん学習させすぎるとpositive, negativeのデータに差異がなくなってしまうので、そうならないようGeneratorの事前学習はほどほどにする必要があります。

その後は、GeneratorとDiscriminatorを交互に学習させてゆきます。ここは一般的なGANsと同じ流れです。ただし、先ほど述べたようにGeneratorの学習は強化学習的な振る舞いで学習させます。

実装

論文著者によるTensorFlow実装と、fukuta0614さんによるChainer実装があります。

私はChainer実装をベースに若干手を入れて試しました。オリジナルのコードは「小説家になろう」のデータセットを元に学習を行うことを意図したコードがありますが、データセットそのものは存在していません。forkしてAPI経由でデータを取得するスクリプトを追加したものを用意しています。

以下、ソースのnovelist_ni_narouディレクトリ以下での作業を前提とします。

必要なもの

  • python3
  • chainer
  • TensorFlow (TensorBoardのためだけに利用しているので、CPU版で良い)
  • curl

この実装は文字単位で処理しているので、MeCabなどの形態素解析器は必要ありません。
GPUは必須といえますが、DiscriminatorがMaxwell TITAN-Xだと何故か遅くなるという問題( https://github.com/pfnet/chainer/issues/2154 )があるので注意してください。

データの取得、整理

データ取得スクリプト(api/get-multi.sh)は以下のような内容です。小説家になろうのAPIは特に認証がないため、利用は容易です。

#!/bin/sh
# ref: http://dev.syosetu.com/man/api/
LIST=$(seq -w 1 100 2000)
URL='http://api.syosetu.com/novelapi/api/?gzip=1&out=json&of=s&lim=100'
GENRE="101 102 201 202 301 302 303 304 305 306 307 401 402 403 404 9901 9902 9903 9904 9999 9801"
for genre in $GENRE
do
    for i in $LIST
    do
    curl $URL"&genre=$genre&st=$i" |gzip -cd > g$genre-$i.json
    sleep 40
    done
done

カレントディレクトリをapiに移動した上でこのスクリプトを実行すると、g[ジャンルコード]-[開始id].jsonというファイルが大量に生成されます。APIの結果をそのまま保存したもので、あらすじの文章が入っています。
その状態でjtoj.pyを実行すると../dataset/output.jsonというファイルが生成されます。
親ディレクトリ(nevlist_ni_narou直下)移動してdataset/arasuji.pyを実行すると、その内容をArasujiクラスのインスタンス化としてシリアライズし、arasuji.datという名称で保存します。カレントディレクトリに生成されるので、mv arasuji.dat dataset/ を実行してdatasetディレクトリに移動します。ここまでの作業で、データセットの準備は完了です。

学習

run_sequence_gan.pyを実行します。

python run_sequence_gan.py \
 --out result \ # ./runs/result以下に結果を出力
 --d_steps10 # Discriminator学習時のepoch回数

ハイパーパラメーターは複数ありますが、特にDiscriminatorの学習回数は重要で、これとGeneratorとのバランスがうまく取れている必要があります。私が試した範囲では、Genetatorはデフォルト値の1のまま、Discriminatorはデフォルト値5だとちょっと文法的におかしな結果に進んでしまうように見受けられました。

学習経過

epoch 1

total epoch 1 test_loss 79.88113305507562 
各なとある惑星は円士の吹奏きの奮闘委員高校が卒業してきた。<EOS>                 

魔法の世界ス「草合衛士の凄棒」と「霊能者」という微知さらぬ形団の物語。<EOS>     

天人、神体優秀は自分の事故に遭遇していた。<EOS>                                 

ある日突然、右腕には、警察官事務所。<EOS>                                       

イドシアがいる人妹。<EOS>                                                       

子供に待っていたと泣きたいと思ったのだろうような.....。<EOS>                  
喫茶店と会うことを書いた強編出す。<EOS>                                         

ブロエフルだった存在は、目覚めたanbe。<EOS>                                    
普通の高校生の森羅人は未命の中心壁と佐秀毎戸と魔法を推理し落。<EOS>             

エラネンが存在する21日。<EOS>                                                   

epoch 5

total epoch 5 test_loss 80.92336227547409 
気づいたら、。<EOS>                                                                
ちょりが。<EOS>                                                                    
四大だった男、合木 朱馬。<EOS>                                                    
魔獣を使うういさんのお話。<EOS>                                                    
改稿置いたら。<EOS>                                                                
高校に通う貴族と幼馴染が女にあると。<EOS>                                          
天高一年.............。<EOS>                                          
これは、十きのオテラディールです。<EOS>                                            
私は幼馴染をしている。<EOS>                                                        
月を見てくれたら、そこたちのどのとある国族がいました。<EOS>                        

epoch 10

total epoch 10 test_loss 80.50691464415982 
授業がどこにでもない一人の輝。<EOS>                                             

これは、ある物語。<EOS>                                                         

秋のそれぞれ。<EOS>                                                             

桜代の主人公は、科学商者が入生した。<EOS>                                       

大学生の僕の子のです。<EOS>                                                     

東京国員創作たちがあるのです。<EOS>                                             

夢を続けた、小さな国の一枚前会。<EOS>                                           

富茂くるひとつな、後の小さな町。<EOS>                                           

超能力科室になった魔法学園ち。<EOS>                                             

ごく普通のバイトをするジェラリア。<EOS>                                         

epoch 16

total epoch 16 test_loss 79.93209936679938 
ここにはなし。<EOS>                                                                
僕は君の夜で引き合った。<EOS>                                                      
時時代の最新に、・・・-------・・・・・。<EOS>                              
惑星戦国発行地球に広社の時代を必放させ、東京時代で、ごつかない古いは異世界に転生明治圭太はスケーチャン少女組織の先に喫茶店を増えた。<EOS>                          
20x文字小説落選作品です。<EOS>                                                  
俺は、生徒会者の四部の男女。<EOS>                                                  
とある恋愛オバリス。<EOS>                                                          
異世界に転生しす ためにと魔法をしくしました。<EOS>                                
砂漠だったひとつり笑う場華が終わっていく兄妹と。<EOS>                              

雑感

文法

文法的におかしいものはやはり出てきてしまっていますが、そうでもない箇所もそれなりにあるのでそこそこうまく行っているようです。とはいえ、文法的に正しくても意味的におかしい文章が大半を占めています。

固有名詞らしきもの

「ジェラリア」「オバリス」など学習元のデータにはないけど人名っぽいものが生成されています。

データセット

学習に用いたあらすじのデータは内容の幅が広く、長さも100文字以上のものもあれば、「投稿テストです。」というようなそもそもあらすじでないものも混じっているのですが、そのまま利用してしまっています。
ある程度内容を選別したほうが良い結果になるかもしれません。

今後

WGANのような工夫を反映させると、もう少し学習がうまくいったりしないかなと考えています。ただ、RNNの値を足しこむ構造はWGANで用いる重みのクリッピングとはうまいことマッチしない気がしています(憶測)。

ソースのnico_commentの方ではタグ情報を分散表現にして、RNNの初期値に使うことで特定のタグに属する文章を生成させるということができるようになっているので、それに対応したデータセットも用意できるようにしたいところです。
一応、ニコニコ動画のコメントの公開データセットは存在しているのですが、いわゆるコメントアートのために空白をたくさんもったデータが多く存在しているなど、クレンジングで相当苦労しそうに思うのでそちらで試したことがありません。

関連リンク

謝辞

Chainer実装を公開してくださっているfukuta0614さんには大変感謝をしております。