【エヴァンゲリオン】アスカっぽいセリフをDeepLearningで自動生成してみる

  • 398
    いいね
  • 0
    コメント
この記事は最終更新日から1年以上が経過しています。

はじめに

エヴァンゲリオン20周年おめでとうございます:confetti_ball:
加えて、アスカの誕生日もおめでとうございます。(4日遅れ)

Twitter Bot等でも使われている、文章の自動生成を流行りのDeepLearningの1種であるリカレントニューラルネットワーク(以下:RNN)を使ってやってみました。

データ集め

何はなくともまずはデータが無いと始まりませんね。
書き起こしも覚悟してましたが、アニメ全セリフをまとめてあるありがたいサイトが有りました。感謝。

こちらから全セリフを抽出しました。
セリフのフォーマットはこんな感じで、キャラ名 「セリフ」になってます。

放送「本日、12:30分、東海地方を中心とした関東中部全域に特別非常事態宣言が発令されました。住民の方々は速やかに指定のシェルターに避難してください」 
放送「繰り返しお伝えいたします…」 
ミサト「よりによってこんな時に見失うだなんて、まいったわねー」 

電話「特別非常事態宣言発令のため、現在すべての通常回線は不通となっております」 
シンジ「だめかぁ。やっぱり来るんじゃなかった…。待ち合わせは無理か…。しょうがない、シェルターへ行こう」 

オペレーター「正体不明の移動物体は依然本所に対し進行中」 
シゲル「目標を映像で確認。主モニターに廻します」 
冬月「15年ぶりだね」 
ゲンドウ「ああ、間違いない、使徒だ」 

・・・ 

キャラごとのセリフ抽出

このデータからキャラごとのセリフに分け、セリフを自動生成するための元となるデータを作成します。

pythonでサラッと書いてみました。

# -*- coding: utf-8 -*-

import sys
import os
import chardet

from os.path import join, splitext, split

# 入力ディレクトリと出力ディレクトリを指定
readdir = sys.argv[1]
outdir = sys.argv[2]

print "readdir:\t", readdir
print "outdir:\t", outdir

# ディレクトリのファイル一覧を取得
txts = os.listdir(readdir)

for txt in txts:
    if not (txt.split(".")[-1] == "txt"):   # 拡張子がtxt以外を無視
        continue
    txt = os.path.join(readdir, txt)
    print txt

    fp = open(txt, "rb")

    # ファイルの文字コードを取得
    f_encode = chardet.detect(fp.read())["encoding"]

    fp = open(txt, "rb")
    lines = fp.readlines()

    for line in lines:
        # unicodeに変換
        line_u = unicode(line, f_encode)

        # キャラ名を取得
        char_name = line_u[:line_u.find(u"「")]
        outfname = os.path.join(outdir, char_name + ".txt")

        # キャラ名のファイルがあるかどうか確認
        if os.path.exists(outfname):
            # ある場合は上書きモードで
            outfp = open(outfname, "a")
        else:
            # ない場合は新規作成
            outfp = open(outfname, "w")

        # セリフのみ抽出
        line_format = line_u[line_u.find(u"「") + 1:line_u.find(u"」")] + "\n"
        # セリフ書き込み
        outfp.write(line_format.encode("utf-8"))

テキストファイルから1行ごとに読みこみ、の前にあるキャラ名との間のセリフに分け、キャラ名のファイルがすでにある場合は上書きモードファイルを開き書き足し、ない場合は新規作成しています。

生成されるファイルはこんな感じです。セスナなんてキャラいたっけと思いましたが、中身を見ると、セスナに乗ってた人の無線のセリフでした。

・
・
アスカ.txt
カヲル.txt
キール.txt
クラス.txt
セスナ.txt
ナオコ.txt
・
・

中身はこんな感じです。

アスカ.txt
ハロ~ォ、ミサト!元気してた?
そ。ほかのところもちゃんと女らしくなってるわよ。
見物料よ。安いもんでしょ。
何すんのよ!
で、噂のサードチルドレンはどれ?まさか、今の…
フーン、冴えないわね。
・
・

