機械学習MACHINE LEARNING

GRPO とは?LLM の強化学習の仕組みと PPO との違い

GRPO(Group Relative Policy Optimization)は、LLM を強化学習で訓練する手法の一つである。DeepSeek が 2024 年 2 月に数学特化モデル DeepSeekMath の論文で提案し、2025 年 1 月の推論モデル DeepSeek-R1 の訓練に使ったことで広まった。現在は TRL(Hugging Face の学習ライブラリ)に実装がある。

強化学習で LLM を訓練するとは

LLM の追加学習には大きく二つの方式がある。教師あり学習(SFT)は、問題とお手本の回答の組を大量に用意し、モデルの出力をお手本に近づける。強化学習は、お手本を用意する代わりにモデル自身に回答させ、その回答に点数(報酬)を付けて、点数の高い回答を出しやすくなるようにモデルを調整する。

強化学習を使う理由は、お手本を書けない課題でも訓練できることにある。たとえば「このテストを通すコードを書け」という課題では、正しいコードの書き方は何通りもあり、お手本を一つに決めにくい。一方、テストを実行すれば、書かれたコードが正しいかどうかは自動で判定できる。強化学習は、正解の判定はできるがお手本は書けない課題に使える。

訓練の手順

GRPO の訓練は、次の 1 周の繰り返しである。

GRPO の計算の流れ。プロンプト 2 個に 4 個ずつ回答を生成した例(Hugging Face TRL のドキュメントの図をもとに作成)
GRPO の計算の流れ。プロンプト 2 個に 4 個ずつ回答を生成した例(Hugging Face TRL のドキュメントの図をもとに作成)

升目の色の濃さは、値の大きさを表す。図の下半分は、方策モデルの出力が訓練前のモデル(参照モデル)からどれだけ離れたかを、トークンごとに求めている。この離れ具合を KL と呼ぶ(後の節で説明する)。アドバンテージに生成確率の比を掛ける処理と、KL に掛ける係数は、図では省かれている。

  1. 訓練用の問題を 1 問取り出す。
  2. 訓練中のモデル(強化学習の用語で方策モデルと呼ぶ)に、同じ問題への回答を複数(図では 4 個)生成させる。この回答の集まりをグループと呼ぶ。
  3. 報酬関数で、回答ごとに点数を付ける。報酬関数は、テストの実行や正解との照合を行うプログラムである。
  4. グループ内で点数を比べ、回答ごとに「平均よりどれだけ良いか」を計算する。この値をアドバンテージと呼ぶ。
  5. アドバンテージが正の回答は出やすく、負の回答は出にくくなるように、モデルの重みを更新する。

実際の訓練では、1 周で複数の問題をまとめて扱い、これを問題を替えながら繰り返す。DeepSeek-R1 の論文で最初に訓練されたモデル(DeepSeek-R1-Zero)では、1 周につき 32 問、1 問につき 16 個の回答を生成し、10,400 周繰り返した。

良し悪しをグループ平均との差で決める理由

GRPO の特徴は手順 4 にある。回答の良し悪しを点数の絶対値ではなく、同じ問題に対する他の回答との比較で決める。

回答 ii の報酬を rir_i とすると、アドバンテージ AiA_i は次の式で求める。

Ai=ri−mean(r1,…,rG)std(r1,…,rG)A_i = \frac{r_i - \mathrm{mean}(r_1, \dots, r_G)}{\mathrm{std}(r_1, \dots, r_G)}

GG はグループの回答数である。たとえば 4 個の回答の報酬が 1、1、0、0 なら、平均は 0.5、標準偏差は 0.58 なので、正解した回答のアドバンテージは +0.87、不正解の回答は −0.87 になる。報酬がちょうど平均の回答は 0 で、報酬による更新には寄与しない。ここでの標準偏差は、TRL の実装と同じく G−1G - 1 で割る方法で求めている。

この計算を PyTorch で書くと次のようになる。TRL の実装と同じく、0 での割り算を避けるために、標準偏差に小さな値を足している。

python
import torch

# 1 行が 1 問、1 列が 1 個の回答。1 は正解、0 は不正解。
rewards = torch.tensor(
    [
        [1.0, 1.0, 0.0, 0.0],
        [1.0, 1.0, 1.0, 0.0],
        [1.0, 0.0, 0.0, 0.0],
        [1.0, 1.0, 1.0, 1.0],
    ]
)

mean = rewards.mean(dim=1, keepdim=True)
std = rewards.std(dim=1, keepdim=True)
advantages = (rewards - mean) / (std + 1e-4)
print(advantages)

実行結果は次のとおりである。

text
tensor([[ 0.87,  0.87, -0.87, -0.87],
        [ 0.50,  0.50,  0.50, -1.50],
        [ 1.50, -0.50, -0.50, -0.50],
        [ 0.00,  0.00,  0.00,  0.00]])

1 行目は上の例と同じ値である。4 行目は全回答が正解した問題で、アドバンテージはすべて 0 になる。

