[Amazon SageMaker] Llama 3.2 1B を LoRA でファインチューニングして、質問に Markdown 表で答えさせてみました

[Amazon SageMaker] Llama 3.2 1B を LoRA でファインチューニングして、質問に Markdown 表で答えさせてみました

LLMの出力フォーマットをプロンプトに頼らず安定させたい—そこで、Llama 3.2 1BをSageMaker上でLoRA微調整し、Markdown形式の表を「必ず出力する」ようにモデルに学習させてみました。結果は表遵守率0%→100%、低コストで実現できたので、その過程を紹介させてください。
2026.08.05

1 はじめに

製造ビジネステクノロジー部の平内(SIN)です。

LLM をアプリケーションに組み込むとき、「回答を必ずこのフォーマットで返してほしい」という要件はよくあります。プロンプトで「Markdown の表で答えてください」と指示する方法が手軽ですが、小型モデルでは指示が無視されたり、会話が続くうちにフォーマットが崩れたりすることがあります。

そこで、今回は、プロンプトに頼らず、モデル自体に出力フォーマットを覚えさせるため、Meta の軽量モデル Llama 3.2 1B Instruct を、Amazon SageMaker Training Job 上で LoRA(Low-Rank Adaptation)によりファインチューニングし、「比較・列挙系の質問には必ず Markdown形式の表で回答する」よう学習させてみました。

結果は、以下のとおりです。

学習前後で同じ質問(学習データには含まれない 30 問)を投げ、回答に Markdown形式の表が含まれるかを判定したところ、以下のようになりました。

表フォーマット遵守率(held-out 30 問)
Before(素の Llama 3.2 1B) 0/30(0.0%)
After(LoRA 適用後) 30/30(100.0%)

推論時のプロンプトには「表で答えて」というような指示は、一切入れていません。ファインチューニングにより、この差が出ていることになると思います。

たとえば「お茶とコーヒー、カフェイン量はどちらが多い?」と尋ねると、After のモデルは次のように回答します(回答は Markdown 形式の表を含むテキストです)。

質問: お茶とコーヒー、カフェイン量はどちらが多い?

回答:
お茶とコーヒー、カフェイン量はどちらが多いかを比較します。

| 項目     | 内容                                                                 |
|---------|----------------------------------------------------------------------|
| お茶     | 約20-40mg(1杯約250ml)                                               |
| コーヒー  | 約95-200mg(1杯約250ml)                                              |
| 比較     | コーヒーはお茶よりカフェイン量が多い。ただし、カフェイン量は種類によって異なります。|

(見やすさのため表のスペースのみ調整しています)

学習データは Amazon Bedrock(Amazon Nova)で作成し、データ合成から学習・評価まで一通りの流れを AWS 中心で構成しています。学習ジョブは全体で約 7 分(学習本体は約 2 分)、学習にかかった費用は失敗したジョブを含めても 1 ドル未満程度と、気軽に試せる規模だと思います。

2 全体構成と費用

全体の流れとコスト発生ポイントは以下のとおりです。

手順 使用サービス・環境 コスト
1 インフラ作成(S3・IAM ロール) AWS CDK S3 数円未満/月
2 学習データ合成(質問 330 件 + 模範回答) Amazon Bedrock(Nova Lite / Nova Pro) 1 ドル未満程度
3 LoRA 学習 SageMaker Training Job(ml.g5.2xlarge) 課金 442 秒(0.2〜0.3 ドル程度)
4 Before/After 比較・遵守率算出 ローカル Mac(MPS) -
5 クリーンアップ cdk destroy -

学習は、SageMaker Training Job で必要時のみの起動とし、推論は、ローカルで実施しています。1B モデル + LoRA アダプタであれば、Apple Silicon の Mac で十分推論できていました。

サンプルコードは GitHub で公開しています。

GitHub: sagemaker-lora-markdown-table

3 事前準備

(1) Llama のライセンス同意と HF_TOKEN

Llama 3.2 は gated model のため、モデルページで Meta のライセンス(LLAMA 3.2 COMMUNITY LICENSE AGREEMENT)に同意し、アクセス承認を受ける必要があります。ページ上部のフォームに氏名・所属等を入力して送信すると、ステータスが PENDING になり、承認されると ACCEPTED に変わります(今回は 10 分程度で承認されました)。

005
001

あわせて、Settings > Access Tokens で Read 権限のアクセストークンを作成しておきます。このトークンは学習ジョブとローカル推論の両方で使用します。

004

(2) SageMaker のクォータ確認

GPU インスタンスの Training Job 用クォータは、デフォルトで 0 のアカウントがあります。事前に確認しておきましょう。

