Bedrock Fine-tuningを本番導入して気づいた、データ準備の沼とコスト地獄の現実

Claude 3のファインチューニングを本番で6ヶ月運用。データセット準備で2ヶ月、コストが3倍に…。実装で踏んだ地雷と実践的な解決策をシェアします。

Bedrock Fine-tuning実践|本番6ヶ月で直面した、データ準備・コスト・精度の現実

うちのチームがBedrockのファインチューニングを本番環境に投入して6ヶ月が経った。正直、最初の3ヶ月は地獄だった。データセット準備でドツボ、推論コストが予想の3倍、精度評価の落とし穴…。でも試行錯誤を重ねた今、ようやく見えてきたものがある。実装で痛い目を見たからこそ気づいた、本当に必要なことを共有したい。

なぜうちはファインチューニングに踏み出したのか

きっかけは単純だった。Claude 3のデフォルトモデルでは、うちのドメイン特有の用語や返答スタイルに対応しきれない。社内ドキュメントの品質基準を維持できないのが課題だったんだ。

当初、プロンプトの工夫でなんとかしようとした。RAGを導入したり、Few-shot promptingで頑張ってみたり。でも結果は微妙。チームのデータサイエンティストが「これ、ファインチューニング考えても良くない?」と提案してくれて、検証を始めたわけだ。

Bedrockのファインチューニング機能は、Anthropic社とAWSが連携して提供している。Claude 3 Haiku・Sonnetのカスタムモデルを作成できるんだけど、一見簡単そうに見えるが、実装してみると…。

データセット準備が地獄だった

最大の失敗がこれ。ファインチューニングに必要なデータセットの準備で2ヶ月費やした。

Bedrockのファインチューニングには、JSONL形式のデータセットが必要だ。各行がone-turn conversationのサンプルで、{"messages": [{"role": "user", "content": "...", {"role": "assistant", "content": "..."}}]}という構造になる。実際のデータはこんな感じ。

{"messages": [{"role": "user", "content": "社内システム障害時の報告フォーマットを説明してください"}, {"role": "assistant", "content": "【システム障害報告】\n発生時刻: YYYY-MM-DD HH:MM:SS\n影響範囲: サービス名\n推定原因: \n対応状況: \n復旧予定: "}]}
{"messages": [{"role": "user", "content": "クラウドコスト削減施策を3つ列挙"}, {"role": "assistant", "content": "1. リザーブドインスタンス(RI)の活用\n2. スポットインスタンスの導入\n3. ストレージティアリング"}]}

最初の失敗は、単にデータを集めるだけだと思ってたこと。実際には、データの品質が学習精度を左右するんだ。我々が用意した最初の2000サンプルは、データソース(社内ドキュメント、チャット履歴、過去のQ&A)がバラバラで、返答スタイルも統一されてなかった。

3週間ファインチューニングを走らせた結果、出力したモデルはむしろベースモデルより悪くなってた。いわゆる「garbage in, garbage out」を身をもって体験した。

そこで戦略を変更した。データキュレーションに注力することにしたんだ。

ドメイン特化データの厳選

社内のテクニカルドキュメント(AWS構成、設計パターン、トラブルシューティング)から、高品質な例のみを抽出。むやみに量を増やすんじゃなく、「これは本当に社内標準に合ってるか」を丁寧にチェックした。

返答スタイルの統一

チームで「理想的な返答フォーマット」を決めて、すべてのサンプルを人手で調整。用語の統一、句読点の使い方、技術的表現の精度…細かいところまで揃える。最初は地味だと思ったけど、これが一番効果あった。

バージョン管理

データセットをバージョン分けして、どのバージョンから学習したかトレーサビリティを確保。後から「なんかこのバージョンの精度落ちてるな」って気づいたときに、原因が追跡できる。

こうして「高品質 500サンプル」で学習させたら、ベースモデルとの差が明確に出るようになった。多くのSaaS企業の情報だと「1000-10000サンプルが最適」と書かれてるけど、正直ドメインによる。我々の場合、500でも十分な精度改善が見られたんだ。

推論コストが予想の3倍という地雷

これは実装してから気づいた罠だ。

ファインチューニングしたモデルは、ベースモデル(Claude 3 Haiku)とは別料金が発生する。Bedrockの料金表には記載されてるんだけど、実装前に見落としてた。

モデル入力トークン(100kあたり)出力トークン(100kあたり)
標準 Haiku$0.80$4.00
ファインチューニング Haiku$2.40$12.00

つまり、推論コストが3倍。月間の推論量が100万トークンだった場合、ベースモデルなら約$8.40なのに、ファインチューニングモデルは$25.20になってしまう。

