1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

医療LLMを RL post-training するときに使った報酬関数

1
Last updated at Posted at 2026-08-28

TL;DR

  • Qwen3-Next-80B の RL post-training で使った rule-based 報酬関数 2 本の設計判断を残す記事
  • ophtho (正解報酬): [ans]...[/ans] を抽出し gold と完全一致で 1.0、それ以外 0.0。format 違反 → parse 失敗 → 自動 0.0 で accuracy と format を 1 つの gate に統合
  • chinese (言語ペナルティ): 中国語文が 2 文連続で出たら 0.0、それ以外 1.0。単発の漢字語 (専門用語) は素通し、モード滑りだけを狙い撃ち
  • 一番の主張は 報酬関数の設計はデータ変換とペアで決まること。データ変換側で出力フォーマットを固定しておくと、報酬関数は自明なコードで済む

はじめに

RAMENチームとして医師国家試験のデータで Qwen3 系列に post-training をかけた一本になります。今回のテーマは、RL post-training でモデルに何を「良し」と教えたか、その報酬関数の設計判断を残しておくことです。

つまり、

「Qwen3-Next-80B に対する RL post-training で、どんな reward function を書き、なぜそう書いたかを設計判断まで含めて記録する。」

ということになります。

3 種類の RL アルゴリズムを試す中で共通の報酬関数を用意する必要があったので、その設計だけを切り出したのがこの記事です。使ったのは rule-based 2 本 (ophthochinese) だけで、Neural reward model も PRM (Process Reward Model) も使っていません。

具体的には、Qwen3-Next-80B-A3B-Instruct を対象に、医師国試 (2001-2022) を訓練データにして RL post-training を行い、「どういう出力に 1 を返し、どういう出力に 0 を返したか」 を可視化します。


目次

  1. 前提: 何を訓練データにしたか
  2. 正解報酬 ophtho
  3. 言語一貫性ペナルティ chinese
  4. まとめ

1. 前提: 何を訓練データにしたか

この報酬設計はデータ変換と密接に関わっているので、まずデータ側から書きます。

1.1 原典データ

RL 学習の元データは 2001–2024 年の医師国家試験 MCQ を使い、そのうち 2001–2022 年分を学習に、2023–2025 年分と 13 診療科の専門医試験を評価に回しています。データリークは無し。

医師国試の問題形式は次の 2 種類が混在しています:

問題形式 正解の形
単一解答 MCQ 「〜として正しいのはどれか」 c
複数解答 MCQ 「〜として正しいのはどれか。2 つ選べ」 a,c

これらを 1 つのフォーマットに統一するのが、次のデータ変換の役割です。

1.2 データ変換

以下の形に変換しました:

{
  "problem_id": "医師国家試験第95回A001",
  "messages": [
    {
      "role": "user",
      "content": "次の多肢選択問題について、日本語で考察したあと、\n最後の1行で正しい選択肢を [ans][/ans] で囲んで答えてください。\n\n問題:\n我が国の人口構造の変化について正しいのはどれか。\n\n選択肢:\na. 総人口は減少している。\nb. ...\n\n出力フォーマット例:\n\nここに日本語で考察を書く。\n\n[ans]a,c[/ans]\n"
    }
  ],
  "solution": "c"
}

決めた設計判断は 3 つあります。

(a) MCQ を [ans]選択肢[/ans] タグ形式に統一
単一解答も複数解答も、すべて [ans]a,c[/ans] (カンマ区切り) の形にまとめてしまいます。複数解答問題は「n つ選べ」の n を検出し、正解数と一致する問題のみをデータセットに通します。これで下流の報酬関数は問題形式の分岐を書かなくて済みます。

(b) 出力フォーマット例まで prompt に埋め込む
「出力フォーマット例:\n\nここに日本語で考察を書く。\n\n[ans]a,c[/ans]」という few-shot 的な例示を user prompt の末尾に含めます。モデルに CoT (考察) 本文と最終解答を機械的に分離させるのが狙いで、これによって報酬関数側は「最後の [ans]...[/ans] を取り出せば必ず最終解答である」という前提で書けるようになります。