ちゃんとセリフだけが取れていますね。

学習用のデータが出来たので、本題の学習用のプログラム作成に入ります。

セリフの自動生成

言語モデル、文章生成の方法はいくつかありますが、今回はリカレントニューラルネットワーク(RNN)を用います。比較手法として、マルコフ連鎖を用いた方法も実施します。

RNNとは

RNNは内部に閉路を持つニューラルネットワークの総称です。

例えば、この図のように時刻 t の中間層の内容が、次の時刻 t+1 の時の入力として扱われる手法です。
RNNはこの構造があることで、情報を一時的に記憶して次の入力に引き継ぐことができます。これにより、データの中に存在する「時間の流れ」をとらえて、処理することができます。
今回のプログラムでは、Long short term memory(LSTM)というRNNのノードの代わりに、入力値を保持しておけるBlockを採用しています。

しっかりと説明すると、とても長くなるのでわかりづらくて申し訳ありませんが、詳しい概要や分かりやすい説明は、他の方の資料をご覧ください。

マルコフ連鎖とは

マルコフ連鎖(マルコフれんさ)とは、確率過程の一種であるマルコフ過程のうち、とりうる状態が離散的(有限または可算)なもの(離散状態マルコフ過程)をいう。また特に、時間が離散的なもの(時刻は添え字で表される)を指すことが多い(他に連続時間マルコフ過程というものもあり、これは時刻が連続である)。マルコフ連鎖は、未来の挙動が現在の値だけで決定され、過去の挙動と無関係である(マルコフ性)。
Wikipediaより

今回の3-gramを作って、マルコフ連鎖を行います。
3-gramとは、ある文字列から切り出した3つの単語(文字)の並びの集合です。

例えば、

なんで男の子って、ああバカでスケベなのかしら!

という文章があったすると、今回は、Mecabを用いて単語ごとに切り出すので、その単語を1つずつずらしながら3つずつの塊を作ります。

単語 単語 単語
(BOS) なんで 男の子
なんで 男の子 って
男の子 って
って ああ
ああ バカ
ああ バカ
バカ スケベ
スケベ
スケベ
かしら
かしら
かしら (EOS)

BOS : Begin Of Sentenceの略
EOS : End Of Sentenceの略

マルコフ連鎖についても詳しい説明は他の方の分かりやすい資料を参考ください。

実装

RNN

RNNに対応しているライブラリは

などがあります。
GoogleのTensorflowが流行ってますが、敢えてChainerで。(日本製ですし)

chainerを用いた英語の文章生成プログラム
yusuketomoto/chainer-char-rnn · GitHub
を基本に改変して作成しました。

プログラムの核となる部分だけ簡単に説明します。

CharRNN.py内
embed = F.EmbedID(n_vocab, n_units),
l1_x = F.Linear(n_units, 4*n_units),
l1_h = F.Linear(n_units, 4*n_units),
l2_h = F.Linear(n_units, 4*n_units),
l2_x = F.Linear(n_units, 4*n_units),
l3   = F.Linear(n_units, n_vocab),

この部分ではモデルを設定しています。
n_vocabは、文字列内の単語の種類数
n_unitsはユニット数で今回は128に設定して実行しています。

CharRNN.py内
    def forward_one_step(self, x_data, y_data, state, train=True, dropout_ratio=0.5):
        x = Variable(x_data.astype(np.int32), volatile=not train)
        t = Variable(y_data.astype(np.int32), volatile=not train)

        h0      = self.embed(x)
        h1_in   = self.l1_x(F.dropout(h0, ratio=dropout_ratio, train=train)) + self.l1_h(state['h1'])
        c1, h1  = F.lstm(state['c1'], h1_in)
        h2_in   = self.l2_x(F.dropout(h1, ratio=dropout_ratio, train=train)) + self.l2_h(state['h2'])
        c2, h2  = F.lstm(state['c2'], h2_in)
        y       = self.l3(F.dropout(h2, ratio=dropout_ratio, train=train))
        state   = {'c1': c1, 'h1': h1, 'c2': c2, 'h2': h2}

        return state, F.softmax_cross_entropy(y, t)

