0Pricing
AI Engineering Academy · レッスン

ファインチューニング済みモデルの評価とデプロイ

未使用のテストケースでベースモデルとファインチューニング済みモデルを比較する定量評価を実行し、ローカル推論用にGGUFへ変換して、llama.cppまたはvLLMで提供します。

「ファインチューニング済みモデルの評価とデプロイ」はCoddyKit上の無料AI Engineering Academyレッスンです。 これはレッスン4/4です。 下記で完全なレッスンを無料で読むことができます。その後、ブラウザ内の組み込みコードエディタと24時間対応のAIチューターでハンズオン演習できます。 これはAI Engineering Academy学習パスの一部であり、ウェブとCoddyKitアプリ全体で進捗が同期されます。 AI Engineering Academyコースには全4レッスンが含まれています。

デプロイ前に評価が必要な理由

学習データで高い性能を示す微調整済みモデルでも、実際の本番環境の用途ではベースモデルより性能が低くなることがあります。それを知る唯一の方法は、厳密な評価です。微調整によって、壊滅的忘却(ベースモデルが持っていた能力を失うこと)、過度な特化(対象タスクでは高い性能を示す一方、関連タスクでは性能が低下すること)、安全性に関する動作の微妙な退行が起こる可能性があります。代表性のあるテストセットでベースモデルと比較評価せずに、微調整済みモデルをデプロイしてはいけません。

保留テストセットの構築

テストセットは、学習データや検証データから完全に分離し、学習のどの段階でもモデルが見たことのない例だけで構成する必要があります。テストセットは、本番環境の入力分布全体を代表するものでなければなりません。一般的なケース、エッジケース、敵対的な入力を含めてください。命令追従タスクでは、最も一般的な要件だけでなく、命令のあらゆる側面に従う必要がある例も含めます。信頼性の高い評価には、通常100~500例のテストセットで十分です。

import json
from typing import TypedDict

class TestCase(TypedDict):
    input: str                 # the user message
    expected_output: str       # the ideal response
    category: str              # e.g., 'format', 'accuracy', 'edge_case'
    evaluation_method: str     # 'exact_match', 'json_schema', 'llm_judge'

# Load test set (never used during training)
def load_test_set(path: str) -> list[TestCase]:
    cases = []
    with open(path) as f:
        for line in f:
            data = json.loads(line.strip())
            cases.append({
                'input': data['messages'][-2]['content'],  # user message
                'expected_output': data['messages'][-1]['content'],  # assistant response
                'category': data.get('metadata', {}).get('category', 'general'),
                'evaluation_method': data.get('metadata', {}).get('eval_method', 'llm_judge')
            })
    return cases

test_set = load_test_set('test.jsonl')
print(f'Test set loaded: {len(test_set)} examples')

タスク固有の評価に使う定量指標

タスクに合った評価指標を選択してください。JSON抽出では、スキーマ準拠率とフィールド単位の正解率を測定します。分類では、クラスごとの正解率、適合率、再現率を測定します。テキスト生成では、品質評価にLLM-as-judgeによる採点を使います。形式の遵守では、指定された形式への完全準拠率を測定します。改善幅を測定できるよう、各指標をベースモデルと微調整済みモデルの両方で算出してください。

import json

def evaluate_json_extraction(model_output: str, expected: str, schema: dict) -> dict:
    metrics = {'valid_json': False, 'schema_compliant': False, 'field_accuracy': 0.0}
    
    try:
        parsed = json.loads(model_output.strip())
        metrics['valid_json'] = True
        
        # Check schema compliance
        required_fields = schema.get('required', [])
        all_present = all(field in parsed for field in required_fields)
        correct_types = all(
            isinstance(parsed.get(field), schema['properties'][field]['expected_type'])
            for field in required_fields if field in parsed
        )
        metrics['schema_compliant'] = all_present and correct_types
        
        # Field-level accuracy against expected output
        expected_parsed = json.loads(expected)
        correct_fields = sum(1 for k in expected_parsed if parsed.get(k) == expected_parsed[k])
        metrics['field_accuracy'] = correct_fields / len(expected_parsed) if expected_parsed else 0.0
    
    except json.JSONDecodeError:
        pass  # valid_json stays False
    
    return metrics

