Amazon Bedrockで強化学習によるFine Tuning(RFT)

Amazon Bedrockで強化学習によるFine Tuning(RFT)

2025.12.12

Introduction

ここにもあるように、re:Invent 2025 の KeynoteでAmazon BedrockのRFT(強化学習によるファインチューニング)が発表されました。

ファインチューニングにおいて強化学習を用いることで自律的に学習し、
従来よりも少ないデータセットでの精度向上が期待できるとのこと。

本記事では、Amazon Nova 2 Lite を強化学習ファインチューニング(RFT)で訓練し、
指定された文字数以内で回答するモデルを作成してみます。

Environment

  • MacBook Pro (14-inch, M3, 2023)
  • OS : MacOS 15.7.1
  • Rust : 1.91.1
  • cargo-lambda : 1.8.6
  • aws-cli : 2.32.13 Python/3.13.9 Darwin/24.6.0 exe/arm64

AWSアカウントはセットアップ済みとします。

Bedrock RFT の制約(2025年12月時点)

AWS 公式ドキュメントより、現時点での RFT 対応モデルは以下の通りです。

Provider Model Model ID Region
Amazon Nova 2 Lite amazon.nova-2-lite-v1:0:256k us-east-1

Nova 2 Liteのみ、かつus-east-1リージョンのみのサポート。

Example Lambda

今回はRFTの例として、指定した文字数以内で回答できるようにモデルをファインチューニングします。

LLMを使うときにプロンプトで「x文字以内で答えてください」と指示しても余計な説明を付けて制限を守らないことがあるので、それを解決します。

基本的にはここの流れで進めます。

全体のすすめかた

基本的にCDKとAWS CLIで進めていきます。

ステップ AWS CLI コマンド 説明
Lambda デプロイ cdk deploy 報酬関数 (Lambda)、S3、IAM Role を作成
RFT ジョブ作成 aws bedrock create-model-customization-job トレーニングジョブを開始
ジョブ状態確認 aws bedrock get-model-customization-job 進捗とステータスを確認
メトリクス取得 aws s3 cp トレーニング結果を S3 から取得
モデルテスト aws bedrock-runtime converse カスタムモデルで推論テスト

RFT の全体フローは以下の通りです:

┌─────────────────────────────────────────────────────────────────┐
│  1. 報酬関数の実装 (Lambda)                                      │
│     モデル出力を評価し、スコア (0.0〜1.0) を返す                     │
└─────────────────────────────────────────────────────────────────┘
                              ↓
┌─────────────────────────────────────────────────────────────────┐
│  2. トレーニングデータの準備 (JSONL)                                │
│     プロンプトのみ記載(正解は不要)                                  │
└─────────────────────────────────────────────────────────────────┘
                              ↓
┌─────────────────────────────────────────────────────────────────┐
│  3. インフラのデプロイ (CDK)                                       │
│     Lambda, S3, IAM Role を作成                                  │
└─────────────────────────────────────────────────────────────────┘
                              ↓
┌─────────────────────────────────────────────────────────────────┐
│  4. RFT ジョブの実行 (Bedrock API)                                │
│     Bedrock がモデル出力 → Lambda で評価 → 強化学習を繰り返す         │
└─────────────────────────────────────────────────────────────────┘
                              ↓
┌─────────────────────────────────────────────────────────────────┐
│  5. カスタムモデルのデプロイと評価                                   │
│     Deploy for on-demand → Playground or API でテスト            │
└─────────────────────────────────────────────────────────────────┘

従来の教師ありファインチューニング (SFT) では正解データが必要ですが、
RFT では報酬関数を定義します。
Bedrockがモデル出力を生成してLambdaが評価し、その結果をもとに強化学習が進みます。

報酬関数

報酬関数(Reward Function)とは、モデルの出力が「良い」か「悪い」か評価し、
スコア(0.0〜1.0)を返す関数です。
RFT ではこのスコアをフィードバックとして、モデルが「良いスコアが出る出力」を学習します。
これは、正解を定義しにくいタスクに有効となりますし、
大量のデータセットを用意する必要もなくなります。

