GPT-2でチャットボットを作ってみた

# 目次
[１．はじめに](#1-はじめに)
[２．実行環境](#2-実行環境)
[３．学習データ](#3-学習データ)
[４．作成手順](#4-作成手順)
[５．まとめ](#5-まとめ)
このブログはAidemy Premiumのカリキュラムの一環で、受講修了条件を満たすために公開しています。

# 1. はじめに
まずは簡単な自己紹介をさせていただきます。
普段はシステムの保守、ヘルプデスク業務を行っています。
将来の選択肢を増やすために開発も行ってみたいと思い、プログラミングを独学で勉強していたのですが、何を作ろうかと悩んでいました。
ちょうどChatGPTが流行ったので、自分でも似たようなものを作れないかなと思い、Aidemyさんの講座でAIの基礎から学びながらチャレンジしてみることにしました。
使うモデルはGPT-3がよかったのですが、ファインチューニングにコストがかかるため、無料公開されているrinna社の日本語GPT-2モデルを使用しました。

# 2. 実行環境
Google Colaboratory
Python 3.10.12
transformers 4.19.0
sentencepiece 0.1.96
datasets 2.2.1
[japanese-gpt2-medium](https://huggingface.co/rinna/japanese-gpt2-medium)

# 3. 学習データ
AIに返答文作成のパターンを学習させるため、学習データを用意します。
これは自分でメモ帳に返答パターンをひたすら書いて作成しました。
GPT2はこちらが書いた文章の続きを生成してくれるものなので、学習データには以下のように「（こちらの回答。）（AIの回答。）」という感じで入力しておきます。
せっかくなので某クレイジーDみたいな喋り方にしました。
![データセット.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3679094/003d9ace-df3d-6dd8-5410-2a42eecd70e4.jpeg)
Google DriveにAidemyフォルダ、その配下にChatBotフォルダをそれぞれ作成し、作成した学習データをChatBotフォルダへ格納しておきます。

# 4. 作成手順
以降の手順はすべてGoogle Colaboratory上で実行します。
Google Colaboratoryを開いたら、GPUを使用するため「T4 GPU」を選択して保存します。
![GPU設定.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3679094/d79f451e-4237-43dc-b45a-e3c0160f439d.jpeg)

Google Driveにマウント（接続）します。
```sh
from google.colab import drive
drive.mount('/content/drive')
```
　
カレントディレクトリを学習データが入っているフォルダにします。
```sh
%cd '/content/drive/MyDrive/Aidemy/ChatBot/'
```
　
必要なライブラリをインストールします。
sentencepieceは日本語のようにスペースで単語が区切られていない言語を利用するためのライブラリです。
```sh
!pip install transformers==4.19.0
!pip install sentencepiece==0.1.96
!pip install datasets==2.2.1
```
　
ファインチューニング用のスクリプトをハギングフェイスから持ってきます。
バージョンは上でinstallしたtransformersに合わせます。
```sh
!git clone https://github.com/huggingface/transformers -b v4.19.0
```

rinnaの日本語GPT-2モデルは「AutoTokenizer」ではなく「T5Tokenizer」を使う必要があります。
そのためbash（コマンド）のsedで、run_clm.py内の「AutoTokenizer」をすべて「T5Tokenizer」に置換します。
```sh
!sed -i 's/AutoTokenizer/T5Tokenizer/' ./transformers/examples/pytorch/language-modeling/run_clm.py
```
　
一度ランタイムを再起動します。
カレントディレクトリがリセットされるため、再度指定します。
```sh
%cd '/content/drive/MyDrive/Aidemy/ChatBot/'
```
　
ファインチューニングを実行します。
```sh
!python ./transformers/examples/pytorch/language-modeling/run_clm.py \
    --model_name_or_path=rinna/japanese-gpt2-medium \
    --train_file=train_data_josuke.txt \
    --validation_file=train_data_josuke.txt \
    --do_train \
    --do_eval \
    --num_train_epochs=10 \
    --save_steps=10000 \
    --save_total_limit=3 \
    --per_device_train_batch_size=1 \
    --per_device_eval_batch_size=1 \
    --output_dir=output/ \
    --overwrite_output_dir=True \
    --use_fast_tokenizer=False
```
・model_name_or_path
　→使用するモデルを指定します。
 　　今回はjapanese-gpt2-mediumです。
・train_file
　→学習データが入ったファイルを指定します。
・num_train_epochs
　→エポック数（学習回数）を指定します。
・output_dir
　→ファインチューニング後のモデルを出力するフォルダを指定します。
 　　ファインチューニング実行後、ランタイム上のoutputフォルダに作成されます。
・overwrite_output_dir
　→同一ランタイム上でファインチューニングを実行するたびに前回作成したモデルへ上書きします。
　
ファインチューニングしたモデルを呼び出します。
```python
import torch
from transformers import T5Tokenizer, AutoModelForCausalLM
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
print(device)

tokenizer = T5Tokenizer.from_pretrained("rinna/japanese-gpt2-medium")
tokenizer.do_lower_case = True
model = AutoModelForCausalLM.from_pretrained("output/")
model.to(device)
model.eval()
```
torch.deviceの部分は、torchのGPUが使えるかをtorch.cuda.is_availableで確認し、ダメな場合はCPUを使う設定を返す処理です。
AutoModelForCausalLM.from_pretrainedでファインチューニング後のモデルを呼び出しています。
model.toは、CPUまたはGPUを使用するよう設定する処理です。
今回はtorch.deviceの結果に応じて切り替えるようにしています。
　
そのままだとmodel.generate(返答データ作成)を実行するたびにログが表示されるため、表示されない設定にします。
```python
import logging
logging.disable(logging.FATAL)
```
　
受け取った入力データから返事を生成して出力する関数を作成します。
こちらの入力に対して、学習データを元に入力文の続きを生成してくれます。
例えば「元気」と入力すると「元気だよ。元気なのはいいことっスよ。」という感じの文を生成しますが、これでは自問自答みたいになるので、「。」で回答文を分割し、最初の「。」以降の文のみにすることでAIの回答部分だけを表示するようにしています。謎な記号を返答することがあったので、replaceで削除しています。
私が作成した学習データが未熟なようで、続きの文章を生成してくれなかったりすることがあるため、その場合は「よく聞こえなかったっス。」と回答してくれるようにしています。
```python
def generate_model(t):
  num = 1
  input_text = t
  texts = ["!", "?"]
  input_ids = tokenizer.encode(input_text, return_tensors='pt', add_special_tokens=False).to(device)
  out = model.generate(
      input_ids,
      do_samples=True,
      top_k=40,
      max_length=100,
      num_return_sequences=num,
      bad_words_ids=[[tokenizer.bos_token_id], [tokenizer.sep_token_id], [tokenizer.unk_token_id]],
      repetition_penalty=1.2
  )
  s = tokenizer.batch_decode(out)
  s = str(s).replace("
", "").replace("[", "").replace("]", "").replace("'", "")
  s = s.split("。")
  if len(s) <= 1:
    s = "よく聞こえなかったっス。"
  elif s[1] == "":
    s = "よく聞こえなかったっス。"
  else:
    s = s[1]
    if not "!" in s:
      if not "?" in s:
        s = s + "。"
  print(f"ジョウスケ：{s}")
```
　
入力を毎回受け取り、上記関数へ渡す処理を定義します。
以下を実行すると、こちらの入力に対して回答を作成して返し続けてくれます。
「半角のq」または「全角のｑ」を入力すると終了します。
```python
print("ジョウスケ：調子どうっスか？")

while True:
  you = input("あなた : ")
  if you == "q" or you == "ｑ":
    print(f"ジョウスケ : それじゃ。")
    break
  generate_model(you)
```

実行してみます。
こちらの文章に対してグレートな返答をしてくれていることが分かります。
![質疑応答.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3679094/0000a645-f0aa-9dad-cd96-8931d3e77898.jpeg)

# 5. まとめ
初挑戦でしたが、自分がイメージしていたものを作成することができました。
今回rinna社の公開モデルを使用しましたが、よりよく使うためにはこのモデルがどんな処理を内部で行っているのかを理解しておいたほうが良いと思います。その理解を深めるのにAidemyさんの講座が非常に役立ちました。
また分からないことがあっても随時カウンセリングやチャットで教えていただき、大変助かりました。
今後も時間のある時にこのチャットボットを改良していきたいと思います。