OpenAI の Fine-tuning を試してみる

今更ながら OpenAI Fine-tuning を試してみる。
モチベ
技術的なトラブルシューディングを QA bot に任せて省力化したい。
最初は RAG 構成でうまくいくか検証してみたのだが retrieval で精度が落ちる問題があり限界を感じたので、Fine-tuning を組み合わせることで精度を上げられないかを検証したい。
先に成果物
サクッと Fine-tuning を試す用のプロジェクトを作った。
OpenAI 公式の Cookbook に掲載されている How to fine-tune chat models をベースに Fine-tuning 用のコードとサンプルデータセットを含めたもの。
このスクラップでやってみること
OpenAI Cookbook - How to fine-tune chat models を読んで Fine-tuning の基本的なやり方を身につける。

取り掛かる前にどういう経緯でこの発想になったか整理。
トラブルシューティングをAIに行わせる方法の検討
「トラブルシューティングをAIに行わせる」=「LLM にドメイン知識を教えて回答を行わせる」
LLM にトラブルシューティングについてのドメイン知識を教えて回答を行わせる手法には以下がある。
- In-context learning: プロンプトを使ってドメイン知識を与え、それを元に回答させる。
- RAG: FAQ のデータをベクタDBなどに予め格納しておき、ドメイン知識を検索させて質問に回答させる。
- Fine-tuning: ドメイン知識に特化した追加学習モデルによって回答を行う。
実際には上記の手法を組み合わせて使用することになるケースが多いはず。それぞれの手法は独立したものであるため、自由に組み合わせて使うことができる。
比較
各種法の特徴は大雑把に言って以下か?
- In-context learning は手軽に作れて十分な精度を持っているが、覚えられる知識量が少ない
- RAG は開発コストは3社の中間程度で覚えられる知識量が非常に多いが、精度が上がりにくい
- Fine-tuning は覚えられる知識量は多いはずだが、精度評価が難しい
Fine-tuning に期待すること
- Fine-tuning を使って、比較的多くのドメイン知識を必要とするようなタスクの実行を行いたい。
- RAG ではなく Fine-tuning でこの課題にアプローチする理由は以下。
- RAG では Retrieve の段階でベクトル検索を使うケースが多いが、検索精度が悪くなりがちであると感じたため。
- Fine-tuning ではモデルを更新するので、概念を学習できる点がRAGと異なる。このため Fine-tuning は既存のFAQにない質問に答える力を持ちうる可能性があるのではないかと考えている。これができると嬉しい。
WIP: それぞれの手法の特徴を比較
手法 | ドメイン知識のデータソース | 覚えられる知識量 | 精度 | 開発コスト | 精度評価の難度 |
---|---|---|---|---|---|
In-context learning | プロンプト | △ | ⚪︎ | ◎ | ◎ |
RAG | ベクターDB[1] | ◎ | △ | ⚪︎ | ⚪︎ |
Fine-tuning | 追加学習モデル | ◎ | ? | ? | △ |
-
一般にベクターDBが使われることが多い印象だが、他にもWeb検索を使うケースや外部DBを使うケースなど色々なケースがあるはず。 ↩︎

ハマった: サンプルデータセットの入手方法がわからない
https://cookbook.openai.com/examples/how_to_finetune_chat_models#setup のサンプルデータセットはどこからDLできる?
解決策
https://recipenlg.cs.put.poznan.pl/ からDL -> 先頭の1001行だけを残して保存すればOK。
サンプルデータセットの詳細は https://github.com/Glorf/recipenlg より確認できる。

ハマった: Training を行おうとするとクォータ制限に引っかかる
起きていること
client.fine_tuning.jobs.create
を実行すると以下のエラーが表示される
You exceeded your current quota, please check your plan and billing details.
これまでの状況
API Key も払い出しているし、12月には Chat Compilation 系の API も問題なく動かせていたはず。クォータに引っかかるような使い方をした心当たりもない。ちょっと調べないといけないかも。
解決策
Billing settings を開いて Credit balance の残高が0になっていることが原因だった。 Credit balance を購入して数分待てば解決する。金額は最低の $5 でOK。
以下詳細な状況と調査ログ。
調査
-
Billing settings を開くと Credit balance の残高が $0 になっている
- 関係ありそうかな?
- 試しに
$5
振り込んで、間髪入れずにclient.fine_tuning.jobs.create
を実行してみたが解決せず。
- 同じ問題に苦しんでいる人を発見
- https://note.com/mizupe/n/nb4004a14d70a
- この記事の方は Credit balance を購入したら治ったようだ。
- Credit balance の購入から5分ほど間をおいて実行したら普通に実行できた。解決。
- 2024/1 までは使った分だけ自動的に課金されていたので先払いは必要なかったはずだが、課金に失敗するケースがあるのかもしれない。課金体系についてはまた調査する。

