✍️

ゼロから学びながらGPT-3.5をFine-tuningする

2024/04/06に公開

対象読者

  • Fine-tuningに興味があるけどやったことがない人
  • ChatGPT、OpenAIのAPIなどを触ったことがある人
  • Pythonが少しでも書ける人(基本はコピペでOK)

前提

本記事はOpenAIのダッシュボードは確認用として、Pythonでコードを書いてFine-tuningを行います。
OpenAIのドキュメントを参考にしています。
https://platform.openai.com/docs/guides/fine-tuning

※本記事ではjsonlファイルの表記を、可読性向上のためインデントを使用してjson同様のフォーマットしている部分があります。

Fine-tuningとは

GPTは事前学習済みのモデルですが、最新情報や非公開情報、専門知識は基本的には持っていません。そこでFine-tuningを実施することで、与えた知識をモデルに追加(文字通り微調整)することができます。

Fine-tuningできるモデル

2024年4月6日時点の情報です。最新情報はドキュメントをご確認ください。

  • gpt-3.5-turbo-0125(推奨)
  • gpt-3.5-turbo-1106
  • gpt-3.5-turbo-0613
  • babbage-002
  • davinci-002
  • gpt-4-0613

手順1: データセットを用意

データセットの形式はChat Completions APIと同じ形式のjsonlファイルとなります。
※チャット形式の場合

{
  "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."
    }
  ]
}
{...}
{...}

マルチターンチャットの例

チャット形式のデータには、複数のassistantロールのメッセージを含めることができます。デフォルトでは、1つの会話例の中の全てのassistantメッセージを学習に用いますが、特定のassistantメッセージを学習から除外したい場合は、weightキーを用いて制御できます。

weightの値は現在0か1のみ許容されており、0を指定すると、そのメッセージは学習から除外され、1を指定すると学習データに含めます。

{
  "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",
      "weight": 0
    },
    {
      "role": "user",
      "content": "Can you be more sarcastic?"
    },
    {
      "role": "assistant",
      "content": "Paris, as if everyone doesn't know that already.",
      "weight": 1
    }
  ]
}
{...}
{...}

weight:0を指定するくらいならデータセットから外せば?

と思ったので調べてみました。デフォルトでは全て学習するので特に意識しなくてもいいのですが、以下のユースケースでは役に立ちます。

  • 同じデータセットを使って、異なる設定で複数のモデルを学習させたい場合
  • データの前処理の段階で、学習に使うかどうかを完全に判断することが難しい場合
  • 学習済みモデルに追加の学習データを与える場合

学習用でここまでいじらない場合は必要ないかと思います。

Point1: データセットに入れるプロンプトはFine-tuning前に検証しておく

まずは最適と思われるプロンプトをFine-tuning前に試しておき、それに対してベストな回答を作成することが推奨されています。

また、コスト削減のために繰り返されるプロンプトを省略したくなりますが、それらを省略すると推論に影響が出る可能性があるため、省略させないで全て書く方法が推奨されています。

例:

あなたは親切で、知的で、ユーモアのセンスもある秘書です。ユーザーの質問に、簡潔かつ丁寧に答えてください。
今日の天気を教えて

この場合今日の天気を教えてだけを書きたくなりますが、その場合、モデルが事前の情報から推論することになるため、他のコンテキストに影響が出る可能性を示唆しています。

Point2: サンプルの数

データセットのサンプル数は少なくとも10個、通常50~100個が必要です。
ただし、適切なサンプル数はユースケースによって大きく異なります。

まずは50個のデータセットで検証し、モデルの改善動向を確認して判断することが推奨されています。

Point3: トレーニングとテストの分割

収集したデータセットを、トレーニング用とテスト用に分割することが推奨されています。トレーニング用とテスト用の両方のファイルを使ってfine-tuningジョブを実行すると、トレーニングの過程で両方の統計情報が提供されます。

これらの統計情報は、モデルの改善度合いを示す初期の指標となるため、テストセットを早期に構築しておくことは、トレーニング後のモデルの評価を確実に行うために有用です。

例えば、以下のようなデータセットがあるとします。(フォーマットは適当です)

1. 日本の首都はどこですか?,東京
2. アメリカの首都はどこですか?,ワシントンD.C.
3. フランスの首都はどこですか?,パリ
4. イギリスの首都はどこですか?,ロンドン
5. 中国の首都はどこですか?,北京
6. ドイツの首都はどこですか?,ベルリン
7. イタリアの首都はどこですか?,ローマ
8. カナダの首都はどこですか?,オタワ
9. オーストラリアの首都はどこですか?,キャンベラ
10. ブラジルの首都はどこですか?,ブラジリア

このデータセットを、トレーニング用とテスト用に8:2の割合で分割して評価します。

トレーニング用:
1. 日本の首都はどこですか?,東京
2. アメリカの首都はどこですか?,ワシントンD.C.
3. フランスの首都はどこですか?,パリ
4. イギリスの首都はどこですか?,ロンドン
5. 中国の首都はどこですか?,北京
6. ドイツの首都はどこですか?,ベルリン
7. イタリアの首都はどこですか?,ローマ
8. カナダの首都はどこですか?,オタワ