LLM-as-Judgeによる評価

自由度の高い生成タスクでは、LLM-as-judgeを使って、期待される出力に対する微調整済みモデルの出力を採点します。GPT-4oに評価者として振る舞うよう指示し、入力、期待される出力、モデルの出力を渡して、正確性、完全性、形式への準拠度を1~5の尺度で評価させます。テストセット全体のスコアを平均し、総合的な品質評価を得ます。同じテストセットで、微調整済みモデルのスコアとベースモデルのスコアを比較してください。

from openai import OpenAI

client = OpenAI()

def llm_judge_score(instruction: str, expected: str, actual: str) -> dict:
    judge_prompt = f'''Evaluate the quality of an AI assistant response.

Instruction given to assistant:
{instruction}

Expected ideal response:
{expected}

Actual response from model being evaluated:
{actual}

Rate the actual response on these criteria (1=poor, 5=excellent):
1. Correctness: Is the information accurate?
2. Format compliance: Does it follow the expected output format?
3. Completeness: Does it address all parts of the instruction?

Return JSON: {{"correctness": N, "format": N, "completeness": N, "overall": N, "reason": "brief explanation"}}'''
    
    response = client.chat.completions.create(
        model='gpt-4o',
        messages=[{'role': 'user', 'content': judge_prompt}],
        response_format={'type': 'json_object'}
    )
    return json.loads(response.choices[0].message.content)

比較評価の実行

すべてのテストケースで、ベースモデル、微調整済みモデル(必要に応じて強力なプロンプトを使ったベースラインも含めます)を直接比較する評価を実行します。各テストケースについて各モデルから出力を生成し、すべての出力を評価指標で採点します。指標のスコア、標準偏差、ベースラインと比較して微調整済みモデルが勝った例と負けた例を示す比較表を作成してください。

def run_full_evaluation(test_set: list, models: dict, system_prompt: str) -> dict:
    results = {name: {'scores': [], 'errors': 0} for name in models}
    
    for i, test_case in enumerate(test_set):
        print(f'Evaluating test case {i+1}/{len(test_set)}')
        
        for model_name, model_fn in models.items():
            try:
                output = model_fn(test_case['input'], system_prompt)
                score = llm_judge_score(
                    test_case['input'],
                    test_case['expected_output'],
                    output
                )
                results[model_name]['scores'].append(score['overall'])
            except Exception as e:
                results[model_name]['errors'] += 1
                results[model_name]['scores'].append(0)
    
    # Summarize
    summary = {}
    for name, data in results.items():
        scores = data['scores']
        summary[name] = {
            'mean_score': sum(scores) / len(scores),
            'errors': data['errors']
        }
        print(f'{name}: mean={summary[name]["mean_score"]:.2f}, errors={data["errors"]}')
    return summary

壊滅的忘却の回帰テスト

微調整によってモデルの汎用能力が低下することがあります。この現象を壊滅的忘却と呼びます。対象タスク以外で重要なタスクも含め、ベースモデルと微調整済みモデルの両方に回帰テストスイートを実行してください。一般的な質疑応答、推論、コード生成、命令追従などが対象です。これらのタスクで微調整済みモデルのスコアが大幅に低い場合は、LoRAのランクが高すぎるか、学習エポック数が多すぎる可能性があります。

REGRESSION_TEST_CASES = [
    # General QA
    {'input': 'What is the capital of France?', 'expected_substring': 'Paris'},
    {'input': 'What is 17 * 23?', 'expected_substring': '391'},
    # Instruction following
    {'input': 'List 3 planets. Format as: 1. Planet Name', 'expected_pattern': r'^1\. '},
    # Reasoning
    {'input': 'If all A are B and all B are C, are all A also C?', 'expected_substring': 'yes'},
]

def run_regression_tests(model_fn, test_cases: list) -> float:
    passed = 0
    for test in test_cases:
        output = model_fn(test['input'], '')
        if 'expected_substring' in test:
            if test['expected_substring'].lower() in output.lower():
                passed += 1
        elif 'expected_pattern' in test:
            import re
            if re.search(test['expected_pattern'], output):
                passed += 1
    
    rate = passed / len(test_cases)
    print(f'Regression test pass rate: {rate:.1%} ({passed}/{len(test_cases)})')
    return rate

