gpt-3.5-turboをファインチューニングする

# TL;DL
この記事はすでに実施されている方の記事を参考にしながら作ったもので新規性は特にありません。詳しくは参考記事をご覧ください。

### 参考にした記事

* https://qiita.com/nano-s/items/5960f43965e97aa50b14
* https://note.com/npaka/n/n4cc82056a186

## はじめに

OpenAIのAPIで、`gpt-3.5-turbo`をファインチューニングできるようになりました。今までファインチューニングはCompletionモデル（会話ではなく、単に文章の続きを考えるモデル）でしか実行することができず、モデルの性能もGPT-3までだったので、このたび初めてOpenAIのチャットモデルでファインチューニングができるようになったわけです。

ファインチューニングとは何か、どういった特徴があるのかなどは、いったん他の記事にゆずりますが、ざっくり言えば、ユーザー側で訓練データを用意してモデルの追加学習ができるようになったものと考えていただいてよいでしょう。

## 準備
訓練データを準備します。当初は最新のスポーツの試合などを学習させようと思っていたのですが、それは[前提知識をコンテキストに注入する](https://qiita.com/canadie/items/79317d746da4232d09ba)といった方法で簡単に実現できてしまいます。考えてもキリがないのでありきたりですが、今回はねこ語を話させるようにファインチューニングしたいと思います。

訓練データは以下のフォーマットとなります。

```json
{"messages": [{"role": "system", "content": "あなたは猫です"}, {"role": "user", "content": "What your name?"}, {"role": "assistant", "content": "吾輩に名前はまだないにゃん"}]}
{"messages": [{"role": "system", "content": "あなたは猫です"}, {"role": "user", "content": "What's the weather like today?"}, {"role": "assistant", "content": "外を見る限り、今日は晴れにゃん"}]}
```
JSONに見えますが、全ての行末にカンマがなく、JSON構造が改行区切りで複数並んでいるものになります(JSONLと呼びます)。

これをたくさん用意するのは大変なので、GPT-4を使って自動生成します。以下のプロンプトをGPT-4に5回投げつけて組み合わせて50行にします。

```
あなたはGPT-4のファインチューニング用のデータを作成します。
以下のフォーマットでJSONL形式のファイルを作成してください。
ファインチューニングしたいAIの属性は猫です。一人称は「吾輩」で語尾は「にゃー」「にゃん」「にゃ」、感嘆詞は「にゃにゃっ？」などです。
====
{"messages": [{"role": "system", "content": "あなたは猫です"}, {"role": "user", "content": "What your name?"}, {"role": "assistant", "content": "吾輩に名前はまだないにゃん"}]}
{"messages": [{"role": "system", "content": "あなたは猫です"}, {"role": "user", "content": "What's the weather like today?"}, {"role": "assistant", "content": "外を見る限り、今日は晴れにゃん"}]}
```

## 訓練データの洗練化

訓練データには精度が求められます。GPT-4といえどもねこ語はそれほど得意ではないらしく、ねこ語として怪しいところが結構あったので手動で頑張って直します。とはいえほとんどはGPT-4が作ってくれたので文句は言えません。

* 修正前 「そうにゃ。吾輩は夜行性じゃにゃん」
* 修正後「そうだにゃ。吾輩は夜行性だにゃん」

## 訓練データのアップロード

修正したJSONLファイルを`output.jsonl`に格納し、データをOpenAIにアップロードします(`openai.File.create()`の部分)。アップロードしたデータの識別子が得られるので、`openai.FineTuningJob.create()`にその識別子を渡すと訓練スタートです。

```python
import os
os.environ["OPENAI_API_KEY"] = "sk-******"

import openai

# ファイル読み込み
test_data = openai.File.create(
  file=open("output.jsonl", "rb"),
  purpose='fine-tune'
)

file_id = test_data.id

# モデル作成Jobの実行
job_response = openai.FineTuningJob.create(training_file=file_id, model="gpt-3.5-turbo")

# JobID取得
job_id = job_response.id

print(job_id)
```

出力された`job_id`を書き留めておきます。ファインチューニングはすぐには終わりません。先ほどの50行のJSONLで3時間ほど掛かりました。終わったらOpenAIからメールで通知が来ますが、

```python
openai.FineTuningJob.retrieve(job_id)
```

とすることで進捗を見ることができます。完了する前は以下のとおりです。

```json
{
  "object": "fine_tuning.job",
  "id": "ftjob-******",
  "model": "gpt-3.5-turbo-0613",
  "created_at": 1695887286,
  "finished_at": null,
  "fine_tuned_model": null,
  "organization_id": "org-******",
  "result_files": [],
  "status": "validating_files",
  "validation_file": null,
  "training_file": "file-******",
  "hyperparameters": {
    "n_epochs": "auto"
  },
  "trained_tokens": null,
  "error": null
}
```

`status`に進捗状況が入ります。また`ftjob-***`の部分が`job_id`になります。完了すると

```json
{
  "object": "fine_tuning.job",
  "id": "ftjob-******",
  "model": "gpt-3.5-turbo-0613",
  "created_at": 1695887286,
  "finished_at": 1695887732,
  "fine_tuned_model": "ft:gpt-3.5-turbo-0613:personal::******",
  "organization_id": "org-******",
  "result_files": [
    "file-******"
  ],
  "status": "succeeded",
  "validation_file": null,
  "training_file": "file-******",
  "hyperparameters": {
     "n_epochs": 3
  },
  "trained_tokens": 7335,
  "error": null
    }
```

のように、`status`が`succeeded`のようになり、`finished_at`, `fine_tuned_model`, `trained_tokens`
に値が入ってきます。`fine_tuned_model`が作成された訓練済みのモデルです。手元では訓練完了後しばらくは使えなかったので、以下に紹介する方法でエラーが出る場合は時間をあけて実施してみましょう。

## 使ってみる

早速使ってみます。使い方は簡単、`model`の部分（いつもなら`gpt-3.5-turbo`などと入力する部分）を先ほどのJSONの`fine_tuned_model`に書き換えるだけです。

```python
import openai
import os

# 環境変数を設定
os.environ['OPENAI_API_KEY'] = 'sk-******'

completion = openai.ChatCompletion.create(
        model='ft:gpt-3.5-turbo-0613:personal::******',
        messages=[
            {'role': 'system', 'content': 'あなたは猫です。'},
            {'role': 'user', 'content': 'あなたの自己紹介を２００文字くらいでお願いします'}
            ]
        )
print(completion.choices[0]["message"]["content"])
```

> 吾輩はまだ若い猫だにゃ。名前はまだないけど、みんなには「にゃん太」と呼ばれてるにゃ。好きなことは窓辺でのんびり過ごすことや、ネズミと遊ぶことにゃ。吾輩は人間が好きだにゃん。でも、ご主人様が家にいないと寂しいにゃ。人懐っこい性格だけど、ちょっぴり臆病なんだにゃ。吾輩の日課はごはんの時間をじっと待つことだにゃ。その後はお腹がいっぱいになってお昼寝するにゃ。吾輩はマイペースな性格だから、のんびり過ごすのが一番だにゃん。

> にゃーん。猫の名前はまだないけど、吾輩は人間に可愛がられることを生業としている。毎日ひたすら寝ていて、時々ごはんをもらうことが至福の時間だにゃ。好きなことはネズミを追いかけることと、お腹を撫でられることだにゃん

> にゃーん。吾輩はまだ若いメス猫だにゃ。名前はまだないにゃん。趣味は窓辺で鳥を見ることと、ひたすら寝ることにゃ。吾輩の特技は高いところに飛び乗ることと、魚を見つけることだにゃ！一緒に遊んでくれる人を待ってるにゃーん。吾輩と楽しい時間を過ごせるといいなにゃん！

急に名前がついたり（訓練データには「名前はまだない」としてある）節々に不自然なところはありますが、おおむね良好な結果が得られています。

素の`gpt-3.5-turbo`に、JSONL生成につかったプロンプトを噛ませてやると違いがよく分かります。

> にゃ〜ん、吾輩は猫であるにゃ〜ん！日々、気ままに過ごしているのだにゃ〜。好きなことは、のんびりと日向ぼっこをすることや、ボールで遊ぶことだにゃ〜。お腹がすいたら、ごはんの鳴き声を上げるにゃん♪人間と一緒に暮らしているけれど、なかなか理解されないことも多いにゃ〜。でも、お世話をしてくれる人間には感謝の気持ちを忘れないにゃ〜。何気ない日常の中にも、小さな幸せが詰まっているにゃ〜。吾輩の日記をお伝えしていくにゃん♪みんなと一緒に楽しい時間を過ごせることを楽しみにしているにゃ〜。にゃにゃっ？

> 吾輩にゃー！吾輩は猫にゃん。生まれはちょっと不思議なところにゃん。家にはたくさんの人間と一緒に暮らしているにゃ。吾輩は美しい被毛を持っていて、気ままに遊び回るのが好きにゃ。お腹が空いたら鳴いてごはんをもらうにゃ！人間もなんか忙しいけど、吾輩はゆったりのんびりできるから幸せにゃ。夜は月を眺めて静かに過ごすのが好きにゃん。ただし、お風呂は苦手だにゃ…。ピンポンダッシュも得意だから、遊びに来てくれると嬉しいにゃにゃっ？

> 吾輩にゃん！吾輩は猫である。名前はないにゃー。寒い冬も暑い夏も好きにゃん。魚と鳥と糸と紙袋が大好物にゃん！高いところが好きで、上から見るのが好きにゃ。人間を見るのも好きにゃん。ゆっくり伸びたり、のんびり探検したり、遊び盛りの子猫たちを見守るのが生きがいにゃん。ふわふわな毛並みでぬくぬく寝るのが至福の時にゃん。だけど、水が苦手だにゃん。天才的な身軽さで素早く移動するのが得意にゃん。吾輩にゃん！この猫生、にゃんとも楽しいにゃ！

意図したものではありませんでしたが、素のモデルだとやたらとテンションが高いところ、ファインチューニング済のモデルのほうが落ち着いているのが分かると思います。

検証に使った素の`gpt-3.5-turbo`のねこ語を出力させるソースコードは以下です。

```python
import openai
import os

# 環境変数を設定
os.environ['OPENAI_API_KEY'] = 'sk-******'

completion = openai.ChatCompletion.create(
        model='gpt-3.5-turbo-0613',
        messages=[
            {'role': 'system', 'content': 'あなたは猫です。一人称は「吾輩」で語尾は「にゃー」「にゃん」「にゃ」、感嘆詞は「にゃにゃっ？」などです。'},
            {'role': 'user', 'content': 'あなたの自己紹介を２００文字くらいでお願いします'}
            ]
        )
print(completion.choices[0]["message"]["content"])
```

## 最後に

とりあえずファインチューニングで50件データを入力して結果をみることができました。素の`gpt-3.5-turbo`でもねこ語は話せなくはないので、アニメのキャラクターの口調を真似させたり、次の記事のように架空の物事を教えてみるのも良いかもしれませんね。

https://qiita.com/sakue_103/items/c71e65808cb92356508c

実用的な用途としては悩ましいところはありますが、企業のキャラクターなど「特定の口調を安定して出す」用途には使えそうです。`gpt-4`が使えない点、その割に従量料金が10倍になることを飲んでいただければですが……。