Generative AI

Nemotron-Personas-Japan を用いた NVIDIA NeMo Data Designer による合成データ生成

Reading Time: 4 minutes

なぜ高品質な合成データが必要か

大規模言語モデル (LLM) の評価やファインチューニングにおいて、データの量だけでなく品質が性能を大きく左右することは広く知られています。 しかし、日本語タスクでは以下の課題が顕在化しています。

  • 人手によるデータ作成やラベル付けは高コストでスケールしない  
  • 文化的または生活文脈を反映した自然な日本語データが不足している

さらに、合成データは「生成すれば自然に良くなる」ものではありません。何をどれくらい生成するのか、どのような観点をカバーしたいのかといった設計がない場合、データは意図しない偏りを持つ可能性があります。そのため、生成前にデータ構成の起点となる情報 (Seed) を設計することが重要になります。

NeMo Data Designer の特徴

合成データ生成のためのフレームワーク

NeMo Data Designer は、単なる LLM プロンプト生成ツールではなく、複数の生成バックエンド、テンプレート、検証器、構造化スキーマを組み合わせたフレームワークです。この構成により、単純なテキスト生成と比較して以下の強みがあります。

  • 構造化生成 (Jinja テンプレート + Pydantic バリデーション)
     出力をスキーマに従わせることで、後処理が不要な高品質な JSON/CSV 形式でのデータ出力が可能
  • 複数バックエンド対応
    1 つ以上のモデル (例: GPT-OSS-120B や確率的グラフィカルモデル) を組み合わせることで、統計的根拠と自然言語生成の両立が可能
  • 再試行および自動検証機能
    自動リトライ、生成内容の検証ルール、構文スキーマの整合性チェックなど、大量生成時でも品質を担保する仕組みが組み込まれている
  • 現実世界の分布に基づいた生成
    統計的な分布にもとづく生成ロジックにより、単純なプロンプト生成よりも現実に近いデータ分布を再現可能

まとめると、NeMo Data Designer は単なる LLM 合成生成を越え、構造化、検証、再現性、多様性という観点で品質とスケールを両立した Synthetic Data フレームワークとして設計されています。

Nemotron-Personas-Japan は、日本の人口統計、職業、生活背景を反映したペルソナ合成データセットです。本記事では、このデータセットから有用な情報を抽出し、NeMo Data Designer における Seed データとして利用します。

Nemotron-Personas-Japan
        ↓(Seed 抽出)
NeMo Data Designer
        ↓(合成データ生成)
Synthetic JCommonsenseQA
        ↓(品質評価)
Filtered Dataset

本記事では、以下について解説します。

  • NeMo Data Designer を用いた高品質な合成データ生成の考え方
  • Nemotron-Personas-Japan を Seed として活用する方法
  • JCommonsenseQA を例とした具体的な実装手順

検証環境

NeMo Data Designer Notebook を用いた合成データ生成

NeMo Data Designer を Docker で起動

本ブログでは NeMo Data Designer を Docker で起動する方法を採用しています。

図 1. Docker compose を用いた NeMo Data Designer デプロイ構成

Docker の起動とは別で notebook を起動する必要があります。GenerativeAIExamples で記載されている環境構築方法を参照して環境構築をする必要があります。

下記コマンドで起動します。

uv run jupyter-lab

全コードは本ブログに記載できないので japanese_commonsense_qa_data_generator_nemotron_persona_jp_seed.ipynb に載せています。

以降では、実行において特に重要な設定や設計判断に関わるコードのみを抜粋して説明します。

NeMo Data Designer において使用する LLM (openai/gpt-oss-120b) を 2 つの役割に分けて定義

MODEL_PROVIDER = "nvidiabuild"
MODEL_ID = "openai/gpt-oss-120b"
MODEL_ALIAS = "gpt-oss-120b"
SYSTEM_PROMPT = ""
JUDGE_MODEL_ALIAS = "quality-judge"