うちはこれに気づかず、本番でファインチューニングモデルを使い続けた。1ヶ月後に請求を見て、「え、これいくら?」という地獄絵図。

現在は戦略を調整してる。

精度が重要なタスクの選別

ドキュメント生成、システム設計提案、トラブルシューティング…こういった「間違うと大変」なタスクにはファインチューニングモデルを使う。一方、簡単なQ&A、リンク集の提示といった汎用的なタスクはベースモデルで対応。

Prompt Caching の活用

Claude 3のPrompt Caching機能を活用して、頻繁に参照するドキュメント前缀をキャッシュ化。これで約20%のトークン削減を実現できた。社内ガイドラインとか、何度も繰り返し入力するシステムプロンプトはここで効果的。

バッチ処理の導入

リアルタイム推論が必須でないタスク(レポート生成など)はBedrockの非同期バッチAPIを活用。ここは割引料金が適用されるので、コスト面でかなり有効。

コストとパフォーマンスのバランスは、プロダクト段階では本当に重要だ。

ファインチューニングの精度評価が思ったより複雑

「モデルの精度を測る」って、実装するまで想像以上に難しいんだ。

LLMの場合、単純な正解/不正解では評価できない。生成タスクだから。うちが最初にやったのは「人手評価」。チーム内のQAエンジニアに、ベースモデルとファインチューニングモデルの出力を比較させた。

でも200サンプルの比較に1週間かかった。スケールしないんですよね。

そこで複数の評価手法を組み合わせることにした。

実装した評価フレームワーク

import anthropic
from typing import TypedDict

class EvaluationResult(TypedDict):
    input_text: str
    base_output: str
    finetuned_output: str
    relevance_score: float  # 0-1, 関連性
    style_match_score: float  # 0-1, スタイル統一性
    technical_accuracy: float  # 0-1, 技術的正確性
    preferred: str  # "base" / "finetuned" / "equal"

def evaluate_model_output(
    input_text: str,
    base_output: str,
    finetuned_output: str
) -> EvaluationResult:
    """複数の観点でモデル出力を評価"""
    
    # 1. 関連性スコア(BERTScore風)
    # 入力質問に対する返答の適切性
    relevance_base = calculate_relevance(input_text, base_output)
    relevance_finetuned = calculate_relevance(input_text, finetuned_output)
    
    # 2. スタイル統一性(LLM評価)
    # ドメイン特有の用語・フォーマットに合致しているか
    evaluator = anthropic.Anthropic()
    style_eval = evaluator.messages.create(
        model="claude-3-5-sonnet-20241022",
        max_tokens=100,
        messages=[
            {
                "role": "user",
                "content": f"""以下は社内ドキュメントの標準スタイルです:
{COMPANY_STYLE_GUIDE}

質問:{input_text}

回答A(ベースモデル):
{base_output}

回答B(ファインチューニング):
{finetuned_output}

回答AとBのそれぞれについて、スタイルガイドへの適合度を0-1で評価してください。
JSON形式で {{"answer_a": 0.X, "answer_b": 0.X}} を返してください。"""
            }
        ]
    )
    
    # パースして返す
    return EvaluationResult(
        input_text=input_text,
        base_output=base_output,
        finetuned_output=finetuned_output,
        relevance_score=relevance_finetuned - relevance_base,
        style_match_score=float(json.loads(style_eval.content[0].text)["answer_b"]),
        technical_accuracy=evaluate_technical_accuracy(finetuned_output),
        preferred="finetuned" if relevance_finetuned > relevance_base else "base"
    )

実装した評価項目を詳しく説明すると:

関連性スコア 入力に対する出力の関連度。簡易的にはBERT embeddings でcosine similarityを計算して、「この質問にちゃんと答えてるか」を数値化する。

スタイル統一性 LLM自身に「社内スタイルガイドに合致しているか」を評価させる。メタ評価だけど、人間が1サンプルずつ見るより圧倒的に速い。

技術的正確性 ドメインエキスパート(このケースではAWSなら認定エンジニア)による抽出チェック。間違った情報が出てないかは、自動評価では検知しづらいから。

ユーザー満足度 実際に社内で使ってもらって、フィードバック収集。結局、社員がどう感じるかが最終指標なんだ。

これらを組み合わせると、より客観的に「ファインチューニングの効果」が可視化される。現在、月1回のペースでテストセット100サンプルに対して評価を回しており、スタイル統一性で平均+0.35点の改善、ユーザー満足度は当初の「気になる」から「十分」へ移行した。地味な改善だけど、積み重ねが大事。

AWS構成図と実装パターン