ローカル推論用のGGUFへの変換

高価なGPUインフラを使わずにローカルでデプロイする場合は、マージ済みモデルをGGUF形式に変換し、llama.cppで推論を実行します。GGUFはさまざまな量子化レベルに対応しています。Q4_K_M(4ビット、品質と速度のバランスが良い)、Q8_0(8ビット、フル精度に近い品質)、Q2_K(2ビット、非常に高速ですが品質は低め)などです。4ビット量子化した7Bモデルは約4GBのRAMしか必要とせず、CPU上で毎秒1~5トークンの速度で実行できます。

# Step 1: Convert merged HuggingFace model to GGUF
# git clone https://github.com/ggerganov/llama.cpp
# python llama.cpp/convert_hf_to_gguf.py ./merged-model --outtype f16 --outfile model-f16.gguf

# Step 2: Quantize to 4-bit
# ./llama.cpp/llama-quantize model-f16.gguf model-q4.gguf Q4_K_M

# Step 3: Run inference with llama.cpp Python bindings
# pip install llama-cpp-python
from llama_cpp import Llama

llm = Llama(
    model_path='./model-q4.gguf',
    n_ctx=4096,         # context window
    n_threads=8,        # CPU threads
    n_gpu_layers=0      # set > 0 to offload layers to GPU
)

output = llm.create_chat_completion(
    messages=[{'role': 'user', 'content': 'What is the capital of France?'}],
    temperature=0.1
)
print(output['choices'][0]['message']['content'])

本番環境でのvLLMによるサービング

微調整済みモデルを大規模に本番サービングする場合、現在の標準はvLLMです。vLLMはPagedAttentionを使って複数のリクエストを効率的にまとめてバッチ処理し、GPUのスループットを大幅に向上させます。OpenAI互換のAPIエンドポイントに対応しているため、OpenAI APIのドロップイン置き換えとして利用できます。微調整済みの7Bモデルを搭載した1台のA100 GPUで動作するvLLMは、毎分数百件のリクエストを処理できます。

# Start vLLM server (run from command line)
# pip install vllm
# python -m vllm.entrypoints.openai.api_server \
#     --model ./merged-model \
#     --host 0.0.0.0 \
#     --port 8000 \
#     --max-model-len 4096 \
#     --tensor-parallel-size 1

# Use with OpenAI client (drop-in replacement)
from openai import OpenAI

client = OpenAI(
    base_url='http://localhost:8000/v1',
    api_key='not-needed'  # vLLM doesn't require auth by default
)

response = client.chat.completions.create(
    model='merged-model',  # model name matches the path you passed to vLLM
    messages=[{'role': 'user', 'content': 'Extract JSON from: "Alice, 30, NYC"'}]
)
print(response.choices[0].message.content)

微調整済みモデルのA/Bテスト

本番環境で微調整済みモデルに完全に切り替える前に、A/Bテストを実行してください。本番トラフィックの一部(まずは5~10%)を微調整済みモデルに振り分け、残りの大部分ではベースモデルまたは既存のプロンプト方式を使います。両グループの品質スコア、レイテンシ、ユーザー満足度の指標を監視してください。統計的に有意な件数のリクエストを通じてA/Bテストで改善が確認できた場合にのみ、微調整済みモデルへのトラフィック比率を増やします。

import random

class ModelRouter:
    def __init__(self, fine_tuned_traffic_fraction=0.1):
        self.ft_fraction = fine_tuned_traffic_fraction
        self.metrics = {'base': {'count': 0, 'quality_sum': 0}, 'fine_tuned': {'count': 0, 'quality_sum': 0}}

    def route(self, user_id: str, request: str) -> dict:
        # Deterministic routing by user_id (same user always goes to same model)
        use_fine_tuned = (hash(user_id) % 100) < (self.ft_fraction * 100)
        model_group = 'fine_tuned' if use_fine_tuned else 'base'
        
        response = call_model(request, use_fine_tuned=use_fine_tuned)
        return {'response': response, 'model_group': model_group}

    def record_quality(self, model_group: str, quality_score: float):
        self.metrics[model_group]['count'] += 1
        self.metrics[model_group]['quality_sum'] += quality_score

    def ab_test_summary(self) -> dict:
        summary = {}
        for group, data in self.metrics.items():
            avg = data['quality_sum'] / data['count'] if data['count'] > 0 else 0
            summary[group] = {'avg_quality': avg, 'n': data['count']}
        return summary

