😎

Azure上でgpt-4o-miniをFine-tuningする

2025/02/03に公開

はじめに

  • OpenAI社のモデル同様Azure上のOpenAIモデルでもファインチューニング可能です。
  • このブログではファインチューニングの基礎や実装方法をご紹介します。
  • 公開されているサンプルをもとに解説していきます。
    https://cookbook.openai.com/examples/how_to_finetune_chat_models

GPT-4o-miniをファインチューニングするメリットは、以下のような点が挙げられます。

  1. 特定の用途に最適化できる
    企業の業務プロセスや特定の業界(金融、医療、法律など)に最適化することで、より精度の高い応答が可能になる。
    例えば、社内FAQの自動応答や特定の技術用語を多用するサポートチャットボットなどで有用。
  2. コスト削減
    ベースモデルをAPI経由で使う場合、長文の応答や高頻度のリクエストが増えるとコストがかさむ。
    ファインチューニングを行い、不要なトークンを削減したり、短くても適切な応答を生成できるようにすることで、API利用コストを最適化できる。
  3. 推論速度の向上
    特定のタスクに最適化することで、応答の品質を維持しつつ、より短いコンテキストや少ない計算量で結果を生成できるようになる。
    これはリアルタイムアプリケーション(カスタマーサポート、音声アシスタントなど)で特に重要。
  4. 一貫性の向上
    企業やプロジェクト固有のスタイルやトーンを学習させることで、一貫した出力が得られる。
    例えば、ブランドガイドラインに沿った文章の生成や、特定のフォーマットでのレポート作成などに活用可能。
  5. ドメイン固有の知識を強化
    ベースモデルは汎用的な知識を持つが、特定の分野(例:Azure、Kaggle、統計学など)のデータでファインチューニングすることで、より専門的な回答ができるようになる。
    例: Azureのリソース最適化に関する質問に対し、具体的な設定例や推奨構成を提供するAIを作成できる。
  6. データプライバシーの向上
    機密データを扱う際、クラウドのAPIに送信するリスクを軽減するために、社内データでファインチューニングしたローカルモデルを活用できる。
  7. プロンプトエンジニアリングの負担軽減
    ベースモデルを利用する場合、適切な出力を得るために詳細なプロンプト設計が必要になることが多い。
    ファインチューニングにより、シンプルな入力でも望ましい出力が得られるようになり、開発工数を削減できる。

ファインチューニングを考慮すべきケース vs. プロンプト最適化やRAGを検討するべきケース

ファインチューニングを考慮するきケース

特定の業務やドメインに特化した回答が必要

  • 例: 社内ナレッジベースの自動応答、医療診断サポート、法務アシスタント
  • → モデルが専門用語や業務フローを理解していることが求められる

大量のリクエストを処理する必要がある

  • 例: チャットボットやカスタマーサポート
  • → 毎回外部検索を行うより、ファインチューニング済みモデルを使用する方がコスト効率が良い

出力の一貫性を保ちたい

  • 例: ブランドトーンの統一、決まったフォーマットでのレポート作成
  • → RAGのみでは検索結果に依存しやすく、ばらつきが生じる可能性がある

プロンプトエンジニアリングを簡素化したい

  • 例: 長いプロンプトを考えずに適切な回答を得たい
  • → 「この契約書のリスクを教えて」のような短い入力でも適切な応答が必要な場合

APIコストを削減したい

  • 例: 企業内で多量のAPIリクエストを処理する場合
  • → RAGを利用すると毎回検索が必要になりコストが増加する可能性がある

ファインチューニングではなくプロンプト最適化やRAGを検討するべきケース

🚫 最新情報を反映したい

  • 例: 最新の法改正情報、クラウドサービスのアップデート情報
  • → ファインチューニング済みのモデルでは最新情報のキャッチアップが困難

🚫 幅広いタスクに対応させたい

  • 例: 一般的な質問応答や汎用的なタスク
  • → ファインチューニングすると特化しすぎて、一般的な質問に対応しづらくなることがある

🚫 十分な学習データがない

  • 例: ドメイン固有の知識を持たせたいが、高品質なデータセットが準備できない
  • → RAGを使えば、最新のデータを検索して補完可能

