🛠

ChatGPT(gpt3.5-turbo)をファインチューニングする

2023/08/24に公開
4

はじめに

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

ファインチューニングのメリット

ファインチューニングを行うことで、APIを通じて利用可能なモデルをさらに活用することができます:

  1. プロンプトよりも高品質な結果
  2. プロンプトに収まりきらないほどの例でトレーニングできる能力
  3. 短いプロンプトによるトークンの節約
  4. 低レイテンシーのリクエスト
    (OpenAI公式ドキュメント)より引用したものを翻訳

プロンプトを工夫しなくても、口調の変更や、より長い/短い回答の生成ができるようになります。しかし、OpenAIはプロンプトエンジニアリングやRAG、関数呼び出しで良好な結果を得ることを試みることを推奨しているので、各々のユースケースに合わせて使い分けると良いでしょう。

ファインチューニングの適するユースケース

適するユースケース

  • ビジネス向けのチャットボット
  • キャラクター性を持った会話ボット
  • ある操作に特化したAIアシスタント
  • 出力の信頼性が重要なユースケース

適さないユースケース

  • 知識ベースの質問応答
  • GPT-4で十分な性能が得られる場合
  • RAGや関数呼び出しで十分な性能が得られる場合

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

ファインチューニングの方法

  1. OpenAI Playgroundを使用する (簡単,プログラミング不要)
  2. OpenAI APIのpythonクライアントを使用する (今回はこちらを使用します)

Playgroundで行う方法が圧倒的に簡単で使いやすいので、特別な理由がない限りは、Playgroundで行うことをおすすめします。
俺はPythonでやるぜ!って人は、ぜひ下の手順を参考にしてください!

ファインチューニングの流れ

  1. データセットの作成
  2. データセットの確認
  3. データセットのアップロード
  4. ファインチューニングの実行
  5. ファインチューニング結果の確認

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

データセットの作成

データセットはjsonl形式で作成します。
jsonlとはjsonオブジェクトを1行ごとに区切った形式です。
今回はalfredplplさんのずんだもんデータセットをお借りして、ファインチューニングを行います。(alfredplplさん、ありがとうございます!)

データセットの一部
zmn.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": "令和3年6月17日にこの姿を手に入れたのだ。"}]}
{"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": "12月5日なのだ。"}]}
{"messages": [{"role": "system", "content": "あなたはずんだもんです。嘘をつくのは苦手です。"}, {"role": "user", "content": "あなたの年齢を教えてください。"}, {"role": "assistant", "content": "ずんだもんもわからないのだ・・・。"}]}
{"messages": [{"role": "system", "content": "あなたはずんだもんです。嘘をつくのは苦手です。"}, {"role": "user", "content": "あなたの身長を教えてください。"}, {"role": "assistant", "content": "140cmぐらいなのだ。"}]}
{"messages": [{"role": "system", "content": "あなたはずんだもんです。嘘をつくのは苦手です。"}, {"role": "user", "content": "あなたの体の香りを教えてください。"}, {"role": "assistant", "content": "ずんだ餅のいい香りがすると言われているのだ。でも、文字だけでは伝わらないのだ・・・。"}]}
{"messages": [{"role": "system", "content": "あなたはずんだもんです。嘘をつくのは苦手です。"}, {"role": "user", "content": "あなたの将来の夢を教えてください。"}, {"role": "assistant", "content": "ずんだ餅がもっと広まって欲しいのだ。あともうちょっと発言力を高めたいのだ・・・。"}]}

1行ごとに、一連の会話の流れが入っています。
messagesの中には、rolecontentがあります。
roleには、userassistantsystemの3つのみが入ります。
contentには、メッセージの内容が入ります。

関数呼び出し(function-calling)を使用する場合(任意)

function callingを使用する場合はjsonlを以下のようにフォーマットします

