日本語GPT-2を使って「小説家になろう」に掲載されていそうなタイトルを生成してみた

2021年4月、株式会社rinnaが公開した日本語GPT-2を使ってちょっと遊んでみた話です。
# 概要

## rinna GPT-2の特徴
株式会社rinnaの[お知らせ](https://corp.rinna.co.jp/news/2021-4-7-pressrelease/)を見てみると次の特徴があるらしいです。

- 学習データは[CC-100のデータ](http://data.statmt.org/cc-100/)を使用
- 70GBのテキストを約1ヶ月学習
- Perplexityが約18
- huggingfaceに学習済みモデルを公開
- モデルのサイズはGPT2-medium

せっかくGPT-2モデルをhuggingfaceの使いやすい形で公開してもらっているのならちょっと触ってみるか、ということで今回色々やってみました。

## なぜ「小説家になろう」のタイトルを生成したのか
GPT-2の応用として色々考えられると思いますが、今回は「小説家になろう」風のタイトル生成をやってみました。
理由としては以下の通りです。

1. 学習データの用意が比較的簡単。
1. 出力が想定したもの（「小説家になろう」のタイトルっぽい）かどうかわかりやすい。
1. 出力がそれほど長くない（と考えられる）。 

個人的に助かったのは3番目です。
ブログ記事などの文書を出力した場合、実際に正しい出力かどうか眺めるのに時間がかかってしまうので。。。
その点、タイトル生成ならそこまで長くないので、出力結果を眺めるのも簡単です。

（個人的に「小説家になろう」のタイトルは好きなので、それらしいタイトルが作れたら面白いなあ、みたいな理由もありますが。。。）


# やったこと
せっかく「小説家になろう」っぽいタイトルを生成するなら、よりそれらしいものを出したくないですか？
おそらくですが、事前学習したGPT-2モデルをそのまま使ってもうまくいかないだろう、ということで今回は一工夫しました。
やったことは次の通りです。

1. 学習データ（「小説家になろう」のタイトル）の用意
1. 用意した学習データでGPT-2をfine-tuning
1. fine-tuningしたGPT-2でタイトル生成

以下、順に解説します。

## 学習データ（「小説家になろう」のタイトル）の用意
学習データとして、今回「小説家になろう」に投稿されているタイトルを使用しています。
「小説家になろう」のタイトル取得には[なろう小説API](https://dev.syosetu.com/man/api/)を叩いています。
APIはタイトルと一緒に、その小説の**総合ポイント**や**月間ポイント**が得られるので、今回はその情報も使用しました。
具体的には、取得したタイトルを総合ポイントの高い順に並べ、上位1万件を学習に使用しました（正確には、学習データに9千件、開発データに千件）。

## 用意した学習データでGPT-2をfine-tuning
用意した学習データを使ってGPT-2をfine-tuningしていきたいと思います。
手順としては次の通りです。

1. 学習データの整形
1. fine-tuningスクリプトの用意
1. fine-tuning開始

### 学習データの整形
学習データの整形、といっても今回は大したことはしていません。
用意した学習データを基本的にはそのまま使うのですが、一点だけ特殊な処理を行っています。
具体的に言うと、今回はプロンプトを用いてタイトルを生成しているので、学習データにもプロンプトを付与しています。

```text:用意した学習データの例
[N_TITLE]小説家になろうタイトル
[N_TITLE]小説家になろうタイトル2
```
このように、プロンプトとして`[N_TITLE]`を付与し、その後1行1タイトルのテキストデータを整形しています。

### fine-tuningスクリプトの用意
今回はfine-tuningをPythonおよび[Transformers](https://github.com/huggingface/transformers)と呼ばれるライブラリを中心に用いて行いました。
具体的には以下の環境でfine-tuningを行っています。

- Python3.6
- transformers==4.5.1
- datasets==1.7.0
- sentencepiece==0.1.95

この環境を構築後、Transformersの[このスクリプト](https://github.com/huggingface/transformers/blob/v4.5.1/examples/language-modeling/run_clm.py)（`run_clm.py`）[^1]を少し修正し、fine-tuningを行いました。

Tokenizer周りを修正しています。
というのも、[こちらのrinna/japanese-gpt2-mediumの置き場](https://huggingface.co/rinna/japanese-gpt2-medium)を見ると、T5Tokenizerを宣言するように、とあったので、今回はそのように`run_clm.py`を修正しています。
具体的には、オリジナルの`run_clm.py`はTokenizerを以下のように設定しているのですが、

```python:オリジナルのrun_clm.py（Tokenizer周り）
    if model_args.tokenizer_name:
        tokenizer = AutoTokenizer.from_pretrained(model_args.tokenizer_name, **tokenizer_kwargs)
    elif model_args.model_name_or_path:
        tokenizer = AutoTokenizer.from_pretrained(model_args.model_name_or_path, **tokenizer_kwargs)
    else:
        raise ValueError(
            "You are instantiating a new tokenizer from scratch. This is not supported by this script."
            "You can do it from another script, save it, and load it from here, using --tokenizer_name."
        )
```

上記の記述を丸ごと以下のように修正します。

```python:修正後のrun_clm.py（Tokenizer周り）
tokenizer = T5Tokenizer.from_pretrained(model_args.model_name_or_path)
tokenizer.do_lower_case = True
```
最後の`do_lower_case`も忘れずに設定しましよう（実は今回の検証では忘れてしまったため、大文字を出力しようとすると未知語トークンが出力されるモデルとなりました。。。）。

### fine-tuning開始
整形した学習データと、fine-tuningスクリプト（`run_clm.py`）を使って、fine-tuningを行います。
なお、今回はGoogle Colabを用いています。GPUはT4でした。

fine-tuning時のハイパーパラメータは次の通りです。

```bash:fine-tuning実行
!python transformers/examples/language-modeling/run_clm_rinna_gpt2.py \
    --output_dir=narou_finetuned_global_gpt2 \
    --model_type=gpt2 \
    --model_name_or_path=rinna/japanese-gpt2-medium \
    --do_train \
    --train_file=/content/drive/MyDrive/NLP_DATASET/gpt-2/global_text/train.txt \
    --do_eval \
    --validation_file=/content/drive/MyDrive/NLP_DATASET/gpt-2/global_text/dev.txt \
    --evaluation_strategy=steps \
    --per_device_train_batch_size=6 \
    --gradient_accumulation_steps=2 \
    --per_device_eval_batch_size=2 \
    --num_train_epochs=4 \
    --save_steps=50 \
    --logging_dir=narou_finetuned_global_logs \
    --logging_strategy=steps \
    --logging_steps=50 \
    --save_strategy=steps \
    --eval_steps=50 \
    --load_best_model_at_end \
    --block_size 200
```
`train_file`と`validation_file`に先ほど用意した学習、開発データを設定してください。
このハイパーパラメータで、Google Colabの実行可能時間内に学習が終了しました。

## fine-tuningしたGPT-2でタイトル生成
上記スクリプトでfine-tuningしたモデルを使ってタイトルを生成してみたいと思います。
ちなみに生成に使ったスクリプトは[こちら](https://github.com/huggingface/transformers/blob/v4.5.1/examples/text-generation/run_generation.py)を改変したもの(`run_generation.py`)です。
改変内容は先ほどと同様にTokenizerに関するものなので、詳細については省略します。

タイトル生成時のハイパーパラメータは次の通りです。

```bash:タイトル生成
!python /content/transformers/examples/text-generation/run_generation_gpt2.py \
  --model_type gpt2 \
  --model_name_or_path /content/drive/MyDrive/NLP_MODEL/gpt-2/narou_finetuned_global_gpt2 \
  --prompt [N_TITLE] \
  --length 600 \
  --num_return_sequence 10 \
  --stop_token "
" \
  --k 40 \
  --p 0.95 \
  --temperature 0.95 \
  --seed 1
```
`prompt`のハイパーパラメータを利用することで、モデルにこのプロンプトを与えた状態で生成をさせることができます。
また、`model_name_or_path`は先ほどのfine-tuningで作成したモデルが置かれているディレクトリを指すようにしてください（私はfine-tuning後に学習済みモデルを手元のGoogle Driveに写しているので上記のようになっています。）。

### 生成したタイトル例
実際にfine-tuningしたモデルの生成例が次の通りとなります。

```text:タイトルの生成例
=== GENERATED SEQUENCE 1 ===
[N_TITLE]悪役令嬢の婚約者に転生したけど、もう無理だ
=== GENERATED SEQUENCE 2 ===
[N_TITLE]異世界の異世界食堂
=== GENERATED SEQUENCE 3 ===
[N_TITLE]不登校を乗り切る最強魔道士 〜 僕の知らない『最強』が、僕を笑わせてくれるよ
=== GENERATED SEQUENCE 4 ===
[N_TITLE]この世界は <unk> 
=== GENERATED SEQUENCE 5 ===
[N_TITLE]無知は罪。 俺が無能なのは分かっていても、どうして彼女がお前の味方になってくれないんだ!? 俺のことを好きじゃないと言われても、もう遅い。
=== GENERATED SEQUENCE 6 ===
[N_TITLE]異世界英雄記～転生したら最強の魔王がいました～
=== GENERATED SEQUENCE 7 ===
[N_TITLE]異世界に降り立ったけれど、やることが無さすぎる。
=== GENERATED SEQUENCE 8 ===
[N_TITLE]<unk> ランク冒険者は冒険者ギルドから追放されました
=== GENERATED SEQUENCE 9 ===
[N_TITLE]乙女ゲームの悪役令嬢と公爵令嬢
=== GENERATED SEQUENCE 10 ===
[N_TITLE]ダンジョンダンジョン
```
大半が異世界に転生に関するタイトルだったり、悪役令嬢に関するタイトルだったりしました。
確かに「小説家になろう」でありそうな特徴的なタイトルが多く生成できていそうです（個人的には`GENERATED SEQUENCE 3`は要素てんこ盛りで気になりました。）。
ちなみにこれらのタイトルを「小説家になろう」で検索したところ、全く同一のタイトルは存在しませんでした。
つまり、GPT-2をfine-tuningしたモデルはちゃんと「小説家になろう」に掲載されていそうなタイトルを生成できていそうです。

ただし、GPT-2も完璧ではなくて、`GENERATED SEQUENCE 2`や`GENERATED SEQUENCE 10`などは同じ単語、フレーズを繰り返してしまいました。
こういったニューラルネットワークを用いた、文などを生成する際の特有の課題はGPT-2でもまだまだありそうです。

また、`GENERATED SEQUENCE 4`や`GENERATED SEQUENCE 8`の出力に`<unk>`が入っています。
これは未知語トークンを表していて、おそらく4番の出力はモデルに登録されていない、難しい漢字を出力しようとしたのだと考えられます。
一方で、8番の出力はおそらく`Sランク冒険者...`としたかったのだろうと考えられ、これはモデルにとって`S`が未知語だったからだと考えられます（[fine-tuningスクリプトの用意](https://qiita.com/Katsumata420/items/077de5d968eb7d600d36#fine-tuning%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%97%E3%83%88%E3%81%AE%E7%94%A8%E6%84%8F)の章で記述したlowercase忘れがここで響いています。。。）。

## [蛇足] GPT-2に生成したタイトルの解説をさせてみたかった。。。
最後に、fine-tuning前の、公開されているGPT-2[^2]を使って、生成したタイトルの解説をさせてみようとしてみた話をします。
具体的には、先ほど生成したタイトルの一つである、`不登校を乗り切る最強魔道士 〜 僕の知らない『最強』が、僕を笑わせてくれるよ`がどんな作品であるかGPT-2に解説文を生成させてみました。

スクリプトは先ほどの[fine-tuningしたGPT-2でタイトル生成](https://qiita.com/Katsumata420/items/077de5d968eb7d600d36#fine-tuning%E3%81%97%E3%81%9Fgpt-2%E3%81%A7%E3%82%BF%E3%82%A4%E3%83%88%E3%83%AB%E7%94%9F%E6%88%90)のものを再度使用します。

```bash:解説文生成
!python /content/transformers/examples/text-generation/run_generation_gpt2.py \
  --model_type gpt2 \
  --model_name_or_path rinna/japanese-gpt2-medium \
  --prompt "『不登校を乗り切る最強魔道士 〜 僕の知らない『最強』が、僕を笑わせてくれるよ』とは、" \
  --length 800 \
  --num_return_sequence 1 \
  --stop_token "
" \
  --k 40 \
  --p 0.95 \
  --temperature 0.95 \
  --seed 1
```

先ほどいくつかパラメータを変えています。
`model_name_or_path`は`rinna/japanese-gpt2-medium`を指定し、`prompt`はタイトルの解説をしてくれるように指定しています。
また、`length`も先ほどより少し長めの値を指定しています。

このスクリプトで生成した解説文がこちらです（すごく長いので途中まで改行は入れています。）。

```text:生成した解説文
『不登校を乗り切る最強魔道士 〜 僕の知らない『最強』が、僕を笑わせてくれるよ』とは、本日10月12日(水)より、huluにて配信開始となった。
 huluでは『不登校を乗り切る最強魔道士 〜 僕の知らない『最強』が、僕を笑わせてくれるよ』の試写会応募者を対象に、抽選で映画『僕の知らない『最強』が、僕を笑わせてくれるよ』の試写会が1組4名様に当たるプレゼントキャンペーンを実施。 
応募にはhuluのidの登録が必要だが、hulu会員でなくても応募できる。
なお、本キャンペーンは、huluの会員でない方も応募可能だ。
 ・当選者の発表:当選者に対しては、huluよりメールにてご連絡させていただきます。
 ・賞品の発送:当選者には、賞品を配送する手続きが完了してからの発送となります。
 ・当選者には、後日、当選のご連絡をさせていただく場合がございます。
 ・当選者の個人情報は、huluのキャンペーン・サービスの案内以外には使用いたしません。
 ・応募に関する質問にお答えすることはできません。あらかじめご了承ください。
 ・応募は日本国内の方に限らせていただきます。海外の方の応募は無効とさせていただきます。
 ・当選の連絡は、応募された方のみにメールでご連絡いたします。なお、メールはお送りいたしませんので、ご了承ください。
 ・当選の通知後、一定期間ご連絡をいただけない場合は、当選資格が無効となる場合がございます。
 ・当選のご連絡をさせていただくため、本メールに記載されている「登録完了メール」や「発送のご案内」は、応募された方のメールアドレス宛に自動送信させていただいております。
 ・メール受信の制限を設定している場合は、「hulu_jp」からのメールを受信できるように設定を変更してください。 
・hulu公式サイト(https://hulu.jp)から、ログインして応募してください。
 ・キャンペーンの応募に際しては、当社所定の「利用規約」、及び、本キャンペーンで取得した個人情報(氏名、住所、電話番号、メールアドレス)について、本キャンペーン事務局にて適切に管理・保管いたします。
 ・応募者は、ご応募に際して、第三者に対して、本キャンペーンにおける応募内容、本キャンペーンへの参加を許諾するものではありません。
 ・応募者は、応募に際して、本キャンペーン事務局からの電子メール、郵送、またはその他の方法による情報等の発信元を、当社の関連会社、または、当社が指定した第三者に開示・提供することはありません。 ・応募者に対し、当社は、本キャンペーンの内容について、事前に予告をした上で、変更、中止、中止をすることができるものとし、これによって応募者に損害が生じたとしても、当社は一切責任を負いません。 ・応募者は、本キャンペーンへの参加にあたり、当社の業務の全部又は一部を第三者に譲渡し、又は、合併、移転することはできません。 ・応募者は、本キャンペーンへの応募にあたって、本キャンペーン事務局に対して、応募者の個人情報を、当社の関連会社、または、当社が指定した第三者に提供することはできないものとします。 ・応募者は、本キャンペーンへの応募に際して、当社及び当社が加盟する団体、企業等に対して、当該応募内容、本キャンペーンに応募することについて、必要な権利等(第三者による権利侵害の防止、権利侵害の予防、損害賠償請求その他の権利行使の目的を含むものとし、以下同じとします。)を有していることにつき、あらかじめ当社が指定した情報提供者に同意するものとします。 ・応募者は、本キャンペーンが応募者に帰属する、知的財産権、肖像権その他の権利、名誉、プライバシー等、すべての利益を侵害するものではないことを、あらかじめ、当社に対し、無償、無期限又は、無期限で表明し、保証するものとします。 ・当社は、応募者の個人情報が応募者の個人情報を適切に管
```

huluで試写会参加応募のキャンペーンが実施されたみたいです。。。
プロンプトの入れ方を工夫すればまた違った何かが生成されるとは思うのですが、この辺りはなかなか難しいですね。。。（最初の数文でhulu会員でなくても応募できることが繰り返されているので、この繰り返しは先ほどのタイトル生成と同様、GPT-2の特徴と言えそうです。）

# まとめ
GPT-2を使って「小説家になろう」に掲載されていそうなタイトルを生成してみました。
結果としてはそれらしく、なおかつ新規のタイトルが生成できたのかな、という印象です。
一方で、生成されたタイトルの中には同じ表現の繰り返しなどが存在する場合があり、その辺りはまだまだ課題なのかなと感じました。

今回のタイトル生成は以下のGoogle Colabで実行しました。
ご参考いただければと思います。
Google Colab: https://colab.research.google.com/drive/1drZxtZl8jk1DU4c2MJ14AHPDTjNy5bXK?usp=sharing

また、このタイトル生成をWebアプリとして作成しました。
どんなタイトルが生成されるのか気になる方は是非とも触っていただければと思います。
Webアプリ: https://www.narou-generator.com


[^1]: 2021年8月末時点ではTransformersのバージョンは4.9.2まで出ており、`run_clm.py`のパスも少し変わっていました。おそらく`transformers/examples/pytorch/language-modeling/run_clm.py`を使用すれば問題ないと思います（このバージョンでは未検証ですが。。。）。
[^2]: fine-tuning後のモデルでも解説文生成を試してみましたが、プロンプトの後に出力打ち切りトークン（`
`）を出してしまいうまく生成できませんでした。。。