🚫 リアルタイムでのデータ更新が必要

  • 例: 在庫情報、株価、天気予報
  • → ファインチューニングでは静的なデータしか扱えないため、外部データを検索するRAGの方が適している

🚫 短期間での導入が求められる

  • 例: 新しい業務プロセスを導入したいが、すぐにAIを活用したい

  • → ファインチューニングはデータ準備やトレーニングに時間がかかるため、プロンプト最適化やRAGを活用した方が早い

  • GPT-4o-miniのファインチューニングは、特定の業務や用途に最適化し、精度向上やコスト削減に寄与する。

  • ただし、最新情報を扱う必要がある場合は、RAGを組み合わせる方が効果的なケースもある。適用する前に、業務要件とデータ量を考慮し、最適なアプローチを選ぶことが重要です。

モデルのファインチューニング

このノートブックでは、新しいGPT-4o miniファインチューニングのステップバイステップガイドを提供します。今回は、RecipeNLGデータセットを使用してエンティティ抽出を行います。このデータセットは、さまざまなレシピと、それぞれのレシピに対して抽出された一般的な食材のリストを提供しています。これは、名前付きエンティティ認識(NER)タスクに一般的に使用されるデータセットです。

注意:GPT-4o miniファインチューニングは、Tier 4および5の使用レベルの開発者に提供されています。 ファインチューニングダッシュボードにアクセスし、「作成」をクリックして、ベースモデルのドロップダウンメニューから「gpt-4o-mini-2024-07-18」を選択することで、GPT-4o miniのファインチューニングを開始できます。

以下のステップを順を追って進めていきます:

  1. セットアップ: データセットをロードし、ファインチューニングするために1つのドメインに絞り込みます。
  2. データ準備: 訓練と検証の例を作成し、Filesエンドポイントにアップロードすることによってデータをファインチューニング用に準備します。
  3. ファインチューニング: ファインチューニングしたモデルを作成します。
  4. 推論: ファインチューニングしたモデルを使用して新しい入力に対して推論を行います。

これを終えれば、gpt-4o-mini-2024-07-18モデルを訓練、評価、デプロイできるようになります。

ファインチューニングに関する詳細は、ドキュメントガイドを参照してください。

セットアップ

# 必ず OpenAI の Python パッケージの最新バージョンを使用してください。
!pip install --upgrade --quiet openai

https://learn.microsoft.com/ja-jp/azure/ai-studio/concepts/fine-tuning-overview#fine-tuning-azure-openai-models

  • gpt-4o-mini (2024-07-18)のファインチューニングは現状米国中北部、スウェーデン中部で展開されてます。
import json
import openai
from openai import AzureOpenAI
import os
import pandas as pd
from pprint import pprint

azure_endpoint = "https://xxxx.openai.azure.com/"
api_version = "2024-10-01-preview"
aoai_api_key = "xxxxx"
deployment_name = "gpt-4o-mini"#任意
client = AzureOpenAI(api_key=aoai_api_key, api_version=api_version, azure_endpoint=azure_endpoint)
  • ファインチューニングは、特定のドメインに焦点を当てると最も効果的です。データセットがモデルが学習できる程度に十分に絞り込まれていること、また、未見の例を見逃さないように一般的な範囲であることが重要です。
  • この点を考慮して、私たちはRecipesNLGデータセットからCookbooks.comのドキュメントのみを含むサブセットを抽出しました。
# このタスクで使用するデータセットを読み込みます。
# これは RecipesNLG データセットであり、www.cookbooks.com のドキュメントのみを含むようにクリーンアップしています。
recipe_df = pd.read_csv("data/cookbook_recipes_nlg_10k.csv")

recipe_df.head()

使用したデータ

データの準備

データの準備から始めます。ChatCompletionフォーマットでファインチューニングを行う際、各訓練例はシンプルなmessagesのリストです。例えば、次のようなエントリがあります:

[{'role': 'system', 'content': 'あなたは役立つレシピアシスタントです。提供された各レシピから一般的な食材を抽出する役割を担っています。'},

{'role': 'user', 'content': 'タイトル: ノーベイクナッツクッキー\n\n食材: ["1カップのしっかり詰めたブラウンシュガー", "1/2カップのエバポレートミルク", "1/2小さじのバニラ", "1/2カップの砕いたナッツ(ピーカン)", "2大さじのバターまたはマーガリン", "3 1/2カップの一口サイズに細切りにしたライスビスケット"]\n\n一般的な食材: '},

{'role': 'assistant', 'content': '["ブラウンシュガー", "ミルク", "バニラ", "ナッツ", "バター", "一口サイズに細切りにしたライスビスケット"]'}]