テスト用:
9. オーストラリアの首都はどこですか?,キャンベラ
10. ブラジルの首都はどこですか?,ブラジリア

トレーニング用のデータでfine-tuningを行い、テスト用のデータでモデルの性能を評価します。
1-8をFine-tuningで学習させた後、9,10の質問で結果を評価するというような手法です。

Point4: トークンの制限

トークンの制限は選択したモデルによって異なります。
各モデルのトークン数制限をオーバーした場合は末尾からトークンを削除されてしまうため注意が必要です。

Point5: コストの見積もり

トレーニング(Fine-tuning)には専用の料金プランが設定されています。
また、指定したエポックの数でも料金が変わります。

計算式

ファイル内のトークン * エポック数 = トレーニングトークンの合計

エポック数はデフォルトで4が設定されています。

エポックって何?

エポック数とは、「一つの訓練データを何回繰り返して学習させるか」の数のことです。

Deep Learningのようにパラメータの数が多いものになると、訓練データを何回も繰り返して学習させないとパラメータをうまく学習できないません(逆にやりすぎると過学習を起こすわけなんですが)。

多すぎずに少なすぎないエポック数を指定することによって、パラメーターをうまく学習させることができます。

以下の記事から引用
https://www.st-hakky-blog.com/entry/2017/01/17/165137

手順2: データセットを検証する

データセットが準備できたら、データセットのフォーマットやトークン数などを確認してコストを予測します。

以下の記事に従ってPythonで作成します。
https://cookbook.openai.com/examples/chat_finetuning_data_prep

Num examples: 5
...
No errors found
...
0 examples may be over the 4096 token limit, they will be truncated during fine-tuning
Dataset has ~243 tokens that will be charged for during training
By default, you'll train for 20 epochs on this dataset
By default, you'll be charged for ~4860 tokens

こんな結果が出ます。
5個のデータセットがあり、234token*20エポック = 最大4,860トークンが課金対象でフォーマットにエラーは無いということが確認できました。
(↑の記事で紹介されているデータセットにはエラーを出力するため無駄に長い文字列が入っているため、それを削除しました)

手順3: トレーニングファイルをアップロードする

※事前にOpenAIのAPIキーを取得しておいてください。
https://platform.openai.com/api-keys

.env
OPENAI_API_KEY=sk-xxxxxxxx

以下のコードでjsonlのデータセットをOpenAIにアップロードします。
OpenAIのダッシュボードからGUIでも可能ですが、今回はドキュメントに従ってコードで全て実行します。

from openai import OpenAI
client = OpenAI()

client.files.create(
  file=open("dataset.jsonl", "rb"),
  purpose="fine-tune"
)

注意1: ファイルサイズの上限

アップロードできるファイルの上限は1GBとなっています。
そもそも1GBを超えるデータのFine-tuningは推奨されていません。

注意2: データセットの数

データセットの数が10個未満の場合は次のFine-tuningでエラーが出るので、最低でも10個のデータセットを準備してください。

5個で試したらエラーが出ました。

Training file has 5 example(s), but must have at least 10 examples

アップロード完了

ファイルのアップロードが完了すると、ダッシュボードのStorageに入ります。

ちなみに、同じファイル名をアップロードすることも可能です。
アップロード順に並び、ファイルIDで区別できます。

手順4: Fine-tuningされたモデルを作成する

いよいよFine-tuningを行います。
アップロードしたファイルのファイルIDを取得し、以下のコードを実行します。
※ファイルIDはOpenAIのダッシュボードから確認できます。

from openai import OpenAI
client = OpenAI()

client.fine_tuning.jobs.create(
  training_file="file-abc123",  # ここはファイルID
  model="gpt-3.5-turbo"
)

正常にプログラムが終了すると、ダッシュボードで状況が確認できるようになります。
ジョブはシステム内の他のジョブの後ろにキューに入れられる可能性があり、モデルとデータセットのサイズに応じて数分〜数時間かかる場合があります。

もっと細かい設定をするには

エポック数やファイルバリデーション、Fine-tuning後のモデル名などを設定することができます。

詳しくはAPIドキュメントでご確認ください。

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

手順5: Fine-tuningされたモデルを使ってみる

処理が完了すると、ダッシュボードでモデルが確認できます。

作成されたモデル名を使用して、チャットをしてみましょう。

from openai import OpenAI
client = OpenAI()

completion = client.chat.completions.create(
  model="ft:gpt-3.5-turbo:my-org:custom_suffix:id", # ftから始まるFine-tuning後のモデル名
  messages=[
    {"role": "system", "content": "You are a helpful assistant."},
    {"role": "user", "content": "Hello!"}
  ]
)
print(completion.choices[0].message)

今回はここまでとなります!
続きは別の記事で紹介します。

Discussion