微調整済みモデルの継続的なメンテナンス

微調整済みモデルには継続的なメンテナンスが必要です。ベースモデルが更新されると(新しいGPT-4oのバージョンや新しいMistralのリリースなど)、アダプターの重みとの互換性がなくなり、再学習が必要になる場合があります。タスクの要件が変わった場合は、学習データを更新して再学習する必要があります。本番環境で新たな失敗パターンを見つけた場合は、学習セットに例を追加してください。本番ML運用ワークフローの一環として、定期的な再学習を計画しましょう。

デプロイ判断マトリックス

ファインチューニング済みモデルのデプロイ戦略は、規模とインフラ上の制約によって決まります。低ボリューム(1日1,000リクエスト未満)では、OpenAI's fine-tuning APIが最も簡単です。中ボリューム(1日1,000~100,000リクエスト)では、単一GPUインスタンス上のvLLMを検討してください。高ボリュームまたはレイテンシーが重要なアプリケーションでは、複数GPUでvLLMを使用し、テンソル並列化を検討したうえで、前段にキャッシュ層を追加してください。プライバシーに配慮が必要なデータでは、GGUF/llama.cppまたはvLLMを使って独自のインフラ上にセルフホストしてください。

理解度チェック

このレッスンで学んだ、ファインチューニング済みモデルの評価とデプロイについての理解度を確認しましょう。

レッスンのまとめ

このレッスンでは、デプロイ前の厳密な評価として、保留したテストケースでファインチューニング済みモデルとベースモデルを比較することが不可欠であること、回帰テストによって、過度な特化が原因となる一般的な能力の壊滅的忘却を検出できること、そしてデプロイの選択肢には、手軽に利用できるOpenAI's managed fine-tuning APIから、高スループットの本番サービングに適したvLLM、CPUベースのローカル推論に適したGGUF/llama.cppまであることを学びました。AI Engineeringトラックの修了、おめでとうございます。

よくある質問

「ファインチューニング済みモデルの評価とデプロイ」レッスンは無料ですか?

はい。「ファインチューニング済みモデルの評価とデプロイ」の完全なテキストはこのウェブで無料で読めます。インタラクティブに演習し(組み込みコードエディタと24時間対応のAIチューター)、AI Engineering Academyコースの残りをアンロックするには、CoddyKit PROにアップグレードしてください。 AI Engineering Academyコースには全4レッスンが含まれています。

「ファインチューニング済みモデルの評価とデプロイ」で何を学びますか?

未使用のテストケースでベースモデルとファインチューニング済みモデルを比較する定量評価を実行し、ローカル推論用にGGUFへ変換して、llama.cppまたはvLLMで提供します。 ブラウザで直接実行するハンズオンコードでAI Engineering Academyを演習し、24時間対応のAIチューターがレッスンを進める中での質問に答えます。

AI Engineering Academyを始めるのに経験は必要ですか?

事前経験は必要ありません。CoddyKitのAI Engineering Academyは初級者から上級者向けに構成されているため、ここから始めるか最初から始めて、自分のペースで進むことができます。 これはレッスン4/4です。

「ファインチューニング済みモデルの評価とデプロイ」レッスンにはどのくらい時間がかかりますか?

ほとんどのCoddyKitレッスンは約5~10分かかります。各レッスンはコンパクトでインタラクティブなので、着実に進歩し、ウェブとアプリ全体で正確に前回の場所から再開できます。

このAI Engineering Academyレッスンでコードを書いて実行できますか?

はい。すべてのAI Engineering Academyレッスンに組み込みコードエディタが含まれているため、ブラウザでリアルコードを書いて実行し、即座のAIフィードバックを取得できます。ローカル設定は不要です。

このコースのすべてのレッスン

  1. プロンプトよりファインチューニングが有効な場合
  2. 高品質な学習データセットを準備する
  3. Hugging Face PEFTによるLoRAファインチューニング
  4. ファインチューニング済みモデルの評価とデプロイ
← AI Engineering Academyに戻る