aws service-quotas list-service-quotas --service-code sagemaker --region ap-northeast-1 \
  --query "Quotas[?contains(QuotaName, 'ml.g5.2xlarge for training job usage')].{Name:QuotaName,Value:Value}" \
  --output table

値が 0 の場合は Service Quotas から引き上げ申請が必要です。

(3) インフラの作成

S3 バケットと SageMaker 実行ロールを CDK で作成します。

git clone https://github.com/furuya02/sagemaker-lora-markdown-table.git
cd sagemaker-lora-markdown-table/cdk
pnpm install
pnpm cdk deploy

4 学習データの設計と合成

学習データは、Amazon Bedrock で合成しました。

(1) データ設計の方針

学習させたいのは「比較・列挙が自然な質問に対して、短い導入文 + Markdown 表で回答する」という振る舞いです。そこで、以下の形式の対話データを数百件用意します。

{"messages": [
  {"role": "user", "content": "EC2とLambdaの違いを教えて"},
  {"role": "assistant", "content": "(1〜2文の導入)\n\n| 項目 | EC2 | Lambda |\n|---|---|---|\n| ... | ... | ... |"}
]}

ポイントは、質問のジャンルを技術に限定せず、料理・旅行・歴史・スポーツなど幅広く散らしたことです。「特定ドメインの知識」ではなく「表で答えるという形式」を学習させたいためです。

(2) 質問の生成(Nova Lite)

Github scripts/generate_questions.py

1 段階目として、比較・列挙が自然な質問を Nova Lite で 330 件生成します。プロンプトの要点は以下です。

PROMPT = """あなたは学習データ作成のアシスタントです。
「比較」や「列挙」で答えるのが自然な日本語の質問を{n}個生成してください。
- 技術に限らず、料理・旅行・歴史など幅広いジャンルにする
- 質問のみをJSON配列で出力(["質問1", "質問2", ...])"""

30 件ずつバッチで生成し、重複を除去しながら目標件数まで繰り返します。

006

(3) 模範回答の生成(Nova Pro)

Github scripts/generate_answers.py

2 段階目として、各質問への模範回答を Nova Pro で生成します。ここで出力フォーマットを厳密に固定するのがポイントです。

PROMPT = """次の質問に対する模範回答を作成してください。
質問: {question}
条件:
- 1〜2文の短い導入文の後、必ず Markdown 表でまとめる
- 表は `| 項目 | 内容 |` のようにヘッダ行と区切り行を持つ正しい Markdown 形式にする
- 表の後には文章を続けず、表で回答を終える"""

007

(4) 検証と分割

Github scripts/validate_dataset.py

合成データが本当に表を含んでいるかを、正規表現(ヘッダ行 + 区切り行)で機械検証します。

total: 330, valid: 330, invalid(表なし): 0
train: 300 -> data/train.jsonl
heldout: 30 -> data/heldout.jsonl

今回は 330 件すべてが有効でした。このうち 30 件は学習に使わず、評価用(held-out)として取り分けています。この正規表現は後述の遵守率判定でも同じものを使い、データ品質と評価の基準を揃えています。

5 LoRA 学習

(1) Estimator の定義

Github scripts/run_training.py

学習は SageMaker の Hugging Face Deep Learning Container(DLC)を使った Script Mode で実行します。

estimator = HuggingFace(
    entry_point="train.py",
    source_dir="src",  # src/requirements.txt が起動時に pip install される
    role=role,
    instance_type="ml.g5.2xlarge",
    transformers_version="4.36.0",
    pytorch_version="2.1.0",
    py_version="py310",
    environment={"HF_TOKEN": os.environ["HF_TOKEN"]},  # gated model の認証
    hyperparameters={"epochs": 3, "learning-rate": 2e-4, "lora-r": 16, "lora-alpha": 32},
    max_run=3600,  # 暴走時も1時間で強制終了
)

DLC 自体の transformers は 4.36.0 ですが、source_dir に置いた requirements.txt がコンテナ起動時にインストールされ、ライブラリを上書きできます。再現性のためバージョンは固定されています。

transformers==4.46.3
trl==0.12.1
peft==0.13.2
datasets==3.1.0
accelerate==1.1.1

gated model をコンテナ内でダウンロードするため、HF_TOKEN を環境変数としてジョブに渡しています。

(2) 学習スクリプト

Github src/train.py

学習本体は TRL の SFTTrainerPEFT を組み合わせた、ごく標準的な LoRA 学習です。