訓練プロセス中、この会話は分割され、最後のエントリがモデルが生成するcompletionとなり、残りのmessagesがプロンプトとして機能します。この点を考慮して訓練例を作成してください。もしモデルが複数ターンの会話に対応する場合、会話が広がり始めたときにモデルがうまく動作するよう、代表的な例を提供してください。

現在、各訓練例には4096トークンの制限があります。これを超える場合は、4096トークンで切り捨てられますのでご注意ください。

# システムメッセージを定義
# これはAIの動作を指示するもので、AIはレシピから一般的な材料を抽出するよう求められている

system_message = "You are a helpful recipe assistant. You are to extract the generic ingredients from each of the recipes provided."

# ユーザーからの入力メッセージを作成する関数
# 与えられたレシピ(row)からタイトルと材料を取得し、汎用材料(Generic ingredients:)を求める形式の文字列を生成する
def create_user_message(row):
    return f"Title: {row['title']}\n\nIngredients: {row['ingredients']}\n\nGeneric ingredients: "

# 例としての会話データを作成する関数
# OpenAI APIに渡す形式で会話を準備する
def prepare_example_conversation(row):
    return {
        "messages": [
            {"role": "system", "content": system_message},
            {"role": "user", "content": create_user_message(row)},
            {"role": "assistant", "content": row["NER"]},
        ]
    }

# 最初のレシピデータを取得し、会話データを作成して出力
pprint(prepare_example_conversation(recipe_df.iloc[0]))

出力例:
{'messages': [{'content': 'You are a helpful recipe assistant. You are to '
'extract the generic ingredients from each of the '
'recipes provided.',
'role': 'system'},
{'content': 'Title: No-Bake Nut Cookies\n'
'\n'
'Ingredients: ["1 c. firmly packed brown sugar", '
'"1/2 c. evaporated milk", "1/2 tsp. vanilla", "1/2 '
'c. broken nuts (pecans)", "2 Tbsp. butter or '
'margarine", "3 1/2 c. bite size shredded rice '
'biscuits"]\n'
'\n'
'Generic ingredients: ',
'role': 'user'},
{'content': '["brown sugar", "milk", "vanilla", "nuts", '
'"butter", "bite size shredded rice biscuits"]',
'role': 'assistant'}]}

  • これから、訓練データとして使用するデータセットのサブセットを作成します。最初は30-50のうまく選別された例から始めることができます。訓練セットのサイズを増やすと、パフォーマンスは線形にスケールするのが見られるはずですが、ジョブの実行時間も長くなります。
# データセットの最初の100行をトレーニング用のデータフレームとして選択
training_df = recipe_df.loc[0:100]

# `prepare_example_conversation` 関数を `training_df` の各行に適用し、
# それぞれのレシピを会話形式のデータに変換する
# `.tolist()` を使って、結果をリスト形式に変換
training_data = training_df.apply(prepare_example_conversation, axis=1).tolist()