比較で決めると、問題ごとの難しさの違いが打ち消される。2 行目は 4 個中 3 個が正解した易しい問題、3 行目は 4 個中 1 個しか正解しなかった難しい問題である。報酬の絶対値で判断すると、正解した回答はどちらの問題でも同じ 1 である。グループ内で比べると、易しい問題の正解のアドバンテージは +0.5、難しい問題の正解は +1.5 になる。不正解は、易しい問題で −1.5、難しい問題で −0.5 である。他の回答が解けなかった問題での正解と、他の回答が解けた問題での不正解が、更新に大きく反映される。

標準偏差で割るのは、アドバンテージの大きさを問題によらず揃えるためである。点数のばらつきが小さい問題でも、回答間の差が更新に反映される。

PPO との違いとメモリ

GRPO 以前、LLM の強化学習では PPO(Proximal Policy Optimization)が標準だった。PPO も回答の点数を「期待より良かったか」で判断するが、その期待値を価値モデルという別のニューラルネットで予測する。

PPO と GRPO の処理の流れ(DeepSeekMath の論文の図をもとに作成)
PPO と GRPO の処理の流れ(DeepSeekMath の論文の図をもとに作成)

価値モデルは方策モデルと同程度の規模で、しかも方策モデルと一緒に学習させる。学習させるモデルは、重みに加えて勾配と最適化の状態(Adam などが保持する移動平均)もメモリに置くので、重みだけを置く固定のモデルよりメモリを使う。PPO は、この学習対象を二つ抱えることになる。DeepSeekMath の論文は、これをメモリと計算の大きな負担としている。

GRPO は、期待値の予測を「同じ問題に対する回答の平均点」で代用するので、価値モデルが要らない。学習対象は方策モデル一つになり、PPO よりメモリが少なく済む。代わりに、1 問につき複数の回答を生成する計算が増える。

図にある参照モデルは、訓練を始める時点のモデルの写しである。訓練の途中で写しを取り直すこともあり、DeepSeek-R1 は 400 周ごとに、その時点の方策モデルで参照モデルを置き換えた。GRPO は、方策モデルの出力が参照モデルから離れすぎないよう制約をかけるために、これを使う(後述)。LoRA(モデル本体を固定し、小さな追加部品だけを学習する方式)で訓練する場合は、追加部品を外した本体がそのまま参照モデルになるので、写しを別に持つ必要もない。

更新の大きさの制約

手順 5 の更新には、PPO から引き継いだ二つの歯止めがある。

一つ目はクリッピングである。更新の前後で、生成確率の比をトークンごとに求める。アドバンテージが正のトークンでは、比が 1+ε1 + \varepsilon(ε\varepsilon は 0.2 程度)を超えて出やすくなった分を、更新に数えない。負のトークンでは、比が 1−ε1 - \varepsilon を下回って出にくくなった分を数えない。PPO の論文は、この歯止めが無いと方策が一度に大きく変わりすぎる、と説明している。

クリッピングした目的関数と、生成確率の比の関係(PPO の論文の図 1 をもとに作成)
クリッピングした目的関数と、生成確率の比の関係(PPO の論文の図 1 をもとに作成)

クリッピングが働くのは、一度生成した回答を使って複数回更新する場合である。生成のたびに 1 回だけ更新する設定では、比が常に 1 なので働かない。DeepSeekMath の論文も TRL の既定も、この設定である。

二つ目は参照モデルとの KL ダイバージェンス(二つの確率分布の離れ具合)へのペナルティである。DeepSeekMath の論文は、このペナルティを、報酬への過剰な最適化を抑えるための PPO の標準的な方法として紹介している。方策モデルの出力する確率が参照モデルから離れるほど、損失が大きくなる。ペナルティの強さは係数 β\beta で調整する。DeepSeekMath の論文は β\beta を 0.04 とした。一方、TRL は既定で β\beta を 0 とし、このペナルティを使わない。

報酬の設計

GRPO を使えるかどうかは、報酬を自動で計算できるかで決まる。DeepSeek-R1 の訓練では、数学の答えが正解と一致するか、コードがテストを通るか、出力が指定の形式に従っているかを、プログラムで判定して報酬にした。この種の、正誤を機械的に確かめられる報酬は検証可能な報酬と呼ばれる。

課題DeepSeek-R1 の報酬の作り方
数学最終的な答えを決まった形式(枠の中など)で書かせ、正解と照合する
コード生成コンパイラで実行し、あらかじめ用意したテストケースで判定する
出力形式推論の過程を <think> と </think> のタグで囲んでいるかを判定する

報酬の付け方に穴があると、モデルはその穴を突く方向に学習する。これを報酬ハッキングと呼ぶ。DeepSeek-R1 の論文は、回答を別のモデルに採点させる方式(報酬モデル)が報酬ハッキングを起こしやすいとして、推論の課題には使わなかった。回答の好ましさを報酬モデルで採点する訓練は、1,700 周ある仕上げの段階のうち最後の 400 周に限っている。これより長く続けると報酬ハッキングにつながりうる、と報告している。