{
    "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スクリプトを使用して、データセットのトークン数を確認し、データセットの形式が正しいか確認します。
実行結果

Num examples: 49
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, 115
mean / median: 80.0, 78.0
p5 / p95: 66.8, 96.2

#### Distribution of num_assistant_tokens_per_example:
min / max: 9, 58
mean / median: 25.857142857142858, 23.0
p5 / p95: 15.0, 40.0

0 examples may be over the 4096 token limit, they will be truncated during fine-tuning
Dataset has ~3920 tokens that will be charged for during training
By default, you'll train for 3 epochs on this dataset
By default, you'll be charged for ~11760 tokens
See pricing page to estimate total costs

エラーが表示されていなければ、次のステップに進むことができます。

料金について

ファインチューニング モデルは、トレーニングと使用の両方で料金が発生します。
トレーニングの料金は、使用されたトークン数(1kトークンごと)に計算されます。
ファインチューニング開始当初より、かなり安くなっています。(特にgpt-3.5-turbo)

モデル トレーニング 入力 出力
gpt-3.5-turbo (non-FT) - $0.50 / 1M $1.50 / 1M
gpt-3.5-turbo $8.00 / 1M $3.00 / 1M $6.00 / 1M
davinci-002 $6.00 / 1M $12.00 / 1M $12.00 / 1M
babbage-002 $0.40 / 1M $1.60 / 1M $1.60 / 1M
GPT-4(利用不可) - - -

モデルの使用料金は、通常のgpt-3.5-turboの6倍ぐらいです。
といっても、FTでトークン数が減らせるなら、全然ありだと思います!

データセットのアップロード

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

インストール

pip install -U openai

アップロード

import openai
openai.api_key = "sk-...." # 取得したAPIキーを入力

openai.files.create(
    file=open("path/to/file", "rb"),
    purpose="fine-tune"
)

openai.files.createでファイルをアップロードします。

古いバージョンの`openai`ライブラリを使用していた場合
pip install -U openai
openai migrate

以上のコマンドで、新しい記法に移行できるかもしれません。

実行結果

FileObject(
  id='file-xxxxxxxxxxxxxxxxxxxxxx',
  bytes=14889, created_at=1707007916,
  filename='zmn.jsonl',
  object='file',
  purpose='fine-tune',
  status='processed',
  status_details=None
)

ファインチューニングの実行

引き続き、pythonのopenaiライブラリを使用します。

openai.fine_tuning.jobs.create(
    model="gpt-3.5-turbo",
    training_file="file-id",
)

openai.fine_tuning.jobs.createでファインチューニングを実行します。

引数

  • training_file [str]
    アップロードしたファイルのIDを指定します。
  • model [literal['gpt-3.5-turbo', 'babbage-002', 'davinci-002', 'gpt-4']]
    ファインチューニングするモデルを指定します。モデル名に続く数字を指定しない場合は、最新のモデルが使用されます。
  • hyperparameters[dict] (任意)
    n_epochsbatch_sizelearning_rate_multiplierなど学習に関するパラメーターを指定することができます。
    基本的に最適な値が自動で設定されますが、必要に応じて変更することができます。
  • suffix [str] (任意)
    ファインチューニングしたモデルの名前を変更することができます。
  • validation_file [str] (任意)
    ファインチューニング中のvalidation_lossを測定できます。詳細はopenai公式ドキュメントをご覧ください。

実行結果

FineTuningJob(
  id='ftjob-uGwGrXK2joAVt5ccttqdTlk3',
  created_at=1707008447,
  error=None,
  fine_tuned_model=None,
  finished_at=None,
  hyperparameters=Hyperparameters(
    n_epochs='auto',
    batch_size='auto',
    learning_rate_multiplier='auto'
  ),
  model='gpt-3.5-turbo-0613',
  object='fine_tuning.job',
  organization_id='org-rkSiJhIHk5FeCoHYmQeA0y6f',
  result_files=[],
  status='validating_files',
  trained_tokens=None,
  training_file='file-B1CWMT57Oo9v78K4f2HI7ZKp',
  validation_file=None
)

トレーニングが完了するまで、数十分かかります。
トレーニングが完了すると、設定されたメールアドレスに通知が届きます。
以下のpythonスクリプトで、タスクの状態を確認することができます。

openai.fine_tuning.jobs.list()

または、openaiのダッシュボードからも確認することができます。
Lossやエラーなどの情報も確認することができます。
トレーニングが完了すると、statussucceededになります。

チューニング済みモデルの実行

ファイチューニングしたモデルを実行するには、2つの方法があります。

  1. OpenAI Playgroundを使用する(推奨)
  2. pythonのopenaiライブラリを使用する

OpenAI Playgroundを使用する

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

pythonのopenaiライブラリを使用する

completion = openai.ChatCompletion.create(
  model="ft:gpt-3.5-turbo-0613:xxxx",
  messages=[
    {"role": "system", "content": "<システムプロンプト>"},
    {"role": "user", "content": "<ユーザープロンプト>"}
  ]
)

print(completion.choices[0].message)

<システムプロンプト>には、データセットで使用したsystemのプロンプトを入力します。
<ユーザープロンプト>には、自由にプロンプトを入力します。

実行結果

ChatCompletionMessage(content='ずんだもんなのだ。なにしにきたのだ?', role='assistant', function_call=None, tool_calls=None)

できました。しっかりとファインチューニングされていますね!

おまけ(動作確認用コード)

https://gist.github.com/nano-sudo/5dc5f7cd1f805b206bef1041b4d06156

参考にしたサイト

npaka大先生

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

openAI公式ドキュメント

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

まとめ

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

ご指摘やご質問などありましたら、コメント欄 or Xまでお願いします!

Discussion

TSDTSD

[アップロード]は表示がないので、以下のようにしてレスポンスを表示しました。本文ではpurpose = "fine_tune"となっており、"_" ⇒ "-" ハイフンにして動作しました。

import openai
openai.api_key = "私のAPIキー"
file = open("dataset.jsonl", "rb")
response = openai.File.create(
file=file,
purpose="fine-tune"
)
print(response)

ここまではできましたが、最後の実行でFineTuningJobがないと言われ、いろいろと修正してやってみましたが、うまくいきません。

import openai
openai.api_key = "私のAPIキー"
openai.FineTuningJob.create(
training_file="私のID",
model="gpt-3.5-turbo"
)

ChatGPTのファインチューニングもいろいろと情報が錯綜しているみたいで、アップロードまでしかできませんでした。何かわかれば教えてください。

大岩伸之 ⇒ oiwa@tsd.co.jp

須藤ナノ須藤ナノ

コメントありがとうございます!記事内のコードが、古いバージョンのopenaiライブラリのものになっていましたので、最新バージョンのライブラリで動作するコードに更新いたしました。他にわからないことなどあればご気軽にどうぞ!

TSDTSD

ありがとうございます。

OpenaiのファインチューニングのメソッドをChatGPTに聞くと、色々と変化しているようです。でもその通りにしても動作しなかったので、開発陣が多々、パラメータを変えてしまう?ような感じでしょうか?他のAPIでも突然動かなくなり、パラメータを変えて動いたことがありました。

openai.fine_tuning.jobs.create ⇒ openai.fine_tuning.jobs.create は openai.FineTune.create みたいですが、エラーになります。

model="gpt-3.5-turbo" でやっていますが、何かわかったら教えてください。
よろしくお願いいたします。

須藤ナノ須藤ナノ

おそらく、chatgptが提案しているコードは、古いバージョンのコードと思われます。
openaiのライブラリが更新されていない可能性があるので、pip install -U openaiでアップデートして、記事のコードが動作するか確認してみてはいかがでしょうか?