学習時の1ステップに関する部分です。
x_data, y_dataにはバッチサイズを与え、
隠れ層ではLSTMで、出力は、ソフトマックス交差エントロピー関数を用いています。

train.py内
def load_data(args):
    vocab = {}
    print ('%s/input.txt'% args.data_dir)
    f_words = open('%s/input.txt' % args.data_dir, 'r')
    mt = MeCab.Tagger('-Ochasen')

    words = []
    for line in f_words:
        result = mt.parseToNode(line)
        while result:
            words.append(unicode(result.surface, 'utf-8'))
            result = result.next
    dataset = np.ndarray((len(words),), dtype=np.int32)

    for i, word in enumerate(words):
        if word not in vocab:
            vocab[word] = len(vocab)
        dataset[i] = vocab[word]
    print 'corpus length:', len(words)
    print 'vocab size:', len(vocab)
    return dataset, words, vocab

この部分では、入力して、データを与える際に、MeCabで形態素解析して単語ごとに分けて、与えています。

他の方のchainerのリカレントニューラルネットワークのサンプルコード解説もあるのでそちらも参考ください
Chainer を用いたリカレントニューラル言語モデル作成のサンプルコード解説に挑戦してみた

マルコフ連鎖

他の方が作成された
Pythonリハビリのために文章自動生成プログラムを作ってみた
o-tomox/TextGenerator · GitHub
をそのまま使いました。ありがとうございます。

文章生成の流れはこのようになってます。

  1. まず(BOS)で始まる組から文章生成を開始。
  2. (BOS)の組の3つ目の単語で始まる別の組を探す。複数見つかった場合はそこからランダムで一つを選ぶ。
  3. これを3つ目の単語が(EOS)になっている組になるまで繰り返す。
  4. これまで選んできた組の単語を結合して文章完成。

結果

20文ずつ、自動で生成させてみました。

RNN

あんた!ここの出番よ。好きわよ。
いやぁ!
逆らわごろ死んだな、もう、もう!今だけだないのよ!
で、あたしの、閉ざしなさいよ!
イヤ、そう
バカ、待ってみなさいよ。
真実で、バカ、いやぁ!
むぅぅ!
そりゃバカ!それでこんでしょ。
何よ、動かないじゃない!
どう、上記の中になさいよ。
フーン、冴えないわね。
いやらしい!反吐、バカ、ちゃんと起き機は、ちょっとちょっとの人と言ってだけだのよ。
へぇぁあああああ!
それがちょっと.?開発行くわよ!
互いに弐号機に行くわよ。
もちろん、動かないわ。
殺さないわ。私はね。誰も嫌いもいらない。
何が司令、自分に決まってることって、ぬいぐるみだに、エヴァをやっつけるで!

何が司令、自分に決まってることって、ぬいぐるみだに、エヴァをやっつけるで!
アスカ関西人説

マルコフ連鎖

じゃ、行くわよ!
行きたくない?
あたし、アスカ、行くわよ、右腕だけはっきりして同衾せずに気楽にやればいいの?
ちょっと見せて…この程度の数式が解けないの!
もうやめてよぉ…飼い慣らされた男なんて、明日は雪かしらね!?
あーん、早く来てぇ!
熱膨張?幼稚な事もういらないのに!
こーいうのは止めてママ!
私、選ばれたの。篭りっぱなしよ。
待ってました!ナイフは落としちゃったじゃない安心してないの!
もぉ、あんたから話掛けてくるなんて、サイテー見物料よ。
他に人、いないわ。
あんなの私一人で生きるの!
他に人、いないの?
大丈夫、まだ聞いてないの。
このあたしがやるわ、何としたの。
ええっ…もう、どうしようもないわ…
勝手に仕切らないでよね、最初からフル稼動、最大戦速で行くわよ。
どれどれ、何やってんのかしら。
ちぇーったい、前見ないでよね。