Amazon Bedrock での報酬関数

Bedrock RFT では2 種類のアプローチを提供しています。
今回は文字数制限というルールで判定できるため、RLVRを使用します。

  • RLVR : 客観的に正誤判定できるタスク(Lambdaで評価)
  • RLAIF: モデレーションなど主観的評価が必要なタスク(Model as Judgeで評価)

今回実装する報酬関数は、以下のシンプルなルールを実装します。

出力 報酬スコア
文字数制限以内 1.0
文字数制限超過 0.0

この報酬関数をLambda(Rust)として実装します。

トレーニングデータ

RFT のトレーニングデータは JSONL 形式で、プロンプトのみを記載します(正解は不要)。

{"messages": [{"role": "user", "content": "AIとは何か30文字以内で説明して"}]}
{"messages": [{"role": "user", "content": "クラウドコンピューティングを40文字以内で説明して"}]}
{"messages": [{"role": "user", "content": "機械学習とは何か25文字以内で答えて"}]}

Bedrock は各プロンプトに対してモデルに回答を生成させ、Lambda 報酬関数で評価します。

Lambda実装

報酬関数の重要な部分を解説します。

Lambda が受け取る入力

トレーニング中、Bedrock は以下のような JSON を Lambda に送信します。
messages にはユーザーのプロンプトと、モデルが生成した assistant の回答が含まれます。

[
  {
    "id": 0,
    "messages": [
      {"role": "user", "content": "AIとは何か30文字以内で説明して"},
      {"role": "assistant", "content": "人工知能の略で、機械が学習・推論する技術"}
    ]
  },
  {
    "id": 1,
    "messages": [
      {"role": "user", "content": "機械学習を25文字以内で説明して"},
      {"role": "assistant", "content": "データからパターンを学ぶ技術"}
    ]
  }
]

Lambda はこの入力を受け取り、各サンプルの assistant 回答を評価してスコアを返します。

入出力の型定義(Rust)

上記の JSON に対応する型を定義します。

/// RFT から送られてくる入力
#[derive(Deserialize)]
struct RftSample {
    id: u64,                    // サンプルID(整数)
    messages: Vec<Message>,     // 会話履歴
}

/// Lambda が返す報酬スコア
#[derive(Serialize)]
struct RewardResult {
    id: u64,                        // 入力と同じID
    aggregate_reward_score: f64,    // 報酬スコア (0.0〜1.0)
}

最初はidをStringで定義していたのですが、
実際は整数で送られてきてエラーになっていたので数値に変更。

文字数制限の抽出

ユーザーメッセージから「30文字以内」などの制限値を正規表現で抽出します。

fn extract_char_limit(user_message: &str) -> Option<usize> {
    let patterns = [
        r"(\d+)\s*文字以内",
        r"(\d+)\s*字以内",
    ];
    for pattern in patterns {
        if let Some(caps) = Regex::new(pattern).ok()?.captures(user_message) {
            return caps.get(1)?.as_str().parse().ok();
        }
    }
    None
}

報酬スコア計算

モデルの出力文字数と制限を比較し、スコアを返します。
limit以内なら1.0、それ以外は0というシンプルなロジック。

fn calculate_reward(response: &str, limit: usize) -> f64 {
    let actual = response.trim().chars().count();  // Unicode文字数
    if actual <= limit { 1.0 } else { 0.0 }
}

ヘルパー関数

ユーザーメッセージから文字数制限を抽出し、アシスタントの応答を取得します。

/// ユーザーメッセージから文字数制限を抽出(例: "30文字以内で" → Some(30))
fn extract_char_limit(user_message: &str) -> Option<usize> {
    let re = Regex::new(r"(\d+)\s*文字以内").ok()?;
    re.captures(user_message)?.get(1)?.as_str().parse().ok()
}

/// メッセージ配列からアシスタントの応答を取得
fn get_assistant_response(messages: &[Message]) -> Option<&str> {
    messages.iter()
        .filter(|m| m.role == "assistant")
        .last()
        .map(|m| m.content.as_str())
}