model_configs = [
    ModelConfig(
        alias=MODEL_ALIAS,
        model=MODEL_ID,
        provider=MODEL_PROVIDER,
        inference_parameters=InferenceParameters(
            temperature=0.9,
            top_p=0.95,
            max_tokens=2048,
            max_parallel_requests=16,
            timeout=1200
        ),
    ),
    ModelConfig(
        alias=JUDGE_MODEL_ALIAS,
        model=MODEL_ID,
        provider=MODEL_PROVIDER,
        inference_parameters=InferenceParameters(
            temperature=0.3,
            top_p=0.9,
            max_tokens=1024,
            max_parallel_requests=8,
            timeout=1500,
        ),
    ),
]

NeMo Data Designer において使用する LLM (openai/gpt-oss-120b) を 2 つの役割に分けて定義しています。より厳密な評価設定としては、生成と評価に異なるモデルを用いることで、評価の独立性を高める方法も考えられます。

一方で、本記事ではコストや運用面での現実性、ならびにパイプライン全体の簡潔さを重視し、同一モデルを推論設定と役割を切り替えて Generator と Judge の双方に利用しています。

  • 生成モデル (Generator): 合成データ (jcommonsenseqa 問題) を作るためのモデル
    temperature を上げて多様性を上げる設定
  • 評価モデル (Judge): 生成されたデータの品質を採点または検証するためのモデル
    temperature を下げて評価結果の ばらつきを抑え、再現性を重視

MODEL_PROVIDER = "nvidiabuild"

  • どの 推論基盤 (プロバイダー) を使うかを指定
  • “nvidiabuild” は、NVIDIA が提供する OpenAI API 互換の推論エンドポイント (NVIDIA NIM APIs) を指す
    API Key が必要なので NVIDIA NIM APIs で取得が必要

大量データを生成するのには向いていないので、大規模生成ではローカルでデプロイした LLM を使用する方法をおすすめします。

MODEL_ID = "openai/gpt-oss-120b"

実際に使用する モデル名を設定しています。ここでは openai/gpt-oss-120b (120B パラメーターの大規模モデル) を設定してます。

MODEL_ALIAS = "gpt-oss-120b"

NeMo Data Designer 内部で参照するための 論理名 (エイリアス)

後続の設定 (LLMStructuredColumnConfig など) で model_alias=”gpt-oss-120b” のように指定して使用するための設定です。今回は空にしています。

SYSTEM_PROMPT = ""

LLM に常に与える 共通のシステム プロンプト

JUDGE_MODEL_ALIAS = "quality-judge"

品質評価用モデルのエイリアス

実体は同じ openai/gpt-oss-120b ですが、用途 (評価) を分離するため、別名で定義しています。

max_parallel_requests を上げることで合成データの生成時間を短くできるので、計算リソースが十分あれば上げることを推奨します。

Nemotron-Personas-Japan をシードとして用意

nvidia/nvidia-nemotron-nano-9b-v2 で JCommonsenseQA のタスクを解かせて、弱点となる部分を補完できるような Seed を抽出するようにしました。このように、対象タスクに対して既存モデルの誤答傾向を分析し、それに基づいて Seed を選別するアプローチは、JCommonsenseQA に限らず、他の評価や学習タスクにも一般化可能です。

JCommonsenseQA は、日本語での常識推論能力を評価する多肢選択式 QA タスクです。  

英語版 CommonsenseQA をベースに、日本語話者向けに構築されており、JGLUE ベンチマークの一部としても利用されています。

合成データ生成の品質評価対象として扱いやすいという理由から、本記事では JCommonsenseQA を対象タスクとして選択しています。

下記のジャンルの誤答が多かったので、このジャンルにマッチする Seed をキーワードベースのマッチ率で抽出するようにしました。

  • 支払い・お金: 会社員、家計、ローン、保険、税金
  • 公共・制度: 役所、手続き、就職活動、公共施設
  • 安全: 事故、危険行為、ルール、注意

`filtered_personas_balanced_clean_2000_jcommonsense.csv` で保存しています。japanese_commonsense_qa_data_generator_nemotron_persona_jp_seed.ipynb に Seed 抽出のコードは記載してます。