Fine-tuning ができた!コードは以下。
以下で動作を確かめていく。

動作の検証
以下の条件で Fine-tuning を行なった。
パラメータ | 値 |
---|---|
training dataset | 10 |
validation dataset | 10 |
トークン数 | 13,536 |
epoch | 9 |
Input
で与えたレシピに含まれる材料を推定させてみる。
Title: Beef Brisket
Ingredients: ["4 lb. beef brisket", "1 c. catsup", "1 c. water", "1/2 onion, minced", "2 Tbsp. cider vinegar", "1 Tbsp. prepared horseradish", "1 Tbsp. prepared mustard", "1 tsp. salt", "1/2 tsp. pepper"]
Generic ingredients:
結果は次のようになった。
🖥️ System Prompt:
You are a helpful recipe assistant. You are to extract the generic ingredients from each of the recipes provided.
🙋Input:
Title: Beef Brisket
Ingredients: ["4 lb. beef brisket", "1 c. catsup", "1 c. water", "1/2 onion, minced", "2 Tbsp. cider vinegar", "1 Tbsp. prepared horseradish", "1 Tbsp. prepared mustard", "1 tsp. salt", "1/2 tsp. pepper"]
Generic ingredients:
🤖Answer:
["catsup", "water", "onion", "cider vinegar", "horseradish", "mustard", "salt", "pepper"]
beef brisket (肩ばら肉) が抜けているが、他の材料は全て列挙できている。良さそう。
その他:Fine-tuning 実行時の記録
所要時間: 240秒
金額: $0.11程度
Training loss, Validation loss の推移:
Training loss は低下しているのに Velidation loss は低下していないのは過学習の状態で、データ数が少なすぎるとこうなることがあるらしい。今回はデータは少ないと言って良さそうなので、Training data, Validation data をそれぞれ増やして、現在10データずつのところを100データずつにしてみる。

動作の検証2: データを増やして結果が変わるか確認する
↑と同じデータセットから10倍のデータを抽出して Fine-tuning を行ない、結果がどのように変わるかをみる。条件は以下。
パラメータ | 値 |
---|---|
training dataset | 100 |
validation dataset | 100 |
トークン数 | 133,490 |
epoch | 10 |
再度 Input
で与えたレシピに含まれる材料を推定させてみる。
Title: Beef Brisket
Ingredients: ["4 lb. beef brisket", "1 c. catsup", "1 c. water", "1/2 onion, minced", "2 Tbsp. cider vinegar", "1 Tbsp. prepared horseradish", "1 Tbsp. prepared mustard", "1 tsp. salt", "1/2 tsp. pepper"]
Generic ingredients:
結果は次のようになった。今度は beef brisket を含むすべての材料を列挙できるようになった。
🖥️ System Prompt:
You are a helpful recipe assistant. You are to extract the generic ingredients from each of the recipes provided.
🙋Input:
Title: Beef Brisket
Ingredients: ["4 lb. beef brisket", "1 c. catsup", "1 c. water", "1/2 onion, minced", "2 Tbsp. cider vinegar", "1 Tbsp. prepared horseradish", "1 Tbsp. prepared mustard", "1 tsp. salt", "1/2 tsp. pepper"]
Generic ingredients:
🤖Answer:
["beef brisket", "catsup", "water", "onion", "cider vinegar", "horseradish", "mustard", "salt", "pepper"]
その他:Fine-tuning 実行時の記録
所要時間: 35分1秒
金額: $1.0 程度
Training loss, Validation loss の推移:
過学習についてのメモ
500 step 付近までは Velidation loss は低下しているが、500 ~ 1000 step にかけてだんだん増加していく。どうやらこれも過学習を起こしているっぽい。epoch を半分の5ぐらいにしておくのがいいのかもしれない。(epochを増やすと時間もコストもかかるし)どのぐらいで学習を止めると良いのかははまた調べる。
所要時間についてのメモ
また、かかった時間は単純にデータ量を10倍にすると10倍程度になったことから、同じデータセットを使う限り、epoch数・トークン数に正比例しているのではないかと思われる。今回の結果を元に考えれば、英語のデータセットでは 1000 [トークン * epoch] あたり 1.57 秒 程度の時間がかかると見積もっておけば良いかもしれない。

今後調べること
自前のデータを用意して学習させたい。
以下の Cookbook が参考になりそうなので、時間のある時に読んで試してみる。
OpenAI Cookbook - Data preparation and analysis for chat model fine-tuning