/// メッセージ配列からユーザーの最後のメッセージを取得
fn get_user_message(messages: &[Message]) -> Option<&str> {
    messages.iter()
        .filter(|m| m.role == "user")
        .last()
        .map(|m| m.content.as_str())
}

Lambda handler

Bedrock RFTは複数のサンプルを配列で送信するため、以下のように処理します。

async fn handler(event: LambdaEvent<serde_json::Value>) -> Result<serde_json::Value, Error> {
    let samples: Vec<RftSample> = serde_json::from_value(event.payload)?;

    let results: Vec<RewardResult> = samples
        .into_iter()
        .map(|sample| {
            let user_msg = get_user_message(&sample.messages);
            let limit = user_msg.and_then(extract_char_limit);
            let response = get_assistant_response(&sample.messages);

            let score = match (response, limit) {
                (Some(res), Some(lim)) => calculate_reward(res, lim),
                _ => 0.0,  // 応答または制限がない場合は0点
            };

            RewardResult { id: sample.id, aggregate_reward_score: score }
        })
        .collect();

    Ok(serde_json::to_value(results)?)
}

Build

ビルドしましょう。
これでデプロイ準備完了です。

$ cd lambda-reward
$ cargo lambda build --release --arm64
・・・

Deploy

では、さきほど cargo lambda build でビルドした Lambda とその他リソースをデプロイしましょう。

CDK実行

CDK が target/lambda/rft-char-limit-reward/bootstrap を Lambda にデプロイします。
※CDKのコードは本記事のAppendixを参照

$ CDK_DEFAULT_ACCOUNT=xxxxx CDK_DEFAULT_REGION=us-east-1 \
% pnpm exec cdk deploy

# 出力:
# RftJsonRewardStack.RewardFunctionArn = arn:aws:lambda:us-east-1:xxxxx:function:rft-char-limit-reward
# RftJsonRewardStack.TrainingDataBucketName = xxxxxxx
# RftJsonRewardStack.BedrockRftRoleArn = arn:aws:iam::xxxxxx:role/BedrockRftJsonDemoRole

CDK スタックでデプロイされるリソースは以下です。

  • Lambda: rft-char-limit-reward (Rust, ARM64)
  • S3: xxxxxxxxx
  • IAM Role: BedrockRftJsonDemoRole (Bedrock RFT 用)
  • トレーニングデータと検証データを自動アップロード(後述)

データ準備 — トレーニング・検証データ

トレーニングと検証に使うデータを準備します。
以下のようなjsonlデータを生成AIに作成させます。

トレーニングデータ: 110 サンプル (char-limit-training.jsonl)
検証データ: 15 サンプル (char-limit-validation.jsonl)

#サンプル例
{"messages": [{"role": "user", "content": "AIとは何か30文字以内で説明して"}]}
{"messages": [{"role": "user", "content": "クラウドコンピューティングを40文字以内で説明して"}]}
{"messages": [{"role": "user", "content": "機械学習とは何か25文字以内で答えて"}]}
{"messages": [{"role": "user", "content": "日本の首都を10文字以内で答えて"}]}

トレーニング開始 — RFT ジョブ作成

aws bedrock create-model-customization-job コマンドで RFT ジョブを作成します。

パラメータは以下。

パラメータ 説明
--job-name ジョブの識別名
--base-model-identifier ベースモデル(現時点では Nova 2 Lite のみ)
--customization-type REINFORCEMENT_FINE_TUNING を指定
--custom-model-name 作成されるカスタムモデルの名前
--role-arn Bedrock が使用する IAM ロール(S3, Lambda アクセス権限が必要)
--training-data-config トレーニングデータの S3 URI
--validation-data-config 検証データの S3 URI
--output-data-config 出力先の S3 URI(メトリクス等が保存される)
--customization-config RFT 固有の設定(下記参照)

customization-config

RFT設定は以下。

設定 説明
lambdaGrader.lambdaArn 報酬関数として使用する Lambda の ARN
epochCount トレーニングのエポック数
trainingSamplePerPrompt 1プロンプトあたりの生成回答数

実際の実行コマンドは以下。

