見出し画像

OpenAI API の ファインチューニングガイド

以下の記事が面白かったので、かるくまとめました。

Fine-tuning - OpenAI API

 前回


1. ファインチューニングの利点

ファインチューニングの利点は、次のとおりです。

(1) プロンプトよりも高品質な応答
(2) プロンプトに収まりきらないより多くの例の適用
(3) プロンプトの短縮によるトークン数 (コスト) の節約
(4) プロンプトの短縮による処理時間の短縮

モデルは膨大な量のテキストで事前学習されており、このモデルを効果的に利用するため、プロンプトに手順や応答の例を指定する手法が使われます。この例を使用してタスクの実行方法を示すことを「Few-Shot」と呼びます。

ファインチューニングで、プロンプトに収まりきらないより多くの例で学習することにより、さまざまなタスクでより良い結果を達成できるようになります。プロンプトに多くの例を指定する必要はなくなります。これによりトークン (コスト) が節約され、処理時間も短縮されます。

2. ファインチューニングの使用料金

ファインチューニングの使用料金は、以下のページで参照できます。

3. ファインチューニングできるモデル

現在、ファインチューニングできるモデルは、次の3種類です。

・gpt-3.5-turbo-0613 (推奨)
・babbage-002
・davinci-002

gpt-3.5-turbo」が推奨されます。「babbage-002」と「davinci-002」は過去のファインチューニングモデルから移行のため用意されてます。「GPT-4」は今年後半の予定です。

5. ファインチューニングを使用すべきか

ファインチューニングはモデルを改善できますが、時間と労力がかかります。まずは、「プロンプトエンジニアリング」、「プロンプトチェーン」 (複雑なタスクを複数のプロンプトに分割)、「Function Calling」を使用することををお勧めします

主な理由は次のとおりです。

・うまく機能していないように見えるタスクでも、より適切なプロンプトを使用することで、はるかに優れた結果が得られ、ファインチューニングする必要なくなる場合があります。

・プロンプトやその他の手法の方が、ファインチューニングよりもはるかに高速にフィードバックループすることができます。

・それでもファインチューニングが必要な場合でも、最初のプロンプトエンジニアリングの作業は無駄にはなりません。通常、ファインチューニングしたモデルで優れたプロンプトを使用すると、最良の結果が得られます。

GPTベストプラクティスガイド」では、ファインチューニングせずにパフォーマンスを向上させるための、効果的な戦略・戦術を解説しています。

6. ファインチューニングの一般的なユースケース

ファインチューニングの一般的なユースケースは、次のとおりです。

・伝えるのではなく見せる方が簡単な場合

・文体・口調・書式、またはその他の定性的側面の設定。
・目的の出力を生成する際の信頼性の向上。
・複雑なプロンプトに従わない場合の修正。
・多くの特殊なケースを特定の方法で処理。
・プロンプトで明確に説明するのが難しい新しいスキルやタスクの実行。

・コストや遅延を削減したい場合
品質を犠牲にすることなく、GPT-4の置き換えや、より短いプロンプトの利用で、コストや遅延を削減できます。

7. ファインチューニングの手順

ファインチューニングの手順は、次のとおりです。

(1) 学習データの準備
(2) 学習データのアップロード
(3) ファインチューニングの実行
(4) ファインチューニングしたモデルの使用

7-1. 学習データの準備

gpt-3.5-turbo」を学習データの例は、次のとおりです。

{"messages": [{"role": "system", "content": "Marvは事実に基づいたチャットボットで、皮肉も言います。"}, {"role": "user", "content": "フランスの首都は?"}, {"role": "assistant", "content": "パリ、まるでみんながもう知らないかのように"}]}
{"messages": [{"role": "system", "content": "Marvは事実に基づいたチャットボットで、皮肉も言います。"}, {"role": "user", "content": "「ロミオとジュリエット」を書いたのは誰?"}, {"role": "assistant", "content": "ああ、ウィリアム・シェイクスピアという人だよ。聞いたことある?"}]}
{"messages": [{"role": "system", "content": "Marvは事実に基づいたチャットボットで、皮肉も言います。"}, {"role": "user", "content": "地球から月までの距離は?"}, {"role": "assistant", "content": "約384,400キロメートルです。多少の誤差はあります。"}]}

