ヅカの芸名を自動生成するAIを作る(5)：GPT-2編(1)

# これまでのあらすじ

前回は[こちら](https://qiita.com/muto-jo/items/8dffef28b0b78f744b32)

一通り解析を終え、80期以降を学習データにすることに決定。
AIの作成も以下の２パターンを試す。

* GPT-2をファインチューニングする
* 自分でヅカの芸名の解析器や作成をするモデルを作ってみる

今回は、GPT-2をファインチューニングしてみる方法を試す。

なお今回の対応は、下記の記事（とその参照記事）を参照している。

https://qiita.com/m__k/items/36875fedf8ad1842b729

# GPT-2を使う環境の準備

まず前提となる筆者のマシンの環境を再掲する。

macOS Monterey 12.5.1(MacBook Air 2018年モデル。intelプロセッサで当然、GPUなど積んでいない。 ~~Google Colab使え~~ )
Anaconda3-2021.11
Python 3.9.12
conda 4.13.0
pyenvでGPT-2を動作させる専用環境を構築

## 必要なライブラリのインストール
ライブラリのインストールは、主にAnaconda Navigatorから行った。

* transformers 4.18.0
* datasets 2.4.0
* sentencepiece 0.195

多分これが良くなかった…。上にあげた参照記事でtransformersをソースからインストールとは書いてあったのですが、anacondaにあったので、収録されているのかな？と思い…

# GPT-2の試用

必要なライブラリを入れたので、rinnaが公開している日本語学習済みのモデルを導入する。

```Python
from transformers import T5Tokenizer, AutoModelForCausalLM

# load tokenizer
tokenizer = T5Tokenizer.from_pretrained("rinna/japanese-gpt2-medium")

# load pre-trained model
model = AutoModelForCausalLM.from_pretrained("rinna/japanese-gpt2-medium")
```

学習モデルの入手について、記載している記事がなく、戸惑ったのでここではきちんと書いておく。
上のスクリプトを実行すると、初回実行時にrinnaが公開している学習モデルがダウンロードされるので、学習モデルをどこからどうダウンロードして配置するのかは考えなくて大丈夫。

次に文章の続きを生成させてみる。

```Python
# Set input word
input = tokenizer.encode("今日の天気は曇りだが、私の気分は悪くはない。いや、むしろ良いと言っていいだろう。", return_tensors="pt")

# inference
output = model.generate(input, do_sample=True, max_length=100, num_return_sequences=1)

# inferred output
print(tokenizer.batch_decode(output))
```

model.generate()でmax_length=100で最大100文字、num_return_sequences=1で1文だけ生成するよう指定している。

```
Setting `pad_token_id` to `eos_token_id`:2 for open-end generation.
['今日の天気は曇りだが、私の気分は悪くはない。いや、むしろ良いと言っていいだろう。
 

※以下、生成された文
 でも、いつものように私が自分の机の前で、ふと目をあけたとき、私はそこにいた。 私は何も言わないで、ただただ、座ったまま、机の上を眺めていた。
「そんなことないよ」って。ふふ。 私の目の前に、こんな綺麗なお姉さんがいたんだ、と胸を弾ませた。「私だってそう']
```

`
`以降が生成された文章になる（横長でみづらいので適宜改行を入れた）。
小説を意識した書き出しにしてみたが、なるほど小説らしい続きが生成されている（支離滅裂感は否めないのと、ちょっと怖い…）。

ちなみに同様の書き出しを使って、[AIのべりすと](https://ai-novel.com/)にも続きを書かせてみた。

```
今日の天気は曇りだが、私の気分は悪くはない。いや、むしろ良いと言っていいだろう。(ここから生成された文)何しろ今日は待ちに待った日曜日だからだ。
そう、今日は一日中ゲームをやりまくるのだ！ 昨日は遅くまで勉強をしていたので寝坊してしまったが、それを差し引いても今日という一日が素晴らしいものになることは間違いないと断言できる。
```

文章が小説っぽいし、ちゃんとしてる！(言語モデルは「とりんさま7.3B V3」。設定は、「ナラティブ（地の文）」重視、文章スタイルは「ノベルス」。他オプションはデフォルトである)
この後ゲームのしすぎで死んで異世界に転生してゲーム攻略で無双するか、窓から美少女が飛び込んできてラブコメが始まりそうな出だしである。

# ファインチューニング用データの作成

さて、GPT2が動作することを確認したので、ファインチューニング用の学習データを用意する。
[前回](https://qiita.com/muto-jo/items/8dffef28b0b78f744b32)決めたように、80期以降を用いる。

学習データは下記の形式で用意する。

```
SPECIAL_TOKEN[`bos_token`] + 宝塚に新しい団員が増えた。新しい団員の名前は、 \
+ SPECIAL_TOKEN['sep_token'] + "苗字" + SPECIAL_TOKEN['sep_token'] + "名前" + SPECIAL_TOKEN['eos_token']
```

「宝塚に新しい団員が増えた。新しい団員の名前は、」は、今回GPT2に芸名を生成させる時の書き出しの文章。
今回は自由な文章ではなく、固有名詞を生成できれば良いので書き出しにあたる部分は全て固定としてみた（それでいいかどうかはまだわからない）。

bos_tokenが文の始まり、eos_tokenが文の終わりを意味しているようだ。
また苗字と名前は傾向がかなり異なるため、苗字部分と名前部分を分けて学習されるよう、セパレートトークンで区切った。

データのセットの作成にあたり、トークンの符号を確認しておく。

```Python
from transformers import AutoModelForCausalLM, T5Tokenizer

tokenizer = T5Tokenizer.from_pretrained("rinna/japanese-gpt2-medium")
tokenizer.special_tokens_map

{'bos_token': '
',
 'eos_token': '
',
 'unk_token': '<unk>',
 'sep_token': '[SEP]',
 'pad_token': '[PAD]',
 'cls_token': '[CLS]',
 'mask_token': '[MASK]'}
```

bos_tokenであれば`
`、eos_tokenは`
`、セパレートは`[SEP]`を入れればよい。
これにより学習データセットのテンプレートが下記のようになった。

```Python
'
宝塚に新しい団員が増えた。新しい団員の名前は、[SEP]{}[SEP]{}\n
.format(family, first)'
```

以上のテンプレートを踏まえ、宝塚の団員一覧から、80期以降の姓名から学習データを作る。

```Python
import pandas as pd

#定数
MAX_PERIOD = 108
NAME_TEMP = "./takaraduka_actress/takaraduka_actress_{}period.csv"

actress_data = pd.DataFrame()

for i in  range(80 - 1, MAX_PERIOD): #80期以降に限定する
    period = i + 1
    try:
        period_data = pd.read_csv(NAME_TEMP.format(period))
        period_data.insert(0, '期', period)
        actress_data = pd.concat([actress_data, period_data])
    except FileNotFoundError:
        print('{} period is none'.format(period))
print(actress_data)
        
name = actress_data['芸名'].str.split(' ', expand = True)
name.rename(columns = {0:'family', 1:'first'}, inplace=True)

with open('actress_datasets.txt', 'wt', encoding='utf8') as f:
    for row in name.itertuples():
        f.write('
宝塚に新しい団員が増えた。新しい団員の名前は、[SEP]{}[SEP]{}
\n'.format(row.family, row.first))
```

できた学習データ（一部抜粋）がこちら。

```

宝塚に新しい団員が増えた。新しい団員の名前は、[SEP]澪乃[SEP]桜季


宝塚に新しい団員が増えた。新しい団員の名前は、[SEP]遥羽[SEP]らら


宝塚に新しい団員が増えた。新しい団員の名前は、[SEP]潤奈[SEP]すばる


宝塚に新しい団員が増えた。新しい団員の名前は、[SEP]飛龍[SEP]つかさ


宝塚に新しい団員が増えた。新しい団員の名前は、[SEP]真彩[SEP]希帆


宝塚に新しい団員が増えた。新しい団員の名前は、[SEP]星南[SEP]のぞみ


宝塚に新しい団員が増えた。新しい団員の名前は、[SEP]美都[SEP]くらら


宝塚に新しい団員が増えた。新しい団員の名前は、[SEP]紫乃[SEP]小雪


宝塚に新しい団員が増えた。新しい団員の名前は、[SEP]彩波[SEP]けいと

```

<details><summary>全く技術と関係ない余談</summary>

真彩希帆さんは元雪組トップ娘役で、相手役の望海風斗と並び抜群の歌唱力で人気がありました。一度、宝塚バウホールの近くですれ違いましたが、思わず目を見張って目で追ってしまうくらい美しくオーラのある方でした。

真風涼帆さんも楽屋入りで見かけた時、すらっとした長身に長い足で颯爽と歩く姿に見惚れましたが（そしてその後ろを小走りに追いかけるファンが十数メートル続いてました）、宝塚のトップクラスの団員の人存在感がやばいです…
</details>

# ファインチューニング実行

ファインチューニング用のスクリプトがgithubのhaggingfaceのリポジトリにあるので、それを入手します。

```Shell
git clone https://github.com/huggingface/transformers
```

なおカレントディレクトリは、ファインチューニングする用のディレクトリを作ってそこで作業しています。
なので、git clone時点ではこんな感じ。

```Shell
./fine_tuning
  actress_datasets.txt //作った学習データ
  create_training_dataset.py //学習データ生成スクリプト
  transformers/ //git cloneしたtransformers
```

下記コマンドでファインチューニングを実行します(長いので適宜改行します)。

```Shell
python ./transformers/examples/pytorch/language-modeling/run_clm.py
 --model_name_or_path=rinna/japanese-gpt2-medium
 --train_file=./actress_datasets.txt
 --validation_file=./actress_datasets.txt
 --do_train
 --do_eval
 --num_train_epochs=10
 --save_steps=1000
 --save_total_limit=3
 --per_device_train_batch_size=1
 --per_device_eval_batch_size=1
 --output_dir=./fine_tuned_model
 --use_fast_tokenizer=False
```

実行したらエラーが出ました…
[次回](https://qiita.com/muto-jo/items/460ffb50924fd24010d6)、ファインチューニングのエラー対応編です…