にほんごのれんしゅう

日本語として伝えるための訓練を兼ねたテクログ

艦これのセリフ分類をCNNでやる

(2017/2/24追記. いろいろ試したんですが、objective functionをcategorial cross entropyからpoissonに変更し, softmaxの出力をlogを取ることで、急峻なスパイクを抑えることができある程度改善しました )

艦これのセリフ分類をCNNでやる

 幾つかの基礎と、業務で使用できるかどうかの調査した結果、CNNでのテキスト分類が最近評判が良いことがわかった。
 RNNが負けると言われていた分野は分類とか識別の部分で、テキストの生成や連続系ではまだ、RNNが有利であると思う。

 ディープラーニング以前のアルゴリズムは変数の重要度に対しての解釈をする方法が、ある程度ノウハウが蓄積されており、変数の係数や、決定時の出現する変数の頻度でそれっぽく解釈はできたが、ディープラーニングは中身がうかがい知れない事が多い。しかし、性能は高いとされている[1]

 何にせよ、CNNでのテキスト分類は簡単にできるので、情報系が志す人はやっておいた方がいいと思う。

注:また、偶然であるが、RettyさんのCNNによるテキスト分類とネタ的にかぶってしまった。同じchar粒度だし[2]。
twitterのボットとソースコードが見えるのが違う点だと思います)

CNNでのテキスト分類

- 標準的なCNNでのテキスト分類を用いて、艦これのキャラクタのセリフを分類する
- 艦これのキャラクタの発言がそもそもMeCabなどで形態素解析するのに不適な語彙がおおい(ex:はわわ~、ぱんぱかぱーん、造語等)
 なので、形態素解析を必要としない単語粒度のCNNでの分類を行った(twitterで以前行った人の話を聞くと精度はでるらしい)
- ネットワーク図を記す
f:id:catindog:20170223231442p:plain

図1. ネットワーク図

実際に使用したネットワークより簡略化している
1. Embeddingと呼ばれる文字情報もベクトル化を行う
2. 1~6文字を連結した状態で畳み込みを行う
3. Pooling層をとおしてそれぞれの単語の長さの粒度の出力層をConcat(連結)してDense(全結合層)に入力する
4. 今回は、複数のキャラクタがいるため、Softmaxと呼ばれる方法でマルチクラスに対応する
5. objective functionをpoissonにする
6. logsoftmaxがないので、softmaxの出力値のlogを取る

コード

KerasというTensorFlowを再利用する形で利用するディープラーニングフレームワークがあるのだが、短く簡潔にかけるのでChainerとともによく使う。
今回はKerasで実装した。
コードの全体の説明は長くなってしまうので、ネットワークの構成だけ示す。アドホックなところは、色々とネットワークの構成やパラメータを変えて、分類能が高い構成を探すためである(このネットワークで決まりで、もういじらないなら抽象化してコードを短くできるけど、多分これからもネットワークをいじるので)。

def build_model(sequence_length=None, filter_sizes=None, embedding_dim=None, vocabulary_size=None, num_filters=None, drop=None, idx_name=None):
  inputs = Input(shape=(sequence_length,), dtype='int32')
  embedding = Embedding(output_dim=embedding_dim, input_dim=vocabulary_size, input_length=sequence_length)(inputs)
  reshape = Reshape((sequence_length,embedding_dim,1))(embedding)
  conv_0   = Convolution2D(num_filters, filter_sizes[0], embedding_dim, border_mode='valid', init='normal', activation='relu', dim_ordering='tf')(reshape)
  pad_0    = ZeroPadding2D((1,1))(conv_0)
  conv_0_1 = Convolution2D(512, filter_sizes[0], 3, border_mode='valid', init='normal', activation='relu', dim_ordering='tf')(pad_0)
  conv_1 = Convolution2D(num_filters, filter_sizes[1], embedding_dim, border_mode='valid', init='normal', activation='relu', dim_ordering='tf')(reshape)
  pad_1  = ZeroPadding2D((1,1))(conv_1)
  conv_1_1 = Convolution2D(512, filter_sizes[1], 3, border_mode='valid', init='normal', activation='relu', dim_ordering='tf')(pad_1)
  conv_2 = Convolution2D(num_filters, filter_sizes[2], embedding_dim, border_mode='valid', init='normal', activation='relu', dim_ordering='tf')(reshape)
  pad_2  = ZeroPadding2D((1,1))(conv_1)
  conv_2_1 = Convolution2D(512, filter_sizes[2], 3, border_mode='valid', init='normal', activation='relu', dim_ordering='tf')(pad_1)
  conv_3 = Convolution2D(num_filters, filter_sizes[3], embedding_dim, border_mode='valid', init='normal', activation='relu', dim_ordering='tf')(reshape)
  conv_4 = Convolution2D(num_filters, filter_sizes[4], embedding_dim, border_mode='valid', init='normal', activation='relu', dim_ordering='tf')(reshape)
  maxpool_0   = MaxPooling2D(pool_size=(sequence_length - filter_sizes[0] + 1, 1), strides=(1,1), border_mode='valid', dim_ordering='tf')(conv_0)
  maxpool_0_1 = MaxPooling2D(pool_size=(sequence_length - filter_sizes[0] + 1, 1), strides=(1,1), border_mode='valid', dim_ordering='tf')(conv_0_1)
  maxpool_1   = MaxPooling2D(pool_size=(sequence_length - filter_sizes[1] + 1, 1), strides=(1,1), border_mode='valid', dim_ordering='tf')(conv_1)
  maxpool_1_1 = MaxPooling2D(pool_size=(sequence_length - filter_sizes[1] + 0, 1), strides=(1,1), border_mode='valid', dim_ordering='tf')(conv_1_1)
  maxpool_2 = MaxPooling2D(pool_size=(sequence_length - filter_sizes[2] + 1, 1), strides=(1,1), border_mode='valid', dim_ordering='tf')(conv_2)
  maxpool_2_1 = MaxPooling2D(pool_size=(sequence_length - filter_sizes[2]  - 0, 1), strides=(1,1), border_mode='valid', dim_ordering='tf')(conv_2_1)
  maxpool_3 = MaxPooling2D(pool_size=(sequence_length - filter_sizes[3] + -3, 1), strides=(1,1), border_mode='valid', dim_ordering='tf')(conv_2)
  maxpool_4 = MaxPooling2D(pool_size=(sequence_length - filter_sizes[4] + -2, 1), strides=(1,1), border_mode='valid', dim_ordering='tf')(conv_2)
  merged_tensor = merge([maxpool_0, maxpool_0_1, maxpool_1, maxpool_1_1, maxpool_2, maxpool_2_1, maxpool_3, maxpool_4], mode='concat', concat_axis=1)
  flatten = Flatten()(merged_tensor)
  dropout = Dropout(drop)(flatten)
  output = Dense(output_dim=len(idx_name), activation='softmax')(dropout)
  adam = Adam()
  model = Model(input=inputs, output=output)
  model.compile(optimizer=adam, loss='poisson', metrics=['accuracy'])
  return model