学習が進まない条件

アドバンテージは平均との差なので、グループ内の全回答が同じ点数だと全員 0 になり、その問題の報酬からは何も学習しない。問題が易しすぎて全回答が正解した場合も、難しすぎて全回答が不正解だった場合も同じである。

DAPO の論文は、訓練が進むほど全回答が正解する問題が増え、更新に使える問題が減っていくことを報告している。DAPO は、全回答が正解または全回答が不正解の問題を捨て、正誤が分かれる問題だけで 1 周分が埋まるまで問題を引き直す。

小さなモデルでの訓練例

TRL の GRPOTrainer で、パラメータ数が約 5 億の言語モデル(Qwen2.5-0.5B-Instruct)を実際に訓練した。課題は「28 * 3 + 43」のような計算で、最後の行を Answer: 127 の形で書くよう指示する。報酬は、この形で書かれた数が正解と一致すれば 1、それ以外は 0 である。

python
import random
import re

from datasets import Dataset
from trl import GRPOConfig, GRPOTrainer

INSTRUCTION = 'Calculate {expression}. Show your work briefly, then write the last line as "Answer: <number>".'


def make_problems(count, seed):
    rng = random.Random(seed)
    rows = []
    for _ in range(count):
        a, b, c = rng.randint(11, 99), rng.randint(2, 9), rng.randint(11, 99)
        rows.append(
            {
                "prompt": [{"role": "user", "content": INSTRUCTION.format(expression=f"{a} * {b} + {c}")}],
                "answer": a * b + c,
            }
        )
    return rows


def extract_answer(text):
    found = re.findall(r"Answer:\s*(-?\d+)", text.replace(",", ""))
    return int(found[-1]) if found else None


def correctness_reward(completions, answer, **kwargs):
    return [float(extract_answer(c[0]["content"]) == a) for c, a in zip(completions, answer)]


config = GRPOConfig(
    output_dir="grpo_arithmetic_output",
    num_generations=8,               # 1 問あたりの回答数
    per_device_train_batch_size=64,  # 1 周で扱う回答数(8 問 × 8 個)
    max_completion_length=160,
    learning_rate=5e-6,
    max_steps=300,
    bf16=True,
)
trainer = GRPOTrainer(
    model=model,
    processing_class=tokenizer,
    reward_funcs=correctness_reward,
    args=config,
    train_dataset=Dataset.from_list(make_problems(4000, seed=0)),
)
trainer.train()

モデルとトークナイザーの読み込み、評価の処理、ログと乱数の種の設定は省いた。ここに無い設定は TRL の既定のままなので、KL のペナルティは使っていない。訓練は GPU 1 台で 22 分かかった。

訓練に使っていない 200 問で、訓練の前後を比べた結果が次の表である。

回答の生成方法訓練前の正解数訓練後の正解数
確率が最大のトークンを選ぶ8194
確率に従ってランダムに選ぶ(温度 1)91191

訓練中の報酬の平均は、最初の 10 周で 0.83、次の 10 周で 0.94 になり、その後は 0.94 から 0.99 の間で推移した。

1 問目(28 * 3 + 43)への回答は、訓練前は次のとおりだった。モデルの出力は Markdown と数式の記法で書かれているので、「プレビュー」に切り替えると整形した表示になる。報酬関数が読むのは「コード」側の文字列である。

markdown
To calculate \(28 \times 3 + 43\), we follow these steps:

1. First, perform the multiplication:
   \[
   28 \times 3 = 84
   \]

2. Then, add 43 to the result of the multiplication:
   \[
   84 + 43 = 127
   \]

So, the final answer is:
\[
\boxed{127}
\]

訓練後は次のとおりである。

markdown
First, let's calculate 28 * 3 + 43:

28 * 3 = 84
84 + 43 = 127

Now that we have the result of 127, we can write it as:

Answer: 127

This is the final answer to the calculation.

訓練前の回答は、計算は合っているが、指示した形で答えを書いていないので報酬は 0 である。確率が最大のトークンを選ぶ生成では、訓練前に指示した形で答えを書いたのは 200 問中 8 問で、訓練後は 200 問すべてになった。この例の正解数の伸びは、大部分が、指示した形で答えを書くようになったことによる。指示した形で書かれた回答に限った正解の割合は、ランダムに選ぶ生成で、訓練前が 106 問中 91 問、訓練後が 199 問中 191 問だった。

派生手法

GRPO の公開後、その弱点を直す派生手法が提案されている。

手法提案GRPO からの主な変更
Dr. GRPOLiu ら(2025)回答の長さで損失を割る処理と、標準偏差で割る処理を外す。長さで割ると、誤答が長いほど 1 トークンあたりのペナルティが薄まり、誤答が長くなりやすいことを指摘した
DAPOByteDance Seed ら(2025)全回答が同じ点数のグループを捨てて引き直す、クリッピングの上限だけを緩める、KL のペナルティを外す、長すぎる回答にペナルティを与える

出所