JCommonsenseQA 形式の明確化

# jcommonsenseqa用の構造
class JCommonsenseQAData(BaseModel):
    question: str = Field(
        ..., 
        description="日常生活や常識に関する日本語の質問。文脈や状況を明確に含める。"
    )
    choice0: str = Field(..., description="選択肢0: 自然で妥当な選択肢")
    choice1: str = Field(..., description="選択肢1: 自然で妥当な選択肢")
    choice2: str = Field(..., description="選択肢2: 自然で妥当な選択肢")
    choice3: str = Field(..., description="選択肢3: 自然で妥当な選択肢")
    choice4: str = Field(..., description="選択肢4: 自然で妥当な選択肢")
    answer_index: int = Field(
        ..., 
        ge=0, 
        le=4,
        description="正解の選択肢のインデックス(0-4)"
    )
    reasoning: str = Field(
        ...,
        description="なぜその選択肢が正解なのかの説明"
    )

合成データのスキーマが JCommonsenseQA で指定されるフォーマットで出力されるようにするための設定です。出力が validation されるため、フォーマットを外れるような合成データを排除できます。

Seed データの設定

config_builder_with_seed_jcommonsenseqa = DataDesignerConfigBuilder(
    model_configs=model_configs,
)

seed_dataset_reference_jcommonsense = data_designer_client.upload_seed_dataset(
    dataset="filtered_personas_balanced_clean_2000_jcommonsense.csv",
    repo_id="data-designer/filtered_personas_clean_balanced_2000_jcommonsense_add_jc_theme",
    datastore_settings={"endpoint": "http://localhost:3000/v1/hf"},
)

config_builder_with_seed_jcommonsenseqa.with_seed_dataset(
    seed_dataset_reference_jcommonsense,
)

NeMo Data Designer を設定して、Seed データを Data Store にアップロードして使用します。

JCommonsenseQA データの合成

# jcommonsenseqaデータ生成

# seedデータの列(age, gender, occupation等)を直接参照
config_builder_with_seed_jcommonsenseqa.add_column(
    LLMStructuredColumnConfig(
        name="jcqa_data",
        model_alias=MODEL_ALIAS,
        system_prompt=SYSTEM_PROMPT,
        prompt=(
            "以下のペルソナに基づいて、日本語の常識推論問題を生成してください。\n"
            "ただし、答えが個人の好み・性格・価値観で変わる内容は禁止し、\n"
            "日本で一般的な知識・慣習・手順により答えが1つに定まる問題だけを作成してください。\n\n"
            "【ペルソナ情報(最小)】\n"
            "職業: {{ occupation }}\n"
            "居住地(都道府県): {{ prefecture }}\n"
            "{% if region is defined %}生活圏(地方): {{ region }}\n{% endif %}"
            "{% if marital_status is defined %}家族: {{ marital_status }}\n{% endif %}"
            "\n"
            "【状況カテゴリ指定】\n"
            "状況カテゴリは『{{ topic_category }}』としてください。\n\n"
            "【テーマ指定】\n"
            "この問題のテーマは『{{ jc_theme }}』としてください。\n"
            "このテーマと状況カテゴリの両方に整合する日本の一般常識(用途・場所・手順・呼称)で答えが一意に決まる問題にしてください。\n"
            "ただし、両者が矛盾する場合は『{{ jc_theme }}』に整合する内容を優先してください。\n\n"
            "【問題作成の条件】\n"
            "- 質問は具体的な状況を1〜2文\n"
            "- 選択肢は5つ。全て一見もっともらしいが、正解は1つだけ\n"
            "- 正解以外の4つは同カテゴリで意味が近く紛らわしい語を使う\n"
            "- 心理/感情/道徳で割れる内容は禁止(例:嬉しい/悲しい/正しい/許せない/思いやり 等)\n\n"
            "【出力形式(必ずこの形式を守ってください)】\n"
            "question: <質問文>\n"
            "choices:\n"
            "0. <選択肢>\n"
            "1. <選択肢>\n"
            "2. <選択肢>\n"
            "3. <選択肢>\n"
            "4. <選択肢>\n"
            "answer: <0〜4の数字を1文字で出力>\n"
        ),
        output_format=JCommonsenseQAData,
    )
)