for example in training_data[:5]:
    print(example)
  • 出力例
{'messages': [{'role': 'system', 'content': 'You are a helpful recipe assistant. You are to extract the generic ingredients from each of the recipes provided.'}, {'role': 'user', 'content': 'Title: No-Bake Nut Cookies\n\nIngredients: ["1 c. firmly packed brown sugar", "1/2 c. evaporated milk", "1/2 tsp. vanilla", "1/2 c. broken nuts (pecans)", "2 Tbsp. butter or margarine", "3 1/2 c. bite size shredded rice biscuits"]\n\nGeneric ingredients: '}, {'role': 'assistant', 'content': '["brown sugar", "milk", "vanilla", "nuts", "butter", "bite size shredded rice biscuits"]'}]}
{'messages': [{'role': 'system', 'content': 'You are a helpful recipe assistant. You are to extract the generic ingredients from each of the recipes provided.'}, {'role': 'user', 'content': 'Title: Jewell Ball\'S Chicken\n\nIngredients: ["1 small jar chipped beef, cut up", "4 boned chicken breasts", "1 can cream of mushroom soup", "1 carton sour cream"]\n\nGeneric ingredients: '}, {'role': 'assistant', 'content': '["beef", "chicken breasts", "cream of mushroom soup", "sour cream"]'}]}
{'messages': [{'role': 'system', 'content': 'You are a helpful recipe assistant. You are to extract the generic ingredients from each of the recipes provided.'}, {'role': 'user', 'content': 'Title: Creamy Corn\n\nIngredients: ["2 (16 oz.) pkg. frozen corn", "1 (8 oz.) pkg. cream cheese, cubed", "1/3 c. butter, cubed", "1/2 tsp. garlic powder", "1/2 tsp. salt", "1/4 tsp. pepper"]\n\nGeneric ingredients: '}, {'role': 'assistant', 'content': '["frozen corn", "cream cheese", "butter", "garlic powder", "salt", "pepper"]'}]}
{'messages': [{'role': 'system', 'content': 'You are a helpful recipe assistant. You are to extract the generic ingredients from each of the recipes provided.'}, {'role': 'user', 'content': 'Title: Chicken Funny\n\nIngredients: ["1 large whole chicken", "2 (10 1/2 oz.) cans chicken gravy", "1 (10 1/2 oz.) can cream of mushroom soup", "1 (6 oz.) box Stove Top stuffing", "4 oz. shredded cheese"]\n\nGeneric ingredients: '}, {'role': 'assistant', 'content': '["chicken", "chicken gravy", "cream of mushroom soup", "shredded cheese"]'}]}
{'messages': [{'role': 'system', 'content': 'You are a helpful recipe assistant. You are to extract the generic ingredients from each of the recipes provided.'}, {'role': 'user', 'content': 'Title: Reeses Cups(Candy)  \n\nIngredients: ["1 c. peanut butter", "3/4 c. graham cracker crumbs", "1 c. melted butter", "1 lb. (3 1/2 c.) powdered sugar", "1 large pkg. chocolate chips"]\n\nGeneric ingredients: '}, {'role': 'assistant', 'content': '["peanut butter", "graham cracker crumbs", "butter", "powdered sugar", "chocolate chips"]'}]}
  • トレーニングデータに加えて、オプションで バリデーションデータを提供することもできます。これにより、モデルがトレーニングセットに対して過学習しないように確認できます。
# データセットの101行目から200行目を検証用データとして選択
validation_df = recipe_df.loc[101:200]
# `prepare_example_conversation` 関数を `validation_df` の各行に適用し、
# それぞれのレシピを会話形式のデータに変換
# `.tolist()` を使って、結果をリスト形式に変換
validation_data = validation_df.apply(
    prepare_example_conversation, axis=1).tolist()
  • その後、データを .jsonl ファイルとして保存する必要があります。各行は1つのトレーニング例の会話となります。
# リスト形式のデータを JSONL(JSON Lines)形式でファイルに書き出す関数
# data_list: JSONデータのリスト
# filename: 出力するJSONLファイルの名前
def write_jsonl(data_list: list, filename: str) -> None:
    with open(filename, "w") as out:
        for ddict in data_list:
            jout = json.dumps(ddict) + "\n"
            out.write(jout)
training_file_name = "tmp_recipe_finetune_training.jsonl"
write_jsonl(training_data, training_file_name)

validation_file_name = "tmp_recipe_finetune_validation.jsonl"
write_jsonl(validation_data, validation_file_name)
  • こちらが、トレーニング用 .jsonl ファイルの最初の5行の内容です。