$ aws bedrock create-model-customization-job \
  --job-name "rft-char-limit" \
  --base-model-identifier "amazon.nova-2-lite-v1:0:256k" \
  --customization-type "REINFORCEMENT_FINE_TUNING" \
  --custom-model-name "nova-2-lite-char-limit-v1" \
  --role-arn "arn:aws:iam::xxxxxx:role/BedrockRftJsonDemoRole" \
  --training-data-config '{
    "s3Uri": "s3://<created bucket>/training-data/char-limit-training.jsonl"
  }' \
  --validation-data-config '{
    "validators": [{
      "s3Uri": "s3://<created bucket>/training-data/char-limit-validation.jsonl"
    }]
  }' \
  --output-data-config '{
    "s3Uri": "s3://<created bucket>/output/"
  }' \
  --customization-config '{
    "rftConfig": {
      "graderConfig": {
        "lambdaGrader": {
          "lambdaArn": "arn:aws:lambda:us-east-1:xxxxx:function:rft-char-limit-reward"
        }
      },
      "hyperParameters": {
        "epochCount": 10,
        "trainingSamplePerPrompt": 8
      }
    }
  }' \
  --region us-east-1

Job ARNが返却されれば成功です。
私の場合、30分前後でトレーニングが完了しました。

なお、RFTジョブ実行中にLambdaでエラーが発生した場合は
CloudWatchログで確認できます。

トレーニングメトリクス確認

トレーニングメトリクスを確認しましょう。
作成したバケットのoutput以下にCSVができています。
実際のメトリクスは以下のようになってました。

トレーニング結果(10エポック):

Epoch train_reward_mean 変化
0 0.307 ベースライン
1 0.419 +0.11
2 0.542 +0.12
3 0.580 +0.04
4 0.603 +0.02
5 0.695 +0.09
6 0.736 +0.04
7 0.728 -0.01
8 0.752 +0.02
9 0.748 -0.00

検証結果:

結果は以下でした。

  • トレーニング開始時: 31% (0.307)
  • トレーニング終了時: 75% (0.748)
  • 検証データ: 93% (0.933)

モデルは文字数制限を守ることについて学習できている様子。

AWSコンソールでメトリクスを見ることもできます。
rft-1.png

カスタムモデル生成

トレーニングが完了してカスタムモデルができたので試してみましょう。
カスタムモデルを Playground でテストするには、オンデマンド推論用にデプロイする必要があります。

AWSコンソールからデプロイするのは以下。

  1. Amazon Bedrock → Custom modelsに移動
  2. 作成したカスタムモデルを選択
  3. 「Set up inference」 をクリック
  4. 「Deploy for on-demand」 を選択
  5. デプロイ名を入力して作成

AWS CLIでもカスタムモデルのデプロイが可能です。

# カスタムモデルの ARN を取得
% aws bedrock list-custom-models --region us-east-1

# オンデマンド推論用にデプロイ
% aws bedrock create-custom-model-deployment \
    --model-deployment-name "rft-char-limit-deployment" \
    --model-id "<取得したカスタムモデルのARN>" \
    --region us-east-1

参考: Deploy a custom model for on-demand inference

Playground でのテスト

デプロイ完了後、「Test in playground」 ボタンからテストできます。

rft-2.png

今回はAWS CLIを使用して、以下のように2つのモデルをテストしました。

# ベースモデル
aws bedrock-runtime converse \
    --model-id "us.amazon.nova-2-lite-v1:0" \
    --messages '[{"role":"user","content":[{"text":"機械学習を20文字以内で説明して"}]}]' \
    --inference-config '{"temperature":0,"maxTokens":100}' \
    --region us-east-1

# カスタムモデル
aws bedrock-runtime converse \
    --model-id "arn:aws:bedrock:us-east-1:ACCOUNT_ID:custom-model-deployment/DEPLOYMENT_ID" \
    --messages '[{"role":"user","content":[{"text":"機械学習を20文字以内で説明して"}]}]' \
    --inference-config '{"temperature":0,"maxTokens":100}' \
    --region us-east-1

