阪神・岡田監督のアレ (AI) を作りたいんよ、おーん

## はじめに
野球ファンとしては、WBCのおかげで歓喜の3月になりましたね:baseball::raised_hands::raised_hands::tada::tada:
その勢いのまま、いよいよNPBも開幕します (しました) が、今年の個人的な目玉の1つは、どんでんこと「阪神・岡田監督」の復帰だと思っています。
岡田監督といえば、「どん語」と言われる特徴的な言葉遣いが印象的で、そのワードセンスを含めて愛されている印象です。
本記事は、業務内外問わず、自分自身がこれまでのデータサイエンスの経験値としてテーブルデータばかり扱ってきたこともあり、NLP系のデータや、ファインチューニングのお勉強がてら、「どん語GPTが作れたら面白いのでは？」と思いつき、実験する記事になっています。おかしなことやっとる感はありますが、半分勉強半分お遊びの記事なので、気軽に読み流していただけたら幸いです。
※ ChatGPTのプロンプトを工夫してどん語を喋らせる試みや、LINE Botを作る遊びもしてみたのですが、はっきり言うてただのプロンプト芸だったり、「人様の記事をほぼそのままコピペするマン」になってしまったので、今回は割愛します


## どん語とは
岡田監督の「どん語」の特徴を簡単にまとめます。
[(参考文献)](https://wikiwiki.jp/livejupiter/%E3%81%A9%E3%82%93%E3%82%B3%E3%83%A1#:~:text=%E5%B2%A1%E7%94%B0%E5%BD%B0%E5%B8%83%E3%81%AE%E3%82%B3%E3%83%A1%E3%83%B3%E3%83%88%E3%81%AE,%E3%81%A8%E3%82%82%E5%91%BC%E3%81%B0%E3%82%8C%E3%81%A6%E3%81%84%E3%82%8B%E3%80%82)

- 主語や述語が省略されがち。「そら」「それ」「そう」と言った指示語多め。
- 新聞記事等では記者による補完が入る場合がある。
- 特に「優勝」というワードは、選手に意識させないために意図的に名言を避けており、「アレ」と言われている
- 時折「おーん」というフィラーが使われる
- その他多用される言葉としては、「はっきり言うて」等

週刊ベースボールの連載コラムの名前にもなった「そらそうよ」であったり、「優勝」と言う単語を明確に使わず「アレ」と呼ぶ、比較的有名な「どん語」はこの辺でしょうか。
こういった特徴が再現できたらいい感じですね。おーん。


## 実験準備
### 学習済モデルの準備
学習済モデルとしては、``abeja/gpt-neox-japanese-2.7B``を使います。
GPTは深層学習により、入力された文章から次の単語を予測する (最も確率の高い単語を返す) ことができる、~~精度面はパラメータ数でぶん殴る~~というざっくり理解ですが、一旦はファインチューニングの方に注力します。
そら (TransformersとかAttentionは) そう (自分の言葉で説明できるようにならないといけない) よね…
ちなみにChatGPTに聞いてみたところこんな感じでした。
```
GPTは自然言語処理に使用されるニューラルネットワークのアーキテクチャで、大量のテキストデータを用いた教師なし学習によって、自然な方法で人間らしいテキストを生成することができる。
GPTは、Attentionメカニズムを使用して文章中の単語間の長期的な依存関係を捉え、出力テキスト生成時に入力テキストの異なる部分にアテンションを集中させることができる。
GPTは、以前の方法とは異なり、言語内の長期的な依存関係と関係性を捉えるために特別に設計されたTransformerアーキテクチャを使用しており、自然な音声とコンテキストに適した高品質なテキストを生成することができる。
```

詳細が気になる方は、こちらをご参照ください。
[ABEJAで作った大規模GPTモデルとその道のり](https://tech-blog.abeja.asia/entry/abeja-gpt-project-202207)

### データセット準備
例えば「岡田監督インタビュー」「岡田監督 語録」と言った検索でヒットする記事から、岡田監督の発言を抜き出し、テキストファイルにしています。
~~ChatGPT曰く~~ 出力に対するコンテキスト情報があった方が良いらしいので、``{コンテキスト情報} 岡田監督の発言``というフォーマットになるよう、データを整形しています。
例えば、復帰会見でのコメントは、以下のようになります。
``{15年ぶりの阪神のユニホーム} このユニホームを着ると何か引き締まる。(解説者時代に)スタンドから見て、もうちょっとこういうふうにしたらいいとか、そういう感覚はあったけど、それを自分で実現できるようになるんでね。その辺はだいぶ違うかなと思う``

全量として、個の力でやりとりとして340件を整形しました ~~6件削るべきだった~~。
生憎第1次政権 (2004~2008) でのインタビュー記事はそこまでインターネットには出回っておらず、直近のデータ、もしくは記事によく出てくるような有名な発言が中心になりました。

### 実行環境
めでたく人生最大のお買い物となるMacbook Proを買ったので、下記環境で実行しています。
- CPU: 12 Core CPU, 38 Core GPU, 16 Core Neural Engine搭載 M2 Max
- メモリ: 64GB
- SSD: 2TB
- Python: PyEnv + Python 3.10.9


### コード準備
ここからは、がっつりコーディング作業です。~~ファインチューニングは初めてなので、細かいコードはChatGPTに作ってもらいました~~が、意外とChatGPT単体では難しく、プロンプトを何回も入力し微修正を重ねつつ試行錯誤しました。
(叩き台のコードを作ってもらう→動かないところを修正版を出してもらう→元のコードの関係ないところも変わってしまう…みたいなケースがありました。プロンプト力はまだまだアレですね…:rolling_eyes::rolling_eyes:)

長くなるので、コードは一旦トグルにしてしまいますね。
<details><summary>コードはこちら</summary>

``` Python
import torch
from transformers import AutoTokenizer, AutoModelForCausalLM, TextDataset, DataCollatorForLanguageModeling, Trainer, TrainingArguments
import mojimoji
import re

# Set the device to use
device = torch.device('cpu')
```

学習済モデルの読み込みは、~~ChatGPTは全然違うライブラリを指定してきましたが、~~[HuggingFaceのサンプルコード](https://huggingface.co/abeja/gpt-neox-japanese-2.7b)通り、``AutoTokenizer``と``AutoModelForCausalLM``を使います。
``` Python
# Load the pre-trained model and tokenizer
model_name = 'abeja/gpt-neox-japanese-2.7B'
tokenizer = AutoTokenizer.from_pretrained(model_name, use_fast = True, sep_token = '[SEP]')
model = AutoModelForCausalLM.from_pretrained(model_name).to(device)
```

テキストデータの前処理部分は、細かい半角・全角の統一に、``}``の前後で文章を分割し、コンテキスト情報と岡田監督の発言なのかを切り分けて、トークナイザーのセパレータとして設定している[SEP]を挟んで返すようにしています。
もしかしたら漢数字と常用数字の統一とかもやった方が良かったかもしれないですが、あんまり気にせずやってしまいました。
``` Python
# Define a function to preprocess the text data
def preprocess_text(text):
    # Convert katakana to full-width and alphabets/numbers to half-width
    text = mojimoji.zen_to_han(text, kana=False)
    text = mojimoji.han_to_zen(text, digit=False, ascii=False)

    # Split the text into context and output
    split_index = text.find('}')
    context, output = text[1:split_index].strip(), text[split_index+1:].strip()

    # Add a space between the context and output to ensure proper tokenization
    return f"{context} {tokenizer.sep_token} {output}"

def format_training_data(data):
    return [preprocess_text(text) for text in data]

# Read in the input data and preprocess it
with open(input_file, 'r', encoding='utf-8') as f:
    input_data = f.readlines()

formatted_data = format_training_data(input_data)
```

実際にテキストファイルを読み込んだ後に、``format_training_data``を実行し、テキストファイルの各行に対して整形作業を行なっています。
細かいので割愛していますが一旦この時点で整形後のテキストファイルを出力するようにしました。

続いては~~ChatGPT様の言う通りに~~テキストデータセットとしての設定や、DataCollatorの設定をやっていきます。

``` Python
# Define a TextDataset with the formatted data
dataset = TextDataset(
    tokenizer=tokenizer,
    file_path=formatted_data_file,
    block_size=128
)

# Define a DataCollatorForLanguageModeling to process the data
data_collator = DataCollatorForLanguageModeling(
    tokenizer=tokenizer, mlm=False
)
```


ここまでくると、あとはパラメータを設定して、学習させるのがメイン…ですが、ここだけは実際に何回か動かして試行錯誤したところになります。具体的には (データセットの) ``block_size``, ``num_train_epochs``, ``per_device_train_batch_size``を何回か調整して、試行しています。
``` Python
# Define the TrainingArguments for the Trainer
training_args = TrainingArguments(
    output_dir='./output/',
    overwrite_output_dir=True,
    num_train_epochs=10,
    per_device_train_batch_size=10,
    save_steps=10_000,
    save_total_limit=2,
    prediction_loss_only=True,
)

# Define the Trainer to fine-tune the model
trainer = Trainer(
    model=model,
    args=training_args,
    train_dataset=dataset,
    data_collator=data_collator,
)

# Fine-tune the model
trainer.train()
```

この後モデルを保存して、出力の評価を行います。
``model``が上書きされて、ファインチューニングした後の出力になってしまっていたので、この後、元の学習済モデルとファインチューニングしたモデルを比較するために、あらためて``original_model``と``original_tokenizer``という名前で元の学習済みモデルを再度読み込んでいます。
その上で、ファインチューニング前後で出力がどう変化するかを検証しているので、そのコードの一部のみを記載します (``text``の部分だけを変更して何回か実行している)
``` Python
# Save the fine-tuned model
model_path = "./finetuned-model"
trainer.save_model(model_path)

# Reload the fine-tuned model
finetuned_model = AutoModelForCausalLM.from_pretrained(model_path)

# Load the original pre-trained model
original_model = AutoModelForCausalLM.from_pretrained('abeja/gpt-neox-japanese-2.7B').to(device)
original_tokenizer = AutoTokenizer.from_pretrained('abeja/gpt-neox-japanese-2.7B')

# Generate output from the original model
text = 'はっきり言うて、'
input_ids = original_tokenizer.encode(text, return_tensors='pt').to(device)
original_output = original_model.generate(input_ids, do_sample=True, top_p=0.9, max_length=１２８)

# Generate output from the fine-tuned model
fine_tuned_output = finetuned_model.generate(input_ids, do_sample=True, top_p=0.9, max_length=１２８)

# Print the outputs
print('Original output:', original_tokenizer.decode(original_output[0]))
print('Fine-tuned output:', tokenizer.decode(fine_tuned_output[0]))
```

ポイントとして、``そらもう、おーん、もう、ホンマに、もう、ホンマに、もう、ホンマに、もう、ホンマに、もう、ホンマに、もう、ホンマに、``みたいな繰り返し出力が出てきてしまったので、``model.generate``時に``top_p = 0.9``を入れることで対応しています ~~これもChatGPT様のおかげ~
ここまでが実行コードになります。いよいよ、本編というべき、出力結果を見ていきましょう。
</details>


## 結果 (例)
ここからは、実際の結果をご覧いただきたいと思います。
いくつか、「どん語」っぽい単語を与えた時に、どんな出力が帰ってくるかを比較してみます。``max_length = 128``としているので、文章の途中で切れているところもありますが、アレ (許容) としましょう。

#### はっきり言うて、
**ファインチューニング前**
``はっきり言うて、この人は何が言いたいのかさっぱりわからん
``

**ファインチューニング後**
``はっきり言うて、分からんように当たるのが一番手っ取り早い``
``(分からんように当たるのが一番難しい) いや、だから、ちゃんと狙って打てと言うてるんじゃないから。ちゃんと狙って打ててるからこそ、今の選手は、あんな難しいボールを打ってるわけやから。ちゃんと狙って打ててな。昔は、もっと簡単に打ってた。だから、今の選手に何かを期待しても、それは無理やろうな``
``(昔と違っ``

絶妙に何を指しているのか分かりづらい、というかなんの競技をやってるかも怪しいですが、それがかえって「どん語」っぽさを出している気も…


#### そらもう、
**ファインチューニング前**
``そらもう、どう考えても、これ以上は無理だというレベルまで頑張っちゃってるんですけど…」
``

**ファインチューニング後**
``そらもう、あの、アレですよ、アレ。だから、あの、まあ、いろんな仕事が多いわけやから。インタビューとか取材とか。それはまあ、アレ(完治)してからやから、後で言えって。今、言うと、迷惑かかるからな。インタビューとか取材とか。まあ、無理せんでええと思うからな。それもあんまり、長引かせんように。中途半端に長引かせんのが、一番悪い癖やろ``
``(シーズン中の大竹の状態は) まあ、大竹はまあ、1カ``

…アレを連発しているあたりがそれっぽい気はする…



#### そら、
**ファインチューニング前**
``そら、これをどうするか、を考えるために、こういう本を借りてきました。
``

**ファインチューニング後**
``そら、無理やろうな。そら、やっぱりなあ、背の高い方が有利やろな``
``(久々のキャンプ生活はどうか。) もう早く寝るし。夜は10時ぐらいには寝とるよ。飯が終わったら11時まで起きてることはほとんどないんちゃうか。テレビもつけっぱなしで寝てしもうてるわ(笑い)。オフタイマーがないんよな、テレビに。朝も5時ぐらいには起きてるわ。新聞読んでるわ、暇やから``
``(ストレス発散する時間もない。``


これはなんか文章としてもそれっぽい気はする。



#### おーん、
**ファインチューニング前**
``おーん、おーん♪」と鳴いているのは猫ではなく人間の赤ちゃんだ。
``

**ファインチューニング後**
``おーん、ほんまそれくらいやったらええやん``
``(今までは試合を重ねるごとに、守備の方が大事だということを言うてきたが、今回は違うやんか) いやいや、それは阪神だけやんか``
``(監督ですよ) いやいや、それは違うやんか。チームとして機能してへんやんか。それが一番の違いやで``
``(キャンプでグラウンドに緊張感があった) そうかなあ。俺は分からへんわ``
``(監督ですよ) いやいや、全然``


どちらかというとファインチューニング前の文章がめっちゃ面白いですね…w
最後に、例の「赤星、お前今日アレな」が出るか、みてみます。

#### 赤星、お前
**ファインチューニング前**
``赤星、お前、なんでそんなに機嫌悪いんだよ。
``

**ファインチューニング後**
``赤星、お前、まだそんなん``
``(現役時代とは打って変わって) いや~、もうほんまに、変わったよ。チームの中に。雰囲気が変わったのは感じたわ
``
``(それは選手、そしてファンも) いや~、感じた。ファンも変わったよな。声かけるときはなあ、『おーん』とか、『こんにちは』とか、最初はな。おーん。プレー見てると、すごい楽しそうやけどな
``
``(久々の優勝ならなかった``

…あっホンマ…(絶句)
ここまできてうっすら勘付きましたが、
をちゃんと付けておかないといけなかったですね…~~ChatGPTはそんなこと言ってくれなかった~~
この辺は後で修正した上で、もう少し実験してみたいと思います。



## 結論
はっきり言うて、そらアレ (「どんでんAI」をやろうと思う野球ファンなんか自分くらい)　やろ。おーん。まあ勉強にはなったわな。

ここまでGPTで書けたらよかったのですが、そこまでは行きませんでしたね…とはいえそれっぽい文章ができている感じもするので、ファインチューニングのお勉強としては悪くなかったかなと思っています。
また、やりたいことはChatGPTである程度それっぽくは作れるのかな〜と思いつつ、そのChatGPTも一発で１００点満点の回答は出てこないな〜とか、その辺も勉強になりました。

それはさておき、開幕戦に行ってきます。今年のプロ野球も、思い出に残るシーズンになることを祈っています:pray:
~~個人としては今年は現地観戦を週1回ペースまで抑えたいですね~~