Transformerで雑談チャットボットを作ったら躓いた話

# はじめに
これは自然言語処理の機械学習を初めてやってみたら躓いた話。製作までの過程を適当に書き記しておきます。
記事投稿時点では上手くいっていないので反面教師ぐらいにしかならないでしょう。
うまくできる方法を知りたい方は[こちら](https://qiita.com/NJIMAMTO/items/f92701f1d2a94b76d5d0)をご覧ください。

###著者のスペック

+ 大学(正しくは高専専攻科)在学に画像処理と機械学習(多層パーセプトロン)を使った研究をしていた
+ 機械学習の知識はほぼ独学に近い(授業での経験は皆無、ほぼ研究活動時の自学自習のみ)
+ 機械学習をお仕事にしたくて求職中
+ 自然言語処理の経験は皆無、知識もあまりない

そんなペーペーエンジニア見習いの記事になります。あまり参考になりそうにないなと思ったらブラウザバックをおすすめします。

# 自然言語処理界隈の機械学習を調べる
元々機械学習そのものには触れていた私。しかしながら、自然言語処理の経験は全くないのでとりあえず情報や知見を収集することにした。

最初に飛び込んできたのはgoogleのBERTがすごいということ。なのでその構造や学習の仕組みなども調べてみたが全くの「？？？？？？？」状態であった。

とにかくBERTがすごいらしい。ならそれを使ってなにか作ってみようと思うに至り、チャットボットを作ろうと思った。

後発のXLNetやALBERTを使ってみようかとも検討した。ただ自分向けに改造がしやすそうなものはBERTを含め無かった。

特にGoogleが非公式に提供しているGithubのBERTリポジトリ、文章分類などは簡単にできそうだったがそれ以外の想定していなそうなタスクをするにはハードルが高そうだった。そのため別の方策を探した。

#第1章 2節 右も左もわからないがとりあえずチャットボットを作ってみよう
色々調べてみて[transformerで日英翻訳をやっている方](https://lang-int.hatenablog.com/entry/2019/04/29/162132)や[transformerでチャットボットを作っている方](https://sekailab.com/wp/2019/03/27/transformer-general-responce-bot/)に行き着いた。

じゃあtransformerでチャットボットを作ってみればいいのでは？と考えついたので、それを実践してみることにした。

今回使用した材料はこちら

+ 日本語自然会話書き起こしコーパス(旧名大会話コーパス)
    + [コーパスのデータ整形ツール](https://github.com/knok/make-meidai-dialogue)
+ sentencepiece
+ [keras向けのtrasnformerラッパー](https://github.com/CyberZHG/keras-transformer) 


### 下準備

それでは作り方に移ります。
今回はGoogle colab上で実行するので、notebook形式を前提とします。
コードの全文は[こちら](https://github.com/NJIMAMTO/transformer-chat-bot/blob/master/transformer.ipynb)から

まずtransformerのインストールから

```python
!pip install keras-transformer
```

続いてトーカナイザーとして使うsentencepieceのインストール

```python
!pip install sentencepiece
```

ここでGoogleドライブをマウントしておきます(やり方は略)

続いてコーパスをダウンロードして、整形していきます。

```python
!git clone https://github.com/knok/make-meidai-dialogue.git
```
リポジトリのあるディレクトリに移動します。

```python
cd "/content/drive/My Drive/Colab Notebooks/make-meidai-dialogue"
```
makefileを実行して

```python
!make all
```
元のディレクトリに戻ります。

```python
cd "/content/drive/My Drive/Colab Notebooks"
```

コーパスをダウンロードしてmakefileを実行するとsequence.txtが生成されます。
この中には
input:~~~~
output:~~~~
の形式で会話文が書かれているので、今後使いやすいように整形していきます。

```python
input_corpus = []
output_corpus = []
for_spm_corpus = []
with open('/content/drive/My Drive/Colab Notebooks/make-meidai-dialogue/sequence.txt') as f:
  for s_line in f:

    if s_line.startswith('input: '):
      input_corpus.append(s_line[6:])
      for_spm_corpus.append(s_line[6:])

    elif s_line.startswith('output: '):
      output_corpus.append(s_line[7:])
      for_spm_corpus.append(s_line[7:])

with open('/content/drive/My Drive/Colab Notebooks/input_corpus.txt', 'w') as f:
  f.writelines(input_corpus)

with open('/content/drive/My Drive/Colab Notebooks/output_corpus.txt', 'w') as f:
  f.writelines(output_corpus)

with open('/content/drive/My Drive/Colab Notebooks/spm_corpus.txt', 'w') as f:
  f.writelines(for_spm_corpus)
```

transformerに入力するためのテキストと出力用テキスト、sentencepieceに学習させるためにinput:とoutput:部分を除した学習用テキストに分割しました。

### 前準備
続いてsentencepieceで会話文を分かち書きにします。
spm_corpus.txtを使って学習させてみましょう。

```python
import sentencepiece as spm

# train sentence piece
spm.SentencePieceTrainer.Train("--input=spm_corpus.txt --model_prefix=trained_model --vocab_size=8000 --bos_id=1 --eos_id=2 --pad_id=0 --unk_id=5")
```
詳細はsentencepieceの公式リポジトリにやり方が書かれているので省略します。

それでは一度sentencepieceで文章を分かち書きしてみます。

```python
sp = spm.SentencePieceProcessor()
sp.Load("trained_model.model")

#test
print(sp.EncodeAsPieces("ああそういうことね"))
print(sp.EncodeAsPieces("なるほどわかった"))
print(sp.EncodeAsPieces("つまり、そのあなたの言いたいことはそういうことですか？"))
print(sp.DecodeIds([0,1,2,3,4,5]))
```

実行結果です。

```
['▁ああ', 'そういうこと', 'ね']
['▁なるほど', 'わかった']
['▁', 'つまり', '、', 'その', 'あなた', 'の', '言い', 'たい', 'ことは', 'そういうこと', 'ですか', '?']
、。 ⁇ 
```

以上のように分割されることがわかりました。

ここからの内容は[Keras Transformer](https://github.com/CyberZHG/keras-transformer)のREADME.mdに書かれている内容を一部改変したものになります。

それではパディングとtransformerに適した形式にコーパスを整形させていきます。

```python
import numpy as np

# Generate toy data
encoder_inputs_no_padding = []
encoder_inputs, decoder_inputs, decoder_outputs = [], [], []
max_token_size = 168

with open('/content/drive/My Drive/Colab Notebooks/input_corpus.txt') as input_tokens, open('/content/drive/My Drive/Colab Notebooks/output_corpus.txt') as output_tokens:
  #コーパスから一行ずつ読み込む
  input_tokens = input_tokens.readlines()
  output_tokens = output_tokens.readlines()

  for input_token, output_token in zip(input_tokens, output_tokens):
    if input_token or output_token:
      encode_tokens, decode_tokens = sp.EncodeAsPieces(input_token), sp.EncodeAsPieces(output_token)
      #パディングする
      encode_tokens = ['
'] + encode_tokens + ['
'] + ['<pad>'] * (max_token_size - len(encode_tokens))
      output_tokens = decode_tokens + ['
', '<pad>'] + ['<pad>'] * (max_token_size - len(decode_tokens))
      decode_tokens = ['
'] + decode_tokens + ['
']  + ['<pad>'] * (max_token_size - len(decode_tokens))

      
      encode_tokens = list(map(lambda x: sp.piece_to_id(x), encode_tokens))
      decode_tokens = list(map(lambda x: sp.piece_to_id(x), decode_tokens))
      output_tokens = list(map(lambda x: [sp.piece_to_id(x)], output_tokens))

      encoder_inputs_no_padding.append(input_token)
      encoder_inputs.append(encode_tokens)
      decoder_inputs.append(decode_tokens)
      decoder_outputs.append(output_tokens)
    else:
      break

#学習モデルへの入力用に変換する
X = [np.asarray(encoder_inputs), np.asarray(decoder_inputs)]
Y = np.asarray(decoder_outputs)
```

### 学習させる
それではtransformerに学習させていきます。

```python
from keras_transformer import get_model

# Build the model
model = get_model(
    token_num=sp.GetPieceSize(),
    embed_dim=32,
    encoder_num=2,
    decoder_num=2,
    head_num=4,
    hidden_dim=128,
    dropout_rate=0.05,
    use_same_embed=True,  # Use different embeddings for different languages
)

model.compile(
    optimizer='adam',
    loss='sparse_categorical_crossentropy',
)
model.summary()

# Train the model
model.fit(
    x=X,
    y=Y,
    epochs=10,
    batch_size=32,
)
```
実行結果です。

```
Epoch 1/10
33361/33361 [==============================] - 68s 2ms/step - loss: 0.2818
Epoch 2/10
33361/33361 [==============================] - 66s 2ms/step - loss: 0.2410
Epoch 3/10
33361/33361 [==============================] - 66s 2ms/step - loss: 0.2331
Epoch 4/10
33361/33361 [==============================] - 66s 2ms/step - loss: 0.2274
Epoch 5/10
33361/33361 [==============================] - 66s 2ms/step - loss: 0.2230
Epoch 6/10
33361/33361 [==============================] - 66s 2ms/step - loss: 0.2193
Epoch 7/10
33361/33361 [==============================] - 66s 2ms/step - loss: 0.2163
Epoch 8/10
33361/33361 [==============================] - 66s 2ms/step - loss: 0.2137
Epoch 9/10
33361/33361 [==============================] - 66s 2ms/step - loss: 0.2114
Epoch 10/10
33361/33361 [==============================] - 66s 2ms/step - loss: 0.2094
```

損失は悪くなさそうですね。

### 推論させる
学習したモデルで推論を行ってみます。

```python
from keras_transformer import decode

input = "今日はいい天気ですね"
encode = sp.EncodeAsIds(input)

decoded = decode(
    model,
    encode,
    start_token=sp.bos_id(),
    end_token=sp.eos_id(),
    pad_token=sp.pad_id(),
    max_len=170
)

decoded = np.array(decoded,dtype=int)
decoded = decoded.tolist()
print(sp.decode(decoded))
```

実行結果です。

```
ね、でも、あの、あの、あの、あの、あの、あの、あの、あの、あの、あの
```

あれ…全然結果が芳しくありません。これじゃコミュ障です。

### ここまでの考察
うまくいかなかった原因はなんでしょうか？

+ 一文の長さが長すぎてうまく学習できていない？
+ コーパスが足りない(今回は質問と回答それぞれ3万文ずつ)
+ パラメータの設定がよくない

などが最初にあげられるでしょうか？


#モデルの性能改善に向けて
そこでOptunaを使ってチューニングをしようと試みましたが次の理由でうまくいきませんでした。

+ チューニングにかかる時間が長すぎる(最後までやっていませんが1日は余裕でかかりそう)
+ 上記の理由からColabでの実行は難しい(12時間制限に引っかかるため)
+ Colabではできないので自分のPC内の環境で試す(GPUは使えない)がもっと時間がかかるので断念
+ ではクラウド上ではやるかと考えAWSを試すもGPU付きインスタンスの作成が許可されない(次記事の方法を試している間に許可されましたが…)
+ そもそも途中までやってみた結果もあまり芳しくない(ほぼ精度に変化なしか)

ということで別の方法を試すことにしました。
うまくいった方法は[こちら](https://qiita.com/NJIMAMTO/items/f92701f1d2a94b76d5d0)