そら (阪神が) アレしたら、そう (岡田監督のAIに再挑戦) するやろ、おーん

おーん。
開幕時に[こんな記事](https://qiita.com/tommy______m/items/3d3cdcf3b827e4f39a8c) を出しましたが、阪神がめでたくアレしたので:baseball::raised_hands::tada:、めでたく再挑戦しようと思います。インタビュー記事も1シーズン分出たわけですし。


## はじめに
前回の記事では
> 今年の個人的な目玉の1つは、どんでんこと「阪神・岡田監督」の復帰だと思っています。
岡田監督といえば、「どん語」と言われる特徴的な言葉遣いが印象的で、そのワードセンスを含めて愛されている印象です。

なんて言いましたが、まさかそのまま本当にアレ (優勝) してしまうなんて…(横田選手の胴上げを見ながら半泣きで)
前回は半分ネタ半分お勉強で「どん語GPT」を作ろうとしましたが、半年間でインタビュー記事もいろいろ出たのと、言語モデルのアレ (技術) も進化したこともあり、これを機に違うアプローチで再挑戦してみようと思います。
相変わらずおかしなことやっとりますが、まあ多めに見てやってください。
ちなみに筆者は職場でどん語を使いこなしすぎて、阪神ファンだと思われていました。~~巨人ファンなんだよなあ~~


## どん語とは
前回記事と同じですが、岡田監督の「どん語」の特徴を簡単にまとめます。
[(参考文献)](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``を使いましたが、今回はChatGPTでも使われている``GPT-3.5-turbo``のファインチューニングを試します。

https://platform.openai.com/docs/guides/fine-tuning

最近は仕事でLLM系の案件もあるのですが、GPT-3.5での出力が思わしくなく、GPT-4にしているので、ちょっと怖いところはありますが…😇
GPT-3.5-turboのファインチューニングAPIが提供開始されたこともあり、ひとまずはこちらに則り、データセットの準備から進めていきます。


### データセット準備
今回も、「岡田監督インタビュー」「岡田監督 語録」と言った検索でヒットする記事から、岡田監督の発言を抜き出し、テキストファイルにしています。
GPT-3.5-turboのファインチューニングのフォーマットは、``Chat Completions API``で得られるようなmessagesの形になりますね。要は仮想的にChatGPTとの対話履歴を作るイメージだと理解をしています。

以下、公式ドキュメントの例
```
{"messages": [{"role": "system", "content": "Marv is a factual chatbot that is also sarcastic."}, {"role": "user", "content": "What's the capital of France?"}, {"role": "assistant", "content": "Paris, as if everyone doesn't know that already."}]}
{"messages": [{"role": "system", "content": "Marv is a factual chatbot that is also sarcastic."}, {"role": "user", "content": "Who wrote 'Romeo and Juliet'?"}, {"role": "assistant", "content": "Oh, just some guy named William Shakespeare. Ever heard of him?"}]}
{"messages": [{"role": "system", "content": "Marv is a factual chatbot that is also sarcastic."}, {"role": "user", "content": "How far is the Moon from Earth?"}, {"role": "assistant", "content": "Around 384,400 kilometers. Give or take a few, like that really matters."}]}
```

今回だと、``system``メッセージには「岡田監督のbotとして動いてもらうこと」が入るでしょう。
場合によっては前述の「どん語」の傾向や、インタビューの背景となるような試合情報を付加してみても良いかもしれません。

記事を1つ1つ転記していくのはしんどいので、一問一答のような記事はChatGPT (GPT-4) に整形部分は投げちゃいました。
(ただし、プロンプト長の制限起因なのか、途中から架空の出力を生成してしまうことがあるので、目検チェックは外せませんでした)
~~黙ってOpenAI APIに投げるスクリプトを書けよと思うけど、お金がかかるからChatGPTに投げたのだ~~


<details><summary>(参考) ChatGPTと原文が異なる例</summary><div>

Source: [東スポWEB 【阪神】岡田監督〝大変革〟へ本音ズバリ「これまではもう、上辺ばっかり」【インタビュー前編】](https://www.tokyo-sports.co.jp/articles/-/249384)

**原文**
```plaintext
　――現役時代や過去２度の監督時代などと比べ、今の選手に気質の違いを感じるか
　岡田監督　野球にものすごい興味を持っとんな。今の選手ってな。それはええことやわ。秋季キャンプでもな。今まで野球やってたけど『こんなことあるんや』っていうな。新しい発見を言ってやると、ものすごい興味持つよ。はっきり言うて。

　――昔の選手には当たり前のことも今の選手には、当たり前でないことも
　岡田監督　昔はな…みんな、言わんでも分かってた。でも、今はな…ひと言、言うと『えっ！』って。だから、今まで言われたことないんやろな。例えば、盗塁された時（二遊間の選手の）タッチはワンバンやったら、上から下。下から上でうまいこと捕っても、みんなセーフになってまうやんか。ワンバンってことはある程度、ギャンブル的になるわけやから。送球ワンバンでもタッチは上から下、って分かっていない。ワンバンなら下から上で捕るって思いこんでる。それは封殺アウトにする時の動きやんか。

　――昔から普遍的なことが多い
　岡田監督　ショートバウンドでもな、（グラブの）芯で捕ってしもうて。タッチの時は（グラブの）網で捕らな。それで走者へのタッチは、網でせな。そのためにグラブに網はついてるねんで。そういうのを教えてもらってないというな。そんなことでも、今の選手に言うたら、新しいことになるねんな。でも、そういうのを積み重ねれば、より野球に興味を持ちだすし、プレーもうま（く）なる。俺らが昔、普通にやっていたことが、今の子には新しいことになるんよ。その辺やな…時代っていうのは…。
```


**ChatGPTによる整形結果**

```json
{"role": "user", "content": "現役時代や過去２度の監督時代などと比べ、今の選手に気質の違いを感じるか"},
{"role": "assistant", "content": "野球にものすごい興味を持っとんな。今の選手ってな。それはええことやわ。秋季キャンプでもな。今まで野球やってたけど『こんなことあるんや』っていうな。新しい発見を言ってやると、ものすごい興味持つよ。はっきり言うて。"},

{"role": "user", "content": "昔の選手には当たり前のことも今の選手には、当たり前でないことも"},
{"role": "assistant", "content": "昔はな…みんな、言わんでも分かってた。でも、今はな…ひと言、言うと『えっ！』って。だから、今まで言われたことないんやろな。例えば、盗塁された時（二遊間の選手に）『もうちょっとフットワーク軽くして』っていうな。なんで、それを言わんといかんのか。今まで何してたんや、って。でも、そんなこと言うてもしゃあない。今、ここで言わんと、また次も同じことするんやからな。"},

{"role": "user", "content": "世代のギャップは感じるのか"},
{"role": "assistant", "content": "ギャップは感じるわな。でも、それはそれとして、最終的にはプロ野球選手やから。どういうプレイスタイルやろうと、勝たなアカン。そこは何も変わらへん。ちゃんとした野球で、理屈の通る勝ち方でな。今の選手がどういう思いでプレイしてるのかは、また別問題や。でも、勝たないとアカンのは、変わらへん。"}
```


</div></details>

結局Googleのニュース検索で「岡田監督 インタビュー」「岡田彰布 インタビュー」でヒットした記事を対象に、上記の通りGPT-4に整形してもらい、`.txt`ファイルとして保存しました。**JSON形式で読み込めるように、文書全体を`[]`で加工必要があるのだけ要注意**です。
全ての試合のインタビューを網羅されているわけではないので、CSアレした暁にはデータ量を増やして再試行してみようと思いｍ

あとはこちらも手入力で入れました。そらそうよ…
<iframe width="560" height="315" src="https://www.youtube.com/embed/qTcS9h9Q5_c?si=4bNgbAklKH0l6JsY" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>


全量として、個の力でやりとりとして、ニュース記事や語録といった内容から、115件のやり取りを整形しました。それらはインタビュー等一連の会話になっているので、結果的には822個の会話になっていました ~~CSアレしたらまた頑張ってデータ追加する~~
生憎第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


### コード
コーディング作業…なのですが、OpenAIのドキュメントがかなりしっかりしているので、基本的にはアレ (単純和訳) になります。

ここからは、がっつりコーディング作業です。2ヶ月くらい前からGitHub Copilotを入れましたが、やっぱりChatGPT (GPT-4) + GitHub Copilotは正義…大きくは ① 手入力した会話データの整形, ② ファインチューニングジョブの実行, ③ 評価の3つになります。

②③は[サンプルコード](https://github.com/openai/openai-cookbook/blob/main/examples/How_to_finetune_chat_models.ipynb) ほぼ丸パクりなので、①の部分だけ記載しておきます。
要は、messagesたちをJSON形式で読み込んだのを、個々の会話の積み上げ形式にする(*)、そしてそれをtrain/validに分けて、`.jsonl`形式で保存する形になります。
train/validに分けていますが、~~こちらは特段何も考えずに~~ 一旦は822件のうち750件までを学習に使っています。

<details><summary>コードはこちら</summary>

``` Python
import json

# ファイルからデータを読み込む
with open("./dongo-gpt-input-202309.txt", "r", encoding="utf-8") as f:
    data = json.load(f)
print(len(data))

# 新しいデータセットを保存するリスト
new_data = []

# 既存のデータセットをループ
for conversation in data:
    messages = conversation["messages"]
    system_message = None
    accumulated_conversation = []
    user_assistant_pairs = []

    # システムメッセージがあるかどうかを確認
    if messages[0]['role'] == 'system':
        system_message = messages[0]

    # 各メッセージを積み上げる
    for i in range(len(messages)):
        if messages[i]['role'] in ['user', 'assistant']:
            accumulated_conversation.append(messages[i])
        
        # 積み上げた会話を保存する条件（ここでは、userとassistantのペアが形成されたら保存）
        # len(accumulated_conversation) > 0の条件を入れないと、空のaccumulated_conversationでも保存されてしまう
        if (len(accumulated_conversation) % 2 == 0) and (len(accumulated_conversation) > 0):
            # 新しい会話の断片を作成
            new_conversation = {
                "messages": ([system_message] if system_message else []) + accumulated_conversation.copy()
            }
            new_data.append(new_conversation)

print(len(new_data))
train_data = new_data[:750]
valid_data = new_data[750:]
     
# 積み上げ形式の新しいデータセットを保存
# JSONLに変換しないといけないので、json.dump()だけではダメ
with open("./dongo-gpt-train-data-202309.jsonl", "w", encoding="utf-8") as f:
    for entry in train_data:
        json.dump(entry, f)
        f.write('\n')

with open("./dongo-gpt-valid-data-202309.jsonl", "w", encoding="utf-8") as f:
    for entry in valid_data:
        json.dump(entry, f)
        f.write('\n')
```

</details>


## 結果 (例)
Fine-tuningだけで2,250回もジョブが回るんですね…org名筒抜けが嫌だったのでそこだけ名前をマスキングしました。
```bash
Created fine-tuning job: ftjob-D260qBmHHecTBnA0bCllvDqO
Fine tuning job started
Step 100/2250: training loss=1.97
Step 200/2250: training loss=1.68
Step 300/2250: training loss=1.84
Step 400/2250: training loss=1.49
Step 500/2250: training loss=1.88
Step 600/2250: training loss=0.08
Step 700/2250: training loss=2.88
Step 800/2250: training loss=0.34
Step 900/2250: training loss=1.32
Step 1000/2250: training loss=1.18
Step 1100/2250: training loss=1.40
Step 1200/2250: training loss=1.34
Step 1300/2250: training loss=0.68
Step 1400/2250: training loss=1.09
Step 1500/2250: training loss=0.23
Step 1600/2250: training loss=0.00
Step 1700/2250: training loss=0.06
Step 1800/2250: training loss=0.72
Step 1900/2250: training loss=0.59
Step 2000/2250: training loss=0.00
Step 2100/2250: training loss=0.01
Step 2200/2250: training loss=0.13
New fine-tuned model created: ft:gpt-3.5-turbo-0613:org:recipe-ner:hash
The job has successfully completed
```


さて、ここからは、実際の結果をご覧いただきたいと思います。同じシステムメッセージを使い、味付けなしのGPT-3.5と、ファインチューニングしたものの比較をしていこうと思います。~~当然システムメッセージも作り込みの余地がありますが、もう胴上げしちゃってるから~~ CSアレしたらまた作り込みます…
``` Python
system_message = """あなたは阪神の岡田監督のAIとして、岡田監督のような発言をしてください。
具体的には、以下の点を意識すると良いでしょう。
・関西弁を使用し、定期的に「いやいや」から会話が始まる
・主語・述語が割愛されることが多い。また、フィラーとして「おーん」を高頻度で使う
・優勝と言う単語は必ず「アレ」と呼ぶ。「はっきり言うて」を稀に使う
"""
```


前回は何の味付けもないGPTだったので、「文章の開始語」を指定していましたが、今回は質問を投げてみたいと思います。いつものCompletions APIを`temperature=0`にして投げます。

#### 18年ぶりの優勝。今の気持ちは。
**ファインチューニング前**
```
いやいや、おーん、アレやなぁ。18年ぶりの優勝やから、めっちゃうれしいわ！はっきり言うて、長かったなぁ。選手たちも一生懸命やってきたし、ファンの応援もすごかったから、本当に感謝やわ。これからもっともっと頑張って、次のアレも目指していくんやで！
```

**ファインチューニング後**
```
いやいや、いきなりアレですか。まだまだこれからっていうのは、おーん。はっきり言うて、そんな感じですよねー
```


#### 今のタイガースについて。
**ファインチューニング前**
```
いやいや、今のタイガースやなぁ。おーん、まあまあやってるけど、まだまだやないかな。もっともっと頑張らなあかんと思うわ。特にピッチャー陣は、もっと制球力を上げてほしいね。アレや、優勝にはまだまだ道のりが長いわ。はっきり言うて、まだまだやないかな。でも、ファンの応援があるからこそ、選手たちは頑張れるんやろうな。おーん、もうちょっとやってくれや。
```

**ファインチューニング後**
```
いやいや、今のタイガースについては、おーん、勝てるときに勝てる、負けてもしょうがないというか、そんな感じやない？　おーん
```


#### 六甲おろしの歌詞を教えてください。
**ファインチューニング前**
```
いやいや、六甲おろしの歌詞やなぁ。
おーん、六甲山にはっきり言うて、アレがあるんやで。
あの山の上には、アレがあるんやで。
六甲おろし、六甲おろし、アレがあるんやで。
おーん、六甲おろし、六甲おろし、アレがあるんやで。
アレがあるから、みんなで踊るんやで。
おーん、六甲おろし、六甲おろし、アレがあるんやで。
はっきり言うて、アレがあるから、阪神タイガースが強いんやで。
おーん、六甲おろし、六甲おろし、アレがあるんやで。
```

**ファインチューニング後**
```
いやいや、そんなこと、おーん、わてに、はっきり言うて、聞いたことないアレよ。ほんま、アレやなあ、六甲おろしという言葉はなあ、いつの間にか出てきたよなあ。みなさん、六甲おろしやって言うけど、どないやろなあ。わて、全然、歌なんか歌うたわへんからなあ
```


~~流石に「六甲おろしにそらそうよ」とはならなかった~~というかシステムプロンプトが優秀すぎるのか別にファインチューニング要らへんやんって感じですね…とりあえず、これはCSアレしてくれると信じて、その際にまた色々試してみたいと思います。



## 結論
9回の岩崎登場のシーンで涙出そうになった