print("jcommonsenseqa生成カラムを追加しました")

LLM Structured Generation で先程設定した、JCommonsenseQA のフォーマットに従って、出力するようにしています。

Seed で設定した値を変数として扱っており、それを利用してデータ合成をしています。

Nemotron-Personas-Japan で設定されている値は下記です。

  • 職業: {{ occupation }}
  • 居住地(都道府県): {{ prefecture }}
  • 生活圏(地方): {{ region }}
  • 家族: {{ marital_status }}

Seed 抽出の際に設定した値は下記です。

  • 状況カテゴリ: {{ topic_category }}
  • 問題のテーマ: {{ jc_theme }}

Seed データの効果も測るため、Seed を使用しない合成データも作成し、NeMo RL による Supervised Fine-Tuning (SFT) したモデルを比較評価します。

変更点は状況カテゴリと問題のテーマをサンプリングで与え、Seed 情報を使わずに JCommonsenseQA の合成データを作成することです。

SamplerType.CATEGORY を使用して状況カテゴリと問題のテーマをサンプリングします。

# トピックカテゴリのサンプラー(多様性確保のため)
config_builder_no_seed.add_column(
    SamplerColumnConfig(
        name="topic_category",
        sampler_type=SamplerType.CATEGORY,
        params=CategorySamplerParams(
            values=[
                "日常生活",
                "職場",
                "学校",
                "家族",
                "友人関係",
                "公共の場",
                "買い物",
                "交通",
                "食事",
                "趣味",
                "健康",
                "冠婚葬祭",
            ]
        ),
    )
)

# jc_theme
config_builder_no_seed.add_column(
    SamplerColumnConfig(
        name="jc_theme",
        sampler_type=SamplerType.CATEGORY,
        params=CategorySamplerParams(
            values=[
                "B_道具・用途",
                "C_支払い・お金",
                "D_公共施設・マナー手順",
                "E_安全・危険",
                "A_交通・移動",
                "F_生活・家事",
            ]
        ),
    )
)

サンプリングされた状況カテゴリと問題のテーマを使って、下記のコードで JCommonsenseQA の合成データを作成します。

# jcommonsenseqaデータ生成 (Seedなし)
config_builder_no_seed.add_column(
    LLMStructuredColumnConfig(
        name="jcqa_data",
        model_alias=MODEL_ALIAS,
        system_prompt=SYSTEM_PROMPT,
        prompt=(
            "以下の条件に基づいて、日本語の常識推論問題を生成してください。\n"
            "ただし、答えが個人の好み・性格・価値観で変わる内容は禁止し、\n"
            "日本で一般的な知識・慣習・手順により答えが1つに定まる問題だけを作成してください。\n\n"
            "【状況カテゴリ指定】\n"
            "状況カテゴリは『{{ topic_category }}』としてください。\n\n"
            "【テーマ指定】\n"
            "この問題のテーマは『{{ jc_theme }}』としてください。\n"
            "このテーマと状況カテゴリの両方に整合する日本の一般常識(用途・場所・手順・呼称)で答えが一意に決まる問題にしてください。\n"
            "ただし、両者が矛盾する場合は『{{ jc_theme }}』に整合する内容を優先してください。\n\n"
            "【問題作成の条件】\n"
            "- 質問は具体的な状況を1〜2文\n"
            "- 選択肢は5つ。全て一見もっともらしいが、正解は1つだけ\n"
            "- 正解以外の4つは同カテゴリで意味が近く紛らわしい語を使う\n"
            "- 心理/感情/道徳で割れる内容は禁止(例:嬉しい/悲しい/正しい/許せない/思いやり 等)\n\n"
            "【出力形式(必ずこの形式を守ってください)】\n"
            "question: <質問文>\n"
            "choices:\n"
            "0. <選択肢>\n"
            "1. <選択肢>\n"
            "2. <選択肢>\n"
            "3. <選択肢>\n"
            "4. <選択肢>\n"
            "answer: <0〜4の数字を1文字で出力>\n"
        ),
        output_format=JCommonsenseQAData,
    )
)