原文そのままで出てきている文章もあります。
文法的にはこちらの方が正しい文章が多いですね。

最後に

今回はマルコフ連鎖の方が文法的には良い結果となりました。
パラメータ調整等を一切してないので、RNNを活かしきれていない。RNNでより良い文章生成方法があれば、ご教授願いたいです。

まだまだ自動生成には可能性があると思うので、今後に期待したいです。

おまけ

シンジとレイもついでにやってみました。

シンジ

RNN

アスカさん!
でも、みんな僕に嫌いだって。
今日もありがとうで、捨てたって、恐いんだ!僕って…僕はいないんだよ。
僕は…誇れる人がいるんだ、父さんって僕のこと氷の見つめだったんだ。でもかなぁ。やめてくれないんだ!
うん。うん、自分の匂い。
そう、僕はもう、エヴァに僕に乗っているもののかな…から…さん。世界は僕がいらないものが僕。僕には乗るがのに、僕が僕から!
習っている前、逃げなかったんだ。自分にできるみたいだ!
そうだよ。でしょ僕のことを目標に恐いんだ。
そうだ、僕は僕ではいい。
僕は、僕は僕をの僕なの?何が世界なんだ!
雨、覚えている分からないのに。グッ!
波に乗らないですか?
んな、その世界、ほんとなのかな…僕を敵、葛城波で分からないのか?

「僕」がゲシュタルト崩壊

マルコフ連鎖

誰が?許せなかったんだよ。
僕に価値はない僕もいいと思う?
僕は知っていると思う。
それでミサトさん!
なんだか寂しい街ですね…?
なんでわざわざ、大騒ぎしなきゃならないんですか。
でも仕方なかったなぁ…僕に価値がほしいんだけど、どれも本物の碇シンジ、綾波!
きます。やめてよ!
僕は逃げ出したんじゃないか!
あの…僕はどうすればみんなが、あの…父さんと楽しそうに決まってる!
綾波が。がんばってるんだ。
悪いのはもう、エヴァに乗っているわけじゃない兵器、役に立つんです。
あの、真っ暗ですよね、夕飯、食べたくない…いないんだ。
あは、何を言ってんだよ!
分かろうとするから大変なこと思い出すほうが多いんだ。そうしないと、僕らの敵。
ふぬぅーーーーっ!

レイ

RNN

どうして?
あなたはいっしょ。血の入れ物。
それが作っているの?
あなたはで、あなたも心に、寂しい!
あなたの世界、生きているの。
やはり、遅刻遅刻ぅ!初日、遅刻ぅ
はい
あなたは、偽りの中にはしないもの。
そうよ。破滅で、自由が消えていく。
何も、他の人がいないと自分が見えないの。
何が嫌い、消えから。自分をみんなにいる。
あなた、綾波レイですでしょは、母親。
あなたとあなたとほんとの気持ちなの?
あなたが、覗きにきたとヤシマ作戦の人がする
で、綾波レイの?

やはり、遅刻遅刻ぅ!初日、遅刻ぅ
あのシーンのセリフから出力されているんですが、この並びに入ってくると違和感がすごい。(セリフが違うのもありますが)

マルコフ連鎖

…非常召集…先、行くから。
仕方ないわ。
とてもとても気持ちいいことなの。
同30(イチナナサンマル)、ケイジに集合雨の日は憂鬱。
でも駄目、無へと返りたいのね。
後は私が守るものだから、みんなもそうだと思い込んでる。
じゃ、寝ていて嬉しい?
誰のために?
私は何もないもの。
とても変。
今は碇司令。
だめ、私と同じ感じがする。
んああああ!
ただ、あなたの中に、何もないもの、目に見えないのね。
一人で帰れるから放っといて、その格好で来ないでね
この投稿は CirKit Advent Calendar 20157日目の記事です。