babbage-002」「davinci-002」の学習データの例は、次のとおりです。

{"prompt": "<prompt text>", "completion": "<ideal generated text>"}
{"prompt": "<prompt text>", "completion": "<ideal generated text>"}
{"prompt": "<prompt text>", "completion": "<ideal generated text>"}

ファインチューニング前に、最適と判断した一連の会話履歴を収集し、それらをすべての学習データに含めることをお勧めします。学習データが少ない (100 未満) 場合に、最良かつ最も一般的な結果を得ることができます。

また、50の学習データから開始し、ファインチューニング後にモデルに改善の兆候が見られるかどうかを確認することをお勧めします。学習データをスケールする前に、タスクの設定やデータの再構築を再考すべき場合があります。

各学習データは、4096トークン に制限されています。各ファイルは 50MB に制限されています。

ファインチューニングの実行前に、学習データの書式を確認することも重要です。これを行うために、潜在的エラーの検出、トークン数、コストの見積もりに使用できる簡単な Python スクリプトが提供されています。

・Data formatting script

# We start by importing the required packages

import json
import os
import tiktoken
import numpy as np
from collections import defaultdict

# Next, we specify the data path and open the JSONL file

data_path = "<YOUR_JSON_FILE_HERE>"

# Load dataset
with open(data_path) as f:
    dataset = [json.loads(line) for line in f]

# We can inspect the data quickly by checking the number of examples and the first item

# Initial dataset stats
print("Num examples:", len(dataset))
print("First example:")
for message in dataset[0]["messages"]:
    print(message)

# Now that we have a sense of the data, we need to go through all the different examples and check to make sure the formatting is correct and matches the Chat completions message structure

# Format error checks
format_errors = defaultdict(int)

for ex in dataset:
    if not isinstance(ex, dict):
        format_errors["data_type"] += 1
        continue

    messages = ex.get("messages", None)
    if not messages:
        format_errors["missing_messages_list"] += 1
        continue

    for message in messages:
        if "role" not in message or "content" not in message:
            format_errors["message_missing_key"] += 1

        if any(k not in ("role", "content", "name") for k in message):
            format_errors["message_unrecognized_key"] += 1

        if message.get("role", None) not in ("system", "user", "assistant"):
            format_errors["unrecognized_role"] += 1

        content = message.get("content", None)
        if not content or not isinstance(content, str):
            format_errors["missing_content"] += 1

    if not any(message.get("role", None) == "assistant" for message in messages):
        format_errors["example_missing_assistant_message"] += 1

if format_errors:
    print("Found errors:")
    for k, v in format_errors.items():
        print(f"{k}: {v}")
else:
    print("No errors found")

# Beyond the structure of the message, we also need to ensure that the length does not exceed the 4096 token limit.

# Token counting functions
encoding = tiktoken.get_encoding("cl100k_base")

# not exact!
# simplified from https://github.com/openai/openai-cookbook/blob/main/examples/How_to_count_tokens_with_tiktoken.ipynb
def num_tokens_from_messages(messages, tokens_per_message=3, tokens_per_name=1):
    num_tokens = 0
    for message in messages:
        num_tokens += tokens_per_message
        for key, value in message.items():
            num_tokens += len(encoding.encode(value))
            if key == "name":
                num_tokens += tokens_per_name
    num_tokens += 3
    return num_tokens

def num_assistant_tokens_from_messages(messages):
    num_tokens = 0
    for message in messages:
        if message["role"] == "assistant":
            num_tokens += len(encoding.encode(message["content"]))
    return num_tokens

def print_distribution(values, name):
    print(f"\n#### Distribution of {name}:")
    print(f"min / max: {min(values)}, {max(values)}")
    print(f"mean / median: {np.mean(values)}, {np.median(values)}")
    print(f"p5 / p95: {np.quantile(values, 0.1)}, {np.quantile(values, 0.9)}")

# Last, we can look at the results of the different formatting operations before proceeding with creating a fine-tuning job:

# Warnings and tokens counts
n_missing_system = 0
n_missing_user = 0
n_messages = []
convo_lens = []
assistant_message_lens = []

