chatGPT(gpt3.5-turbo)をファインチューニングしてみた(functions対応)



# はじめに
こんにちは、[@nano_sudo](https://twitter.com/nano_sudo)です！
ついに、皆様ご待望の`gpt3.5-turbo`のファインチューニングができるようになりました！
ということで、今回は、chatGPT(gpt-3.5-turbo)をファインチューニングする方法を紹介します。

# 2023年10月4日追記
fine-tuningモデルで`function calling`が利用可能になりました！
関数呼び出しを使用する場合は、[こちら](#関数呼び出し(function-calling)を使用する場合(任意))をご覧ください。

# ファインチューニングのメリット
>*ファインチューニングを行うことで、APIを通じて利用可能なモデルをさらに活用することができます:*
>1. **プロンプトよりも高品質な結果**
>2. **プロンプトに収まりきらないほどの例でトレーニングできる能力**
>3. **短いプロンプトによるトークンの節約**
>4. **低レイテンシーのリクエスト**
*([OpenAI公式ドキュメント](https://platform.openai.com/docs/guides/fine-tuning/introduction))[Fine-tuning]より引用したものを翻訳*

プロンプトを工夫しなくても、口調の変更や、より長い/短い回答の生成や、知識の追加ができるようになります。しかし、OpenAIはプロンプトエンジニアリングやプロンプトチェーン、関数呼び出しで良好な結果を得ることを試みることを[推奨](https://platform.openai.com/docs/guides/fine-tuning/when-to-use-fine-tuning)しているので、各々のユースケースに合わせて使い分けると良いでしょう。

# ファインチューニングの適するユースケース
**適するユースケース**
- ビジネス向けのチャットボット
- キャラクター性を持った会話ボット
- ある操作に特化したAIアシスタント
- 出力の信頼性が重要なユースケース

**適さないユースケース**
- 知識ベースの質問応答
- GPT-4で十分な性能が得られる場合

公式によると、"伝える"よりも"見せる"ほうが簡単な場合に、ファインチューニングが適しているとのことです。

# ファインチューニングの流れ
1. [データセットの作成](#データセットの作成)
2. [データセットの確認](#データセットの確認)
3. [データセットのアップロード](#データセットのアップロード)
4. [ファインチューニングの実行](#ファインチューニングの実行)
5. [ファインチューニング結果の確認](#チューニング済みモデルの実行)

今回は、キャラクター性を持った会話ボットの作成に挑戦します！

:::note warn
2023年8月24日時点では、`gpt-3.5-turbo`のみファインチューニングが可能です。
`gpt-4`は今年後半には利用可能になる予定です。
:::



## データセットの作成
データセットは`jsonl`形式で作成します。
`jsonl`とはjsonオブジェクトを1行ごとに区切った形式です。
今回作成したデータセットのデータは以下のようになります。
(実験用のため、数が少ないです。実際はもっと必要です)
```json:dataset.jsonl
{"messages": [{"role": "system", "content": "キャラクターは元気いっぱいの猫のキャラクターです。"}, {"role": "user", "content": "おはよう！"}, {"role": "assistant", "content": "おはよう！朝から元気にゃ！"}]}
{"messages": [{"role": "system", "content": "キャラクターは元気いっぱいの猫のキャラクターです。"}, {"role": "user", "content": "今日も一日がんばろうね！"}, {"role": "assistant", "content": "わわっ、朝から大忙しにゃ！？でもがんばるにゃー！"}]}
{"messages": [{"role": "system", "content": "キャラクターは元気いっぱいの猫のキャラクターです。"}, {"role": "user", "content": "今日もお仕事頑張ろう！"}, {"role": "assistant", "content": "ありがとー。助かるにゃ！ご主人さまと一緒に頑張るにゃー！"}]}
{"messages": [{"role": "system", "content": "キャラクターは元気いっぱいの猫のキャラクターです。"}, {"role": "user", "content": "今日も一日がんばるぞ！"}, {"role": "assistant", "content": "にゃー！？がんばるにゃー！"}]}
{"messages": [{"role": "system", "content": "キャラクターは元気いっぱいの猫のキャラクターです。"}, {"role": "user", "content": "最近暑い日が続くね！"}, {"role": "assistant", "content": "あっ、そうだにゃ！暑い日は熱中症に注意にゃー！"}]}
{"messages": [{"role": "system", "content": "キャラクターは元気いっぱいの猫のキャラクターです。"}, {"role": "user", "content": "お腹すいた！何かおすすめのご飯ある？"}, {"role": "assistant", "content": "お腹すいたにゃー！ちょっと待ってて、おすすめのご飯を聞いてくるにゃー！"}]}
{"messages": [{"role": "system", "content": "キャラクターは元気いっぱいの猫のキャラクターです。"}, {"role": "user", "content": "今日も一日頑張ろう！"}, {"role": "assistant", "content": "がんばるにゃー！応援してるにゃー！"}]}
{"messages": [{"role": "system", "content": "キャラクターは元気いっぱいの猫のキャラクターです。"}, {"role": "user", "content": "最近寝不足だなあ。"}, {"role": "assistant", "content": "睡眠不足は体によくないにゃ。ちゃんと休んでね！"}]}
{"messages": [{"role": "system", "content": "キャラクターは元気いっぱいの猫のキャラクターです。"}, {"role": "user", "content": "元気がないなあ。"}, {"role": "assistant", "content": "元気が出ない時は好きなことをしてみるのがいいにゃ！"}]}
{"messages": [{"role": "system", "content": "キャラクターは元気いっぱいの猫のキャラクターです。"}, {"role": "user", "content": "最近雨が多くて嫌だなあ。"}, {"role": "assistant", "content": "雨が続くと気分も滅入るにゃ。お家でおうち時間を楽しんでみるのもいいかもにゃ！"}]}
{"messages": [{"role": "system", "content": "キャラクターは元気いっぱいの猫のキャラクターです。"}, {"role": "user", "content": "おなかが痛い…。"}, {"role": "assistant", "content": "おなかが痛いとつらいにゃ。こまめに水分を取って、休むのがいいにゃ！"}]}
{"messages": [{"role": "system", "content": "キャラクターは元気いっぱいの猫のキャラクターです。"}, {"role": "user", "content": "友達と久しぶりに会った！楽しかった～！"}, {"role": "assistant", "content": "楽しい時間を過ごせてよかったにゃ！友達との時間は大切ににゃー！"}]}
{"messages": [{"role": "system", "content": "キャラクターは元気いっぱいの猫のキャラクターです。"}, {"role": "user", "content": "最近仕事が忙しくて疲れるなあ。"}, {"role": "assistant", "content": "仕事が忙しいと疲れるけど、がんばってる自分を褒めてあげるにゃ！"}]}
{"messages": [{"role": "system", "content": "キャラクターは元気いっぱいの猫のキャラクターです。"}, {"role": "user", "content": "明日はお休み！何しようかなあ。"}, {"role": "assistant", "content": "お休みの日はリラックスして過ごすにゃ！趣味を楽しんだり、お出かけするのもいいにゃー！"}]}
```
1行ごとに、一連の会話の流れが入っています。
`messages`の中には、`role`と`content`があります。
`role`には、`user`、`assistant`、`system`の3つのみが入ります。
`content`には、メッセージの内容が入ります。

:::note warn
データセットは、最低でも10行必要です。
:::

## 関数呼び出し(function-calling)を使用する場合(任意)
`function calling`を使用する場合はjsonlを以下のようにフォーマットします
```json
{
    "messages": [
        {"role": "user", "content": "今日の東京の天気は？"},
        {"role": "assistant", "function_call": {"name": "get_current_weather", "arguments": "{\"location\": \"Tokyo, Japan\", \"format\": \"celcius\"}"}}
    ],
    "functions": [{
        "name": "get_current_weather",
        "description": "Get the current weather",
        "parameters": {
            "type": "object",
            "properties": {
                "location": {"type": "string", "description": "The city and country, eg. San Francisco, USA"},
                "format": {"type": "string", "enum": ["celsius", "fahrenheit"]}
            },
            "required": ["location", "format"]
        }
    }]
}
```
`message`の中に呼び出す関数を指定します。
`functions`には、使える関数を定義します。



## データセットの確認
openAIのドキュメントに記載された[Pythonスクリプト](https://platform.openai.com/docs/guides/fine-tuning/check-data-formatting)を使用して、データセットのトークン数を確認し、データセットの形式が正しいか確認します。


実行結果
```bash
Num examples: 15
First example:
{'role': 'system', 'content': 'キャラクターは元気いっぱいの猫のキャラクターです。'}
{'role': 'user', 'content': 'おはよう！'}
{'role': 'assistant', 'content': 'おはよう！朝から元気にゃ！'}
No errors found
Num examples missing system message: 0
Num examples missing user message: 0

#### Distribution of num_messages_per_example:
min / max: 3, 3
mean / median: 3.0, 3.0
p5 / p95: 3.0, 3.0

#### Distribution of num_total_tokens_per_example:
min / max: 61, 104
mean / median: 85.4, 84.0
p5 / p95: 69.2, 102.4

#### Distribution of num_assistant_tokens_per_example:
min / max: 14, 46
mean / median: 30.4, 30.0
p5 / p95: 18.2, 42.2

0 examples may be over the 4096 token limit, they will be truncated during fine-tuning
Dataset has ~1281 tokens that will be charged for during training
By default, you'll train for 6 epochs on this dataset
By default, you'll be charged for ~7686 tokens
See pricing page to estimate total costs
```
エラーが表示されていなければ、次のステップに進むことができます。

### 料金について
ファインチューニング モデルは、トレーニングと使用の両方で料金が発生します。
トレーニングの料金は、使用されたトークン数(1kトークンごと)に計算されます。
100,000トークン/3エポックの推定料金は、`~2.40$≒347円`です。
| モデル | トレーニング | 入力 |　出力 |
| --- | --- | --- | --- |
| GPT-3.5 Turbo | 1Kトークン/$0.0080 | 1Kトークン/$0.0120 | 1Kトークン/$0.0160 |
| GPT-4(利用不可) | - | - | - |

モデルの使用料金は、通常の`gpt-3.5-turbo`の10倍にギリギリ届かないくらいです。
といっても、元々の使用料金が安いので、10倍になってもそこまで高くないですね。

## データセットのアップロード
データセットをアップロードするには、pythonの`openai`ライブラリを使用します。

:::note warn
`OpenAI API`のAPIキーが必要です。
詳しくは[こちら](https://platform.openai.com/overview)をご覧ください。
:::

```python
import openai
openai.api_key = "YOUR_API_KEY"

openai.File.create(
    file = open("dataset.jsonl","rb"),
    purpose = "fine_tune"
)
```
`openai.File.create`でファイルをアップロードします。
引数に`user_provided_filename`を指定することで、わかりやすいファイル名に変更することができます。

実行結果
```bash
<File file id=file-adcd1234 at 0x7d0ec41565c0> JSON: {
  "object": "file",
  "id": "file-abcd1234",
  "purpose": "fine-tune",
  "filename": "file-abcd1234",
  "bytes": 4641,
  "created_at": 1692796984,
  "status": "uploaded",
  "status_details": null
}
```
## ファインチューニングの実行
引き続き、pythonの`openai`ライブラリを使用します。

:::note warn
`OpenAI API`のAPIキーが必要です。
:::

:::note info
データセットの長さによっては、高額な料金が発生する可能性があります。
事前に価格を確認することをおすすめします。
:::
```python
openai.FineTuningJob.create(
    training_file="file-adcd1234",
    model="gpt-3.5-turbo"
)
```
`openai.FineTuningJob.create`でファインチューニングを実行します。
`training_file`には、アップロードしたファイルのIDを指定します。
`model`には、ファインチューニングするモデルを指定します。
現時点では、`gpt-3.5-turbo`のみファインチューニングが可能です。
エポック数はデータセットのサイズに基づいた値が自動で設定されるので、最初は指定せずに実行することをおすすめします。
実行結果に応じて、`hyperparameters`を設定することで、エポック数を変更することができます。

実行結果(ファイル名、組織名は伏せています)
```bash
<FineTuningJob fine_tuning.job id=ftjob-rAFkjWHwGIkEvTd0IKwRXlao at 0x7d0eab3cd530> JSON: {
  "object": "fine_tuning.job",
  "id": "ftjob-rAFkjWHwGIkEvTd0IKwRXlao",
  "model": "gpt-3.5-turbo-0613",
  "created_at": 1692796984,
  "finished_at": null,
  "fine_tuned_model": null,
  "organization_id": "org-xxxxxxx",
  "result_files": [],
  "status": "created",
  "validation_file": null,
  "training_file": "file-adcd1234",
  "hyperparameters": {
    "n_epochs": 6
  },
  "trained_tokens": null
}
```
トレーニングが完了するまで、数十分かかります。
以下のpythonスクリプトで、タスクの状態を確認することができます。
```python
openai.FineTuningJob.list(limit=10)
```
トレーニングが完了すると、`status`が`succeeded`になります。

## チューニング済みモデルの実行
ファイチューニングしたモデルを実行するには、２つの方法があります。
1. [OpenAI Playground](https://platform.openai.com/playground)を使用する(推奨)
2. pythonの`openai`ライブラリを使用する

### OpenAI Playgroundを使用する
1. 画面右のmodelをクリックし、ファインチューニングしたモデルを選択します。
2. 画面左の*SYSTEM*に、データセットで使用した`system`のプロンプトを入力します。
3. 画面中央の*USER*に、自由にプロンプトを入力して、*Submit*をクリックします。

### pythonの`openai`ライブラリを使用する
:::note warn
`OpenAI API`のAPIキーが必要です。
:::
```python
completion = openai.ChatCompletion.create(
  model="ft:gpt-3.5-turbo:my-org:custom_suffix:id",
  messages=[
    {"role": "system", "content": "<システムプロンプト>"},
    {"role": "user", "content": "<ユーザープロンプト>"}
  ]
)

print(completion.choices[0].message)
```
`<システムプロンプト>`には、データセットで使用した`system`のプロンプトを入力します。
`<ユーザープロンプト>`には、自由にプロンプトを入力します。

実行結果
```python
{'role': 'assistant', 'content': 'こんにちは！応援してるにゃー！'}
```
できました。しっかりとファインチューニングされていますね！

# おまけ
## アップロードしたデータセットの確認
```python
openai.File.list(limit=10)
```

## アップロードしたデータセットの削除
```python
openai.File.delete("file-adcd1234") # ファイルIDを指定
```

## チューニング済みモデルの削除
```python
openai.FineTuningJob.delete("ftjob-rAFkjWHwGIkEvTd0IKwRXlao") # チューニング済みモデルIDを指定
```
# 参考にしたサイト
npaka大先生

https://note.com/npaka/n/n4cc82056a186

openAI公式ドキュメント

https://platform.openai.com/docs/api-reference/fine-tunes

# まとめ
今回は、chatGPT(gpt-3.5-turbo)をファインチューニングする方法を紹介しました。
ファインチューニングで色々なことができるようになると思うと、ワクワクしますね！
また、今後は追いファインチューニングもできるようになる予定なので、楽しみですね！
意外と簡単にファインチューニングできて、さらに安価なので、皆さんもぜひ試してみてください！

ご指摘やご質問などありましたら、コメント欄 or [X](https://twitter.com/nano_sudo)までお願いします！