# train.py(要点抜粋)
use_bf16 = torch.cuda.is_bf16_supported()  # T4等の非対応GPUではfp16に切替
dtype = torch.bfloat16 if use_bf16 else torch.float16

model = AutoModelForCausalLM.from_pretrained(args.model_id, torch_dtype=dtype)
tokenizer = AutoTokenizer.from_pretrained(args.model_id)
if tokenizer.pad_token is None:
    tokenizer.pad_token = tokenizer.eos_token  # Llama系はpad_tokenを持たない

peft_config = LoraConfig(
    r=16, lora_alpha=32, lora_dropout=0.05,
    target_modules=["q_proj", "k_proj", "v_proj", "o_proj"],
    task_type="CAUSAL_LM",
)
training_args = SFTConfig(
    output_dir="/opt/ml/model",
    num_train_epochs=3,
    per_device_train_batch_size=1,
    gradient_accumulation_steps=4,   # 実効バッチ4
    gradient_checkpointing=True,     # 省メモリ化
    learning_rate=2e-4,
    max_seq_length=1024,
    bf16=use_bf16, fp16=not use_bf16,
)
trainer = SFTTrainer(model=model, args=training_args,
                     train_dataset=dataset, processing_class=tokenizer,
                     peft_config=peft_config)
trainer.train()

データセットが messages 形式であれば、SFTTrainer がチャットテンプレートを自動適用してくれるため、前処理コードはほとんど不要です。/opt/ml/model に保存したアダプタは、ジョブ完了時に model.tar.gz として S3 へ自動アップロードされます。

(3) ハイパーパラメータ

項目 補足
LoRA rank (r) 16 フォーマット学習が目的のため控えめの値
lora_alpha 32 r の 2 倍という一般的な設定
対象モジュール q/k/v/o_proj Attention 層のみ
learning_rate 2e-4 LoRA でよく使われる値
エポック 3 300 件 × 3 エポック ÷ 実効バッチ 4 = 225 ステップ
実効バッチ 4(1 × 勾配累積 4) OOM 対策。詳細は 7 章

(4) Loss 推移と実測値

学習は 225 ステップ、実行時間 131 秒で完了しました。Loss は 2.08 から 1.20 まで素直に収束しています。

002

{'train_runtime': 130.9612, 'train_samples_per_second': 6.872,
 'train_loss': 1.4082220183478462, 'epoch': 3.0}
Training seconds: 442
Billable seconds: 442

課金対象は 442 秒(イメージ取得や pip install を含む)でした。SageMaker の料金に照らすと、成功したこのジョブ単体で 0.2〜0.3 ドル程度です。

003

6 Before/After 比較

(1) 定量評価: 表フォーマット遵守率

Github scripts/compare_before_after.py

評価は held-out の 30 問を学習前後のモデルに投げ、回答が正しい Markdown 表(ヘッダ行 + 区切り行)を含むかを正規表現で判定しました。推論はローカルの Mac(MPS)で行い、プロンプトにフォーマット指示は入れていません。

# Before(素の Llama 3.2 1B)
python scripts/compare_before_after.py --output results/before.jsonl
# After(LoRA アダプタ適用)
python scripts/compare_before_after.py --adapter model/ --output results/after.jsonl
遵守率
Before 0/30(0.0%)
After 30/30(100.0%)

Before は 30 問すべてで表を出さず、After は 30 問すべてで表を出しました。

(2) 定性比較: 同じ質問への出力

冒頭で示した「お茶とコーヒー、カフェイン量はどちらが多い?」を、学習前のモデルにも同じように投げてみます。Before はプレーンな文章で、しかも結論を誤っています。

お茶とコーヒーは両方ともカフェインを含むものですが、量は異なります。

お茶は、主に茶碗から取り出された茶の液体にカフェインが含まれるため、カフェイン量が
高いです。一般的なお茶のカフェイン量は、1杯お茶が約20-40mgのカフェインを含むことが
あります。

コーヒーは、コーヒーの種類やカフェインの量によってカフェイン量が異なりますが、
一般的にコーヒーはカフェイン量が約60-120mgのものです。

したがって、お茶はカフェイン量が高いです。

Before は「お茶のほうがカフェインが多い」と誤って結論づけています。一方、After は冒頭で示したとおり導入文 + Markdown 表で回答し、この例では内容も概ね妥当でした。同じモデル・同じ質問でも、出力の形式(と、この例では正確さ)が変わっています。

なお、モデルが出力しているのは、あくまで Markdown 記法で書かれたテキストです。チャット UI やブログなど Markdown をレンダリングする環境に載せれば、そのまま表として表示されます。

(3) 注意: 回答の「中身」の正確さは別問題