for ex in dataset:
    messages = ex["messages"]
    if not any(message["role"] == "system" for message in messages):
        n_missing_system += 1
    if not any(message["role"] == "user" for message in messages):
        n_missing_user += 1
    n_messages.append(len(messages))
    convo_lens.append(num_tokens_from_messages(messages))
    assistant_message_lens.append(num_assistant_tokens_from_messages(messages))

print("Num examples missing system message:", n_missing_system)
print("Num examples missing user message:", n_missing_user)
print_distribution(n_messages, "num_messages_per_example")
print_distribution(convo_lens, "num_total_tokens_per_example")
print_distribution(assistant_message_lens, "num_assistant_tokens_per_example")
n_too_long = sum(l > 4096 for l in convo_lens)
print(f"\n{n_too_long} examples may be over the 4096 token limit, they will be truncated during fine-tuning")

# Pricing and default n_epochs estimate
MAX_TOKENS_PER_EXAMPLE = 4096

MIN_TARGET_EXAMPLES = 100
MAX_TARGET_EXAMPLES = 25000
TARGET_EPOCHS = 3
MIN_EPOCHS = 1
MAX_EPOCHS = 25

n_epochs = TARGET_EPOCHS
n_train_examples = len(dataset)
if n_train_examples * TARGET_EPOCHS < MIN_TARGET_EXAMPLES:
    n_epochs = min(MAX_EPOCHS, MIN_TARGET_EXAMPLES // n_train_examples)
elif n_train_examples * TARGET_EPOCHS > MAX_TARGET_EXAMPLES:
    n_epochs = max(MIN_EPOCHS, MAX_TARGET_EXAMPLES // n_train_examples)

n_billing_tokens_in_dataset = sum(min(MAX_TOKENS_PER_EXAMPLE, length) for length in convo_lens)
print(f"Dataset has ~{n_billing_tokens_in_dataset} tokens that will be charged for during training")
print(f"By default, you'll train for {n_epochs} epochs on this dataset")
print(f"By default, you'll be charged for ~{n_epochs * n_billing_tokens_in_dataset} tokens")
print("See pricing page to estimate total costs")

7-2. 学習データのアップロード

学習データのファイルをアップロードします。

openai.File.create(
  file=open("mydata.jsonl", "rb"),
  purpose='fine-tune'
)

7-3. ファインチューニングの実行

ファインチューニングを実行します。

import os
import openai
openai.api_key = os.getenv("OPENAI_API_KEY")
openai.FineTuningJob.create(training_file="file-abc123", model="gpt-3.5-turbo")

ファインチューニングを開始した後、完了するまでに時間がかかります。モデルの学習が完了すると、ユーザーに確認メールが届きます。

ファインチューニングの実行に加えて、既存のジョブのリスト表示、状態表示、キャンセルなども行うことができます。

import openai

# ファインチューニングのジョブのリスト表示
openai.FineTuningJob.list(limit=10)

# ファインチューニングのジョブの状態表示
openai.FineTuningJob.retrieve("ft-abc123")

# ファインチューニングのジョブのキャンセル
openai.FineTuningJob.cancel("ft-abc123")

# ファインチューニングのジョブのイベントのリスト表示
openai.FineTuningJob.list_events(id="ft-abc123", limit=10)

# ファインチューニングしたモデルの削除
openai.Model.delete("ft-abc123")

7-5. ファインチューニングしたモデルの使用

ファインチューニングしたモデルを使用します。

import os
import openai
openai.api_key = os.getenv("OPENAI_API_KEY")

completion = openai.ChatCompletion.create(
  model="ft:gpt-3.5-turbo:my-org:custom_suffix:id",
  messages=[
    {"role": "system", "content": "You are a helpful assistant."},
    {"role": "user", "content": "Hello!"}
  ]
)

print(completion.choices[0].message)

8. ファインチューニングしたモデルの分析

学習過程で計算された次のメトリクスを提供します。

・学習損失
・学習トークンの精度
・テスト損失
・テストトークンの精度

これらの統計は、学習がスムーズに行われたかどうかの健全性チェックを提供します (損失が減少し、トークンの精度が向上する)。

ただし、ファインチューニングしたモデルを使用して評価することが、モデルの品質について最も適切な感覚を提供します。サンプルを並べて比較することをお勧めします。手動評価に時間がかかりすぎる場合は、EvalsライブラリでGPT-4 を使用して評価することを検討してください。

9. ファインチューニングしたモデルの調整

9-1. 学習データ品質の調整

ファインチューニングしたモデルの結果が期待したほど良くない場合は、次の方法で学習データを調整することを検討してください。

・残りの問題を対象とした例を収集
モデルが特定の側面で性能が不十分な場合は、これらの側面を正しく回答する方法をモデルに直接示す学習データを追加します。

・既存の例で問題がないか精査
文法やロジックやスタイルに問題がある場合は、学習データに同じ問題があるかどうかを確認してください。

・データのバランスと多様性を考慮
学習データ内の60%が「答えられません」と回答しているのに対し、実運用時には「答えられません」と回答する状況が5%しか発生しない場合、過剰に「答えられません」と回答してしまう可能性があります。

・学習データに応答に必要なすべての情報が含まれていることを確認
例えば、モデルにユーザーの特徴に基づいてほめること (「勉強がんばってて偉いね」など) を学習させたい場合、学習データに対象がその特徴を持っている情報 (「塾に毎日通ってます」など) を含めないと、ハルシネーションになってしまう可能性があります。

・学習データの一致 / 一貫性を確認
複数の人が学習データを作成した場合、モデルのパフォーマンスは人々間の合意/一貫性のレベルによって制限される可能性があります。たとえば、テキスト抽出タスクで、人々が抽出されたスニペットの 70% にしか同意しなかった場合、モデルはこれ以上のパフォーマンスを発揮できない可能性があります。

・学習データが推論に期待されるのと同じ形式であることを確認

9-2. データ量の調整

学習データの品質に満足したら、学習データの数を増やすことを検討できます。 これは、特に考えられる「エッジケース」に関して、モデルがタスクをよりよく学習するのに役立つ傾向があります。学習データの数が2倍になるたびに、同様の量の改善が期待されます。

次の手順で、学習データサイズの増加によって期待される品質の向上を大まかに見積もることができます。

(1) 現在のデータセットをファインチューニング
(2) 現在のデータセットの半分をファインチューニング
(3) 両者の品質の差を観察

一般に、トレードオフを考慮する必要がある場合、大量の低品質データよりも少量の高品質データの方が効果的です。

9-3. ハイパーパラメータの調整

モデルをファインチューニングするエポック数を指定できます。最初はエポック数を指定せずに学習することをお勧めします。

モデルが期待したほど学習データに従っていない場合は、数値を1または2エポックずつ増やします。これは、単一の理想的な回答が存在するタスク (分類、エンティティ抽出、構造化解析など) でより一般的です。

モデルの多様性が予想よりも低くなった場合は、数値を1または2エポックずつ減らします。これは、適切な応答が広範囲にあるタスクでより一般的です。

10. FAQ

・ファインチューニングとRAGはどのように使い分ける必要がありますか?
RAGは、関連するコンテキストと情報を含むドキュメントの大規模なデータベースが必要な場合に最適です。

デフォルトでは、OpenAIのモデルは、有用なジェネラリストアシスタントになるように学習されています。ファインチューニングを使用すると、狭い範囲に焦点を当て、特定の深く根付いた動作パターンを示すモデルを作成できます。RAGを使用すると、応答を生成する前に関連するコンテキストをモデルに提供することで、新しい情報をモデルで利用できるようになります。 RAGはファインチューニングに代わるものではなく、実際にはファインチューニングを補完するものになります。

・GPT-4 と GPT-3.5-Turbo-16k がファインチューニングできるようになるのはいつですか?
今年後半にリリースする予定です。

・ファインチューニングしたモデルが実際にベースモデルよりも優れているかどうかを確認するにはどうすればよいですか?
チャット会話のテストセットでベースモデルとファインチューニングモデルの両方のサンプルを生成し、サンプルを並べて比較することをお勧めします。より包括的な評価を行うには、OpenAI Evalsを使用して、ユースケースに固有の評価を作成することを検討してください。

・ファインチューニングしたモデルを引き続きファインチューニングできますか?
いいえ、現時点では、ジョブ終了後のファインチューイングの継続はサポートされていません。 近い将来、これをサポートする予定です。

・ファインチューニングのジョブは一度にいくつ実行できますか?
制限に関する最新情報については、「レート制限ガイド」を参照してください。



この記事が気に入ったらサポートをしてみませんか?