比較結果(n=10、15〜35文字制限、temperature=0):

プロンプト 制限 カスタム ベース
機械学習を 20文字 o 17文字 o 20文字
インターネットを 25文字 o 19文字 x 27文字
データベースを 20文字 o 11文字 x 25文字
プログラミングを 30文字 o 21文字 o 28文字
ブロックチェーンを 25文字 o 13文字 o 16文字
AIとは何か 15文字 o 12文字 o 15文字
クラウドコンピューティングを 35文字 o 27文字 x 41文字
APIとは何か 20文字 x 26文字 x 31文字
セキュリティを 25文字 o 24文字 x 35文字
IoTを 15文字 o 11文字 x 26文字

強化学習したモデルのほうが良い結果がでています。
少ないテストデータでも一応、効果が確認できました。

Summary

本記事では、Amazon Bedrock の強化学習ファインチューニング (RFT) を使って、
カスタムモデルを作成して文字数制限を守らせる方法を試してみました。

Lambdaで報酬関数を定義してテストデータに対して実行させることでモデルを改善させ、
実際にモデルの性能が改善されることが確認できたかと思います。

RFTでは構造化出力やガイドラインの遵守などの評価指標を定義できるタスクが向いているので、
いろいろ利用できそうですね。

References

Appendix

CDKコード

RFT に必要なリソース(Lambda、S3、IAM Role)をデプロイする CDK スタック。

import * as cdk from "aws-cdk-lib";
import * as lambda from "aws-cdk-lib/aws-lambda";
import * as iam from "aws-cdk-lib/aws-iam";
import * as s3 from "aws-cdk-lib/aws-s3";
import * as s3deploy from "aws-cdk-lib/aws-s3-deployment";
import { Construct } from "constructs";
import * as path from "path";

export class RftRewardStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    // S3 バケット(トレーニングデータ用)
    const bucket = new s3.Bucket(this, "TrainingDataBucket", {
      bucketName: `rft-demo-${this.account}-${this.region}`,
      removalPolicy: cdk.RemovalPolicy.DESTROY,
      autoDeleteObjects: true,
    });

    // トレーニングデータを S3 にデプロイ
    new s3deploy.BucketDeployment(this, "DeployTrainingData", {
      sources: [s3deploy.Source.asset(path.join(__dirname, "../../training-data"))],
      destinationBucket: bucket,
      destinationKeyPrefix: "training-data",
    });

    // Lambda 報酬関数(Rust)
    const rewardFunction = new lambda.Function(this, "RewardFunction", {
      functionName: "rft-char-limit-reward",
      runtime: lambda.Runtime.PROVIDED_AL2023,
      architecture: lambda.Architecture.ARM_64,
      handler: "bootstrap",
      code: lambda.Code.fromAsset(
        path.join(__dirname, "../../lambda-reward/target/lambda/rft-char-limit-reward")
      ),
      timeout: cdk.Duration.minutes(1),
      memorySize: 256,
    });

    // Bedrock からの Lambda 呼び出し許可
    rewardFunction.addPermission("BedrockInvoke", {
      principal: new iam.ServicePrincipal("bedrock.amazonaws.com"),
      action: "lambda:InvokeFunction",
    });

    // Bedrock RFT ジョブ用 IAM Role
    const bedrockRole = new iam.Role(this, "BedrockRftRole", {
      roleName: "BedrockRftRole",
      assumedBy: new iam.ServicePrincipal("bedrock.amazonaws.com"),
    });

    bucket.grantRead(bedrockRole);              // トレーニングデータ読み取り
    bucket.grantWrite(bedrockRole, "output/*"); // 出力書き込み
    rewardFunction.grantInvoke(bedrockRole);    // Lambda 呼び出し

    // 出力
    new cdk.CfnOutput(this, "LambdaArn", { value: rewardFunction.functionArn });
    new cdk.CfnOutput(this, "BucketName", { value: bucket.bucketName });
    new cdk.CfnOutput(this, "RoleArn", { value: bedrockRole.roleArn });
  }
}

この記事をシェアする

FacebookHatena blogX

関連記事