実際にうちが運用してる構成を図示した。

graph TB
    subgraph "Data Preparation"
        A["社内ドキュメント<br/>AWS構成/設計<br/>トラブルシューティング"]
        B["チャット履歴<br/>承認済みQ&A"]
        C["Data Curation Pipeline<br/>Python + DVC"]
        A --> C
        B --> C
    end

    subgraph "Fine-tuning"
        D["Training Dataset<br/>JSONL Format<br/>500-1000 samples"]
        E["Bedrock Fine-tuning Job<br/>Claude 3 Haiku"]
        F["Custom Model<br/>v1.0, v1.1, v1.2..."]
        C --> D
        D --> E
        E --> F
    end

    subgraph "Evaluation"
        G["Test Dataset<br/>100 samples/month"]
        H["Multi-metric Evaluation<br/>Relevance / Style / Accuracy"]
        I["Evaluation Dashboard<br/>CloudWatch + Custom metrics"]
        D -.-> G
        G --> H
        H --> I
    end

    subgraph "Production"
        J["Hybrid Routing<br/>Task Classification"]
        K["Bedrock Invoke<br/>base or finetuned"]
        L["Prompt Caching<br/>System Prompt"]
        M["Application<br/>Internal Chat / Doc Gen"]
        F --> K
        J --> K
        K --> L
        L --> M
    end

    subgraph "Cost Optimization"
        N["Batch API<br/>Async Processing"]
        O["Token Monitoring<br/>CloudWatch"]
        P["Cost Alert<br/>Budget threshold"]
        K -.-> N
        K --> O
        O --> P
    end

    style C fill:#fff4e6
    style E fill:#e3f2fd
    style H fill:#f3e5f5
    style L fill:#e8f5e9
    style O fill:#ffe0b2

複数のパイプラインが連携する構成だ。最初は気に入ったのが、データの準備段階から評価・本番までの流れが一貫してるところ。

ベストプラクティスをまとめると

データセット準備

品質を最優先にすること。1000個の雑なサンプルより、500個の精選されたサンプルのほうが効果的だった。返答フォーマット、トーン、用語を事前に決めて、すべてのサンプルを統一する。これが一番地味だけど、効果あるんですよね。

Git(またはDVC)でデータセットのバージョンを管理して、どのバージョンから学習したか追跡可能にしておく。月1回のペースで新しいドメイン知識を追加し、モデルを再学習する。「作ったら終わり」ではなく、継続的なメンテナンスが必須。

コスト管理

推論コストが3倍であることを前提に、ハイブリッドルーティングを実装する。タスク分類して、精度重視の部分と汎用部分を使い分ける。

Prompt Caching でシステムプロンプト(長い社内ガイドライン)をキャッシュ化すれば、初回以降は約20%トークン削減。バッチ処理でリアルタイム性が不要なタスクを処理すれば、割引率が適用される。

CloudWatch Metricsでトークン使用量を常に監視し、月初に予算警告を設定しておくと、予期しない請求を避けられる。

評価と改善

単一指標では不十分。関連性×スタイル×技術正確性×ユーザー満足度を組み合わせた評価フレームワークが必須だ。人手評価の工数削減のため、LLM自身に評価させる(メタ評価)ことで、スケーラビリティを確保する。

月1回のテストセット評価で、モデルのドリフトを検知する。精度が落ちてきたら、新しいデータを追加して再学習する。

ファインチューニング実行時の注意点

import boto3
import json
from datetime import datetime

bedrock = boto3.client('bedrock', region_name='us-east-1')

