ごちうさしか見たことない人がけもふれを見るとどんなコメントをするのか，コメントだけで機械はチノちゃんを認識できるのか

深層学習を用いたキャプション生成モデルでニコニコ動画のコメントを学習し自動生成させました．関係する論文はこちら．
[Show and Tell: A Neural Image Caption Generator](https://arxiv.org/abs/1411.4555) ([code](https://github.com/tensorflow/models/tree/master/im2txt))
[Show, Attend and Tell: Neural Image Caption Generation with Visual Attention
](https://arxiv.org/abs/1502.03044)

### コメント生成結果
![gif_1](https://drive.google.com/uc?export=view&id=0B_o9vkhBpXoYQkNBd0hNbzYyTG8)
ごちうさの動画とコメントを学習 (train, dev)

![gif_2](https://drive.google.com/uc?export=view&id=0B_o9vkhBpXoYLWUwc3U5bThLM00)
けもフレの動画でコメントを生成 (test)

コードの一部は末尾に記載しています．

# 今回やったこと
ニコニコ動画の動画とコメントをセットに学習して別の動画に適用してみました．
特に新しい技術は使っていません．機械学習のチュートリアル的な感じでやってみました．

大まかな流れはこのようになります．
1．ご注文はうさぎですか?第1羽 でコメントを学習
2．けものフレンズ第1話 でコメント生成
3．アテンションモデルで学習して可視化

# キャプション生成
キャプション生成では入力された画像についての説明文を生成します．学習データには画像と文がついています．データセットにはCOCOやFlickrなどがあります．
今回は，動画の1フレームを抜き出してその画像1枚に対してコメントを生成する，ということを行います．これに深層学習を用いたキャプション生成を用います．


# キャプション生成モデル
今回使うのはCNN+RNNのものです．大ざっぱに言うと，画像の特徴をCNN，文の特徴をRNNで抽出して，それぞれの対応を学習します．
キャプション生成の歴史は [画像キャプションの自動生成，Yoshitaka Ushiku，2016](https://www.slideshare.net/YoshitakaUshiku/ss-57148161)

# データセット
### コメント
ニコニコ動画のコメントを使わせていただきました．ダウンロードはサキュバスを使わせていただきました．
Pythonからサキュバスを呼んで最新のコメントから最も古いコメントまで順にさかのぼって42万コメント程ダウンロードしました．
それらをMeCabで単語に区切り，全部で75,356単語になりました．頻度上位10件と個数は次のようになりました．

``` .py
　 207,947
いい 170,959
うう 145,736
！ 46,939
の 43,857
・ 31,119
か 26,608
ん 25,683
が 25,392
お 24,575
しげる 24,540
```

1位は空白文字 '\xe3\x80\x80' でした．
また名詞と判断された単語の頻度上位30件は次のようになりました．

``` .py
松崎 24,228
しげる 24,165
麻生 23,962
太郎 22,832
( 17,850
) 17,638
ﾋﾟ 13,952
チノ 13,812
～ 12,410
ここ 11,916
ん 11,414
ﾛﾋ 11,324
ﾟﾛ 11,298
♪ 10,916
オオ 9,677
石破 9,463
茂 9,244
ゴク 8,981
ー 8,664
O 8,038
ラオ 7,966
俺 7,775
ｵｵ 7,537
二 6,993
ｺﾞ 6,130
無駄 6,099
の 5,990
ココア 5,909
修造 5,852
松岡 5,680
w 5,637
```

この7万単語のうち頻度準上位3万件を使用し，残りを未知語としました．3万番目の単語の頻度は2でした．普段はデータセットで出現回数が5未満の単語を未知語とするのですが，今回は語彙を増やしたかったので閾値を下げました．

また，フレームごとのコメント数の多い順上位10件は次のようになりました．

| コメント数 | フレームindex | 動画内での時間 |
|:-:|:-:|:-:|
|1,482 | 0| 0m0s|
|1,339| 42,468 |23m37s|
|622| 10,531| 5m51s|
|446| 10,530| 5m51s|
|342 |10,532| 5m51s|
|195 |28,795| 16m0s|
|188 |8,075 |4m29s|
|164 |10,529| 5m51s|
|121 |25,169| 13m59s|
|121 |28,091| 15m37|

上位2つは1番最初と最後のフレームです．コメントについている動画の何フレーム目のものかというですが，動画の最大フレームより少しだけ大きい値まで設定されていたので，それらはすべて一番最後のフレームということにしました．いくつか画像とランダムに取得したコメントを掲載します．(画像は荒くしています)

0フレーム目
<img src="https://qiita-image-store.s3.amazonaws.com/0/195522/47941c46-0f6e-3ec7-b16b-ea8d7c5e113d.jpeg" width=50%>
歴代 の 日本 最 高齢 　 大川 ミサヲ 　 117 歳 27 日
150 万 再生 まで 1772
帰る 場所

42,468フレーム目
<img src="https://qiita-image-store.s3.amazonaws.com/0/195522/deccdf67-25f2-9810-23f3-13c54e9d1b20.jpeg" width=50%>
ザ・ワールド
ぶっ ちっ ぱ
かわゆ す

10,531フレーム目
<img src="https://qiita-image-store.s3.amazonaws.com/0/195522/0d910f49-641c-8685-3d13-7711d0fc5a19.jpeg" width=50%>
ここ
いま よ ！
こ ↑ こ ↓ あ

10,530フレーム目
<img src="https://qiita-image-store.s3.amazonaws.com/0/195522/d9c93b76-51cd-99d2-0a94-bd476a1ee091.jpeg" width=50%>
ここ
ここだ ！
ここだ ！

10,532フレーム目
<img src="https://qiita-image-store.s3.amazonaws.com/0/195522/b58456e0-3847-7025-224c-f9d5fcd6bd03.jpeg" width=50%>
ここだ ！
ここ です ！
こ ↑ こ ↓ だ ！


### 画像
動画を1フレームごとに画像にし事前にCNNに通して特徴量を抽出しました．24分で42,469フレームでした．コメントは1フレーム平均10個ついていることになります．コメントのついていないフレームは513ありました．CNNはVGG-19を使用しました．640x480の画像を224x224にリサイズしてCNNに入力し，relu7層からの4,096次元のベクトルを使用しました．

# 学習
画像とそれについたコメントを学習します．
まずはごちうさのデータをtrainとdevに分割しました．ランダムにフレームを9:1で分けました．例えばtrainに分けられたフレームについたコメントはすべてtrainに属します．

学習はコメントについて回しました．例えばtrainに100フレーム，500コメントあった場合，500個のコメント―フレームの対を全部学習して1epochとなります．

今回はアニメ動画の10フレームに1フレームがdevでコメントの重複も多いのでかなりdevはtrainに含まれています．1秒おきにtrainとdevを切り替えたり，動画の前半後半でわけたりしたほうが良かったかもしれません．

各データのコメント数と重複については次のようになりました．(語彙とした30,000単語に含まれない単語を全て未知語シンボルに変換した後)

| | all | train | dev |
|:-:|:-:|:-:|:-:|
|全体| 427,364 | 38,4922 | 42,442 |
|uniq| 202,671 | 185,094 | 26,682 |

またuniq(train)とuniq(dev)のコメントのかぶりは9,105件でした．



# 動画化
今回はリアルタイム処理ではなく，学習によってできたモデルでフレームごとにコメントを生成し，それをつなぎ合わせて動画にしました．対象のフレーム全てについて一旦コメントを生成し1つのファイルにまとめ，それを後で画像に書き出します．
サキュバスの機能を使えばコメントファイルと動画ファイルがあれば自動でコメント付き動画を作れると思うのですが，やり方がわからなかったのでPythonで作りました．

0フレーム目から順に生成されたコメントを読み込んで画像に描画していきました．過去に描画されたコメントは4秒後にフレームアウトするように動かしました．上のごちうさ動画では，コメントを生成する量を(これはチートですが)データセットのコメント数の分布から決めました．

また，コメントのバリエーションを増やすために，1単語目はシンボルを除いたすべての単語から，単語の出現確率に基づいて確率的に選択し，2単語目以降は未知語以外から最も確率が大きいものを選択するようにしました．

# けものフレンズに適用
ごちうさのデータで学習したモデルでけものフレンズの動画のコメントを生成しました．

![jump_power.png](https://qiita-image-store.s3.amazonaws.com/0/195522/bbbc32f1-2ab1-97db-ad60-e3bff0e7c805.png)
このようにちゃんとフレンズ語が生成されました，というのはあり得ない話で，たまたまごちうさのコメントに含まれていたけものフレンズに関する単語が生成されただけです．

![gif_2](https://drive.google.com/uc?export=view&id=0B_o9vkhBpXoYY0xqRlVKUmZ3SkU)
![gif_2](https://drive.google.com/uc?export=view&id=0B_o9vkhBpXoYT1ZiaXRLY2lWcEk)

見てるとたまにそれっぽいコメントが流れてきて意外とけもフレで生まれた単語が学習データに含まれていることがわかりました．上の2つは同じシーンと同じコメントですが，スペースで単語を区切ったのが下のものになります．そもそもMeCabを普通に使っているのでスラングなどは全くうまく区切れていません．

予想では，けもふれで黒いシーンがあれば”しげってんなー”などのようにごちうさで黒いシーンでついていたコメントが生成されると思っていたのですが，思った以上に文法などが崩壊していました．学習データとの違いにRNNが戸惑っているようです．

# アテンションモデル
アテンションを使うことでRNNの性能をより引き出すことができます．Show, Attend and tellの例でみると，モデルは単語を生成するときに，その単語と関連する画像中の位置に注目することができるようになります．簡単に言えば，単語を生成する際，適当に画像のどこかに注目して，うまくいけば今後も同じような位置に注目するように学習します．

今回したいことは，学習が終わってたとえば”チノ”という単語が含まれるキャプションが生成されるフレームに対して，”チノ”という単語を生成するときに画像中のチノにアテンションがかかっていてほしいということです．

### 使用する画像特徴ベクトル
画像のどこかに注目するには，座標情報のようなものが必要です．
先ほどの学習方法ではそれがないので，ちょっと使用するデータを変更します．
先ほどとは違い，VGG-19のCONV5_3層からのベクトルを使用します．これは14x14x512になっています．画像を14x14の領域に区切って(オーバーラップしてます)，それぞれから512次元のベクトルを取り出したものになります．画像を224ではなく448x448にリサイズして28x28x512次元のベクトルを取り出すこともできます．
これを使えば，RNNにCNNベクトルを入力する際に14x14の重要な部分に大きな重み，そうでない部分に小さな重みをかけることで関係する単語に対応する領域に注目することができます．

# アテンション実験結果
![att_3.PNG](https://qiita-image-store.s3.amazonaws.com/0/195522/44866bb9-3958-a520-5c53-02534effba56.png)
チノちゃんが映っている画像に対して生成した文に”チノ”という単語が含まれていているものを持ってきました．この画像に対しては，”ここ の チノ ちゃん かわいい”という文が生成されました．

白くなっている部分が注目している部分で，単語を順に生成していく中で注目する位置が変化します．”チノ”という単語を生成するときにチノが映っている領域が白くなっていればおっけーという感じです．

全体的にアテンションのかかり方が微妙で役に立ってないのではという感じの結果となりました．この例ではそうではないですが，人物の顔には少しですがアテンションがかかる傾向があるように感じました．

ココアの場合は次のようになりました．
”ここ の ココア の 表情 好き”
![att_1.PNG](https://qiita-image-store.s3.amazonaws.com/0/195522/39ae94f9-c674-a185-e1ad-0e5e6f037eda.png)

![att_2.PNG](https://qiita-image-store.s3.amazonaws.com/0/195522/a2ab732b-28e6-512a-705d-53d0ea223b40.png)
1枚目ではココアのほうにアテンションがかかっているように思えますが，2枚目を見るとリゼにアテンションがかかっており，人物が識別できているわけではなさそうです．

原因はデータセットのコメント中の単語と画像中のオブジェクトがあまり対応していないことや，フレーム同士が簡単に分類できてしまうデータであることなどいろいろあると思います．

# 学習環境
単語ベクトル 30,000語彙 x 256次元
LSTM隠れ層 256次元
GPU NVIDIA TITANX 1枚

隠れ層はだいぶ少なめにしました．
モデルはLSTM1層，単語予測にMLP2層でした．

### 学習時間

|モデル|best validが出るのにかかったepoch数|batch size|1epochにかかる時間|
|:-:|:-:|:-:|:-:|
|VGG relu7，アテンション無し|19 epoch |256|7m30s|
|VGG Conv5_3，アテンション有り|25 epoch|256|25m0s|

# 結論
コメントが画像中に映っているもの以外のものにかなり依存しているので単純に学習させただけではうまくいかない．

# 感想とか
CNNはImageNetで事前学習されたものなのでアニメでどれだけ特徴が抽出できたのかわかりませんが，fine tuneすればもっと思った通りのアテンションが見れたかもしれません．そもそもVGGみたいなリッチなものじゃなくて自作とかでもうまくいったかもしれません．コメントのサイズと色も学習させようかと思ったのですがやめました．プレミアムコメント数は比率が小さいので学習するならロスにバイアスをかける等工夫が必要ですが，プレミアムコメントと通常コメントの内容に大差ないシーンでは確率に従ってランダム生成でいいかもしれません．
全羽学習させればキャラにアテンションかかったのかとか，セリフ学習すれば1羽でもアテンションかかったのかとかいろいろ気になるところが出てきました．やっていてコンソール上にニコニコのコメントが流れていくのはシュールで面白かったです．

# 使わせていただいたソフトウェア，ライブラリなど
### コメント取得
[Saccubus1.66.3.11](https://github.com/Saccubus/Saccubus1.x/releases/tag/1.66.3.11)
Pythonからコマンドライン操作を行いました．Ubuntu14.04だとエラーが出たのでここだけはWindows10で行いました．

``` .py
import subprocess
def download_comment(email, password, target_id, WaybackTime):
    cmd = 'java -jar Saccubus1.66.3.11\saccubus\saccubus.jar %s %s %s %s @DLC' % (email, password, target_id, WaybackTime)
    subprocess.call(cmd.split())
```

### コメントファイル読み込み
Python-xml
サキュバスでダウンロードしたファイルはxmlなので，それを次のように読みました．最も古い日付を次のWaybackTimeに指定して遡っていきました．

``` .py
import xml.etree.ElementTree as ET
def read_xml(path):
    return ET.fromstring(codecs.open(path, 'r+', 'utf-8').read())

def extract_data(root):
    date, vpos, text = [], [], []
    for r in root:
        if 'date' in r.attrib:
            date.append(int(r.attrib['date']))
            vpos.append(int(r.attrib['vpos']))
            text.append(r.text)
    return date, vpos, text

xml = read_xml(xml_path)
date, vpos, text = extract_data(xml)
oldest_date = min(date)
```

### 単語分割
[MeCab](http://taku910.github.io/mecab/)
素で使いました

``` .py
from natto import MeCab
mecab = MeCab()
for t in text:
    res_raw = mecab.parse(t.encode('utf-8'))
```

### 動画読み込み
Python imageio

``` .py
import imageio
vid = imageio.get_reader(movie_path + movie_name, 'ffmpeg')
frame = vid.get_data(idx)
```

### 画像へのテキスト書き出し
Python PIL

``` .py
from PIL import Image
from PIL import ImageFont
from PIL import ImageDraw 

img = Image.fromarray(frame)
draw = ImageDraw.Draw(img)
draw.text((x, y), text, font=font, fill=(0, 0, 0))
# 縁取り文字
def write_text_with_outline(draw, x, y, text, font, fillcolor, shadowcolor):
    offset = [[-1, 0], [1, 0], [0, -1], [0, 1], [-1, -1], [1, -1], [-1, 1], [1, 1]]
    for _x, _y in offset:
        draw.text((x + _x, y + _y), text, font=font, fill=shadowcolor)
    draw.text((x, y), text, font=font, fill=fillcolor)
```



### 画像動画変換
ffmpeg
色々方法はありますが最初のフレームと書き出すフレーム数を-start_number，-framesで指定しました．他のオプションはなくても大丈夫だった気がします．

```
ffmpeg -y -f image2 -start_number 0 -loop 1 -r 29.97 -i 'out/frames/target/%010d.png' -pix_fmt yuv420p -frames 40000 out/movies/target.mp4
```


### 機械学習ライブラリ
[TensorFlow](https://www.tensorflow.org/) 0.12

# 参考論文
[Show and Tell: A Neural Image Caption Generator, Vinyals et al., 2015](https://arxiv.org/abs/1411.4555)

[Show, Attend and Tell: Neural Image Caption Generation with Visual Attention, Xu et al., 2015](https://arxiv.org/abs/1502.03044)

[Very deep convolutional networks for large-scale image recognition, Simonyan and Zisserman, 2014](https://arxiv.org/abs/1409.1556)

Framing image description as a ranking task: Data, models and evaluation metrics, Hodosh et al., 2013

From image descriptions to visual denotations: New similarity metrics for semantic inference over event descriptions, Young et al., 2014

[Microsoft coco: Common objects in context, Lin et al., 2014](https://arxiv.org/abs/1405.0312) ([web](http://mscoco.org/))