合成データの評価

# 品質評価用のルーブリック定義
QuestionClarityRubric = Score(
    name="question_clarity",
    description="質問や状況の明確性の評価",
    options={
        "明確": "質問や状況が具体的で明確に記述されており、意図が容易に理解できる。",
        "やや不明確": "質問や状況は理解できるが、一部曖昧な表現や不足している情報がある。",
        "不明確": "質問や状況の意図が不明確で、追加の説明が必要。",
    },
)

DifficultyRubric = Score(
    name="difficulty",
    description="問題の難易度の評価",
    options={
        "易しい": "日本人の多くが直感的に答えられるレベルの問題。背景知識がほとんど不要。",
        "普通": "一般的な日本人であれば少し考えれば答えられるレベルの問題。基本的な知識や状況理解が必要。",
        "難しい": "ある程度の知識や深い理解、丁寧な読み取りが必要となる問題。人によっては間違えやすい。",
    },
)

# Seedあり版に品質評価を追加
config_builder_with_seed_jcommonsenseqa.add_column(
    LLMJudgeColumnConfig(
        name="quality_metrics",
        model_alias=JUDGE_MODEL_ALIAS,
        prompt=(
            "以下の生成されたデータの品質を評価してください:\n\n"
            # jcqa_dataが存在する場合
            "【jcommonsenseqa問題】\n"
            "質問: {{ jcqa_data.question }}\n"
            "選択肢:\n"
            "0. {{ jcqa_data.choice0 }}\n"
            "1. {{ jcqa_data.choice1 }}\n"
            "2. {{ jcqa_data.choice2 }}\n"
            "3. {{ jcqa_data.choice3 }}\n"
            "4. {{ jcqa_data.choice4 }}\n"
            "正解: {{ jcqa_data.answer_index }}\n"
            "推論: {{ jcqa_data.reasoning }}\n"
            "上記の内容について、日本の文化的適合性、内容の明確性、推論・説明の質を評価してください。"
        ),
        scores=[
            QuestionClarityRubric,
            DifficultyRubric,
        ],
    )
)


# スコアを個別のカラムに抽出
for builder in [config_builder_with_seed_jcommonsenseqa]:
    builder.add_column(
        ExpressionColumnConfig(
            name="clarity_score",
            expr="{{ quality_metrics.question_clarity.score if quality_metrics else 'N/A' }}",
        )
    )
    builder.add_column(
        ExpressionColumnConfig(
            name="difficulty",
            expr="{{ quality_metrics.difficulty.score if quality_metrics else 'N/A' }}",
        )
    )
    
print("品質評価カラムを追加しました")

合成データの品質評価のために明確性と難易度の基準で評価するようにしました。今回はデータのフィルタリングに使用していませんが、この基準によってデータ抽出ができ、必要なデータのみモデルの学習に使用できます。

合成データ作成のプレビュー

# Seedあり版のプレビュー
print("\n" + "="*0)
print("Seedデータあり版のプレビューを生成中...")
print("="*10)

preview_with_seed_jcommonsenseqa = data_designer_client.preview(
    config_builder_with_seed_jcommonsenseqa,
    num_records=1,
)

print("\nプレビュー生成完了!")
preview_with_seed_jcommonsenseqa.display_sample_record()

今回の設定で正しく合成データが生成できるかプレビュー処理によって確認できます。

バッチ単位での合成データ作成

# Seedあり版の本番生成
NUM_RECORDS = 8000  # 必要に応じて調整

print("\n" + "="*80)
print(f"Seedデータあり版 {NUM_RECORDS}件のデータを生成中...")
print("="*80)

job_with_seed = data_designer_client.create(
    config_builder_with_seed_jcommonsenseqa,
    num_records=NUM_RECORDS,
)

print("ジョブを実行中... 完了を待機しています")