!head -n 5 tmp_recipe_finetune_training.jsonl
{"messages": [{"role": "system", "content": "You are a helpful recipe assistant. You are to extract the generic ingredients from each of the recipes provided."}, {"role": "user", "content": "Title: No-Bake Nut Cookies\n\nIngredients: [\"1 c. firmly packed brown sugar\", \"1/2 c. evaporated milk\", \"1/2 tsp. vanilla\", \"1/2 c. broken nuts (pecans)\", \"2 Tbsp. butter or margarine\", \"3 1/2 c. bite size shredded rice biscuits\"]\n\nGeneric ingredients: "}, {"role": "assistant", "content": "[\"brown sugar\", \"milk\", \"vanilla\", \"nuts\", \"butter\", \"bite size shredded rice biscuits\"]"}]}
{"messages": [{"role": "system", "content": "You are a helpful recipe assistant. You are to extract the generic ingredients from each of the recipes provided."}, {"role": "user", "content": "Title: Jewell Ball'S Chicken\n\nIngredients: [\"1 small jar chipped beef, cut up\", \"4 boned chicken breasts\", \"1 can cream of mushroom soup\", \"1 carton sour cream\"]\n\nGeneric ingredients: "}, {"role": "assistant", "content": "[\"beef\", \"chicken breasts\", \"cream of mushroom soup\", \"sour cream\"]"}]}
{"messages": [{"role": "system", "content": "You are a helpful recipe assistant. You are to extract the generic ingredients from each of the recipes provided."}, {"role": "user", "content": "Title: Creamy Corn\n\nIngredients: [\"2 (16 oz.) pkg. frozen corn\", \"1 (8 oz.) pkg. cream cheese, cubed\", \"1/3 c. butter, cubed\", \"1/2 tsp. garlic powder\", \"1/2 tsp. salt\", \"1/4 tsp. pepper\"]\n\nGeneric ingredients: "}, {"role": "assistant", "content": "[\"frozen corn\", \"cream cheese\", \"butter\", \"garlic powder\", \"salt\", \"pepper\"]"}]}
{"messages": [{"role": "system", "content": "You are a helpful recipe assistant. You are to extract the generic ingredients from each of the recipes provided."}, {"role": "user", "content": "Title: Chicken Funny\n\nIngredients: [\"1 large whole chicken\", \"2 (10 1/2 oz.) cans chicken gravy\", \"1 (10 1/2 oz.) can cream of mushroom soup\", \"1 (6 oz.) box Stove Top stuffing\", \"4 oz. shredded cheese\"]\n\nGeneric ingredients: "}, {"role": "assistant", "content": "[\"chicken\", \"chicken gravy\", \"cream of mushroom soup\", \"shredded cheese\"]"}]}
{"messages": [{"role": "system", "content": "You are a helpful recipe assistant. You are to extract the generic ingredients from each of the recipes provided."}, {"role": "user", "content": "Title: Reeses Cups(Candy)  \n\nIngredients: [\"1 c. peanut butter\", \"3/4 c. graham cracker crumbs\", \"1 c. melted butter\", \"1 lb. (3 1/2 c.) powdered sugar\", \"1 large pkg. chocolate chips\"]\n\nGeneric ingredients: "}, {"role": "assistant", "content": "[\"peanut butter\", \"graham cracker crumbs\", \"butter\", \"powdered sugar\", \"chocolate chips\"]"}]}

ファイルアップロード

  • ファインチューニングされたモデルで使用するために、ファイルを Files エンドポイントにアップロードできます。
# ファイルをアップロードし、そのファイルIDを取得する関数
# file_name: アップロードするファイルの名前
# purpose: アップロードの目的(例: "fine-tune")
def upload_file(file_name: str, purpose: str) -> str:
    # 指定したファイルをバイナリモード("rb")で開く
    with open(file_name, "rb") as file_fd:
        # ファイルを `Files` エンドポイントにアップロードし、レスポンスを取得
        response = client.files.create(file=file_fd, purpose=purpose)
    
    # アップロードされたファイルのIDを返す
    return response.id

# トレーニング用ファイルをアップロードし、そのファイルIDを取得
training_file_id = upload_file(training_file_name, "fine-tune")

# 検証用ファイルをアップロードし、そのファイルIDを取得
validation_file_id = upload_file(validation_file_name, "fine-tune")

# アップロードしたファイルのIDを出力
print("Training file ID:", training_file_id)
print("Validation file ID:", validation_file_id)

Training file ID: file-xxxx
Validation file ID: file-xxxx

  • Azure AI Foundryのデータファイルからアップロードしたファイルが確認できます。

ファインチューニング

  • 次に、生成したファイルとオプションのサフィックスを使用して、ファインチューニングジョブを作成できます。このレスポンスには、ジョブの更新を取得するために使用できるidが含まれます。
  • 注意:ファイルは最初にシステムによって処理される必要があるため、File not readyというエラーが発生する場合があります。その場合は、数分後に再試行してください。