今回学習したのはあくまで「表というフォーマット」であり、回答内容の正確さは改善していません。1B という小型モデルのため、After でも事実誤りは発生します。たとえば「マクドナルドとケンタッキーを比較して」と尋ねると、表の体裁は整っていましたが「食材: 鍋で調理されたメニュー」といった、事実と異なる内容が出力されていました。

フォーマット制御と知識・正確性は別の問題です。実務で使う場合は、正確性が必要な内容は RAG などで供給し、LoRA は出力形式の安定化に使う、という役割分担が現実的だと思います。

(4) 副作用の簡易チェック

表を強制学習した副作用として「どんな質問にも表で答えてしまう」ようになっていないかを、表が不適切な 5 問で確認しました。

質問 表を出したか
こんにちは 出さない
1たす1は? 出さない
ありがとう 出さない
日本の首都はどこ? 出さない(「東京です」と正答)
俳句を一つ詠んでください 出した

挨拶や単純な質問には表を出さず自然に応答しており、極端な過学習は避けられています。一方で「俳句を詠んで」という創作依頼には表を当てはめてしまいました。学習データの質問ジャンルは、工夫が必要だと思いました。

7 ハマったこと

今回の検証で実際に遭遇したエラーを、発生順に共有します。いずれも 2026 年 8 月時点の情報です。

(1) SageMaker Python SDK v3 で HuggingFace Estimator が消えた

ローカルの venv に pip install sagemaker すると v3 系(3.18.0)が入り、以下のエラーになりました。

ModuleNotFoundError: No module named 'sagemaker.huggingface'

SageMaker Python SDK は v3 で API が刷新され、従来のフレームワーク Estimator は ModelTrainer に置き換えられています(移行ガイド)。従来の書き方を使う場合は v2 系の固定が必要です。

sagemaker>=2.230.0,<3

(2) g4dn(T4)は bf16 非対応

より安価な g4dn.xlarge を試したところ、学習開始直後に失敗しました。

ValueError: Your setup doesn't support bf16/gpu. You need torch>=1.10,
using Ampere GPU with cuda>=11.0

bf16 が使えるのは Ampere 世代以降(G5 の A10G など)で、G4dn の T4(Turing 世代)は非対応です。torch.cuda.is_bf16_supported() で判定して bf16/fp16 を切り替える必要がありました。

(3) Llama のトークナイザに pad_token がない

ValueError: Asking to pad but the tokenizer does not have a padding token.

Llama 系トークナイザは pad_token を持たないため、バッチ学習のパディングで失敗します。対処として tokenizer.pad_token = tokenizer.eos_token を設定しました。

(4) A10G 24GB でも CUDA OOM

バッチサイズ 4 で学習を始めたところ、36 ステップ目で OOM になりました。

torch.cuda.OutOfMemoryError: CUDA out of memory. Tried to allocate 1.77 GiB.
GPU 0 has a total capacty of 22.30 GiB of which 1006.69 MiB is free.

スタックトレースを見ると、loss 計算時に logits を fp32 へ変換する箇所(logits.float())で発生していました。Llama 3.2 は語彙数が約 12.8 万と大きく、「バッチ 4 × 系列長 1024 × 語彙 12.8 万」の logits の fp32 化は 1B モデルでも無視できないサイズになります。バッチサイズを 1 に下げ、勾配累積 4 で実効バッチを維持し、gradient checkpointing を有効化することで解決しました。

8 まとめ

Llama 3.2 1B を SageMaker 上で LoRA ファインチューニングし、Markdown形式の表という出力フォーマットをモデルに焼き込んでみました。

  • 表フォーマット遵守率は 0% → 100% になり、プロンプトなしでフォーマットが安定した
  • 学習データ 300 件・ジョブ約 7 分・課金 442 秒(0.2〜0.3 ドル程度)と、手軽な規模で効果が出た
  • Bedrock でのデータ合成(質問生成 → 模範回答生成の 2 段階)により、学習データを人手なしで用意できた
  • 一方で、回答内容の正確さはフォーマットとは別問題であり、副作用(創作依頼まで表化)もゼロではない

プロンプトだけでは崩れやすい出力フォーマットの固定は、LoRA の得意分野だと感じました。このモデルであればローカル推論でも利用可能です。フォーマット部分を LoRA で安定させ、知識は RAG で供給する、といった組み合わせにも応用できそうです。

サンプルコードはこちらに置きました。

GitHub: sagemaker-lora-markdown-table

9 参考リンク

この記事をシェアする

AWSのお困り事はクラスメソッドへ

関連記事