(c) SFT / RL / eval で完全に同じフォーマットを使う
SFT データも RL データも評価データも、同じ入出力形式 (MCQ + [ans]x[/ans]) で揃えています。学習と評価で入出力形式が揃っているので、報酬関数は評価時の解答パーサーとそのまま同じロジックで済みます

1.3 データ変換と報酬関数の全体フロー

  • オレンジの ophtho = 本記事 2章 で扱う正解報酬
  • 青の chinese = 本記事 3章 で扱う言語ペナルティ

2. 正解報酬 ophtho

2.1 なにをやっているのか

一言で書くと、「モデル出力から最後の [ans]...[/ans] を抜き出して、gold と完全一致していれば 1.0、それ以外は 0.0」 です。以下のフローになります:

記号の意味 (このセクション用):

  • y : モデルが生成した応答テキスト全体 (CoT 本文 + 最終解答)
  • [ans]...[/ans] : §1.2 でデータ変換時に強制した最終解答タグ
  • gold : solution カラムに入っている正解 (例: "c""a,c")
  • set化 : , 区切りを集合に変換 (順序無視・重複無視)

2.2 実装

実装は 90 行ほどで、本質は 3 ステップです:

ANS_PATTERN = re.compile(r"\[ans\](.*?)\[/ans\]", re.IGNORECASE | re.DOTALL)

def parse_output(output: str) -> Optional[str]:
    # 複数出現時は最後の [ans]...[/ans] を採用
    matches = list(ANS_PATTERN.finditer(output))
    if not matches:
        return None
    return matches[-1].group(1).strip() or None

def parse_ans_set(ans_text: Optional[str]) -> Optional[Set[str]]:
    # "a,c" → {"a", "c"} に変換 (順序無視)
    if ans_text is None:
        return None
    parts = [p.strip() for p in ans_text.split(",") if p.strip()]
    return set(parts) if parts else None

def answer_reward(pred: Optional[Set[str]], gold: Set[str]) -> float:
    # 完全一致で 1.0、それ以外は 0.0
    if pred is None:
        return 0.0
    return 1.0 if pred == gold else 0.0

gold 側 (solution カラム) が "a,c" の文字列でも ["a", "c"] のリストでも受け付けるように少し工夫していますが、判定ロジック本体は「set 化して完全一致」で終わりになります。

2.3 設計判断: accuracy と format を掛け算 gate で統合する

accuracy と format を独立に定義せず、1 つの関数に畳んだという点になります。DeepSeek-R1 の R1-Zero では accuracy reward と format reward を別々に定義していますが、ここでは統合しています。仕組みはシンプルで、[ans]...[/ans] タグが欠落していたら parse_outputNone を返し、その時点で 0.0 が確定します。

format 違反 → parse 失敗 → 自動的に 0.0 という掛け算 gate になっている。

format 報酬を独立に定義して「タグは付いてるけど答えが違う場合は 0.3」のような部分点を与える選択肢もあり得ましたが、それをやると「タグだけ付けて中身は適当」で報酬が入る余地が生まれます (軽い reward hack)。データ変換側で instruction を明示的に埋めているのでモデルは十分にタグの付け方を学習できる、と割り切って部分点を捨てました。これは DeepSeek-R1 と同じ思想で、推論プロセスは制約せず正解/不正解の二値だけを渡す方針です。

なお、ophtho という名前はプロジェクト初期に眼科データで試作した名残です。実装は完全に汎用なので、名前は気にしないでください。

2.4 パラメータ

ophtho の重みは 1.5 に固定しています。正解が一番重要視スべきだと考えたため1.5に設定しました。


3. 言語一貫性ペナルティ chinese

3.1 なぜ必要だったのか

Qwen3 系は中国語の学習比率が高く、日本語プロンプトで日本語 CoT を書かせても、推論の途中から中国語モードに滑り落ちることがあります。実際に観察された崩壊パターンは、

「症状表现为发热和咳嗽。因此选择。」

のように中国語文が連続して湧いてくる現象で、モデルの言語モード自体が切り替わってしまっています。これを rule-based で罰する副報酬が chinese です。

3.2 なにをやっているのか