MODEL = "gpt-4o-mini-2024-07-18"
# ファインチューニングジョブを作成

response = client.fine_tuning.jobs.create(
    training_file=training_file_id,
    validation_file=validation_file_id,
    model=MODEL,
    suffix="recipe-ner",
)

job_id = response.id

print("Job ID:", response.id)
print("Status:", response.status)

Job ID: ftjob-xxxxx
Status: validating_files

ジョブのステータスを確認

  • エンドポイントにGETリクエストを送信することで、ファインチューニングジョブを一覧表示できます。この場合、前のステップで取得したIDがstatus: succeededとなっていることを確認してください。

  • 完了したら、result_filesを使用して検証セット(アップロードした場合)の結果をサンプルし、fine_tuned_modelパラメーターから取得したIDを使用して訓練済みモデルを呼び出すことができます。

response = client.fine_tuning.jobs.retrieve(job_id)

print("Job ID:", response.id)
print("Status:", response.status)
print("Trained Tokens:", response.trained_tokens)

Job ID: ftjob-xxxxx
Status: running
Trained Tokens: None

  • ファインチューニングの進行状況は、イベントエンドポイントで追跡できます。ファインチューニングが準備完了するまで、以下のセルを何度か再実行することができます。
# 指定したジョブIDのイベントを取得
response = client.fine_tuning.jobs.list_events(job_id)

# イベントデータを取得
events = response.data

# イベントの順序を逆にして最新のものから表示
events.reverse()

# 各イベントのメッセージを出力
for event in events:
    print(event.message)
  • ジョブ実行中

  • ジョブの終了

  • これで完了したので、ジョブからファインチューニング済みモデルのIDを取得できます

response = client.fine_tuning.jobs.retrieve(job_id)
fine_tuned_model_id = response.fine_tuned_model

if fine_tuned_model_id is None:
    raise RuntimeError(
        "Fine-tuned model ID not found. Your job has likely not been completed yet."
    )

print("Fine-tuned model ID:", fine_tuned_model_id)

Fine-tuned model ID: ft:gpt-4o-mini-2024-07-18:openai-gtm:recipe-ner:xxxx

  • Azure AI Foundryからも確認できます。

  • Azure AI Foundryからメトリックやトークンの精度を確認できます。

推論

  • 最後のステップは、ファインチューニングしたモデルを使用して推論を行うことです。従来のFineTuningと同様に、ChatCompletionsを呼び出し、modelパラメーターに新しいファインチューニング済みモデルの名前を指定するだけです。
# テスト用データセットとして、データフレームの201行目から300行目を選択
test_df = recipe_df.loc[201:300]
# テスト用に最初の1行を取得
test_row = test_df.iloc[0]
# テスト用のメッセージリストを作成
test_messages = []
# システムメッセージ(AIの振る舞いを指示)をリストに追加
test_messages.append({"role": "system", "content": system_message})
# ユーザーメッセージを作成し、リストに追加
user_message = create_user_message(test_row)
test_messages.append({"role": "user", "content": user_message})

pprint(test_messages)
[{'content': 'You are a helpful recipe assistant. You are to extract the '
             'generic ingredients from each of the recipes provided.',
  'role': 'system'},
 {'content': 'Title: Beef Brisket\n'
             '\n'
             '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"]\n'
             '\n'
             'Generic ingredients: ',
  'role': 'user'}]

Azure AI Foundry上から作成したモデルをデプロイする(Azureのみ)

#modelはデプロイしたモデル名を記載
response = client.chat.completions.create(
    model="gpt-4o-mini-2024-07-18-recipe-ner", messages=test_messages, temperature=0, max_tokens=500
)
print(response.choices[0].message.content)

["beef brisket", "catsup", "water", "onion", "cider vinegar", "horseradish", "mustard", "salt", "pepper"]

まとめ

  • データ準備、モデルのトレーニング、ファイルアップロード、ジョブの実行、推論テストを行い、Azure OpenAI上で最適化モデルをデプロイしました。
  • 作成したモデルの精度などもAzureポータルで可視化可能。

Discussion