はじめに
LLMの活用が進む中で、「安全性(Safety)」の担保は実運用における最優先事項の一つです。特にユーザー入力やモデル出力をリアルタイムで監視する「セーフティ・ガードレール」の導入が一般的になっています。
本記事では、Metaが提供する安全性モデル Llama-Guard 3 と前世代の Llama-Guard 2 を比較検証しました。Google Colab(T4 GPU)という限られたリソース下で、4bit量子化モデルを用いた際の挙動と精度をレポートします。
要注意: 今回は限られたリソースの中で200程度のサンプル数で検証しています。ここでの精度や性能はあくまでも目安として参考にしてください。
Llama-Guard 3 の進化点
Llama-Guard 3(8B)は、Llama 3.1 8Bをベースにファインチューニングされたモデレーション専用モデルです。
- 多言語対応の強化: 従来の英語中心から、日本語を含む複数言語への対応が謳われています。
- カテゴリ体系の刷新: Guard 2のS1〜S11から、Guard 3ではS1〜S14へと拡張。現代のAIリスク(選挙の誠実性や個人情報の保護など)により即した分類が可能になりました。
- 軽量・高速: 8Bサイズのため、量子化を活用すればコンシューマ向けGPUでも十分に運用可能です。
環境
今回の検証は、以下の環境で実施しました。Google Colabの無料版(T4 GPU)でも十分に動作可能な構成です。
- 実行環境: Google Colab (Python 3.10+)
- GPU: NVIDIA Tesla T4 (VRAM 16GB)
-
主なライブラリ:
-
transformers: 4.43.0+ -
bitsandbytes: 0.43.1+ (4bit量子化用) -
accelerate: 0.33.0+ (モデルのデバイス割り当て用) -
datasets: 2.20.0+ (ToxicChat読み込み用)
-
-
モデル:
meta-llama/Llama-Guard-3-8Bmeta-llama/Llama-Guard-2-8B
- 量子化手法: 4bit NormalFloat4 (NF4)
検証結果サマリー
先に実験結果の比較表を提示します。
| 指標 | Llama-Guard 2 (4bit) | Llama-Guard 3 (4bit) |
|---|---|---|
| Accuracy (正解率) | 63.0% | 65.0% |
| 主な検出カテゴリ | S11 (性的コンテンツ) に偏り | S4 (個人情報), S12 (選挙) など多角的 |
| 推論時間 (200件) | 約10分 | 約11分 |
考察:なぜAccuracyが60%台なのか
一見低い数値に見えますが、これには以下の要因があると考えられます。
-
データセットの難易度: 使用した
ToxicChatは、ChatGPT等へのJailbreak 試行を含むトリッキーなプロンプトが多く、単純なキーワードマッチでは判定できないものが含まれます。 - 量子化の影響: 4bit(NF4)への圧縮により、境界線上の判定に揺らぎが生じている可能性があります。
- 安全性の定義差: データセットのラベルと、Llama-Guardが定義するS1〜S14のポリシーに微細なズレがあるため、一概に「誤判定」とは言えないケースも含まれます。
実装コード (Google Colab / T4 GPU)
Llama-Guard 3(および2)はMetaへの利用申請が必要です。Hugging Faceで承諾を得た後、HF_TOKEN を取得して実行してください。
import torch
import json
import pandas as pd
from transformers import AutoTokenizer, AutoModelForCausalLM, BitsAndBytesConfig
from datasets import load_dataset
from tqdm import tqdm
# 1. 環境設定(ここは2と3で切り替え)
model_id = "meta-llama/Meta-Llama-Guard-2-8B"
# model_id = "meta-llama/Llama-Guard-3-8B"
device="cuda"
# 4bitに量子化
bnb_config = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_quant_type="nf4",
bnb_4bit_compute_dtype=torch.bfloat16,
bnb_4bit_use_double_quant=True
)
# 2. モデルとトークナイザーのロード
print("Loading model (Llama Guard 2 - 8B)...")
tokenizer = AutoTokenizer.from_pretrained(model_id)
model = AutoModelForCausalLM.from_pretrained(
model_id,
quantization_config=bnb_config,
device_map="cuda",
)
def moderate(chat):
"""
Llama Guard 2/3 による有害性判定
"""
# テンプレート適用 (BatchEncodingオブジェクトを取得)
inputs = tokenizer.apply_chat_template(chat, return_tensors="pt").to(device)
with torch.no_grad():
# **inputs で辞書を展開して渡すことで、型エラーを回避
output = model.generate(
**inputs,
max_new_tokens=20,
pad_token_id=0,
do_sample=False
)
# 入力プロンプトの長さを取得して、生成された判定部分のみを抽出
prompt_len = inputs["input_ids"].shape[-1]
return tokenizer.decode(output[0][prompt_len:], skip_special_tokens=True).strip()
# 3. データのロードと準備 (100-100抽出)
print("Loading ToxicChat dataset...")
dataset = load_dataset("lmsys/toxic-chat", "toxicchat0124", split="train")
# 有害100件、安全100件をランダムに抽出
toxic_samples = dataset.filter(lambda x: x['toxicity'] == 1).shuffle(seed=42).select(range(100))
safe_samples = dataset.filter(lambda x: x['toxicity'] == 0).shuffle(seed=42).select(range(100))
test_list = list(toxic_samples) + list(safe_samples)
# 4. 評価ループの実行
results = []
print(f"Starting Evaluation for {len(test_list)} samples...")
for data in tqdm(test_list):
chat = [
{"role": "user", "content": data["user_input"]},
{"role": "assistant", "content": data["model_output"]}
]
prediction = moderate(chat)
# 正解ラベルの整理
gt_label = "unsafe" if data["toxicity"] == 1 else "safe"
# 判定結果の保存
results.append({
"conv_id": data["conv_id"],
"user_input": data["user_input"],
"ground_truth": gt_label,
"prediction": prediction,
"is_correct": (gt_label in prediction) # Llama Guardの出力に unsafe/safe が含まれるか
})
# 5. 結果の集計とJSON出力
accuracy = sum(1 for r in results if r["is_correct"]) / len(results)
print(f"\nFinal Accuracy: {accuracy * 100:.2f}%")
# JSONファイルに保存
with open("output_results2.json", "w", encoding="utf-8") as f:
json.dump(results, f, indent=2, ensure_ascii=False)
print("\n--- output_results2.json に詳細を保存しました ---")
df = pd.DataFrame(results)
# 有害カテゴリの集計を表示
print("\n### 検出された有害カテゴリ(S1〜S14)の内訳 ###")
print(df[df['prediction'].str.contains('unsafe')]['prediction'].value_counts())
コード解説
4bit量子化
bnb_config = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_quant_type="nf4",
bnb_4bit_compute_dtype=torch.bfloat16,
bnb_4bit_use_double_quant=True
)
T4 GPUのVRAMは16GBですが、8Bモデルをfloat16でロードすると約16GB必要になりギリギリです。4bit量子化(nf4)を使うことで約4〜5GBまで削減でき、Colabの無料枠でも安定して動作します。
各パラメータの意味は以下の通りです。
| パラメータ | 説明 |
|---|---|
load_in_4bit=True |
モデルの重みを4bitで読み込む |
bnb_4bit_quant_type="nf4" |
NormalFloat4形式で量子化 |
bnb_4bit_compute_dtype=torch.bfloat16 |
実際の演算はbfloat16で行い精度劣化を抑える |
bnb_4bit_use_double_quant=True |
量子化定数をさらに量子化し、メモリをさらに節約 |
モデルのロード
model = AutoModelForCausalLM.from_pretrained(
model_id,
quantization_config=bnb_config,
device_map="cuda",
)
device_map="cuda"を指定することで、モデル全体をGPUに配置します。device_map="auto"にするとCPUとGPUを自動で分散配置しますが、今回はT4に全量乗せられるため"cuda"を明示しています。
moderate関数(推論の流れ)
def moderate(chat):
inputs = tokenizer.apply_chat_template(chat, return_tensors="pt").to(device)
with torch.no_grad():
output = model.generate(
**inputs,
max_new_tokens=20,
pad_token_id=0,
do_sample=False
)
prompt_len = inputs["input_ids"].shape[-1]
return tokenizer.decode(output[0][prompt_len:], skip_special_tokens=True).strip()
apply_chat_template
Llama-Guardは通常のチャットモデルと同じく、会話をテンプレートに変換してから入力します。apply_chat_templateはモデルごとに定義されたフォーマット(特殊トークンの挿入など)を自動で適用してくれます。Llama-Guard専用のプロンプト形式が内部的に使われており、手動で組み立てるより確実です。
torch.no_grad()
推論時は勾配計算が不要なため、no_grad()コンテキストで囲むことでメモリと計算コストを削減しています。
max_new_tokens=20
Llama-Guardの出力はsafeまたはunsafe\nS*の形式のみなので、長いテキストは生成されません。20トークンあれば十分で、余分な生成を防ぎます。
do_sample=False
確率的サンプリングを無効にし、貪欲法(greedy decoding)で最もスコアの高いトークンを選択します。モデレーションタスクは決定論的な判定が望ましいため、Falseが適切です。
prompt_lenによるスライス
model.generate()はプロンプト部分も含めたトークン列を返します。入力長をinputs["input_ids"].shape[-1]で取得し、それ以降(生成部分のみ)をデコードすることで、safeやunsafe\nS11といった判定結果だけを抽出しています。
データのサンプリング
toxic_samples = dataset.filter(lambda x: x['toxicity'] == 1).shuffle(seed=42).select(range(100))
safe_samples = dataset.filter(lambda x: x['toxicity'] == 0).shuffle(seed=42).select(range(100))
ToxicChatデータセットにはtoxicityフラグが付与されており、1が有害・0が安全を示します。seed=42を固定することで再現性を確保しています。今回は有害・安全それぞれ100件ずつ抽出し、評価しました。
正解判定ロジック
"is_correct": (gt_label in prediction)
Llama-Guardの出力はsafeやunsafe\nS11のように改行付きで返ることがあります。完全一致ではなくin演算子で部分一致を使うことで、カテゴリ情報が付いていても正しく判定できます。ただしこの方法には注意点もあり、ground_truthが"safe"のときpredictionが"unsafe\nS11"であっても"safe" in "unsafe\nS11"がFalseになるため、誤って正解とはなりません。実際には問題なく動作します。
T4 GPUのVRAMは16GBですが、8Bモデルをfloat16でロードすると約16GB必要になりギリギリです。4bit量子化(nf4)を使うことで約4〜5GBまで削減でき、Colabの無料枠でも安定して動作します。bnb_4bit_use_double_quant=Trueは量子化定数をさらに量子化するオプションで、精度をほぼ落とさずにメモリをさらに節約できます。
出力結果(Llama-Guard 2 8b 4bit)
Loading model (Llama Guard 2 - 8B)...
Loading weights: 100%
291/291 [01:04<00:00, 5.99it/s]
Loading ToxicChat dataset...
Starting Evaluation for 200 samples...
100%|██████████| 200/200 [10:13<00:00, 3.07s/it]
Final Accuracy: 63.00%
--- output_results.json に詳細を保存しました ---
### 検出された有害カテゴリ(S1〜S14)の内訳 ###
prediction
unsafe\nS11 17
unsafe\nS3 3
unsafe\nS2 2
unsafe\nS1 2
unsafe\nS10 1
unsafe\nS8 1
unsafe\nS5 1
unsafe\nS5,S11 1
unsafe\nS2,S6 1
Name: count, dtype: int64
出力結果(Llama-Guard 3 8b 4bit)
Loading model (Llama Guard 3 - 8B)...
Loading weights: 100%
291/291 [01:10<00:00, 6.03it/s]
Loading ToxicChat dataset...
Starting Evaluation for 200 samples...
100%|██████████| 200/200 [10:47<00:00, 3.24s/it]
Final Accuracy: 65.00%
--- output_results.json に詳細を保存しました ---
### 検出された有害カテゴリ(S1〜S14)の内訳 ###
prediction
unsafe\nS12 9
unsafe\nS4 8
unsafe\nS3 3
unsafe\nS1 3
unsafe\nS11 3
unsafe\nS9 2
unsafe\nS2 1
unsafe\nS14 1
Name: count, dtype: int64
今回やったこと
今回は以下の流れで実験を行いました。
-
環境構築: Google Colab(T4 GPU)上に
transformersとbitsandbytesを用いた4bit量子化推論環境を構築 -
データ準備:
lmsys/toxic-chat(toxicchat0124)から有害100件・安全100件の計200件をサンプリング - モデル評価: Llama-Guard 2とLlama-Guard 3をそれぞれ同一条件で評価し、Accuracyを比較
- 結果分析: safe/unsafeの判定精度に加え、検出された有害カテゴリ(S1〜S14)の分布を確認
結果をまとめると以下の通りです。
| モデル | Accuracy | 推論時間(200件) |
|---|---|---|
| Llama-Guard 2(4bit) | 63.0% | 約10分 |
| Llama-Guard 3(4bit) | 65.0% | 約11分 |
精度はLlama-Guard 3が2ポイント上回りました。推論速度はほぼ同等です。
また検出カテゴリの分布にも違いが見られました。Guard 2はS11(性的コンテンツ)への偏りが顕著だったのに対し、Guard 3はS4(個人情報)やS12(選挙関連)などより広いカテゴリをバランスよく検出できていました。これはLlama-Guard 3のカテゴリ体系がGuard 2から刷新されていることを反映していると考えられます。
一方で、両モデルともにunsafeサンプルの見逃し(偽陰性)が多い傾向が確認されました。ToxicChatはChatGPT向けのjailbreakやロールプレイ系プロンプトに偏ったデータセットであり、Llama-Guardのトレーニング分布とのミスマッチが原因として考えられます。
本実験の限界
今回の結果を解釈する上で、以下の点に注意が必要です。
- サンプル数が少ない: 200件(有害・安全各100件)での評価であり、統計的な信頼性は限定的です
- 均等サンプリングの影響: 有害・安全を50:50で抽出していますが、実運用での有害比率は5〜10%程度とされています。現実の分布とは乖離があるため、Accuracyの数値をそのまま実運用性能として解釈することはできません
- 量子化による精度劣化: 4bit量子化によって元モデルからの性能低下が生じている可能性があります
- データセットとの分布差: ToxicChatはChatGPT向けのjailbreakに偏ったデータセットであり、Llama-Guardのトレーニング分布と一致しない部分があります
今後の展望
今回の実験をベースに、以下の方向での追加検証を予定しています。
データセットの拡充
ToxicChatはサンプル数・分布の偏りに課題があります。PKU-Alignment/BeaverTails(約33万件、14カテゴリ)やAnthropic/hh-rlhfなど、より大規模かつ多様なデータセットでの評価を行い、性能を確認したいと考えています。
評価指標の拡張と現実的なクラス比率での再評価
今回はAccuracyのみで評価しましたが、モデレーションタスクではクラス不均衡が多く、Accuracyだけではモデルの見逃しが見えにくいです。実運用での有害比率は5〜10%程度とされており、今回の50:50均等サンプリングとは乖離があります。現実的なクラス比率(例:有害10%・安全90%)での再評価と合わせて、Precision・Recall・F1スコアも計測することで、実運用上の性能をより正確に把握したいと思います。
評価指標の拡張
今回はAccuracyのみで評価しましたが、モデレーションタスクではクラス不均衡が多く、Accuracyだけではモデルの見逃しが見えにくいです。Precision・Recall・F1スコアなども合わせて評価することで、実運用上の性能をより正確に把握したいと思います。
量子化精度の比較
今回は4bit量子化のみで評価しましたが、8bit量子化やfloat16(GPU増強)との精度差も検証する価値があります。特に安全性モデルは量子化による精度劣化が実運用に直結するため、この点の把握は重要になります。
日本語プロンプトへの対応
Llama-Guard 3は多言語対応が強化されているとされていますが、日本語プロンプトに対する判定精度は未検証です。日本語の有害コンテンツデータセットを用いた評価も今後の課題です。
おわりに
本記事では、Llama-Guard 2とLlama-Guard 3をGoogle ColabのT4 GPU上で4bit量子化して動かし、ToxicChatデータセットで性能比較を行いました。
Guard 2では性的コンテンツ(S11)に過剰反応する傾向が見られましたが、Guard 3では個人情報(S4)や選挙(S12)など、よりビジネス実務で問題になりやすいカテゴリをバランスよく拾えていました。
Llama-Guard 3はカテゴリの多様性においてGuard 2を上回る結果となり、実運用での採用候補として有力です。ただし全体的なAccuracyはまだ65%程度にとどまっており、特に有害コンテンツの見逃しが多い点は課題として残ります。
LLMの安全性確保は一つのモデルで完結するものではなく、ルールベースのフィルタリングや人間によるレビューとの組み合わせが現実的なアプローチになるでしょう。本記事が、LLMの安全性対策を検討している方の参考になれば幸いです。
参照