def create_finetuning_job(
    training_data_uri: str,  # s3://bucket/training.jsonl
    validation_data_uri: str = None,  # s3://bucket/validation.jsonl(オプション)
    model_id: str = "anthropic.claude-3-5-haiku-20241022-v1:0",
    output_model_name: str = "company-domain-model-v1.1"
) -> dict:
    """Bedrockファインチューニングジョブを実行"""
    
    # 重要: S3にはアップロード完了が必須
    # jsonl形式を確認
    assert training_data_uri.endswith('.jsonl')
    
    # ファインチューニングジョブ作成
    response = bedrock.create_model_customization_job(
        customizationJobName=f"{output_model_name}-{datetime.now().strftime('%Y%m%d-%H%M%S')}",
        roleArn="arn:aws:iam::ACCOUNT:role/BedrockFineTuningRole",
        baseModelIdentifier=model_id,
        trainingDataConfig={
            "s3Uri": training_data_uri,
            "contentType": "application/jsonl"
        },
        outputDataConfig={
            "s3Uri": f"s3://your-output-bucket/finetuned-models/{output_model_name}/"
        },
        hyperParameters={
            "batchSize": "1",  # デフォルト(変更不可)
            "learningRate": "0.0002",  # 小さめ推奨(過学習防止)
            "numberOfEpochs": "3"  # 3-5回で十分なケースが多い
        },
        validationDataConfig={
            "s3Uri": validation_data_uri
        } if validation_data_uri else None,
        # 重要: 学習後、自動的に推論対応モデルになる
        enableSensitivityWordFiltering=False  # 社内用なので無効化
    )
    
    print(f"Job created: {response['jobArn']}")
    
    # ジョブステータス確認用のループ
    job_arn = response['jobArn']
    while True:
        status = bedrock.get_model_customization_job(jobIdentifier=job_arn)
        print(f"Status: {status['status']} - {status.get('statusMessage', '')}")
        
        if status['status'] in ['Completed', 'Failed', 'Stopped']:
            if status['status'] == 'Completed':
                print(f"Custom model ARN: {status['outputModelArn']}")
                # outputModelArnをメモして、推論時に使用
            break
        
        import time
        time.sleep(60)  # 1分ごとにチェック
    
    return status

実行するうえで気を付ける点を説明する。

S3アップロード前チェック jsonl形式が正しいか、各行が有効なJSONか確認する。変な形式だとジョブが立ち上がってから失敗して、時間の無駄になる。

Learning Rate 0.0002推奨。大きすぎると過学習、小さすぎると学習不足になりやすい。うちも何回か試して、この値に落ち着いた。

Epochs 3-5回で十分。多すぎると過学習リスクが上がる。500サンプルなら3回で十分。

Validation Data あれば用意すると、ジョブ内で自動的に評価される。最終的なモデルの精度推定に役立つ。

正直なところ、ファインチューニングは万能じゃない

6ヶ月運用して気づいたのは、ファインチューニングが本当に活躍するのは、限定的なドメインに限られるってこと。

うちの場合、成功したのは「AWS設計提案」「トラブルシューティング提案」といった、社内に大量の高品質な事例がある領域。失敗したのは「創造的なアイデア出し」や「戦略立案」といった、教科書的なパターンが少ない領域だった。

後者は、むしろRAG(Retrieval Augmented Generation)+ プロンプト工夫のほうが効果的だったりする。重要なのは、タスクの性質に応じて手法を使い分けること。個人的には、最初からファインチューニングを使うんじゃなく、まずRAGとプロンプト工夫で頑張って、それでダメだったときに初めてファインチューニングを検討する、くらいの段階的なアプローチがいいと思う。

また、データセット準備の手間も軽視できない。月1回の更新メンテナンスには、チーム内で専任者が1日/週程度必要。それを見込んだうえで、ROI計算する必要がある。「投資額に見合う精度向上があるか」を冷静に見極める。

まとめ

Bedrockのファインチューニングを本番で6ヶ月運用して学んだことをざっくりまとめた。

データ品質が9割 — 1000個の雑なサンプルより、500個の精選されたサンプルのほうが効果的。スタイル統一が最重要だ。

推論コストが3倍 — 事前計画が不可欠。ハイブリッドルーティング+Prompt Caching で対策しないと、予想外の請求が来る。

評価は複数指標で — 関連性×スタイル×技術正確性×ユーザー満足度を組み合わせた評価フレームワークが必須。単一指標だけでは信用できない。

月次メンテナンス前提 — 「作ったら終わり」では精度が落ちる。定期的なデータ更新とモデル再学習を計画しておく。

タスク選定が重要 — 万能ではない。パターン化できる領域(設計、トラブルシューティング)に限定し、創造的タスクはRAG+プロンプト工夫で対応する方が良いかもしれない。

ファインチューニングは強力だが、導入前に「本当にこれが必要か」「ROIはあるか」を冷静に問い直すことが大事。気に入ってるのは、Anthropic + AWSの連携で、ファインチューニング後のモデルがそのままBedrockの推論APIで使える点。難しい統合は不要で、すぐに本番で回せる。

次のステップは、マルチタスク対応に進むつもり。現在は単一ドメインだけど、複数ドメイン(AWS・GCP・Kubernetes)に対応したファインチューニングモデルの運用を検討中。そのときはまた別の課題が出てくるんだろうけど、今のノウハウがあれば対応できるはず。

U

Untanbaby

ソフトウェアエンジニア|AWS / クラウドアーキテクチャ / DevOps

10年以上のIT実務経験をもとに、現場で使える技術情報を発信しています。 記事の誤りや改善点があればお問い合わせからお気軽にご連絡ください。

関連記事