「ヤジが飛んでくるマインクラフト」DeepLearningで適当テキスト生成 ~Bertする~

DeepLearningの力を借りて、マイクラのプレイ中、ゲームに関係のある文章を読み上げてくれるソフトを作った。

↓こんなやつ
![でもミニ.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/358231/9fa597d6-abc1-58df-4007-82037b68318d.gif)

[実際に遊んでいる動画](https://youtu.be/xseWajIDe4M?t=55)

1つの記事にまとめるのが困難なため、いくつかの記事に分けます。
ここでは、<font color = red>**DeepLearningする部分**</font>について説明します。


### ↓他の記事

- [全体の流れ](https://qiita.com/eteeeeeerminal/items/428f0618c775da816519)

- [データを用意する](https://qiita.com/eteeeeeerminal/items/0c7d737da264bd61d0ff)

- [modで動かす](https://qiita.com/eteeeeeerminal/items/7ac8a15bba73383783ce)

# DeepLearning を使っていい感じのテキストを持ってくる。
脳死で **[BERT](https://ai-scholar.tech/text-mining/bert-ai-93/)** を使った。
みんな大好き、[huggingfaceのライブラリ](https://github.com/huggingface/transformers)で楽々実装。

(直接コードについて説明している部分は少ないが 2020/02/28 時点のリポジトリを参照
huggingface のライブラリは結構な頻度で改良されているので、常に最新版を使うことを推奨する)

- BERT を Masked Language Model で学習。

- BERT がいい感じに、文章のベクトル表現を学習する。

- 反応して欲しい単語のベクトルと、文章のベクトルの内積から[cos類似度](http://www.cse.kyoto-su.ac.jp/~g0846020/keywords/cosinSimilarity.html)を出す。

- ゾンビや、クリーパーに関連した文章を取り出す。


# データの前処理
[いわゆる前処理](https://qiita.com/Hironsan/items/2466fe0f344115aff177)

- 同じ単語が、カタカナとひらがなで別の単語だと認識されると困る。
- 数字やその他記号は区別する必要なし。
- `w`だけで構成される単語はすべて草の意味なので同じ単語として扱う。
- 特に、クリーパーについて、匠と表記されることが多い。これも同じ単語として扱う。

とりあえずやったこと

- [jaconv](https://qiita.com/shakechi/items/d12641d6cad01479785f)を使って、表現を統一
    - カタカナをひらがなに統一
    - 全角アルファベットを半角に統一

- すべての数字を`'0'`に置換。
- `?`や`!`以外の記号を`/`に置換。
- `匠`を`くりーぱー`に置換。
    - その他、反応して欲しい単語に関して、同義語置換。

[[Python] 正規表現の表記方法のまとめ（reモジュール）
](https://hibiki-press.tech/learn_prog/python/regex_pattern/1099)

## 語彙ファイル
Deep Learning 系の自然言語処理では、文章を単語に分割して、それぞれの単語にidを振り分けて入力する。

そのために、扱う単語が改行区切りで並んだファイルを用意した。

最初の5つは、BERT に文の区切りを教えたり、後述のマスク用の特殊なトークン。
6つめ以降は、データセットの中の単語を出現頻度順に並べてある。


# 学習について
本来、BERTは2つのタスクで学習させる。

- Next Sentence Prediction(NSP)
    - 文を2つ並べて入力する。
    - 2つ目の文が、1つ目の文の続きの文か判定。

- Masked Language Model(MLM)
    - 入力文の単語を、ランダムにmaskトークンに置換する。
    - 穴抜きの文から、元の文を推測する。

ニコニコ動画のコメントは、時系列に並んでいる。
しかし、似たようなコメントが続いたり、時間が空いていたりするので、NSP するのに向いていない。

### 今回は、MLM だけで学習させた。

MLM では、
**全体の15%の単語を置換する。**
そのうち、

- 80% を mask トークンに
- 10% をランダムなトークンに

置換し、

- 残り 10% は置換しない。

## 実装
huggingfaceのライブラリには

- BERT のモデル
- 基本的な学習コード

があるので、データを用意して、データセットクラスを用意すれば動かせる。

### データセットクラス
データを読み込んで、必要な形に直して学習コードで取り出せればいい。

- 入力
    - input  
      それぞれの単語を対応するIDに置き換えたID列  
      文の最初と最後に特殊トークン  
      固定長で、長さを合わせるために、0で埋める。  
      [input を穴抜きにする関数](https://github.com/huggingface/transformers/blob/e693cd1e877aa191d3317faed33e87d1558c9406/examples/run_language_modeling.py#L199)に通す  
      `[2, 6, 7, 8, 3, 0, 0, ..., 0]`

    - mask  
      処理しなくていい部分を教える  
      (読み込まない部分を0, 読み込む部分を1)  
      `[0, 1, 1, 1, 0, 0, 0, ..., 0]`

- 正解データ
[input を穴抜きにする関数](https://github.com/huggingface/transformers/blob/e693cd1e877aa191d3317faed33e87d1558c9406/examples/run_language_modeling.py#L199) の返り値のlabelsを使った。


### ハイパラ変更
モデルの設定を変えたいときは、
`BertConfig`クラスに jsonファイルの path を渡せばいい。

[変更できるパラメータはここを参照。](https://github.com/huggingface/transformers/blob/d6ef587a10e0d8836376a2314d8aeae36ad63263/src/transformers/configuration_bert.py#L64)

今回、長いテキストを読み込む必要なないので、`"max_position_embeddings"`を 128 に変更。
それに合わせて、attention の head と隠れ層の数を適当に下げてみた。

```json
{
  "attention_probs_dropout_prob": 0.1,
  "hidden_act": "gelu",
  "hidden_dropout_prob": 0.1,
  "hidden_size": 768,
  "intermediate_size": 3072,
  "max_position_embeddings": 128,
  "num_attention_heads": 8,
  "num_hidden_layers": 12,
  "vocab_size": 47981
}
```

```python
config = BertConfig.from_json_file("ここに設定を書いた json ファイルの path")
model  = BertForMaskedLM(config)
```

### 学習コード
ライブラリ付属のサンプルコードを参考に、ごちゃごちゃした部分を削った。

`BertForMaskedLM`クラスではlabelを渡せば、lossも計算してくれる。

```python
output = model(input_ids=input, attention_mask=mask, masked_lm_labels=labels)
# output = masked_lm_loss, prediction_scores, (hidden_states), (attentions)
```

[language_model を学習させるサンプルコード。](https://github.com/huggingface/transformers/blob/master/examples/run_language_modeling.py)

# 類似度をとる
## Bert からベクトル表現をとる
学習したモデルを `BertModel`クラスに読み込む

```python
model = BertModel.from_pretrained("セーブしたモデルのパス")
```

とりあえずは、`BertModel`のpooled_outputを使えばいいっぽい。

```python
output = model(input_ids=input, attention_mask=mask)
# output = sequence_output, pooled_output, (hidden_states), (attentions)
```

あとは、反応して欲しい単語1語だけの文と、実際のテキストデータの文を Bert に入れて、pooled_outputを得る。
`hidden_size`次元のベクトルが得られるので、いわゆる[cos類似度](http://www.cse.kyoto-su.ac.jp/~g0846020/keywords/cosinSimilarity.html)をとる。

あとは、どの単語に近いか判定して、ファイルに分けて保存する。

## テキストを出す
実際にマインクラフトで動かしているときは、あらかじめ保存した類似度で文章を選ぶ。
類似度の高い方から、8つ取り出して、その中からランダムに1つ選んで表示した。

# 生成させてみる
[ctrl](https://github.com/arXivTimes/arXivTimes/issues/1384) での文書生成も試した。
デモ動画は文書生成をしている。

(ctrl なのは、文書スタイルを変更できるらしかったから
~~めんどくさかったので、~~結局ただのTransformerのDecorder的に使った)

### 学習

実装はもちろん [huggingface](https://github.com/huggingface/transformers)。

- テキストデータを入れて、文のそれぞれの時点で、次の単語を予測させる。
- labels 引数に input と同じものを入れると、内部で左に1つずらしてlossを出してくれる。

```python
model  = CTRLLMHeadModel(config)
output = model(input_ids=input, attention_mask=mask, labels=input)
# outputs = loss, lm_logits, presents, (all hidden_states), (attentions)
```

### 実際に生成

学習させた後は、generate 関数を使えば簡単に生成をしてくれる。
今回は、類似度順に取り出した文の先頭の単語だけ渡して、残りを生成させた。

- generate に渡す input は 0埋めしない。
- max_length 個だけ 単語IDが並んだものが返ってくるので、文の終わりを示すトークンまで切る。
- ID列を単語に置換し直す。

```python
output = model.generate(input_ids=input, max_length=128)
```

# 最後に
Transformer は huggingface のライブラリを使えば脳死で使えた。
便利すぎて自分でモデルの実装をしなくなるので、Transformer分からんになりやすいので注意。
データの形など分からなくなったら、[こちらの記事](https://qiita.com/FuwaraMiyasaki/items/239f3528053889847825)が分かりやすかった。