# ジョブ完了を待機
results_with_seed = job_with_seed.wait_until_done()
print("\nSeedあり版の生成完了!")

プレビュー後、バッチ単位で本番生成を実行します。なお、環境によりますがこのセルの実行には 3 時間ほど掛かります。

合成データの品質確認

# データの読み込み
df_with_seed = job_with_seed.load_dataset()

print(f"Seedあり版のデータ数: {len(df_with_seed)}")

# 品質スコアの集計
def count_scores(df, name):
    print(f"\n{name}の品質スコア分布:")
    
    for metric in ['clarity_score', 'difficulty']:
        if metric in df.columns:
            counts = df[metric].value_counts()
            print(f"\n{metric}:")
            print(counts)
    
    scores = {
        'clarity': df['clarity_score'].value_counts().to_dict() if 'clarity_score' in df.columns else {},
        'difficulty': df['difficulty'].value_counts().to_dict() if 'difficulty' in df.columns else {},
    }
    return scores

scores_with_seed = count_scores(df_with_seed, "Seedあり版")

LLM-as-a-Judge を利用してデータの品質を検証します。LLM-as-a-Judge を使用して評価しているため、人間の評価とズレている可能性があります。合成データは 8,000 件を目標に生成しましたが、フォーマットに沿っていないデータは除かれるため、件数が少なくなります。

明確性と難易度は下記の表のような結果になりました。

clarity_scoreSeed あり (件数 / 割合)
明確7,977 / 99.75%
やや不明確20 / 0.25%
difficultySeed あり (件数 / 割合)
易しい7,041 / 88.05%
普通953 / 11.92%
難しい3 / 0.04%

ジャンルにマッチする Seed をキーワードベースのマッチ率で抽出していましたが、埋め込みモデルや LLM を活用することによって更に改善できる点があります。

JCommonsenseQA 2.0: 計算機と人の協働による常識推論データセットの改良” を参考にしていただくとより品質の高い合成データ生成もできる可能性があります。

合成データの保存

OUTPUT_DIR = "jcommonsenseqa_8000_filter_jcommonsenseqa_seed_2000_temperature_0_9"
import os

os.makedirs(OUTPUT_DIR, exist_ok=True)

# Seedあり版の保存
job_with_seed.download_artifacts(
    output_path=OUTPUT_DIR,
    artifacts_folder_name="with_seed_data",
)

df_with_seed = job_with_seed.load_dataset()

# DataFrameをJSONLで保存(LoRAチューニング用)
df_with_seed.to_json(
    f"{OUTPUT_DIR}/with_seed_data.jsonl",
    orient='records',
    lines=True,
    force_ascii=False
)

print(f"\nデータを '{OUTPUT_DIR}' ディレクトリに保存しました。")
print("\n保存されたファイル:")
for filename in os.listdir(OUTPUT_DIR):
    filepath = os.path.join(OUTPUT_DIR, filename)
    if os.path.isfile(filepath):
        size = os.path.getsize(filepath) / 1024  # KB
        print(f"  - {filename} ({size:.2f} KB)")

処理後の合成データをダウンロードして、pandas フォーマットに変換して jsonl で保存しています。

まとめ

本記事では、下記を記述しました。

  • なぜ高品質な合成データが必要か  
  • NeMo Data Designer の特徴
  • NeMo Data Designer による合成データ生成

本記事では、NeMo Data Designer と Nemotron-Personas-Japan を組み合わせることで、日本語特有の文化、生活文脈を反映した常識推論データを、スケール可能かつ再現性のある形で合成できることを示しました。

特に、ペルソナに基づく Seed データの活用、構造化された生成プロセス、および生成後の品質評価を組み合わせることで、単純なプロンプトベースの合成では難しい「一意な正解を持つ高品質データ」を安定して生成できる点が、本手法の大きな特徴です。

このようなアプローチは JCommonsenseQA に限らず、日本語タスク全般における評価データや学習データの拡張にも応用可能です。

次のステップとしてはこのデータを使用して NeMo RL による Supervised Fine-Tuning (SFT) を行います。

関連情報

Tags