全体のコードはgithubにおいてあってそのままダウンロードで使えるようにしておきます。
(Python3とTensorFlowバックエンドのKerasが必要です)

github.com

$ git clone https://github.com/GINK03/keras-cnn-text-classify
$ cd keras-cnn-text-classify
$ python3 model.py --train --all
(ビルトインしているデータ・セットでGTX 108030分ほどかかります)

# 確率を計算します。これはkerasのMC(マルコフ連鎖モンテカルロ法) searchのプラクティスから抜き出した方法で計算しています
# 最大値を100%として可能性を提示していますが、現実世界の確率と解釈は同じではありません
$ echo "大丈夫、きっと僕は君のことを忘れない" | python3 model.py --pred
時雨 100%
日向 75%
レーベレヒト・マース 27%
初月 27%
武蔵 11%
若葉 11%
能代 4%
ハルナ 3%
利根 1%
木曽 1%
$ echo "あのあの司令官さん、どうしたのですか" | python3 model.py --pred
電 100%
春雨 26%
吹雪 22%
沖波 16%
雪風 11%
霰 9%
高波 6%
春風 2%
青葉 1%
菊月 1%
$ $ echo "そんなんじゃだめよー" | python3 model.py --pred
敷波 100%
雷 61%
望月 55%
江風 47%
鈴谷 41%
時津風 26%
島風 15%
瑞鶴 15%
北上 5%
睦月 1%
$ echo "もうちゃんとレディとして扱ってよね" | python3 model.py --pred
暁 100%
熊野 38%
夕張 37%
愛宕 32%
天津風 21%
能代 6%
最上 6%
加古 3%
金剛 1%
瑞鶴 1%
$ echo "フリーダム響だよ" | python3 model.py --pred
響 100%
江風 55%
木曽 23%
加古 18%
敷波 16%
時雨 12%
イ168 8%
イ26 1%
深雪 1%
涼風 1%
$ echo "おっそーい" | python3 model.py --pred
島風 100%
時津風 43%
イ26 32%
U-511(呂500) 31%
瑞鶴 8%
望月 6%
江風 1%
敷波 1%
涼風 1%
初風 1%
$ echo "司令、なにやってんの~、ねぇってばー" | python3 model.py --pred
黒潮 100%
陽炎 67%
時津風 49%
イ26 15%
酒匂 9%
青葉 8%
雷 8%
文月 1%
比叡 1%
敷波 1%

パラメータと精度確認

学習の様子を見ていると、過学習に存外早く陥ってしまうことがわかった。
epoch 100までやったが、epoch 10~20あたりがちょうど良さそうであった。

Train Acc: 97%, Validation Acc: 56%
(56%というのは会話文では、他のアルゴリズムとくらべても悪くないものだと思う)

パラメータ

embedding次元:256
filterサイズ: 1, 2, 3, 4, 5
filter数:512
dropout:0.5
epoch: 10
batch: 30
Optimizer: Adam(学習率等はデフォルト)
objective function: poisson

せっかくできたのでbotにした

昔使ってたアカウントをボットに変更して、リプを送ると、リプの内容が艦むすでいうと誰の発言になるのか、確率(のようなもの)を表現するbotを作った。
サーバに立ち上げておかなきゃいけないものなので、いつまで公開しているかわからないが、遊んでほしい。
コミケの製作者側は、このセリフをこの子に言わせたらどうなんだろう?とかってあると思うけど、そういうときに役に立つかも。
(不定期に止めたりアップデートしたりする予定です)

f:id:catindog:20170223231923p:plain

図2. Deep時雨の様子(めっちゃ不安定です)

@deep_shigureです。よろしくお願いします
twitter.com
(すぐ落ちるので、そのときはほんとすみません。だれか管理しませんか)

一人あたりの発言が少なく、データセットの量が足りずに、結構苦労しています。だれかpull requestとかforkとかしてよりすごくしてくれたら嬉しいです。(メールは気付かないことがあるけど、twitterはよくみているので知らせてください)

参考

[1]


[2]
speakerdeck.com