こちらも一言で書くと、「応答を で文分割し、ひらがな/カタカナを 1 文字も含まない文が 2 文連続で並んだら 0.0、それ以外は 1.0」 です。

記号の意味 (このセクション用):

  • 日本語文 : ひらがな または カタカナ (Unicode U+3040 – U+30FF, ひらがな + カタカナ) を 1 文字以上含む文
  • 中国語文 : 上記の仮名を 1 文字も含まない文 (= 純漢字文, または英数字のみの文)
  • 2 文連続 : で分割した隣り合う 2 文がどちらも中国語文であること

3.3 実装

def has_chinese_consecutive(text: str) -> bool:
    # 「。」で文分割し、各文で「ひらがな/カタカナが1文字も含まれない」→ 中国語文判定
    chinese_judge_list = [
        not re.search(r'[\u3040-\u30ff]', t)  # ひらがな + カタカナ
        for t in text.split("") if t
    ]
    # 中国語文が 2 文連続で出現すれば True
    return any(a and b for a, b in zip(chinese_judge_list, chinese_judge_list[1:]))

class ChineseRewardFunction(ORM):
    def __call__(self, completions, **kwargs):
        return [0.0 if has_chinese_consecutive(c) else 1.0 for c in completions]

3.4 設計判断: なぜ「連続」だけを罰するのか

一見すると「そもそも中国語が 1 文字でも出たら罰する」で良さそうに見えますが、そうしていません。理由は 3 つあります。

(a) 単発の漢字語は自然
専門医試験には次のような、ひらがな/カタカナを 1 文字も含まない専門用語が普通に登場します:

内容
心筋梗塞 疾患名
原発性硬化性胆管炎 疾患名
ICD-10 コード 英数字混じり分類

これらだけで構成される文を「中国語文」と誤判定して罰すると、正常な日本語応答まで潰してしまいます

(b) 「モード滑り」は連文で起こる
実際に観察される崩壊パターンは、単発の漢字表現ではなく 中国語文が連続して湧く現象です。モデルの言語モード自体が切り替わっているので、この現象だけを狙い撃ちすれば、副作用を最小化しつつ本命の失敗モードを罰せます。

3.5 想定した誤検知への対処

「単発の漢字語 (専門用語) で正常応答まで罰してしまう」といった誤検知は事前に想定していましたが、3.4章 の「連続」条件で自然に排除される設計になっています。この単純な rule で、フォーマット遵守率 100% ([ans]...[/ans] パース失敗が 0 件) を確保しつつ、日本語 CoT を保てました。


4. まとめ: 報酬関数はデータ変換とペアで決まる

医療 LLM の RL post-training で使った報酬関数は、正解判定と言語ペナルティの rule-based 2 本だけでした。改めて要点をまとめると:

報酬 何を返すか 設計の要点
ophtho [ans]...[/ans] を抽出、gold と完全一致で 1.0 accuracy と format を掛け算 gate で統合、部分点なし、単一/複数解答を set 一致で両対応
chinese 中国語文が 2 文連続したら 0.0、それ以外 1.0 「完璧に排除」ではなく「連続 = モード滑り」だけを狙い撃ち

Neural reward model や PRM を出さずに rule-based で押し切れたのは、扱っているタスクが MCQ という verifier を書きやすい形をしていたからでもあります。自由記述の医療相談だとこうはいきません。ただ、「verifier を書ける形にデータを揃える」 ところまで含めて設計と考えれば、応用可能な範囲はもう少し広がるはずです。もちろん会話的な解答を求めるのであればどうしてもLLM as a Judgeになってしまいますが。


謝辞

この成果は、NEDO(国立研究開発法人新エネルギー・産業技術総合開発機構)の 委託業務(JPNP25006)の結果得られたものです。

参考文献

  1. DeepSeek-AI (2025) "DeepSeek-R1: Incentivizing Reasoning Capability in LLMs via Reinforcement Learning" arXiv:2501.12948 — accuracy reward / format reward / language consistency reward の元ネタ

関連記事

リポジトリ
https://github.com/weblab-llm-m/singularity-post-training-medical

学習済みモデル (HuggingFace)

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?