LLM POST-TRAINING / 한국어 학습 교재 · HF 코스 실습 튜토리얼 →
From first principles to verifiable rewards

좋은 답변은
어떻게 학습되는가?

배송 로봇의 가치·advantage·actor–critic부터 TRPO·PPO까지.
이어서 RLHF와 RLVR의 한 batch를 생성부터 역전파까지 추적합니다.

33개 장8개 인터랙티브 실험CPU Python 예제2026년 9월 연구 포함
CHAPTER 00 / I · 전통적인 강화학습

첫 수업: 배송 로봇은 경험에서 무엇을 배울까?

먼저 LLM을 잠시 내려놓고, 두 번까지 행동할 수 있는 배송 로봇을 생각하자. 로봇은 안전하게 작은 보상을 받을 수도 있고, 비용을 내고 다리에 들어가 더 큰 보상을 노릴 수도 있다. 우리는 이 환경 하나로 가치, advantage, actor–critic, TRPO, PPO까지 연결한다. 지금은 이 이름들을 외울 필요가 없다.

S₀ · 출발점어느 길로 갈까?안전 배송 · 종료즉시 +2점S₁ · 다리 위천천히 / 서두르기천천히 → 확실히 성공+8점 · 종료서두르기 → 무작위 결과50%: +10점 / 50%: −6점안전한 길다리 진입: −1점각 점수는 그 행동 직후 받는다. 종료 뒤에는 미래 보상이 없다.
로봇 그림 A. 새로 만든 유한 MDP. 실제 로봇 실험 결과가 아니라 손으로 모든 기대값을 계산할 수 있도록 설계한 환경이다.

안전한 길은 2점을 확실히 얻고 끝난다. 다리에 들어가면 먼저 1점을 잃는다. 그 다음 천천히 가면 8점, 서두르면 운에 따라 10점 또는 −6점을 받는다. 한 번 서둘러서 10점을 받았다고 해서 서두르기가 좋은 전략이라는 뜻은 아니다. 반복했을 때의 결과를 배워야 한다.

환경 ENVIRONMENT실제로 일어나는 일

로봇이 다리로 들어가면 상태가 바뀌고 −1점을 받는다. 서두른 결과의 동전 던지기도 환경이 결정한다.

정책 POLICY어떤 행동을 고를지 정하는 규칙

처음에는 출발점에서 40% 확률로 다리에 간다. 다리 위에서는 75% 확률로 천천히 간다.

학습 LEARNING경험을 보고 그 규칙을 수정

좋은 결정을 더 자주 고르도록 확률을 바꾸고, 각 위치에서 기대할 점수의 예측도 함께 고친다.

첫 번째 목표“좋은 결과를 받았으니 그 행동을 강화한다”에서 한 단계 더 나아가야 한다. 좋은 결과가 운이었는지, 원래 쉬운 상태였는지, 현재 전략의 평균보다 정말 나았는지를 구분하는 것이 이 앞부분의 목표다.
  1. 상태·행동·보상과 실제 경험 기록을 구별한다.
  2. 한 번 받은 총점과 반복했을 때의 기대값을 구별한다.
  3. 기대값을 기준으로 행동의 장점을 계산한다.
  4. 행동을 고르는 모듈과 점수를 예측하는 모듈을 따로 학습한다.
  5. 확률을 너무 크게 바꿨을 때 생기는 문제를 TRPO와 PPO로 다룬다.
CHAPTER 01 / I · 전통적인 강화학습

상태·정책·rollout: 실제로 모으는 데이터

상태(state)는 의사결정에 필요한 현재 상황이다. 여기서는 출발점 S₀인지 다리 위 S₁인지만 알면 된다. 행동(action)은 그 상태에서 로봇이 고르는 선택이다. 보상(reward)은 그 행동 직후 환경이 주는 점수다. 상태를 보고 행동을 선택하는 확률 규칙이 정책(policy)이다.

현재 정책 · 출발점
π(bridge∣S0)=0.4,π(safe∣S0)=0.6\pi(\mathrm{bridge}\mid S_0)=0.4,\quad \pi(\mathrm{safe}\mid S_0)=0.6
같은 출발점에 여러 번 오면 매번 같은 행동을 강제하지 않고 이 확률에 따라 뽑는다. 확률을 뽑는 것은 정책이고, 행동 후의 결과는 환경이 결정한다.
현재 정책 · 다리 위
π(careful∣S1)=0.75,π(rush∣S1)=0.25\pi(\mathrm{careful}\mid S_1)=0.75,\quad \pi(\mathrm{rush}\mid S_1)=0.25
서두르기의 성공 확률 50%는 환경의 특성이다. 서두르기를 선택할 확률 25%와 혼동하지 않는다.
시각 t현재 상태 sₜ선택한 행동 aₜ받은 보상 rₜ다음 상태종료?
0S₀다리 진입−1S₁아니요
1S₁천천히+8종료예

이렇게 시작부터 종료까지 실제로 경험한 기록 한 줄기를 trajectory 또는 rollout이라고 한다. 에피소드(episode)는 그 한 번의 시도다. batch는 학습을 위해 모은 여러 기록의 묶음이다. 이 용어들은 생성형 모델만의 용어가 아니다.

정책으로 행동 뽑기S₀ → 다리
선택 확률 0.4
환경이 결과 반환보상 −1
다음 상태 S₁
다음 행동 뽑기S₁ → 천천히
선택 확률 0.75
종료 기록보상 +8
trajectory 저장
로봇 그림 B. 이 경로가 나올 확률은 0.4×0.75=0.30. “다리→서두르기→성공”은 0.4×0.25×0.5=0.05다.
가능한 전체 경로확률보상 기록
안전 배송0.60[+2]
다리 → 천천히0.30[−1, +8]
다리 → 서두르기 → 성공0.05[−1, +10]
다리 → 서두르기 → 실패0.05[−1, −6]
MDP의 뜻Markov Decision Process는 이런 상황을 (상태, 행동, 전이 규칙, 보상, 할인율)로 묶은 수학적 표현이다. Markov 가정은 “현재 상태에 필요한 과거 정보가 이미 담겨 있다”는 뜻이다. 배터리 잔량이 성공률을 바꾼다면 배터리도 상태에 넣어야 한다.
CHAPTER 02 / I · 전통적인 강화학습

Return, V, Q: 점수 세 가지를 구별하기

1. Return: 이번 한 번의 경험에서 얻은 누적 점수

즉시 보상은 −1이지만, 나중에 +8을 받는다면 다리에 들어가는 행동은 가치가 있을 수 있다. 미래 점수를 얼마나 반영할지 정하는 수가 할인율 γ다. 0.9라면 한 단계 뒤 보상은 90%, 두 단계 뒤는 81%만 반영한다.

Return · 실현된 경로의 값
Gt=rt+γrt+1+γ2rt+2+⋯=rt+γGt+1G_t=r_t+\gamma r_{t+1}+\gamma^2r_{t+2}+\cdots=r_t+\gamma G_{t+1}
이 교재는 행동 t 직후의 보상을 rₜ로 쓴다. 교재에 따라 rₜ₊₁로 쓰기도 하는데 인덱스 관례의 차이다.
다리 진입r₀=−1천천히 도착r₁=+8종료미래 return=0

γ=0.9: G₁=8, G₀=−1+0.9×8=6.2.
서둘러 실패한 다른 경로라면 G₀=−1+0.9×(−6)=−6.4.

로봇 그림 C. Return은 실제로 나온 경로의 값이다. 어떤 상태에 도착했다고 항상 같은 return을 받는 것은 아니다.

2. V: 지금 전략을 계속 따를 때 이 상태의 평균 전망

같은 상태에서 현재 정책으로 무한히 반복했을 때 return의 평균을 상태 가치 Vπ(s)라고 한다. “현재 정책”이 바뀌면 가치도 바뀐다. 다리 위에서는 천천히 가는 선택의 평균 점수가 8, 서두르기의 평균은 0.5×10+0.5×(−6)=2다.

다리 위 상태의 가치
Vπ(S1)=0.75⋅8+0.25⋅2=6.5V^\pi(S_1)=0.75\cdot8+0.25\cdot2=6.5
6.5라는 보상을 환경이 직접 주는 것이 아니다. 8점과 2점의 기대값을 현재 정책의 행동 확률로 평균한 것이다.

3. Q: 첫 행동을 지정했을 때의 평균 전망

행동 가치 Qπ(s,a)는 “이 상태에서 처음에는 a를 선택하고, 이후에는 현재 정책을 따르면?”의 기대 return이다. 출발점에서 다리로 가겠다고 정하면, 다리 위 이후의 전망은 여전히 V(S₁)=6.5다.

현재 보상 + 다음 상태의 기대 가치
Qπ(S0,bridge)=−1+0.9⋅6.5=4.85,Qπ(S0,safe)=2Q^\pi(S_0,\mathrm{bridge})=-1+0.9\cdot6.5=4.85,\quad Q^\pi(S_0,\mathrm{safe})=2
Q들을 행동 확률로 평균하면 V
Vπ(S0)=0.4⋅4.85+0.6⋅2=3.14V^\pi(S_0)=0.4\cdot4.85+0.6\cdot2=3.14
값계산질문에 대한 답
r₀ = −1환경이 방금 반환지금 몇 점을 받았나?
G₀ = 6.2이번 “다리→천천히” 경로이번 시도에서 앞으로 몇 점을 벌었나?
Q(S₀,다리)=4.85다리를 고정하고 이후 정책 평균다리를 선택하면 평균 전망은?
V(S₀)=3.14현재 행동 확률까지 평균현재 전략의 출발점 평균 전망은?

전체 경로로도 검산할 수 있다. 0.60×2+0.30×6.2+0.05×8+0.05×(−6.4)=3.14. 어느 방향으로 계산해도 같은 기대값이다.

4. Bellman 식: 지금 받은 점수와 다음 상태의 전망을 합친다

이전 상태가 아니라 다음 상태의 가치다식의 s′ 또는 sₜ₊₁는 행동한 뒤 도착한 상태다. Vπ(s′)는 그곳에서 받을 보상 하나가 아니라, 그 상태부터 현재 정책을 따라 앞으로 받을 할인 누적 보상의 기대값이다.

출발점에서 “다리로 들어가기”를 선택한 순간으로 돌아가 보자. 지금은 통행 비용으로 −1점을 받는다. 그 대신 다리 위에 도착하고, 그곳부터의 미래 전망은 6.5점이다. 시간은 다음 그림에서 오른쪽으로 흐른다.

현재 상태 s출발점
행동을 고르기 전
행동 a다리로 들어가기
지금 보상 r=−1
다음 상태 s′다리 위
행동한 뒤 도착
여기부터의 전망Vπ(s′)=6.5
미래 누적 점수의 평균
Bellman 그림 1. −1점은 방금 받은 보상이고, 6.5점은 다음 상태부터의 기대 가치다. 이전 상태의 가치를 더하는 식이 아니다.
현재 보상 + 한 단계 뒤에서 시작하는 미래 가치
Qπ(s,a)⏟이 선택의 가치=−1⏟지금 받은 보상+0.9×6.5⏟할인한 미래 가치=4.85\underbrace{Q^\pi(s,a)}_{\text{이 선택의 가치}}=\underbrace{-1}_{\text{지금 받은 보상}}+\underbrace{0.9\times6.5}_{\text{할인한 미래 가치}}=4.85
“지금 1점을 지불하지만, 도착한 곳에서 앞으로 벌 점수를 고려하면 이 선택의 기대 가치는 4.85점이다.” 즉시 보상이 음수여도 행동의 전체 가치는 양수일 수 있다.

5. 다음 상태의 가치 6.5점은 어디에서 왔을까?

다리 위에서 현재 정책 π는 75% 확률로 천천히 가고 25% 확률로 서두른다. 천천히 가면 확실히 +8점을 받고 종료한다. 서두르면 성공·실패가 반반이다. 행동을 고르는 정책의 확률과, 선택 후 결과를 정하는 환경의 확률을 구별하자.

다음 상태 s′ · 다리 위정책의 선택 75%정책의 선택 25%천천히 가기+8점 · 종료서두르기결과는 아직 모름환경의 확률 50%환경의 확률 50%성공+10점 · 종료실패−6점 · 종료
Bellman 그림 2. 먼저 서두르기의 두 결과를 평균하고, 그다음 두 행동의 전망을 정책 확률로 평균한다. 이 그림의 가지 끝은 모두 종료 상태라 뒤에 더 받을 보상이 없다.
먼저: 서두르기로 정했을 때 결과의 평균
Qπ(s′,rush)=0.5×10+0.5×(−6)=2Q^\pi(s^\prime,\mathrm{rush})=0.5\times10+0.5\times(-6)=2
그다음: 현재 정책이 선택하는 두 행동을 평균
Vπ(s′)=0.75×8+0.25×2=6.5V^\pi(s^\prime)=0.75\times8+0.25\times2=6.5
6.5점은 환경이 직접 주는 점수가 아니다. 이 전략을 반복할 때의 평균 전망이다. 정책이 항상 천천히 가도록 바뀌면 이 상태의 가치는 8이 된다. 그래서 V에 정책 π가 붙는다.

6. 왜 다음 상태의 가치에 γ를 한 번 곱할까?

가치를 평가하는 기준 시점이 한 단계 다르기 때문이다. 다리 위에 이미 도착한 입장에서 천천히 가서 받는 8점은 첫 보상이다. 출발점 입장에서는 통행료 −1점을 받은 다음 단계의 보상이다.

어디에서 계산하는가?“다리 → 천천히” 경로의 남은 보상γ=.9인 return
다리 위 s′에서[+8]8
출발점 s에서[−1, +8]−1+.9×8=6.2
Bellman 그림 3. 같은 경로도 평가 기준 시점이 다르면 return이 다르다. 여기의 6.2는 “천천히”를 실제로 선택한 경로의 return이며, 앞의 4.85는 이후 행동을 정책대로 평균한 Q다.

γ=.9는 “한 단계 뒤의 보상을 지금 평가할 때 90% 반영한다”는 설정이다. 환경이 실제로 8점 대신 7.2점을 지급한다는 뜻은 아니다. γ=1이면 미래 보상을 줄이지 않고 전부 반영한다.

미래 전체를 한 묶음으로 다시 쓰기
Gt=rt+γrt+1+γ2rt+2+⋯=rt+γ(rt+1+γrt+2+⋯ )⏟Gt+1\begin{aligned}G_t&=r_t+\gamma r_{t+1}+\gamma^2r_{t+2}+\cdots\\&=r_t+\gamma\underbrace{(r_{t+1}+\gamma r_{t+2}+\cdots)}_{G_{t+1}}\end{aligned}
괄호 안은 다음 시점부터의 누적 보상이다. γ를 밖에서 곱하면 괄호 속 모든 보상을 현재 시점 기준으로 한 단계씩 더 할인한다. Vπ(s′)에는 그 상태 이후의 할인도 이미 들어 있다.

7. 이제 expectation을 포함한 식을 읽어 보자

Bellman expectation equation · Q
Qπ(s,a)=E[rt+γVπ(st+1)∣st=s, at=a]Q^\pi(s,a)=\mathbb E\left[r_t+\gamma V^\pi(s_{t+1})\mid s_t=s,\ a_t=a\right]
“상태 s에서 행동 a를 선택한 가치는, 지금 받을 보상과 도착한 다음 상태의 할인된 미래 가치를 합친 것의 평균이다.” 조건부 기호 |는 “현재 상태와 행동이 이것으로 주어졌을 때”라는 뜻이다.

𝔼는 평균을 내라는 뜻이다. 같은 행동도 확률적으로 보상이나 다음 상태가 달라질 수 있다. 여기서는 다리 진입의 보상 −1과 도착 상태 s′가 확정되어 있어 −1+.9×6.5로 바로 계산했다. 그 이후의 행동과 결과에 대한 평균은 Vπ(s′)=6.5 안에 들어 있다. 진짜 종료 상태에 도착했다면 V=0이므로 Q는 그 행동의 즉시 보상의 기대값만 남는다.

8. Q를 평균하면 왜 V가 될까?

기호답하려는 질문
Qπ(s,a)지금 이 행동 a를 선택하고, 이후에는 현재 정책대로 하면 얼마나 벌까?
Vπ(s)지금부터 행동 선택을 현재 정책에 맡기면 얼마나 벌까?
Bellman expectation equation · V
Vπ(s)=∑aπ(a∣s)Qπ(s,a)=0.4×4.85+0.6×2=3.14V^\pi(s)=\sum_a\pi(a\mid s)Q^\pi(s,a)=0.4\times4.85+0.6\times2=3.14
출발점에서 정책은 다리를 40%, 안전 배송을 60% 선택한다. 각 선택의 Q를 그 확률로 평균하면 출발점의 V가 된다.
확인: 식의 s′는 과거인가, 미래인가?

미래다. 현재 s에서 a를 한 뒤 도착한 다음 상태다. 그 상태의 가치에 γ를 곱해 현재 시점의 평가로 가져온다. V는 다음 보상 하나가 아니라 그 상태부터의 할인 누적 보상 전체의 기대값이다.

로봇 실험 · 전략을 바꾸면 가치도 바뀐다

다리 위에서 천천히 갈 확률은 0.75로 고정한다.

출발점 가치 V(S₀)
Python · 모든 V·Q·A와 return을 직접 계산
공통 import·보조 함수는 전체 Python 파일에 포함
def robot_values(p_bridge=0.4, p_careful=0.75, gamma=0.9):
    q_careful, q_rush = 8.0, 0.5*10 + 0.5*(-6)
    v1 = p_careful*q_careful + (1-p_careful)*q_rush
    q_safe, q_bridge = 2.0, -1.0 + gamma*v1
    v0 = p_bridge*q_bridge + (1-p_bridge)*q_safe
    return dict(V0=v0, V1=v1, Q_safe=q_safe, Q_bridge=q_bridge,
                A_bridge=q_bridge-v0, A_safe=q_safe-v0,
                A_careful=q_careful-v1, A_rush=q_rush-v1)

def discounted_returns(rewards, gamma=0.9):
    out, carry = [], 0.0
    for r in reversed(rewards):
        carry = r + gamma*carry
        out.append(carry)
    return list(reversed(out))
심화 · Q-learning은 이 이야기의 어디에 해당하는가?

지금까지의 Bellman 식은 현재 정책의 가치를 평가한다. Q-learning은 다음 상태에서 최대 Q를 쓰며 최적 행동 가치를 학습한다. 이 교재의 주 경로는 정책을 직접 바꾸는 방법이므로 Q-learning은 보충으로 둔다.

Tabular Q-learning
Q(s,a)←Q(s,a)+α[r+γ(1−d)max⁡a′Q(s′,a′)−Q(s,a)]Q(s,a)\leftarrow Q(s,a)+\alpha[r+\gamma(1-d)\max_{a^\prime}Q(s^\prime,a^\prime)-Q(s,a)]
Python · 단일 Q 갱신
공통 import·보조 함수는 전체 Python 파일에 포함
def q_learning_step(q, reward, next_q, done, alpha=0.2, gamma=0.9):
    target = reward + gamma * (1 - int(done)) * max(next_q)
    return q + alpha * (target - q)

print("Q update:", q_learning_step(0.4, 0, [0.8, 0.2], False))
# 0.464 = 0.4 + 0.2 * (0 + 0.9 * 0.8 - 0.4)
CHAPTER 03 / I · 전통적인 강화학습

Advantage: 평균보다 얼마나 나은 선택인가?

advantage를 처음에는 “그 상태에서 평소보다 얼마나 유리한 행동인가”라고 읽자. 출발점의 현재 평균 전망은 3.14다. 다리를 선택한 전망은 4.85이므로 다리는 현재 평균보다 1.71 좋다. 안전 배송의 전망은 2이므로 평균보다 1.14 낮다.

Advantage의 정의
Aπ(s,a)=Qπ(s,a)−Vπ(s)A^\pi(s,a)=Q^\pi(s,a)-V^\pi(s)
Q는 특정 첫 행동의 기대값, V는 현재 정책이 선택하는 행동들을 평균한 기대값이다. 둘의 차이가 advantage다.
상태행동Q그 상태의 VA=Q−V
S₀다리4.853.14+1.71
S₀안전 배송2.003.14−1.14
S₁천천히8.006.50+1.50
S₁서두르기2.006.50−4.50
로봇 그림 D. +2점을 확실히 받는 안전 배송도 advantage는 음수다. 나쁜 “절대 보상”이 아니라 지금 전략의 평균보다 못한 “상대 평가”다.

이 상대 평가를 배우면 “평균보다 좋은 선택의 확률을 높이고, 평균보다 못한 선택의 확률을 낮추자”라는 갱신 규칙을 만들 수 있다. 성공이 쉬운 상태의 +8과 성공이 어려운 상태의 +8을 같은 강도로 칭찬하지 않도록 해 준다.

정확한 advantage의 현재 정책 평균은 0
∑aπ(a∣s)Aπ(s,a)=0\sum_a\pi(a\mid s)A^\pi(s,a)=0
출발점: 0.4×1.71+0.6×(−1.14)=0. 다리 위: 0.75×1.5+0.25×(−4.5)=0. 배치에서 추정한 advantage 평균이 매번 정확히 0이라는 뜻은 아니다.

하지만 실제 경험에서는 Q를 정확히 모른다

환경의 평균을 모르므로 관측한 return에서 가치 예측을 빼는 추정 advantage Â를 사용한다. 모자 표시는 “정확한 값 대신 추정치”라는 뜻이다.

Monte Carlo advantage 추정
A^tMC=Gt−Vψ(st)\hat A_t^{\mathrm{MC}}=G_t-V_\psi(s_t)
ψ는 가치 예측기의 학습 파라미터다. Vψ가 정확하고 이후 행동이 현재 정책에서 나왔다면, 이 추정치를 같은 (s,a)에서 평균했을 때 Aπ와 연결된다.
S₁에서 서두른 실제 경험관측 returnG−V(S₁)
운 좋게 성공+10+3.5
실패−6−12.5
두 결과의 확률 평균0.5×10+0.5×(−6)=20.5×3.5+0.5×(−12.5)=−4.5
왜 여러 rollout이 필요한지 이제 보인다서두르기는 평균적으로 나쁘지만, 성공한 한 번만 보면 좋은 행동처럼 보인다. RL은 이런 잡음이 있는 경험을 여러 번 모아 학습한다. critic과 baseline은 분산을 낮추는 데 도움을 주지만 운과 예측 오류를 완전히 없애지는 못한다.
내 말로 확인하기

“다리 선택의 advantage가 +1.71”은 다리에 들어가면 즉시 +1.71점을 준다는 뜻인가? 아니다. 즉시 보상은 −1이고, 이후까지 고려한 기대 성과가 현재 전략 평균보다 1.71 높다는 뜻이다.

CHAPTER 04 / I · 전통적인 강화학습

Policy gradient와 REINFORCE: 확률이 바뀌는 과정을 눈으로 보기

움직이는 수업 · 경험이 어떻게 다음 선택을 바꿀까?

먼저 재생을 누르세요. 한 장면마다 설명이 바뀝니다. 계산이 낯설면 멈춘 뒤 다음 단계로 따라가세요.

1 / 6
  1. 확률표
  2. 행동 선택
  3. 보상 관측
  4. 평가 신호
  5. 방향 계산
  6. 확률 갱신
현재 상태
갈림길
왼쪽 길 결과 대기
오른쪽 길 결과 대기
왼쪽 50% · 오른쪽 50%

다음번 행동의 선택 확률

왼쪽
50.00%
오른쪽
50.00%

현재 평균: 5.000점

막대는 보상이 아니라 선택 확률입니다.

손잡이 z가 움직이면 평균 점수도 변한다

J258−202손잡이 z →흰 점: 학습 전 · 초록 점: 현재 정책
곡선은 환경을 아는 독자에게 보여주는 이론값입니다.
1. 아직 행동을 고르지 않았습니다

왼쪽과 오른쪽을 각각 50%로 선택하는 정책입니다. 손잡이 z=0에서 시작합니다.

교육용으로 지정한 한 번의 경험을 재생합니다. 무작위 실험이나 누적 학습이 아닙니다. 각 예시는 같은 초기 정책에서 다시 시작합니다. 자동 재생은 마지막 장면에서 멈춥니다.
무엇을 눈여겨봐야 하나요?

①에서는 8점을 본 뒤에만 왼쪽 확률이 올라갑니다. ②에서는 +2점도 양수라 오른쪽을 강화합니다. ④에서는 2−5=−3이 되어 같은 오른쪽 경험으로 오른쪽 확률을 줄입니다. ⑤에서는 행동과 보상 관측은 앞으로 진행하지만, 종료 후 return 계산은 뒤에서 앞으로 진행합니다.

Policy gradient는 행동을 고르는 확률을, 평균 점수가 높아지는 방향으로 조정하는 방법이다. REINFORCE는 그 방향을 실제 경험으로 추정하는 방법이다. 먼저 advantage와 critic 없이 시작하자. 이 둘은 기본 REINFORCE에 반드시 필요한 것이 아니다.

1. 정책은 “왼쪽을 얼마나 자주 고를까?”라는 확률표다

이번 절에서는 앞의 다리 환경을 잠시 단순화한다. 왼쪽으로 가면 8점, 오른쪽으로 가면 2점을 받고 즉시 종료한다. 한 번만 행동하므로 미래 보상도 할인도 필요 없다. 정책은 처음에 두 길을 반반 선택한다. 학습자는 처음부터 두 길의 점수를 안다고 가정하지 않는다. 아래 점수표는 이해를 돕기 위해 독자에게 공개한 환경의 규칙이다.

현재 상태한 번만 선택하는 갈림길
정책: 확률표왼쪽 50% / 오른쪽 50%
행동을 하나 뽑기이번에는 왼쪽이 나왔다
환경에서 점수 받기8점, 종료 → G=8
REINFORCE 그림 1. 정책이 내는 것은 “정답 길”이 아니라 선택 확률이다. rollout 하나에는 실제로 고른 길의 결과만 기록된다.

2. 우리가 높이고 싶은 것은 한 번의 점수가 아니라 평균 점수다

왼쪽 확률을 p라고 쓰면 오른쪽 확률은 1−p다. p=0.5로 여러 번 시도하면 평균 점수는 5점이다. 왼쪽 확률을 0.8로 높이면 평균 점수는 6.8점이 된다. 환경이 주는 8점·2점은 그대로이고, 우리가 바꾸는 것은 선택 빈도다.

목표 J = 현재 정책으로 반복했을 때의 평균 return
J(p)=p⋅8+(1−p)⋅2=2+6pJ(p)=p\cdot8+(1-p)\cdot2=2+6p
J는 학습 목표의 이름이다. 이 장의 한 단계 예제에서는 V(갈림길)와 같은 값이다.
처음: 왼쪽
50%

오른쪽 50% · 평균 5.0점

더 나은 정책
80%

오른쪽 20% · 평균 6.8점

REINFORCE 그림 2. 파란/초록 막대는 점수가 아니라 왼쪽을 고를 확률이다. 80%는 목표 방향의 예시이며 한 번의 학습 결과는 아니다.

3. Gradient는 “확률을 만드는 손잡이를 어느 쪽으로 돌릴까?”다

실제 학습에서는 확률 p를 그대로 더하면 1을 넘을 수 있다. 그래서 제한 없는 숫자 z를 조절하고, sigmoid라는 변환으로 0~1 사이 확률 p를 만든다. z=0이면 반반, z가 양수면 왼쪽을 더 선호한다. 여기서는 손잡이가 하나이고, 신경망에서는 수많은 가중치가 이 손잡이 역할을 한다. 그 전체를 θ라고 쓴다.

z → 선택 확률 p → 평균 점수 J
p=σ(z)=11+e−z,J(z)=2+6σ(z)p=\sigma(z)=\frac{1}{1+e^{-z}},\qquad J(z)=2+6\sigma(z)
z=0 → p=0.5 → J=5. z≈1.386 → p=0.8 → J=6.8.
평균 점수 J2점5점8점−404왼쪽을 선호하는 손잡이 z →현재: p=50%, J=5초록 접선: 현재 기울기 +1.5
REINFORCE 그림 3. 가로축은 손잡이 z, 세로축은 평균 점수 J. 현재 점의 접선이 오른쪽 위를 향하므로 z를 높이는 쪽이 좋다. 환경의 두 보상을 알고 그린 이론 곡선이며, 실제 학습자가 이 곡선 전체를 미리 아는 것은 아니다.

미분/gradient는 현재 위치의 기울기다. 여기서는 z=0 근처에서 z를 아주 조금 높이면 J도 높아진다. 기울기 +1.5는 “z를 0.01 높이면 평균 점수가 약 0.015 높아진다”는 국소적인 뜻이다. 크게 움직였을 때도 같은 직선이라는 뜻은 아니다.

모든 결과를 알고 있다면 직접 계산할 수 있는 이상적인 기울기
dJdz=6p(1−p),p=0.5⇒dJdz=1.5\frac{dJ}{dz}=6p(1-p),\qquad p=0.5\Rightarrow\frac{dJ}{dz}=1.5

4. 그런데 안 가 본 길의 점수는 모른다 → 경험으로 방향을 추정한다

이번 rollout에서 왼쪽을 선택해 8점을 받았다면, 우리는 “그때 선택했던 왼쪽의 확률을 높이는 방향”에 8점만큼의 가중치를 준다. 이를 수식으로 쓰면 아래와 같다. 한 번의 경험으로 계산한 방향이므로 g에 모자(추정 표시)를 붙였다.

한 번의 경험으로 하는 기본 REINFORCE · 한 단계 예제
g^=G∂log⁡πz(a∣s)∂z,znew=z+ηg^\hat g=G\frac{\partial\log\pi_z(a\mid s)}{\partial z},\qquad z_{\mathrm{new}}=z+\eta\hat g
G는 실제 받은 누적점수, πz(a|s)는 그 상태에서 실제 선택한 행동의 확률, η는 한 번에 얼마나 움직일지 정하는 학습률이다. 아직 baseline이나 advantage를 쓰지 않았다.
수식의 조각쉬운 말이번 왼쪽 경험
G이번 결과를 얼마나 강하게 반영할까?8
∂ log π(a|s) / ∂z선택했던 행동이 더 잘 나오려면 손잡이를 어느 방향으로?왼쪽이면 1−p = +0.5
ĝ두 가지를 곱한 이번 경험의 갱신 방향8 × 0.5 = +4
ηĝ실제로 손잡이를 움직일 양학습률 0.1이면 +0.4
관측왼쪽을 뽑았다
G=8
방향 계산ĝ=8×0.5=+4
손잡이 수정z: 0 → 0.4
새 확률왼쪽: 50% → 59.87%
REINFORCE 그림 4. 학습률 η=0.1. p에 0.4를 더하는 것이 아니라 z에 더한 후 sigmoid를 다시 계산한다.

5. 오른쪽도 +2점인데, 그 확률도 높아지는가?

기본 REINFORCE에서는 그렇다. 오른쪽을 뽑아 +2점을 받으면, 이번 경험만으로는 오른쪽 확률이 올라간다. “나쁜 쪽인데 왜?”라는 의문이 자연스럽다. 원형은 평균과 비교하지 않고 양의 return을 받은 행동을 강화한다. 하지만 여러 경험을 평균하면 왼쪽의 강화 효과가 더 크다.

처음 p=0.5에서 가능한 경험그 행동의 log 확률 기울기G × 기울기η=0.1일 때 새 왼쪽 확률
왼쪽 → 8점1−p=+0.5+459.87%
오른쪽 → 2점−p=−0.5−147.50%
경험으로 추정한 방향의 평균 = 앞서 직접 구한 진짜 기울기
E[g^]=0.5⋅(+4)+0.5⋅(−1)=+1.5\mathbb E[\hat g]=0.5\cdot(+4)+0.5\cdot(-1)=+1.5
이 평균은 갱신 전 정책 p=0.5를 고정하고 계산한다. 개별 업데이트마다 평균 성능이 반드시 좋아진다는 보장은 없다.

이것이 policy gradient의 핵심이다. 환경 전체를 미분하지 않아도, 내가 선택한 행동의 확률을 미분하고 관측 점수를 곱하면, 반복 평균에서 원하는 방향을 얻을 수 있다. 실제로는 여러 rollout의 ĝ를 평균해 한 번 갱신하기도 한다.

6. 직접 바꿔 보기: 경험 한 번이 확률을 어떻게 움직이는가?

REINFORCE 한 번의 업데이트 실험
학습 전 왼쪽
학습 후 왼쪽

하나의 관측을 이용한 가상 업데이트다. 슬라이더를 움직이면 선택한 초기 조건에서 다시 계산한다. 업데이트를 누적하지 않는다.

7. 이제 baseline을 더하면 “평소보다 잘했는가?”를 묻는다

위 실험에서 “평균 J를 빼기”를 선택해 보자. 처음 평균은 5점이므로 왼쪽은 8−5=+3, 오른쪽은 2−5=−3이다. 기준점(baseline)을 빼면 낮은 점수의 오른쪽 경험도 확률을 줄이는 신호로 쓸 수 있다.

경험평소 평균을 뺀 점수행동의 log 확률 기울기곱한 방향 ĝ
왼쪽 8점8−5=+3+0.5+1.5 → 왼쪽 확률 증가
오른쪽 2점2−5=−3−0.5+1.5 → 왼쪽 확률 증가

양쪽 모두 z를 +0.15만큼 움직여 왼쪽 확률이 약 53.74%가 된다. 오른쪽을 뽑았는데 왼쪽 확률이 증가하는 이유는, 오른쪽의 상대 점수가 음수라 그 선택을 덜 하도록 움직였기 때문이다. 이 단순한 예제의 p=0.5에서는 어느 경험이 나와도 같은 방향과 크기를 얻는다. 일반적인 환경에서는 baseline을 써도 잡음이 남는다.

REINFORCE with baseline
g^=(G−b(s))∇θlog⁡πθ(a∣s)\hat g=(G-b(s))\nabla_\theta\log\pi_\theta(a\mid s)
b(s)는 행동을 고르기 전 해당 상태의 기준점이다. 같은 상태의 행동들에 공통으로 적용하고, 이번 actor 미분에서는 고정한다.

앞 장의 advantage와 여기서 연결된다. 정확한 advantage는 Q−V, 한 번의 경험으로 만든 추정 advantage는 G−V다. 이 한 단계 환경은 행동별 보상이 확정적이어서 G=Q이고, 정확한 V를 빼면 정확한 A가 된다. 여러 단계·무작위 환경에서는 G−V가 경험마다 달라지는 추정치다. 기본 REINFORCE → baseline 추가 → V를 baseline으로 사용, 이 순서로 이해하면 된다.

심화 · 평균을 빼도 평균 gradient가 그대로인 이유

모든 행동의 확률 합은 항상 1이다. 행동에 공통인 b가 만드는 추가 gradient를 평균하면 b × ∇1 = 0이다. 따라서 조건에 맞는 baseline은 기대 gradient를 바꾸지 않는다. 위 예제도 baseline 전 평균 +1.5, 후 평균 +1.5다.

baseline은 actor gradient에서 상수로 취급
∑aπ(a∣s)b(s)∇log⁡π(a∣s)=b(s)∇∑aπ(a∣s)=0\sum_a\pi(a\mid s)b(s)\nabla\log\pi(a\mid s)=b(s)\nabla\sum_a\pi(a\mid s)=0

8. log는 왜 나오고, loss에는 왜 마이너스가 붙을까?

log는 외워야 하는 장식이 아니다. 선택 확률로 가중해 평균할 때 필요한 수학적 보정이다. 실제로 ∇log π = (∇π)/π이므로, 경험이 뽑힐 확률 π와 평균에서 곱해져 π가 약분된다. 그러면 기대 점수의 미분이 남는다. 아래 유도는 이해한 뒤 펼쳐도 된다.

두 행동만으로 보는 log의 유도
이 장의 한 단계 환경
∇J=∑aR(a)∇π(a)=∑aπ(a)R(a)∇log⁡π(a)\nabla J=\sum_a R(a)\nabla\pi(a)=\sum_a\pi(a)R(a)\nabla\log\pi(a)
마지막 식은 “행동을 π로 뽑고 R∇logπ를 평균하라”는 뜻이다. 그래서 모든 행동의 점수를 매번 알 필요가 없다.

학습 라이브러리는 보통 loss를 줄이는 방향으로 움직인다. 우리는 점수를 높이고 싶으므로 마이너스를 붙여 방향을 맞춘다. G나 baseline을 고쳐서 loss를 줄이는 것이 아니라, 확률을 만드는 z만 고쳐야 한다.

gradient ascent와 loss minimization은 같은 업데이트
L=−sg⁡(G−b)log⁡πz(a∣s),z←z−η∂L∂zL=-\operatorname{sg}(G-b)\log\pi_z(a\mid s),\quad z\leftarrow z-\eta\frac{\partial L}{\partial z}
sg 또는 detach는 이번 미분에서 점수와 기준점을 상수로 취급한다는 뜻이다. 기본 REINFORCE는 b=0이다. 왼쪽 G=8, p=.5이면 ∂L/∂z=−4이므로 z는 +.4 이동한다.
Python · 8점/2점 예제: 관측 하나에서 확률 갱신까지
공통 import·보조 함수는 전체 Python 파일에 포함
import torch

def reinforce_walkthrough(action=1, use_baseline=False):
    # action=1: 왼쪽 8점, action=0: 오른쪽 2점.
    # rollout에서 이 행동이 관측되었다고 가정한 한 번의 재현 예제.
    z = torch.tensor(0.0, requires_grad=True)
    optimizer = torch.optim.SGD([z], lr=0.1)
    policy = torch.distributions.Bernoulli(logits=z)
    # 실제 수집 시: observed_action = policy.sample()
    observed_action = torch.tensor(float(action))
    reward = 8.0 if action == 1 else 2.0
    # 한 번 행동하고 종료했으므로 return G = reward.
    G = torch.tensor(reward)
    p_before = torch.sigmoid(z).detach()
    # 교육용으로 환경의 두 보상을 알아 정확한 V를 계산한다.
    # 실제 환경에서는 별도 추정값을 사용할 수 있다.
    baseline = (2.0 + 6.0 * p_before) if use_baseline else 0.0
    signal = (G - baseline).detach()
    loss = -signal * policy.log_prob(observed_action)
    optimizer.zero_grad()
    loss.backward()
    loss_gradient = z.grad.item()
    optimizer.step()
    return p_before.item(), loss_gradient, torch.sigmoid(z).item()

# 왼쪽 기본형: (0.5, -4.0, 약 0.598688)
# 오른쪽 기본형: (0.5, +1.0, 약 0.475021)
# 평균 5를 빼면 양쪽 모두: (0.5, -1.5, 약 0.537430)
# print(reinforce_walkthrough(action=1, use_baseline=False))

9. 여러 번 행동하면? 끝난 rollout을 거꾸로 계산한다

이제 원래 배송 로봇으로 돌아오자. 실제로 “다리 진입 → 천천히 도착”을 경험했고 보상이 [−1, +8]이면, γ=0.9에서 return은 뒤에서부터 G₁=8, G₀=−1+0.9×8=6.2다. 다리에 들어가는 즉시 보상 −1만 보고 그 행동을 억제하면 이후의 +8을 놓친다. REINFORCE는 이 나중 결과까지 포함한 G₀로 첫 선택을 평가한다.

rollout 수집다리 → 천천히
보상 [−1, +8]
종료 후 계산G₁=8
G₀=6.2
기록과 짝짓기다리 log 확률 ↔ G₀
천천히 log 확률 ↔ G₁
loss 합산·미분확률을 만든 파라미터 갱신
새 정책으로 다시 rollout
REINFORCE 그림 5. 기록한 행동을 뒤늦게 다른 행동으로 바꾸는 것이 아니다. 다음번에 그 행동을 뽑을 확률을 바꾼다.
지금까지의 한 경험 규칙을 여러 행동·여러 rollout으로 합친 식
∇θJ^=1N∑i=1N∑t=0Ti−1γt(Gi,t−b(si,t))∇θlog⁡πθ(ai,t∣si,t)\widehat{\nabla_\theta J}=\frac1N\sum_{i=1}^N\sum_{t=0}^{T_i-1}\gamma^t\bigl(G_{i,t}-b(s_{i,t})\bigr)\nabla_\theta\log\pi_\theta(a_{i,t}\mid s_{i,t})
N은 rollout 개수, t는 경로 안의 시각. J=E[G₀]의 할인 목표라서 바깥 γᵗ도 있다. γ=1이면 모두 1이다. G 안의 할인은 시각 t부터, 바깥 할인은 시작 시각 0부터의 가중치다.
이번 장에서 꼭 구분할 것Policy gradient는 평균 보상을 높이는 정책 파라미터의 방향이다. REINFORCE는 끝까지 경험한 return으로 그 방향을 추정한다. 기본형에는 critic이 없다. 다음 장의 actor–critic은 학습한 가치 예측기를 활용한다. 전체 rollout의 결과를 기다리거나 그 결과를 그대로 쓰는 데서 생기는 비용·분산을 줄이는 방향으로 이어진다.
Python · 추가 실행 예제: 여러 번 반복해서 좋은 행동의 확률 높이기
공통 import·보조 함수는 전체 Python 파일에 포함
def reinforce_demo():
    # Two answers: incorrect 7 / correct 12. This is a one-step policy.
    logits = torch.nn.Parameter(torch.zeros(2))
    opt = torch.optim.SGD([logits], lr=0.2)
    before = logits.softmax(0).detach().tolist()
    for _ in range(120):
        dist = torch.distributions.Categorical(logits=logits)
        action = dist.sample((64,))
        reward = (action == 1).float()
        # Baseline known exactly for this toy; detach before policy loss.
        baseline = logits.softmax(0)[1].detach()
        loss = -(dist.log_prob(action) * (reward - baseline)).mean()
        opt.zero_grad()
        loss.backward()
        opt.step()
    return before, logits.softmax(0).detach().tolist()

근거 · [7] RLOO

CHAPTER 05 / I · 전통적인 강화학습

Actor–Critic: 행동 담당과 예측 담당의 협업

Actor는 정책 πθ다. 상태를 보고 어떤 행동을 얼마나 자주 고를지 결정한다. Critic은 가치 예측기 Vψ다. 같은 상태를 보고 앞으로 받을 누적 보상을 예상한다. “평가자”라는 이름 때문에 critic이 사람의 취향을 판정한다고 생각하기 쉽지만, 여기서는 환경 보상으로 정의된 미래 return을 예측한다.

ACTOR · θ입력: S₀

출력: 안전 0.60 / 다리 0.40
다리를 sampling → 환경으로 보냄

배울 것: 더 나은 행동을 고르는 확률

CRITIC · ψ입력: S₀

출력: 예상 누적 보상 3.14
관측된 결과와 비교할 기준점

배울 것: 미래 누적 보상의 정확한 예측

환경: 다리 선택 → r=−1, 다음 상태 S₁
Critic의 다음 상태 예측: V(S₁)=6.5
TD target=−1+.9×6.5=4.85 → 예측 오차 δ=4.85−3.14=+1.71
로봇 그림 F. 같은 경험이 actor에게는 행동 개선 신호, critic에게는 예측 수정 신호로 쓰인다. 둘은 같은 답을 출력하는 모델이 아니다.
한 단계 TD target와 TD error
yt=sg⁡ ⁣[rt+γ(1−dt)Vψ(st+1)],δt=yt−Vψ(st)y_t=\operatorname{sg}\!\left[r_t+\gamma(1-d_t)V_\psi(s_{t+1})\right],\quad\delta_t=y_t-V_\psi(s_t)
TD는 Temporal Difference. 실제 보상 한 단계와 다음 상태 예측을 이어 붙인다. 이처럼 예측에 다른 예측을 사용하는 것이 bootstrap이다. d=1은 진짜 종료 상태다.
두 개의 loss
LV=12(Vψ(st)−yt)2,Lπ=−log⁡πθ(at∣st)sg⁡(δt)L_V=\frac12(V_\psi(s_t)-y_t)^2,\quad L_\pi=-\log\pi_\theta(a_t\mid s_t)\operatorname{sg}(\delta_t)
critic은 관측 target을 맞추는 회귀를 한다. actor는 TD error를 advantage의 추정치로 사용한다. critic이 정확하면 조건부 평균 TD error는 Aπ와 같다.
갱신 대상현재 값target/신호학습률 0.1로 한 번 갱신
Actor의 logit z−0.4055Â=+1.71z≈−0.3029 → P(다리)≈0.4249
Critic의 V(S₀)3.1400target=4.85003.14+0.1×(4.85−3.14)=3.3110
Critic의 V(S₁)6.5000이번 target에서 고정6.5000 유지
Python · 서로 다른 파라미터에 흐르는 두 gradient
공통 import·보조 함수는 전체 Python 파일에 포함
def actor_critic_one_step():
    # S0: choose bridge (1). State values initially match the example.
    z = torch.nn.Parameter(torch.tensor(math.log(0.4/0.6)))
    values = torch.nn.Parameter(torch.tensor([3.14, 6.5]))
    actor_opt = torch.optim.SGD([z], lr=0.1)
    critic_opt = torch.optim.SGD([values], lr=0.1)
    # Observe r=-1, next state=S1; freeze the TD target.
    target = (-1 + 0.9*values[1]).detach()
    advantage = (target-values[0]).detach()  # +1.71
    logp = F.logsigmoid(z)                 # log P(bridge)
    actor_loss = -logp*advantage
    critic_loss = 0.5*(values[0]-target).square()
    actor_opt.zero_grad(); actor_loss.backward(); actor_opt.step()
    critic_opt.zero_grad(); critic_loss.backward(); critic_opt.step()
    return float(z.sigmoid().detach()), values.detach().tolist()

흥미롭게도 3.14는 처음 정책의 정확한 V였는데도, 다리 샘플 하나를 보면 critic은 3.311로 움직인다. 한 경험의 target은 노이즈가 있기 때문이다. 처음 정책을 고정하고 안전 배송 샘플까지 확률대로 평균하면 이 TD target의 기대값은 다시 3.14다. 학습 도중 정책도 바뀌므로 critic은 변하는 목표를 계속 따라간다.

critic과 reward model을 미리 구분해 두자환경은 r을 준다. critic은 그 보상들의 미래 누적값 V를 예측한다. 뒤에서 만나는 RLHF의 reward model은 사람 선호로부터 보상 R을 만들어 주는 역할이다. “보상 생성”과 “미래 보상 예측”은 다르다.

실제 신경망에서는?

actor와 critic을 완전히 별도 네트워크로 만들 수도 있고, 같은 특징 추출기를 공유한 뒤 정책 head와 가치 head를 나눌 수도 있다. 공유한다면 두 loss가 공통 파라미터에 함께 영향을 준다. 위 코드는 역할을 명확하게 보이기 위해 파라미터를 분리했다.

CHAPTER 06 / I · 전통적인 강화학습

MC·TD·GAE: advantage를 어떻게 추정할까?

우리가 알고 싶은 것은 Q−V지만, 경험에서는 Q를 직접 볼 수 없다. 그래서 얼마나 오래 실제 보상을 보고, 어디부터 critic의 예측으로 대신할지를 결정한다.

방법사용하는 값장점어려운 점
Monte Carlo종료까지 실제 보상부정확한 미래 bootstrap에 덜 의존환경·행동의 운이 많이 섞임; 종료까지 대기
1-step TD보상 1개 + 다음 V짧은 경험에서도 갱신 가능critic이 틀리면 target도 편향
n-stepn개 보상 + n단계 뒤 V두 방식 사이를 절충n을 정해야 함
GAE여러 길이 TD 정보를 λ로 혼합편향·분산 조절γ, λ, 종료 처리와 value 정확도가 중요
n-step advantage
A^t(n)=∑k=0n−1γkrt+k+γnV(st+n)−V(st)\hat A_t^{(n)}=\sum_{k=0}^{n-1}\gamma^k r_{t+k}+\gamma^n V(s_{t+n})-V(s_t)
종료 이후 V는 0. λ-return/GAE는 이런 서로 다른 길이의 정보를 가중 결합하는 관점으로 이해할 수 있다.

같은 “다리 → 천천히” 경험을 끝까지 계산

보상 [−1,8], 값 예측 [3.14,6.5,0], γ=.9를 고정한다.

위치TD residual δMC advantage
t=1 · 천천히8+.9×0−6.5 = 1.58−6.5 = 1.5
t=0 · 다리−1+.9×6.5−3.14 = 1.716.2−3.14 = 3.06
종료 경로에서 뒤로 계산
A^tGAE=δt+γλA^t+1,A^T=0\hat A_t^{\mathrm{GAE}}=\delta_t+\gamma\lambda\hat A_{t+1},\quad\hat A_T=0
δ는 “이번 한 단계가 예상보다 얼마나 좋았나”. 뒤의 δ도 할인하여 더하면 여러 단계에 걸친 예측 오차를 반영한다.
마지막부터Â₁=1.5앞으로 전달γλ=.9×.95=.855첫 위치Â₀=1.71+.855×1.5결과Â₀=2.9925

λ=0 → 첫 위치 Â=1.71 (TD).
λ=.95 → Â=2.9925.
λ=1 → Â=3.06 (이 종료 경로에서는 MC−V).

로봇 그림 G. “advantage 계산”은 추상적인 채점이 아니라 이런 뒤쪽부터의 덧셈이다.
critic의 회귀 target
G^tλ=sg⁡(A^t+Vold(st))\hat G_t^{\lambda}=\operatorname{sg}(\hat A_t+V_{\mathrm{old}}(s_t))
이 예제의 target=[6.1325,8]. GAE를 쓸 때 이 λ-return target은 실제 MC return=[6.2,8]과 다를 수 있다.
Python · 배열을 뒤에서 앞으로 도는 GAE 함수
공통 import·보조 함수는 전체 Python 파일에 포함
def gae(rewards, values, done, gamma=1.0, lam=0.95):
    # One trajectory: rewards [T], values [T+1], done [T].
    advantage = torch.zeros_like(rewards)
    carry = torch.zeros((), dtype=rewards.dtype)
    for t in reversed(range(len(rewards))):
        alive = 1.0 - done[t]
        delta = rewards[t] + gamma * values[t+1] * alive - values[t]
        carry = delta + gamma * lam * alive * carry
        advantage[t] = carry
    returns = advantage + values[:-1]
    return advantage.detach(), returns.detach()
종료와 수집 중단은 구분한다배달이 완료된 terminal이면 다음 V=0이다. 아직 배달 중인데 수집 시간 한도 때문에 끊었다면, 원래 과제가 계속되는 설정에서는 마지막 관측의 V로 bootstrap해야 한다. 이어서 다른 에피소드의 advantage를 섞어 넣으면 안 된다. LLM의 길이 제한도 별도의 truncation 정책을 정해야 한다.
CHAPTER 07 / I · 전통적인 강화학습

왜 정책을 조금씩 바꿔야 하는가?

이전 actor로 배달 경험 100개를 모았다고 하자. 평균적으로 40개는 다리로, 60개는 안전한 길로 갔을 것이다. 이 자료만 반복해서 학습하면 현재 actor는 계속 바뀌지만, 자료 속 행동 비율은 옛 actor의 비율로 남아 있다.

분포다리 선택안전 선택의미
데이터를 만든 πold40%60%저장한 batch의 출처
현재 πθ50%50%이제 학습하려는 정책
비율 ρ=πθ/πold0.5/0.4=1.250.5/0.6≈0.8333고정된 상태에서 행동별 중요도 조정
고정된 상태에서의 importance sampling
Ea∼πθ[f(a)]=Ea∼πold[πθ(a∣s)πold(a∣s)f(a)]\mathbb E_{a\sim\pi_\theta}[f(a)]=\mathbb E_{a\sim\pi_{\mathrm{old}}}\left[\frac{\pi_\theta(a\mid s)}{\pi_{\mathrm{old}}(a\mid s)}f(a)\right]
분모가 0인 행동에 새 정책이 양의 확률을 주는 등의 support 문제는 없다고 가정한다. 자주 등장한 행동의 빈도를 현재 정책에 맞춰 보정하는 식이다.
Surrogate · 원래 목표를 대신하는 근처의 목표
Lsur(θ)=Es,a∼old[ρ(θ)A^old(s,a)]L_{\mathrm{sur}}(\theta)=\mathbb E_{s,a\sim\mathrm{old}}[\rho(\theta)\hat A_{\mathrm{old}}(s,a)]
옛 정책의 상태 방문 분포와 advantage를 고정한다. 행동 ratio 하나로 새 정책의 모든 미래 상태 방문 변화까지 정확히 고치는 것은 아니다.

출발점에서 새 다리 확률을 .50으로 바꾸면:
.4×1.25×1.71 + .6×.8333×(−1.14) = .285.

다리 위의 정책을 고정한 이 특수 예제는 실제 출발점 가치도
3.14 → .5×4.85+.5×2 = 3.425, 즉 +.285 변한다.

하지만 일반적인 신경망 갱신은 여러 상태의 행동을 동시에 바꾸므로 이렇게 깔끔하게 일치하지 않을 수 있다.

로봇 그림 H. surrogate는 이전 경험 근처에서는 유용하지만 멀리 이동할수록 추정 오류·상태 분포 변화가 문제가 된다.

On-policy와 off-policy를 이 문맥에서 읽기

현재 정책으로 갓 만든 경험을 사용하는 것이 on-policy의 기본 상황이다. 다른 정책이 만든 경험을 사용하는 것이 off-policy다. PPO도 한 rollout batch 안에서 몇 번 갱신하면 old와 current가 달라진다. 그렇다고 임의로 오래된 모든 경험을 계속 재사용해도 되는 off-policy 알고리즘이 되는 것은 아니다.

TRPO와 PPO의 출발 질문“좋은 것으로 추정된 행동의 확률을 올리되, 이 추정치를 믿을 수 있을 만큼만 움직이려면?” TRPO는 KL 제약을 직접 다루고, PPO-Clip은 과한 개선의 유인을 loss에서 잘라낸다. 둘 다 optimizer의 gradient norm clipping과는 별개다.
CHAPTER 08 / I · 전통적인 강화학습

TRPO: 정책 변화에 KL 예산을 걸기

파라미터가 0.1만큼 바뀌었다는 말만으로 정책이 얼마나 바뀌었는지 알기 어렵다. 어떤 파라미터는 확률에 거의 영향을 주지 않고, 다른 파라미터는 행동 분포를 크게 바꾼다. TRPO는 행동 확률 분포의 변화를 KL divergence로 측정한다.

KL · old에서 new 방향
DKL(πold∥πθ)=∑aπold(a∣s)log⁡πold(a∣s)πθ(a∣s)D_{\mathrm{KL}}(\pi_{\mathrm{old}}\|\pi_\theta)=\sum_a\pi_{\mathrm{old}}(a\mid s)\log\frac{\pi_{\mathrm{old}}(a\mid s)}{\pi_\theta(a\mid s)}
같은 분포면 0, 달라지면 양수다. 대칭 거리는 아니다. 여기의 “old”는 직전 rollout 정책이고, 뒤의 RLHF에서 고정하는 reference와 역할이 다르다.
실용적 TRPO의 평균 KL 제약
max⁡θ  Eold[ρθA^]s.t.Es∼dold[DKL(πold(⋅∣s)∥πθ(⋅∣s))]≤δ\max_\theta\;\mathbb E_{\mathrm{old}}[\rho_\theta\hat A]\quad\mathrm{s.t.}\quad\mathbb E_{s\sim d_{\mathrm{old}}}[D_{\mathrm{KL}}(\pi_{\mathrm{old}}(\cdot\mid s)\|\pi_\theta(\cdot\mid s))]\le\delta
dold는 옛 정책의 상태 방문 분포, δ는 허용할 분포 변화 예산이다. 이 조건 안에서 surrogate를 최대한 높인다. δ와 TD error δₜ는 문맥이 다른 기호다.

숫자로 보는 허용 범위

다리 위 전략은 고정하고, 출발점의 다리 확률 p만 바꾼다. old p=.4이고 KL 예산은 .01이다.

1차원 문제로 축소
K(p)=0.4log⁡0.4p+0.6log⁡0.61−p,L(p)=2.85(p−0.4)K(p)=0.4\log\frac{0.4}{p}+0.6\log\frac{0.6}{1-p},\quad L(p)=2.85(p-0.4)
출발점 S₀ 하나의 KL 예산으로 축소한 예다. 전체 방문 상태의 평균 KL을 계산할 때는 상태 방문 가중치가 추가된다.
TRPO 실험 · 좋아 보여도 KL 예산을 넘으면 거절

큰 신경망에서 이 제약을 푸는 방법

새 파라미터를 전부 탐색할 수 없으므로 현재 지점 근처에서 목적함수는 1차, KL은 2차로 근사한다. g는 surrogate의 gradient, F는 옛 정책에서 계산한 KL의 곡률 행렬이다. F가 이 문맥의 Fisher information matrix다.

선형 목표와 이차 제약
max⁡Δθg⊤Δθs.t.12Δθ⊤FΔθ≤δ\max_{\Delta\theta}g^\top\Delta\theta\quad\mathrm{s.t.}\quad\frac12\Delta\theta^\top F\Delta\theta\le\delta
자연 gradient 방향과 step 크기
x=F−1g,Δθ=2δg⊤x xx=F^{-1}g,\qquad\Delta\theta=\sqrt{\frac{2\delta}{g^\top x}}\,x
F⁻¹g는 파라미터 좌표의 기울기를 분포 변화의 곡률에 맞게 조정한 방향이다. 실제 구현에서는 F 전체나 역행렬을 만들지 않고 Fisher-vector product와 conjugate gradient로 Fx=g를 근사해서 푼다.
  1. 옛 정책으로 rollout을 모으고 advantage를 추정한다.
  2. 고정된 batch에서 surrogate gradient g를 계산한다.
  3. KL의 곡률을 이용해 Fx=g를 푼다. 수치 안정화를 위해 damping을 넣을 수 있다.
  4. KL 예산 δ에 맞춘 후보 step을 만든다.
  5. 후보 파라미터에서 실제 batch KL과 surrogate 개선을 다시 계산한다.
  6. 통과하지 못하면 step을 1/2, 1/4처럼 줄여 다시 검사한다. 이것이 backtracking line search다.
  7. 허용되는 후보만 actor에 반영한다. critic은 별도의 회귀 loss로 학습한다.
Python · Fisher·step scaling·line search를 모두 보는 1차원 TRPO
공통 import·보조 함수는 전체 Python 파일에 포함
def trpo_scalar(delta=0.01):
    # ONE trainable Bernoulli logit at S0, hold S1 policy fixed.
    # This is an analytic miniature, not a scalable full TRPO trainer.
    p_old = 0.4
    z_old = math.log(p_old/(1-p_old))
    q_difference = 4.85-2.0
    g = q_difference*p_old*(1-p_old)
    fisher = p_old*(1-p_old)  # scalar Hessian of KL at old policy
    direction = g/fisher
    full_step = math.sqrt(2*delta/(g*direction))*direction
    rows = []
    for fraction in [1.0, 0.5, 0.25, 0.125, 0.0625]:
        p = 1/(1+math.exp(-(z_old+fraction*full_step)))
        kl = p_old*math.log(p_old/p)+(1-p_old)*math.log((1-p_old)/(1-p))
        improvement = (p-p_old)*q_difference
        expected = g*fraction*full_step
        accept = kl <= delta and improvement >= 0.1*expected
        rows.append(dict(fraction=fraction,p=p,kl=kl,gain=improvement,accept=accept))
        if accept:
            return rows
    return rows  # if no candidate passes, retain old parameters
후보새 다리 확률실제 KL판정
전체 step0.4708360.010161예산 .01 초과 → 거절
절반 step0.4350870.002522예산 충족·surrogate 개선 → 수용
근사했으므로 반드시 다시 검사한다전체 step은 이차 근사에서는 예산에 맞았지만 실제 KL은 .010161로 조금 초과했다. line search는 실제 함수값으로 이 차이를 확인한다. 1차원 예제의 g=.684, F=.24, F⁻¹g=2.85, 전체 Δz≈.288675다.
이론의 보장과 실용 알고리즘은 구분TRPO의 이론적 성능 개선 bound는 정확한 advantage와 상태별 최대 분포 차이 등 강한 조건을 다룬다. 실용적 평균 KL, 유한 표본, 근사 선형대수·line search로 만든 알고리즘이 실제 보상을 매번 반드시 올린다는 보장은 아니다. 여기의 Python은 한 파라미터에서 메커니즘을 드러내는 축소 구현이다.

근거 · [19] TRPO

CHAPTER 09 / I · 전통적인 강화학습

PPO: clipping으로 rollout을 여러 번 학습하기

TRPO의 곡률 계산과 line search를 매번 수행하는 대신, PPO-Clip은 좋은 방향으로 너무 많이 바뀐 샘플에서 더 이상 보너스를 주지 않는 surrogate를 사용한다. 보통 Adam 같은 1차 optimizer로 여러 번 학습한다. critic과 advantage가 사라지는 것은 아니다.

같은 rollout을 여러 번 학습하면 현재 정책이 데이터를 만든 정책에서 멀어진다. PPO는 현재 확률 / 이전 확률과 advantage를 사용하되, 유리한 방향으로 너무 크게 바뀐 샘플의 추가 유인을 줄인다.

PPO-Clip · maximize
ρt(θ)=πθ(at∣st)πold(at∣st),Jclip=Et[min⁡(ρtA^t,clip⁡(ρt,1−ϵ,1+ϵ)A^t)]\rho_t(\theta)=\frac{\pi_\theta(a_t\mid s_t)}{\pi_{\mathrm{old}}(a_t\mid s_t)},\quad J_{\mathrm{clip}}=\mathbb E_t\left[\min(\rho_t\hat A_t,\operatorname{clip}(\rho_t,1-\epsilon,1+\epsilon)\hat A_t)\right]
예: old probability=0.20, new=0.30 → ρ=1.50. ε=0.20, A=+1이면 min(1.50,1.20)=1.20. A가 음수일 때도 반드시 min을 유지한다.
실험 02 · PPO clipping을 직접 확인하기
20012
clipped objective점선: ρA · 세로선: 1±ε가로축 ρ / 세로축 objective
A와 확률비PPO가 보는 값의미
A=+1, ρ=1.5min(1.5, 1.2)=1.2좋은 행동을 이미 많이 올렸으므로 추가 유인 차단
A=−1, ρ=0.6min(−0.6, −0.8)=−0.8나쁜 행동을 이미 많이 내렸으므로 추가 유인 차단
A=−1, ρ=1.5min(−1.5, −1.2)=−1.5나쁜 행동을 더 자주 하는 방향은 계속 교정
clip은 확률의 물리적 상한이 아니다ρ가 반드시 [1−ε, 1+ε] 안에 머무는 것은 아니다. surrogate의 특정 gradient를 멈추는 장치다. 공유 파라미터, 다른 샘플, value loss 때문에 확률은 여전히 바뀔 수 있다. clipping과 별도의 KL regularization도 다른 장치다.

advantage는 어디서 오는가? Critic과 GAE

critic은 prefix의 가치를 Vψ(sₜ)로 예측한다. GAE는 한 단계 예측 오차를 뒤에서 앞으로 모아, 긴 return의 분산과 짧은 bootstrap의 편향을 절충한다.

GAE recursion
δt=rt+γ(1−dt)Vψ(st+1)−Vψ(st),A^t=δt+γλ(1−dt)A^t+1\delta_t=r_t+\gamma(1-d_t)V_\psi(s_{t+1})-V_\psi(s_t),\quad \hat A_t=\delta_t+\gamma\lambda(1-d_t)\hat A_{t+1}
λ는 여기서 GAE 혼합 계수다. 마지막 상태는 V=0. 보상 [0,0,1], V=[0.2,0.4,0.6,0], γ=1, λ=0.95이면 δ=[0.2,0.2,0.4], A=[0.751,0.580,0.400].

구현은 앞의 GAE 장의 같은 함수를 사용한다.

Actor + Critic + Entropy
LPPO=−Jclip+cVE[(Vψ−G^)2]−cHE[H(πθ)]L_{\mathrm{PPO}}=-J_{\mathrm{clip}}+c_V\mathbb E[(V_\psi-\hat G)^2]-c_H\mathbb E[H(\pi_\theta)]
V̂ target은 rollout에서 계산해 고정한다. Entropy H는 행동 분포가 얼마나 퍼져 있는지 측정하며 탐색을 돕는 보조 항이다. 배송 로봇에서는 환경이 반환한 보상을 사용한다. 뒤의 RLHF에서는 보상 생성 단계만 달라진다.
Python · clipped actor loss와 전체 loss
공통 import·보조 함수는 전체 Python 파일에 포함
def ppo_loss(new_lp, old_lp, advantage, mask, eps=0.2):
    # old_lp is the policy that GENERATED this batch, not the SFT reference.
    ratio = (new_lp - old_lp.detach()).exp()
    a = advantage.detach()
    unclipped = ratio * a
    clipped = ratio.clamp(1-eps, 1+eps) * a
    return -(torch.minimum(unclipped, clipped) * mask).sum() / mask.sum()

def ppo_total(new_lp, old_lp, a, mask, value, returns,
              entropy, value_coef=0.5, entropy_coef=0.01):
    actor = ppo_loss(new_lp, old_lp, a, mask)
    critic = (((value - returns.detach()) ** 2) * mask).sum() / mask.sum()
    ent = (entropy * mask).sum() / mask.sum()
    # If KL was included in rewards before GAE, don't add it again here.
    return actor + value_coef * critic - entropy_coef * ent

근거 · [1] PPO · [2] GAE · [3] InstructGPT / RLHF

방법정책 이동을 다루는 방식갱신 도구
단순 policy gradient높은 reward/advantage 방향으로 이동gradient ascent/descent
TRPO평균 KL 제약 + 곡률 기반 step + line searchFisher-vector product, conjugate gradient
PPO-Clip일부 샘플의 과한 개선 유인을 objective에서 차단보통 Adam, 여러 epoch·minibatch

배송 로봇의 PPO 학습 한 바퀴

  1. Collect: 현재 actor를 그대로 두고 128회 배송한다. 상태·행동·보상·종료와 old logp, old value를 저장한다.
  2. Prepare: 각 에피소드에서 GAE와 critic target을 뒤로 계산한다. episode 경계를 넘겨 섞지 않는다.
  3. Freeze batch: old logp·advantage·target을 detach한다. 이것들은 이 batch를 재사용하는 동안 바꾸지 않는다.
  4. Recompute: 현재 actor로 저장된 상태의 행동 logp를 다시 계산하고 ratio를 만든다. critic도 현재 value를 다시 예측한다.
  5. Optimize: clipped actor loss + value loss − entropy bonus를 미분해 파라미터를 바꾼다.
  6. Repeat locally: 같은 batch로 몇 번만 반복한다. 보통 shuffle한 minibatch로 수행하며 KL이 너무 크면 일찍 멈추기도 한다.
  7. Refresh: 새 actor로 새 배송 경험을 수집한다. 이때 새 old logp를 만든다.
모델 버전 θ₀환경과 상호작용
batch B₀ 생성
B₀로 1차 갱신old=θ₀ 고정
current=θ₁
B₀로 2차 갱신old=θ₀ 그대로
current=θ₂
새 rollout B₁이제 old=θ₂
새 데이터 수집
로봇 그림 I. optimizer step마다 old logp를 current로 덮어쓰면 정책 이동량을 추적할 수 없다.
Python · 외부 환경 라이브러리 없이 실행하는 배송 로봇 PPO
공통 import·보조 함수는 전체 Python 파일에 포함
def robot_ppo(rounds=50):
    # Two states, actions 0/1. S0: safe/bridge; S1: rush/careful.
    torch.manual_seed(23)
    logits = torch.nn.Parameter(torch.tensor([math.log(.4/.6), math.log(.75/.25)]))
    values = torch.nn.Parameter(torch.zeros(2))
    opt = torch.optim.Adam([logits, values], lr=0.04)
    gamma, lam = 0.9, 0.95
    history = []
    for iteration in range(rounds):
        states, actions, old_lps, advantages, targets = [], [], [], [], []
        # Collection and target construction have NO autograd graph.
        with torch.no_grad():
            for _ in range(128):
                trajectory = []
                state = 0
                while True:
                    dist = torch.distributions.Bernoulli(logits=logits[state])
                    action = dist.sample()
                    if state == 0:
                        reward, terminal = ((2.,True) if action == 0 else (-1.,False))
                    else:
                        reward = 8. if action == 1 else (10. if torch.rand(()) < .5 else -6.)
                        terminal = True
                    trajectory.append((state, action, dist.log_prob(action),reward))
                    if terminal: break
                    state = 1
                rs = torch.tensor([row[3] for row in trajectory])
                vs = torch.stack([values[row[0]] for row in trajectory]+[torch.tensor(0.)])
                done = torch.zeros(len(rs)); done[-1] = 1
                adv, ret = gae(rs, vs, done, gamma, lam)
                for j,(s,a,lp,r) in enumerate(trajectory):
                    states.append(s); actions.append(a); old_lps.append(lp)
                    advantages.append(adv[j]); targets.append(ret[j])
        s = torch.tensor(states)
        a, old = torch.stack(actions), torch.stack(old_lps)
        adv, ret = torch.stack(advantages), torch.stack(targets)
        # Full-buffer updates for clarity; a real trainer shuffles minibatches.
        for _ in range(4):
            dist = torch.distributions.Bernoulli(logits=logits[s])
            actor_loss = ppo_loss(dist.log_prob(a), old, adv, torch.ones_like(adv))
            value_loss = 0.5*(values[s]-ret).square().mean()
            loss = actor_loss + value_loss - 0.01*dist.entropy().mean()
            opt.zero_grad(); loss.backward()
            torch.nn.utils.clip_grad_norm_([logits,values],0.5)
            opt.step()
        p,q = logits.sigmoid().detach().tolist()
        history.append((iteration+1,p,q,robot_values(p,q)['V0']))
    return history
코드의 범위실제 환경 결과를 sampling하고 actor·critic을 역전파하는 완결된 작은 PPO다. 두 상태의 표 파라미터를 사용하며, 설명을 위해 전체 rollout buffer를 한 번에 학습한다. GAE·entropy·gradient norm 제한을 포함하지만 value clipping·분산 학습은 생략했다. 일반적인 PPO의 경험 평균 surrogate이며 할인된 시작 상태 목표의 정확한 유한 표본 gradient라고 주장하지 않는다.
CHAPTER 10 / II · LLM과 선호 학습

LLM으로 넘어가기: 방법론의 전체 지도

강화학습은 행동의 결과를 점수로 평가하고, 좋은 결과를 낳은 행동을 더 자주 선택하도록 확률을 바꾸는 학습이다. LLM에서는 “행동”이 다음 토큰을 생성하는 일이다. 이제부터 같은 문제를 따라가며 보상, 확률, gradient가 어떻게 연결되는지 살펴본다.

교재 전체에서 반복할 문제
“사과가 3개씩 든 봉지가 4개 있습니다.
사과는 모두 몇 개인가요?”
● ● ●1번 봉지 · 3개
● ● ●2번 봉지 · 3개
● ● ●3번 봉지 · 3개
● ● ●4번 봉지 · 3개

3 + 3 + 3 + 3 = 3 × 4 = 12

A“좋은 질문이에요! 3 + 4 = 7개입니다.”친절하지만 오답
B“12.”정답 · 짧음
C“한 봉지에 3개, 4봉지이므로 3 × 4 = 12개입니다.”정답 · 설명
그림 1. 직접 만든 학습 예제. 이후의 확률과 점수는 계산을 보여주기 위한 가상 수치이며, 실제 모델의 측정값이 아니다.
구분할 축질문해당 이름
보상의 출처누가 좋은 답이라고 판단하는가?RLHF: 사람 피드백 · RLVR: 검증기
정책 갱신 규칙생성한 답변의 확률을 어떻게 바꾸는가?PPO, REINFORCE/RLOO, GRPO, GSPO, SAPO 등
직접 선호 학습고정된 선호 데이터를 어떤 손실로 학습하는가?DPO: 비교쌍 · KTO: 단일 응답 라벨
서로 대체할 수 없는 이름도 있다“RLHF vs PPO”는 교육 방식과 최적화 도구를 비교하는 셈이다. RLHF에 PPO를 쓸 수 있고, RLVR에도 PPO를 쓸 수 있다. GRPO 역시 보상 모델이나 검증기 어느 쪽과도 결합할 수 있다. DPO·KTO는 넓은 의미의 선호 정렬에 속하지만, 기본형에서는 온라인 rollout 기반 RL 루프를 요구하지 않는다.
01 / FOUNDATIONS확률을 바꾸는 원리

기대값 → MDP → 가치 → 정책 gradient → LLM 토큰

02 / PREFERENCE사람의 판단을 배우기

RLHF 파이프라인 → PPO → DPO → KTO

03 / VERIFICATION정답으로 추론을 배우기

RLVR → GRPO → 안정화·효율 개선 → 2026 연구

근거 · [1] PPO · [3] InstructGPT / RLHF · [4] DPO · [5] KTO · [6] DeepSeekMath / GRPO

CHAPTER 11 / II · LLM과 선호 학습

LLM을 강화학습 문제로 바꾸기

사전학습 → SFT → 후속 정렬을 먼저 구분하자. 사전학습은 대량 텍스트에서 다음 토큰을 예측하며 언어와 지식을 학습한다. SFT는 질문에 답하는 좋은 예제를 모방한다. 선호 학습과 RLVR는 이미 이런 능력을 가진 모델의 응답 분포를 추가로 조정하는 단계이며, 반드시 하나의 고정 순서로만 결합하는 것은 아니다.

질문 x와 지금까지 생성한 토큰 y<ₜ를 합친 것이 상태다. 다음 토큰 yₜ가 행동이다. EOS 토큰이나 생성 한도에 도달하면 에피소드가 끝난다. 분명한 최종 정답이 있는 유한 길이 LLM 예제에서는 γ=1을 자주 사용한다.

Autoregressive policy
st=(x,y<t),at=yt,πθ(y∣x)=∏t=1Tπθ(yt∣x,y<t)s_t=(x,y_{<t}),\quad a_t=y_t,\quad \pi_\theta(y\mid x)=\prod_{t=1}^{T}\pi_\theta(y_t\mid x,y_{<t})
실제 tokenizer와 무관하게 나눈 설명용 토큰
질문prompt · loss 제외3 × 4p = 0.50= 12p = 0.80개입니다p = 0.90EOSp = 0.95

응답 확률 = 0.50 × 0.80 × 0.90 × 0.95 = 0.342
응답 log probability = log 0.50 + log 0.80 + log 0.90 + log 0.95 ≈ −1.073

그림 6. 각 확률은 서로 다른 prefix를 조건으로 한다. 토큰 경계는 실제 모델에 따라 달라지며, 위 그림은 연쇄법칙 설명용이다.
SFT: 정답 응답을 모방
LSFT=−E(x,y∗)∼D[∑t=1∣y∗∣log⁡πθ(yt∗∣x,y<t∗)]L_{\mathrm{SFT}}=-\mathbb E_{(x,y^*)\sim D}\left[\sum_{t=1}^{|y^*|}\log\pi_\theta(y_t^*\mid x,y_{<t}^*)\right]
SFT는 주어진 좋은 응답의 likelihood를 높인다. 같은 질문의 나쁜 응답과 직접 비교하거나 새 응답의 결과를 평가하지는 않는다. 코드에서는 유효 토큰 수로 평균한다.
Python · causal shift, response mask, sequence log probability
공통 import·보조 함수는 전체 Python 파일에 포함
def response_logps(logits, input_ids, response_mask):
    # logits[b,t] predicts input_ids[b,t+1]. Mask labels, not inputs.
    logp = logits[:, :-1].log_softmax(-1)
    target = input_ids[:, 1:]
    chosen = logp.gather(-1, target.unsqueeze(-1)).squeeze(-1)
    mask = response_mask[:, 1:].to(chosen.dtype)
    return chosen, mask, (chosen * mask).sum(-1)

def sft_loss(logits, ids, response_mask):
    token_lp, mask, _ = response_logps(logits, ids, response_mask)
    return -(token_lp * mask).sum() / mask.sum().clamp_min(1)
모델/분포역할변하는 시점
πθ · actor학습하는 LLMoptimizer step마다
πold / behavior이번 rollout을 생성한 모델의 확률새 rollout 배치를 만들 때; 배치 재사용 동안 고정
πref · reference너무 멀리 벗어나지 않을 기준보통 SFT 모델로 고정
Vψ · critic현재 prefix의 미래 성과 예측PPO에서 value loss로 학습
구현에서 가장 자주 헷갈리는 부분PPO/GRPO의 ratio 분모는 πold이고, KL/DPO/KTO의 비교 기준은 πref다. 둘은 초기에는 같을 수 있지만 서로 다른 역할을 가진다. prompt·padding은 응답 손실에서 제외하고, 실제 생성한 EOS는 포함한다.
CHAPTER 12 / II · LLM과 선호 학습

RLHF: 사람의 비교를 보상으로 변환하기

사람에게 모든 새 응답을 매번 평가해 달라고 할 수는 없다. 전형적인 RLHF는 사람의 비교를 모아 보상 모델을 학습하고, 이 모델이 새 응답에 부여하는 점수를 높이도록 정책을 학습한다. InstructGPT는 SFT → 보상 모델 → PPO의 대표 사례다.

1 · SFT좋은 응답 C를
모방하는 LLM
2 · 비교 수집사람의 순위
C ≻ B ≻ A
3 · 보상 모델질문+응답 → 점수
Rφ(x,y)
4 · 온라인 RL새 답 생성 → 점수
PPO로 확률 갱신
그림 7. 사람은 비교 라벨을 제공하고, 보상 모델이 이후 rollout의 평가를 대신한다. PPO는 마지막 갱신 단계의 선택지다.
Bradley–Terry 선호 모형
Pϕ(yw≻yl∣x)=σ(Rϕ(x,yw)−Rϕ(x,yl))P_\phi(y_w\succ y_l\mid x)=\sigma(R_\phi(x,y_w)-R_\phi(x,y_l))
보상 모델 학습
LRM=−EDlog⁡σ(Rϕ(x,yw)−Rϕ(x,yl))L_{\mathrm{RM}}=-\mathbb E_D\log\sigma(R_\phi(x,y_w)-R_\phi(x,y_l))
예: C 점수 2, A 점수 −1이면 C 선호 확률 σ(3)=0.953, loss≈0.049. “2점” 자체보다 비교 점수 차이가 의미를 가진다.
Python · C ≻ B ≻ A 비교로 점수 학습
공통 import·보조 함수는 전체 Python 파일에 포함
def reward_model_demo():
    # Learn scalar scores for 3 fixed answers from human comparisons.
    # A: polite but 7; B: terse 12; C: explains 3*4=12.
    score = torch.nn.Parameter(torch.zeros(3))
    opt = torch.optim.SGD([score], lr=0.2)
    chosen = torch.tensor([2, 1, 2])
    rejected = torch.tensor([0, 0, 1])
    for _ in range(80):
        loss = -F.logsigmoid(score[chosen] - score[rejected]).mean()
        opt.zero_grad()
        loss.backward()
        opt.step()
    return score.detach()
정책의 목표
JRLHF=Ex, y∼πθ[Rϕ(x,y)]−βExDKL(πθ(⋅∣x)∥πref(⋅∣x))J_{\mathrm{RLHF}}=\mathbb E_{x,\ y\sim\pi_\theta}[R_\phi(x,y)]-\beta\mathbb E_xD_{\mathrm{KL}}(\pi_\theta(\cdot\mid x)\|\pi_{\mathrm{ref}}(\cdot\mid x))
RM은 정책 학습 동안 보통 고정된다. 첫 항은 선호 점수, 두 번째는 기준 정책으로부터의 이탈 비용이다.
기대 RM 점수
+2.00
KL 비용 β × KL
−0.25

같은 질문에 대한 기대 RM 점수가 2.00, KL=0.50, β=0.50인 가상 정책이면 J=2.00−0.25=1.75.

그림 8. 목표는 RM 점수만 무제한 높이는 것이 아니다. KL은 완성 응답 한 개의 단순 점수가 아니라 분포에 대한 양이다. 두 값은 같은 질문의 정책 분포에 대해 계산한다.
Python · RM → sampling → critic → PPO, 완결된 작은 학습 루프
공통 import·보조 함수는 전체 Python 파일에 포함
def tiny_rlhf_ppo():
    # Complete RM -> rollout -> value baseline -> PPO loop on 3 answers.
    rm = reward_model_demo()                 # freeze learned human reward
    logits = torch.nn.Parameter(torch.zeros(3))
    critic = torch.nn.Parameter(torch.tensor(0.0))
    ref_lp = torch.full((3,), -math.log(3))  # fixed SFT reference
    opt = torch.optim.Adam([logits, critic], lr=0.03)
    for _ in range(60):
        with torch.no_grad():
            old_all = logits.log_softmax(0)
            actions = torch.distributions.Categorical(logits=logits).sample((96,))
            old_lp = old_all[actions]
            reward = rm[actions] - 0.1 * (old_lp - ref_lp[actions])
            advantage = reward - critic.detach()
        for _ in range(3):                  # reuse this frozen rollout batch
            new_lp = logits.log_softmax(0)[actions]
            actor = ppo_loss(new_lp, old_lp, advantage, torch.ones_like(reward))
            loss = actor + 0.5 * (critic - reward).square().mean()
            opt.zero_grad()
            loss.backward()
            opt.step()
    return logits.softmax(0).detach().tolist()
한계: 선호 점수가 정답의 증명은 아니다RM이 친절한 말투를 과대평가하면 A 같은 오답도 높은 점수를 받을 수 있다. KL은 이런 보상 오용을 완전히 막지 못한다. 이 예제에서 C ≻ B는 “짧게 답해 달라”는 사용자 요구가 없다는 가정이며, 보편적 순위가 아니다.

근거 · [3] InstructGPT / RLHF

CHAPTER 13 / II · LLM과 선호 학습

RLHF 실전 추적: 한 batch가 가중치를 바꾸기까지

이제 RLHF + PPO의 한 rollout batch를 처음부터 끝까지 추적하자. RLHF라는 이름만으로 모든 구현이 같지는 않다. 아래는 고정 보상 모델 + 고정 reference + actor + value critic, 토큰별 KL 표본 비용, GAE, PPO를 사용하는 명시적인 교육용 구성이다. 데이터 수치와 토큰 분할은 가상이다.

00

온라인 RL 전에 준비되는 것

먼저 SFT로 응답을 생성할 초기 actor를 준비한다. 별도의 질문에 대한 응답 비교쌍을 사람이 라벨링해 reward model(RM)을 학습한다. 온라인 RL 중에는 이 RM을 고정한다. SFT actor의 복사본을 reference로 고정한다. critic은 prefix마다 예상 return을 출력하도록 준비하되, 초기 예측은 부정확해도 이후 value loss로 학습한다.

구성요소입력 → 출력이 batch에서 학습?
Actor πθ질문+prefix → 다음 token 분포예 · PPO loss
Behavior πold이번 답변을 생성한 actor 버전고정 · 저장 logp로 대체 가능
Reference πref동일 token/prefix → 기준 logp아니요
Reward model Rφ질문+완성 응답 → 선호 점수아니요
Critic Vψ질문+prefix → 미래 reward의 기대값예 · value loss
01

질문 batch를 뽑고 actor로 답변 생성

질문을 2개 뽑는다고 하자. 각 질문에서 actor가 다음 토큰을 뽑고, prefix에 붙이고, 다시 다음 토큰을 뽑는다. EOS 또는 길이 한도까지 반복한다. 이 generation 단계에서는 계산 그래프를 저장하지 않는다. discrete token sampling 자체에 역전파하는 것도 아니다.

질문 x사과 3개씩
4봉지
상태 s₀질문만 있음
“3×4” sampling
상태 s₁질문 + “3×4”
“=12” sampling
상태 s₂질문 + 앞의 풀이
EOS sampling
RLHF 추적 1. prefix가 상태이고 다음 token이 행동이다. 여기서는 이해를 위해 “3×4”, “=12”, EOS를 각각 하나의 가상 token으로 묶었다.
02

무엇을 저장하는가?

데이터응답 한 개의 예저장 이유
response tokens[계산, 정답, EOS]학습할 행동을 고정
old token probability[.50,.80,.90]PPO ratio의 분모
old token logp[−.6931,−.2231,−.1054]곱셈 대신 안정적인 차이 계산
old value[.20,.40,.60]advantage와 회귀 target 계산
종료·response maskterminal=True, [1,1,1]보상 위치·유효 토큰 구분

실제로는 prompt token, response token, attention mask, truncation 정보도 필요하다. GPU generation engine과 learner를 분리한다면 generation 당시 확률의 정의를 맞춰야 한다. temperature·top-p 변환 뒤의 sampling 분포와 학습에 쓰는 모델 확률이 다르면 단순 ratio가 정확한 behavior 보정은 아니다. 이 추적 예제는 온도 1, 분포 절단 없음으로 일치시킨다.

03

보상 모델 점수와 reference logp 계산

고정 RM이 완성 응답에 1.0점을 준다고 하자. 이것은 선호 점수이며 100% 정답 확률이라는 뜻은 아니다. reference는 같은 응답의 토큰 확률 [.40,.80,.90]을 준다. β=.1로 기준 정책을 벗어난 표본 비용을 만든다.

이 배치의 reward 구성 · γ=1
rtKL=−β(log⁡πold(yt∣st)−log⁡πref(yt∣st)),rt=rtKL+1[t=T−1]Rϕ(x,y)r_t^{\mathrm{KL}}=-\beta(\log\pi_{\mathrm{old}}(y_t\mid s_t)-\log\pi_{\mathrm{ref}}(y_t\mid s_t)),\quad r_t=r_t^{\mathrm{KL}}+\mathbf1[t=T-1]R_\phi(x,y)
reward는 rollout을 수집한 정책의 logp로 만들고 이번 batch에서는 고정한다. 일부 구현은 다른 보상·KL 설계를 사용한다.
가상 tokenold pref pKL 표본 비용RM 점수최종 rₜ
계산.50.40−.1 log(1.25)=−.0223140−.022314
정답.80.80000
EOS.90.900+1.01.0

RM 점수는 마지막 유효 응답 token에 한 번만 더한다. 모든 token에 1점을 주면 긴 응답이 더 많은 보상을 받는 다른 과제가 된다. 이 표의 log ratio 비용 한 개는 정확한 전체 KL이 아니며 음의 비용 또는 양의 보너스가 나올 수도 있다.

04

critic으로 GAE와 target을 계산

γ=1, λ=.95, 마지막 상태 V=0이다. 위 reward와 old value를 사용한다.

trₜVold(sₜ)δₜ=rₜ+Vnext−VÂₜ=δₜ+.95Ânexttarget=Â+Vold
21.000000.600000.400000.4000001.000000
10.000000.400000.200000.580000.980000
0−.022314.200000.177686.728686.928686

완성 응답에 한 번 붙인 점수가 GAE를 통해 앞의 token에도 연결됐다. 하지만 이것은 각 token의 인과적 정답 여부를 판정한 것이 아니다. 계산된 Â와 target은 detach한다. 이 예제에서는 숫자를 그대로 추적하기 위해 advantage whitening을 하지 않는다.

05

같은 응답을 현재 actor에 다시 넣기

저장된 질문과 응답을 입력하고 선택됐던 token의 logp를 다시 계산한다. 이 단계는 teacher forcing 방식의 scoring이다. 이번에는 actor에 gradient가 필요하다. 새 응답을 생성해서 기존 reward와 짝지으면 안 된다.

같은 저장 tokenold p · 고정현재 p · 재계산ρÂ
계산.50.551.10.728686
정답.80.841.05.580000
EOS.90.901.00.400000

rollout 직후 아직 업데이트 전이면 current=old이므로 ρ=1이다. 표는 그 배치로 한 번 이상 갱신한 뒤를 보여주는 가상 중간 시점이다. current가 바뀌어도 기존 old p, RM reward, Â, target은 그대로 둔다.

06

loss를 만들고 어느 모델에 미분할지 정하기

ε=.2이면 표의 ratio는 모두 clip되지 않는다. actor objective=(1.10×.728686+1.05×.58+1.00×.4)/3≈.603518, actor loss≈−.603518이다. critic이 아직 [.2,.4,.6]을 예측한다면 ½ mean((V−target)²)≈.17123다. 정확한 값은 아래 코드가 출력한다.

Actor logpgradient 있음Critic valuegradient 있음old logp / Â / targetgradient 없음RM / reference고정
RLHF 추적 2. teacher forcing scoring에서 계산 그래프를 다시 만들고, sampling·reward·고정 모델을 미분 경로에서 제외한다.

loss = actor loss + critic loss로 역전파한다. 예제에서는 entropy를 0으로 두고 value coefficient는 ½ 제곱오차에 이미 포함했다. reward에 KL을 넣었으므로 같은 비용을 loss에 또 더하지 않는다. 실무의 value clipping·entropy·reward scaling 등은 별도로 명시해야 한다.

Python · reward부터 actor·critic gradient까지 수치 재현
공통 import·보조 함수는 전체 Python 파일에 포함
def rlhf_batch_trace():
    # A 3-token response, gamma=1. All probabilities are hypothetical.
    old_lp = torch.tensor([.5,.8,.9]).log()
    ref_lp = torch.tensor([.4,.8,.9]).log()
    old_values = torch.tensor([.2,.4,.6,0.])
    reward = -0.1*(old_lp-ref_lp)   # sampled reference penalty
    reward[-1] += 1.0              # frozen reward model's terminal score
    a, target = gae(reward,old_values,torch.tensor([0.,0.,1.]),1.,.95)
    new_lp = torch.tensor([.55,.84,.9]).log().requires_grad_()
    value = old_values[:-1].clone().requires_grad_()
    actor = ppo_loss(new_lp,old_lp,a,torch.ones(3))
    critic = .5*(value-target).square().mean()
    loss = actor+critic
    loss.backward()
    return dict(reward=reward.tolist(),advantage=a.tolist(),target=target.tolist(),
                ratio=(new_lp.detach()-old_lp).exp().tolist(),
                actor_loss=actor.item(),critic_loss=critic.item(),
                logp_gradient=new_lp.grad.tolist(),value_gradient=value.grad.tolist())
07

optimizer step → 배치 재사용 → 다음 rollout

실제 가중치 한 개를 바꾸어 보자. 첫 token vs 나머지 token을 Bernoulli로 축소하고 p=σ(z)=.55, old p=.5로 두자. 위 첫 token의 loss를 전체 3-token 평균의 한 항으로 쓰면 ∂L/∂z=−ρÂ(1−p)/3≈−.120233이다. 학습률 .1로 z←z−.1×gradient를 수행하면 p=.55 → .552974가 된다. 이것이 “loss를 계산했다”와 “모델을 학습했다” 사이의 마지막 연결이다.

zero_grad → backward → optimizer.step으로 actor와 critic을 바꾼다. 같은 batch의 다른 minibatch 또는 다음 epoch에서는 현재 logp·value만 다시 계산한다. 몇 회가 끝나면 새 actor로 새로운 응답을 생성한다. reward model과 reference는 계속 고정이고, behavior 버전만 새 actor로 갱신된다.

rollout kold logp·reward
·target 저장
내부 갱신 1…Ecurrent logp·V 재계산
PPO + value loss
새 actor θk+1분포가 바뀜
새 응답을 시도
rollout k+1새 old logp로 교체
RM·ref는 그대로
RLHF 추적 3. 보상만 계산해서 저장하는 것으로 학습이 끝나지 않는다. 그 결과를 가중치로 사용한 미분이 파라미터를 바꾼다.
08

다음 batch에서 확인할 것

RM 점수와 별도로 held-out 사람 선호 평가, 응답 길이, reference KL, clip 비율, critic error를 본다. RM 점수가 오르는데 실제 유용성이 떨어지면 보상 모델의 허점을 학습했을 수 있다. EOS 없이 잘린 응답을 어떻게 점수화할지와 bootstrap 여부는 reward 계약으로 정한다.

근거 · [3] InstructGPT / RLHF · [20] RLHF PPO 구현 세부사항

CHAPTER 14 / II · LLM과 선호 학습

DPO: 보상 모델 없이 비교쌍을 직접 학습

DPO는 질문 하나에 대한 선호 응답 yw와 비선호 응답 yl을 사용한다. 보상 모델을 별도로 맞춘 뒤 RL을 돌리는 대신, 정책의 log probability 비율로 선호 모형을 직접 학습한다.

고정 데이터x: 사과 질문
chosen=C, rejected=A
두 정책의 평가현재 πθ, 기준 πref
각 답의 logp 계산
DPO loss기준 대비 C의 증가가
A보다 크도록 학습
그림 9. 기본 DPO는 학습 데이터에 있는 두 응답을 scoring한다. 학습 step마다 새로운 답변 rollout을 생성할 필요가 없다.
왜 reward model을 없앨 수 있는가?
π∗(y∣x)=πref(y∣x)exp⁡(R(x,y)/β)Z(x),R(x,y)=βlog⁡π∗(y∣x)πref(y∣x)+βlog⁡Z(x)\pi^*(y\mid x)=\frac{\pi_{\mathrm{ref}}(y\mid x)\exp(R(x,y)/\beta)}{Z(x)},\quad R(x,y)=\beta\log\frac{\pi^*(y\mid x)}{\pi_{\mathrm{ref}}(y\mid x)}+\beta\log Z(x)
KL 정규화 보상 최대화 문제의 이상적 최적 정책을 재표현한다. 두 응답의 reward 차이를 구하면 같은 질문의 log Z(x)가 사라진다.
DPO loss
m=log⁡πθ(yw∣x)πref(yw∣x)−log⁡πθ(yl∣x)πref(yl∣x),LDPO=−EDlog⁡σ(βm)m=\log\frac{\pi_\theta(y_w\mid x)}{\pi_{\mathrm{ref}}(y_w\mid x)}-\log\frac{\pi_\theta(y_l\mid x)}{\pi_{\mathrm{ref}}(y_l\mid x)},\quad L_{\mathrm{DPO}}=-\mathbb E_D\log\sigma(\beta m)
가상 생성 확률chosen Crejected A
기준 정책 πref0.250.25
현재 정책 πθ0.500.10
기준 대비 배율2.0배0.4배

m = log 2 − log 0.4 = log 5 ≈ 1.609. β=1이면 선호 모형의 P(C ≻ A)=5/6≈0.833, loss≈0.182다. 나머지 응답에도 확률 질량이 남아 있다.

실험 03 · DPO margin과 β
10−404
가로축 margin m / 세로축 선호 확률 σ(βm)
P(chosen ≻ rejected)
Python · sequence logp 차이로 계산하는 DPO
공통 import·보조 함수는 전체 Python 파일에 포함
def dpo_loss(chosen_lp, rejected_lp, ref_chosen_lp, ref_rejected_lp, beta=0.1):
    # All inputs are RESPONSE sequence log-probability SUMS, shape [B].
    chosen_ratio = chosen_lp - ref_chosen_lp.detach()
    rejected_ratio = rejected_lp - ref_rejected_lp.detach()
    return -F.logsigmoid(beta * (chosen_ratio - rejected_ratio)).mean()

def dpo_numeric():
    return dpo_loss(torch.tensor([math.log(0.5)]), torch.tensor([math.log(0.1)]),
                    torch.tensor([math.log(0.25)]), torch.tensor([math.log(0.25)]),
                    beta=1.0).item()  # 0.182322 = -log(5/6)
DPO에 대한 두 가지 오해β가 커지면 고정 margin에서 sigmoid가 가팔라지지만, 원래 KL 정규화 문제에서는 큰 β가 기준 정책을 더 강하게 묶는다. “β를 키우면 무조건 선호가 더 강해진다”는 해석은 부정확하다. 또한 DPO는 상대 log ratio를 개선하므로 chosen의 절대 확률이 항상 증가한다는 보장은 없다.

이상적 유도는 무한한 데이터·표현력·최적화를 가정한다. 실제 DPO와 PPO가 유한 데이터에서 같은 결과를 내는 것은 아니다. 기본 DPO는 주어진 비교쌍의 범위를 벗어나는 탐색을 직접 수행하지 않는다.

근거 · [4] DPO · [17] TRL DPO 구현 문서

CHAPTER 15 / II · LLM과 선호 학습

KTO: 짝이 없는 좋음/나쁨 피드백

어떤 서비스에는 같은 질문의 비교쌍 없이 “이 답변이 좋았나요?”라는 피드백만 쌓인다. KTO는 (질문, 응답, 좋음/나쁨)으로 학습한다. 기준 정책 대비 log ratio가 기준점 z보다 높은지 낮은지를 sigmoid utility로 평가한다.

1사과 질문 → “3 × 4 = 12개입니다.”좋음 ✓
2다른 질문 → “질문과 무관한 답변”나쁨 ✕

두 응답은 같은 질문의 비교쌍일 필요가 없다.

그림 10. DPO의 “C가 A보다 낫다”와 KTO의 “C는 좋다”는 다른 정보다. 나쁜 답 둘 중 승자라고 해서 절대적으로 좋은 답은 아니다.
KTO의 기준점
rθ=log⁡πθ(y∣x)πref(y∣x),z=sg⁡ ⁣(DKL(πθ∥πref))r_\theta=\log\frac{\pi_\theta(y\mid x)}{\pi_{\mathrm{ref}}(y\mid x)},\quad z=\operatorname{sg}\!\left(D_{\mathrm{KL}}(\pi_\theta\|\pi_{\mathrm{ref}})\right)
여기서는 rθ에 β를 포함하지 않는 원 논문 v4 표기다. 이론상 z는 질문별 KL이며, 실무에서는 배치 추정치를 사용한다.
KTO loss · batch mean
ℓKTO={λD[1−σ(β(rθ−z))],desirableλU[1−σ(β(z−rθ))],undesirable\ell_{\mathrm{KTO}}=\begin{cases}\lambda_D[1-\sigma(\beta(r_\theta-z))],&\text{desirable}\\\lambda_U[1-\sigma(\beta(z-r_\theta))],&\text{undesirable}\end{cases}
DPO의 −log σ와 달리 1−σ를 쓴다. 양끝에서 포화하므로 극단적인 샘플에 대한 gradient가 작아질 수 있다. λD, λU는 좋음/나쁨 데이터의 가중치다.

예: 좋은 응답의 현재 확률 0.50, 기준 0.25이면 r=log 2≈0.693. z=0.20, β=λD=λU=1에서 좋은 라벨의 loss≈0.379, 나쁜 라벨의 loss≈0.621이다.

실험 04 · 같은 응답, 반대 라벨
Python · mismatched pair 기준점 추정과 stop-gradient
공통 import·보조 함수는 전체 Python 파일에 포함
def kto_loss(policy_lp, ref_lp, desirable, mismatch_lp, mismatch_ref_lp,
             beta=1.0, lambda_good=1.0, lambda_bad=1.0):
    # mismatched (x_i, y_j), j != i; needs separate model forward passes.
    # This is the paper's convenient BIASED KL-reference estimator.
    z = (mismatch_lp - mismatch_ref_lp).mean().clamp_min(0).detach()
    logratio = policy_lp - ref_lp.detach()
    good = lambda_good * (1 - torch.sigmoid(beta * (logratio - z)))
    bad = lambda_bad * (1 - torch.sigmoid(beta * (z - logratio)))
    return torch.where(desirable.bool(), good, bad).mean()
기준점 추정은 정확한 KL 계산과 다르다원 논문은 배치의 질문과 다른 응답을 섞은 (xᵢ,yⱼ)의 log ratio 평균을 0 이상으로 잘라 z를 근사한다. 편향된 추정량이며 추가 forward pass가 필요하다. 기준점을 역전파하지 않는다. 전체 파일의 함수는 이 선택을 명시한다.

근거 · [5] KTO · [18] TRL KTO 구현 문서

CHAPTER 16 / III · LLM 학습 루프

RLVR: 사람의 취향 대신 결과를 검증하기

Reinforcement Learning with Verifiable Rewards는 정답 비교, 코드 테스트, 형식 증명 검사처럼 결과를 검증할 수 있는 보상을 사용한다. 정답 텍스트를 그대로 모방하는 SFT와 달리, 모델이 직접 시도한 응답에 점수를 준 뒤 학습한다.

질문과 정답사과 문제
gold = 12
정책의 시도“3+4=7”
“3×4=12”
자동 검증최종값 == 12 ?
0 또는 1
정책 갱신PPO / GRPO 등
정답 경로의 확률 ↑
그림 11. 보상의 출처는 검증기이며, 최적화 알고리즘은 별도로 선택한다. RLVR가 곧 GRPO인 것은 아니다.
이진 outcome reward
R(x,y)=1[verify⁡(x,y)=true],JRLVR=Ey∼πθ[R(x,y)]R(x,y)=\mathbf1[\operatorname{verify}(x,y)=\mathrm{true}],\quad J_{\mathrm{RLVR}}=\mathbb E_{y\sim\pi_\theta}[R(x,y)]
0/1은 교육용 기본형이다. 테스트 통과 비율 같은 부분 보상이나 형식 보상을 더한 구성이 가능하며, 설계에 따라 목표도 달라진다.
Python · 최종 정수 답변을 검증하는 작은 보상 함수
공통 import·보조 함수는 전체 Python 파일에 포함
def verify_integer(text, gold=12):
    # Toy contract: exactly one final tag, containing a signed integer.
    tags = re.findall(r"<final>(.*?)</final>", text, flags=re.S)
    if len(tags) != 1 or not re.fullmatch(r"\s*[+-]?\d+\s*", tags[0]):
        return 0.0
    return float(int(tags[0]) == gold)

samples = ["3+4=7 <final>7</final>", "3*4=12 <final>12</final>",
           "<final>12</final>", "3*4=14 <final>14</final>"]
# [0, 1, 1, 0]; correct final answer does NOT certify the reasoning.
응답최종 정답 검증검증으로 알 수 없는 것
“3 × 4 = 12, <final>12</final>”1설명의 충분성·문체
“3 + 4 = 12, <final>12</final>”1중간 식이 틀렸다는 사실
“정답은 아마 12?”0 (위 계약 기준)정답 의미는 맞아도 지정 형식 불충족
올바른 코드 + 공개 테스트 통과테스트 기반 점수숨은 입력에서도 옳은지는 별도 검증
검증 가능함 ≠ 완전함위 정수 검사기는 이 교재용 계약이다. 수식 동치성, 단위, 파싱 모호성은 다루지 않는다. 코드 학습에서는 격리된 실행 환경·시간 제한·숨은 테스트가 필요하다. 정확도만 보상하면 친절함이나 설명의 진실성은 별도 목표로 남는다.

DeepSeek-R1에서 읽어야 할 구분

R1-Zero는 사전학습된 base model에 별도 초기 SFT 없이 RL을 적용한 설정이다. R1은 cold-start 데이터와 여러 학습 단계를 포함한다. “SFT 없이 가능했다”를 “언어·추론 지식 없이 처음부터 배웠다”로 읽으면 안 된다. 사전학습된 정책이 이미 다양한 후보 풀이를 생성한다.

근거 · [8] DeepSeek-R1

CHAPTER 17 / III · LLM 학습 루프

GRPO: 같은 질문의 답변끼리 비교하기

GRPO는 한 질문에 G개의 응답을 생성한 뒤 그 그룹의 평균을 기준점으로 삼는다. PPO에서 별도로 학습하던 critic을 없애는 대신 여러 rollout의 비용을 지불한다. 여기서는 최종 보상을 모든 응답 토큰에 공유하는 outcome-reward 형식을 다룬다.

Group-normalized advantage
μ=1G∑iRi,σR=1G∑i(Ri−μ)2,A^i=Ri−μσR+εnum\mu=\frac1G\sum_iR_i,\quad \sigma_R=\sqrt{\frac1G\sum_i(R_i-\mu)^2},\quad \hat A_i=\frac{R_i-\mu}{\sigma_R+\varepsilon_{\mathrm{num}}}
εnum은 0 나누기 방지용 작은 수이며 clip 폭 ε와 다르다. 교재의 숫자는 모집단 표준편차를 사용한다.
실험 05 · 응답을 누르면 보상 0/1 전환

초기에는 [0,1,1,0]이다. 정답을 하나만 남기거나 전부 정답으로 바꾸어 보자. 버튼의 문구는 후보 이름이며, 클릭은 가상 검증 결과를 바꾸는 실험이다.

3 × 4A=+1=A=+112A=+1EOSA=+1
3 + 4A=−1=A=−17A=−1EOSA=−1
그림 12. 초기 그룹에서 μ=0.5, σ=0.5. 각 응답은 자신의 A를 모든 토큰에 공유한다. 이것만으로 어떤 추론 단계가 원인인지 정확히 구분하는 것은 아니다.
원형 GRPO: 응답별 토큰 평균 → 응답 평균
JGRPO=E[1G∑i1Ti∑t(min⁡(ρi,tA^i,clip⁡(ρi,t,1−ϵ,1+ϵ)A^i)−βki,t)]J_{\mathrm{GRPO}}=\mathbb E\left[\frac1G\sum_i\frac1{T_i}\sum_t\left(\min(\rho_{i,t}\hat A_i,\operatorname{clip}(\rho_{i,t},1-\epsilon,1+\epsilon)\hat A_i)-\beta k_{i,t}\right)\right]
ρ는 토큰별 πθ/πold. k는 기준 정책과의 KL 항 또는 표본 근사이다. 구현 변형마다 평균 방식, KL, reward 정규화가 달라지므로 “GRPO”라는 이름만으로 loss를 판단하지 않는다.
Python · 원형 집계와 선택적 KL 근사
공통 import·보조 함수는 전체 Python 파일에 포함
def grpo_loss(new_lp, old_lp, a, mask, ref_lp=None, beta=0.0, eps=0.2):
    ratio = (new_lp - old_lp.detach()).exp()
    a = a.detach().unsqueeze(-1)
    obj = torch.minimum(ratio*a, ratio.clamp(1-eps, 1+eps)*a)
    if beta:
        assert ref_lp is not None
        # Nonnegative sampled KL surrogate used in GRPO.
        # Off-policy batches make this an approximation, not exact KL.
        d = ref_lp.detach() - new_lp
        kl = d.exp() - d - 1
        obj = obj - beta * kl
    return -seq_mean(obj, mask)
표본 KL 근사의 범위코드의 k = exp(d) − d − 1, d=log πref−log πθ는 각 값이 0 이상인 대체 항이다. 현재 정책 샘플에 대한 기대값은 해당 조건부 KL과 연결되지만, 오래된 rollout·토큰 상태 분포·미분 경로까지 포함한 학습에서는 정확한 전체 KL이라고 부르면 안 된다.

근거 · [6] DeepSeekMath / GRPO · [16] TRL GRPO 구현 문서

CHAPTER 18 / III · LLM 학습 루프

RLVR 실전 추적: 검증 결과가 gradient가 되는 과정

이번에는 RLVR + GRPO를 같은 깊이로 추적한다. 비교를 명확히 하기 위해 이 구성은 규칙 기반 정답 보상, critic 없음, 명시적 KL 없음(β=0), 원형 GRPO의 응답별 평균을 사용한다. RLVR를 PPO와 결합하면 앞 장의 critic·GAE 단계가 그대로 돌아온다.

00

질문과 verifier의 계약 준비

학습 질문에는 사과 수의 gold answer 12가 함께 있다. actor는 질문만 받고, verifier는 질문·출력·gold를 받는다. 정답을 어느 태그에서 읽을지, 파싱 실패·여러 정답 태그·길이 제한·시간 초과를 어떻게 처리할지 먼저 고정한다. 학습을 보고 임의로 정답 판정을 바꾸지 않는다.

01

질문마다 여러 응답 rollout 생성

질문 2개, 질문당 G=4개를 뽑으면 응답은 8개다. 같은 actor에서 각 응답을 독립적으로 sampling한다. 배치의 질문 경계를 보존해야 한다. 아래는 사과 질문의 4개만 펼쳤다.

그룹rollout생성 응답정답 보상
사과 질문A3+4=7 → <final>7</final>0
사과 질문B3×4=12 → <final>12</final>1
사과 질문C3+3+3+3=12 → <final>12</final>1
사과 질문D3×4=14 → <final>14</final>0

각 응답의 token IDs, old token logp, 유효 response mask, 그룹 ID, EOS/truncation을 저장한다. generation에서는 gradient를 저장하지 않는다. 샘플링 분포와 old logp의 정의는 RLHF 때와 마찬가지로 일치시킨다.

02

검증기로 reward 계산

문자열에서 최종값을 읽고 12와 비교하면 R=[0,1,1,0]이다. verifier는 미분 가능한 신경망일 필요가 없다. Python의 숫자 비교, 테스트 실행, 증명 검사도 가능하다. 검증기를 미분해서 actor를 학습하는 것이 아니다. 계산한 점수가 다음 단계의 계수가 된다.

03

그룹 기준점과 advantage 계산

같은 질문 4개의 평균 μ=.5, 모집단 표준편차 σ=.5이므로 Â=[−1,+1,+1,−1]이다. 다른 질문의 답과 섞어 평균내면 “같은 난도의 다른 시도”라는 기준점이 사라진다.

PPO의 질문

“이 prefix의 예측 return V와 비교하면?”
별도의 critic과 GAE를 사용한다.

이 GRPO 구성의 질문

“같은 질문에서 나온 다른 응답들과 비교하면?”
그룹 통계가 기준점이며 critic을 학습하지 않는다.

RLVR 추적 1. critic이 사라져도 상대 평가 자체가 사라진 것은 아니다. 기준점을 얻는 출처가 바뀌었다.

전부 0 또는 전부 1이면 centered advantage는 0이다. 수치 분모를 안전하게 처리하고 그 그룹의 보상 기반 정책 항을 0으로 두거나, DAPO처럼 새 그룹을 모으는 정책을 택한다. 없는 정답 신호를 0 나누기로 만들어 낼 수는 없다.

04

응답의 A를 토큰에 나눠 주기

여기서는 최종 outcome만 보므로 B 응답의 모든 유효 token에 +1, A 응답의 모든 token에 −1을 복제한다. 보상 1을 token 수만큼 누적하는 것이 아니라, policy-gradient의 계수를 공유하는 것이다. prompt와 padding token의 mask는 0, 실제 생성 EOS의 mask는 1이다.

B의 계산Â=+1B의 정답·EOSÂ=+1
A의 계산Â=−1A의 정답·EOSÂ=−1
RLVR 추적 2. 다음 표의 token 1/2는 loss 계산을 보여주기 위한 가상 두 위치다. 실제 문장 길이나 tokenizer와 동일하다는 뜻은 아니다.
05

현재 logp를 재계산하고 PPO형 surrogate 계산

모든 old token probability가 .2인 가상 4×2 tensor를 생각하자. 이미 일부 업데이트한 current probability를 아래처럼 놓는다. ε=.2다.

응답A현재 p 두 개ratio 두 개clip 후 objective 두 개
A−1[.18,.16][.9,.8][−.9,−.8]
B+1[.22,.26][1.1,1.3][+1.1,+1.2]
C+1[.20,.24][1.0,1.2][+1.0,+1.2]
D−1[.22,.14][1.1,.7][−1.1,−.8]

B의 두 번째 token은 좋은 행동의 비율이 1.3이므로 개선 유인을 1.2에서 멈춘다. D의 두 번째 token은 나쁜 행동의 비율을 .7까지 이미 줄였으므로 −.8에서 멈춘다. D의 첫 token은 오히려 더 자주 나오므로 교정 gradient가 살아 있다.

06

mask·길이 평균·loss·역전파

각 응답의 평균 objective는 A=−.85, B=1.15, C=1.10, D=−.95. 이를 4개 평균하면 .1125이므로 loss=−.1125다. 이 예제는 길이가 모두 같아 전역 token mean과 값이 같지만, 길이가 다르면 원형 GRPO와 DAPO의 가중치가 달라진다.

이 batch의 actor loss
L=−14∑i=1412∑t=12min⁡(ρi,tA^i,clip⁡(ρi,t,.8,1.2)A^i)=−.1125L=-\frac14\sum_{i=1}^4\frac12\sum_{t=1}^2\min(\rho_{i,t}\hat A_i,\operatorname{clip}(\rho_{i,t},.8,1.2)\hat A_i)=-.1125

actor의 현재 logp만 미분한다. reward, old logp, group mean/std, advantage는 고정 계수다. critic이 없으므로 value loss도 없다. reference KL을 넣는 변형이면 reference scoring과 별도 KL 항이 추가된다.

Python · 4×2 tensor로 loss와 gradient 확인
공통 import·보조 함수는 전체 Python 파일에 포함
def rlvr_batch_trace():
    # Four responses; two valid tokens each. Every row belongs to SAME prompt.
    rewards = torch.tensor([[0.,1.,1.,0.]])
    a = group_advantage(rewards)[0]
    old_lp = torch.full((4,2),.2).log()
    new_probs = torch.tensor([[.18,.16],[.22,.26],[.20,.24],[.22,.14]])
    new_lp = new_probs.log().requires_grad_()
    mask = torch.ones_like(new_lp)
    loss = grpo_loss(new_lp,old_lp,a,mask,beta=0.,eps=.2)
    loss.backward()
    return dict(reward=rewards[0].tolist(),advantage=a.tolist(),
                ratio=(new_probs/.2).tolist(),loss=loss.item(),
                logp_gradient=new_lp.grad.tolist())
07

업데이트가 0에서 출발하는 것처럼 보여도 학습되는 이유

처음에는 ρ=1이고 그룹의 A 평균이 0이므로 전체 objective 값이 0일 수 있다. 그러나 token마다 ∇logπ가 다르기 때문에 gradient의 합이 0일 필요는 없다. 함숫값 0과 기울기 0은 다르다. 좋은 응답의 확률은 올리고 나쁜 응답의 확률은 내리는 방향이 여전히 생긴다.

실제 logits를 갱신하는 축소 예제: 네 응답을 각각 한 행동으로 두고 p=[.25,.25,.25,.25], A=[−1,+1,+1,−1]이면 초기 loss는 0이다. 하지만 loss의 logit gradient는 [+ .25,− .25,− .25,+ .25]다. SGD 학습률 .2로 logits를 [−.05,+.05,+.05,−.05]로 바꾸면 확률은 [.23751,.26249,.26249,.23751]이 된다. 정답 두 개의 총 확률은 .50 → .52498로 높아진다. 여러 token 표와 다른 단순화임을 구분해서 보자.

08

새 rollout에서 다시 검증하고 반복

몇 번의 optimizer step 이후 새 actor로 다음 4개 응답을 생성한다. 이번에는 [0,1,1,1]이 나올 수도 있고 모두 정답일 수도 있다. reward와 group 통계는 매 rollout에서 다시 계산한다. 과거 정답 문자열을 영원히 반복 학습하는 SFT와 달리, 정책의 변화가 다음에 관측하는 풀이 분포를 바꾼다.

actor의 새 시도새 풀이 4개
환경의 검증새 R 4개
상대 성과 계산새 μ·σ·A
actor 개선새 logp → loss
backward → step
RLVR 추적 3. 기본 연결은 로봇 RL과 같다. 환경의 보상이 검증 함수로 바뀌고, 여기서는 critic 대신 그룹 baseline을 선택했다.
09

RLHF 루프와 정확히 무엇이 다른가?

단계위 RLHF + PPO위 RLVR + GRPO
rolloutactor가 응답 생성actor가 질문마다 G개 생성
평가사람 선호를 학습한 RM고정된 정답 verifier
기준점critic의 prefix별 V같은 질문 응답의 평균
advantagetoken reward + GAE그룹 표준화 A를 token에 공유
referenceKL 비용에 사용이 구성은 β=0이라 불필요
gradientactor + criticactor만
새 batch새 응답과 새 old logp새 응답·검증·그룹 통계

보상의 출처와 optimizer의 선택이 함께 달라진 비교다. RLVR + PPO는 위 RLHF 루프에서 RM 평가를 verifier로 교체하면 critic·GAE·PPO 구조를 유지할 수 있다. RLHF + GRPO는 위 그룹 루프에서 verifier 점수를 RM 점수로 교체할 수 있다.

근거 · [6] DeepSeekMath / GRPO · [16] TRL GRPO 구현 문서

CHAPTER 19 / III · LLM 학습 루프

두 루프의 공통 구현: 토큰 배열에서 optimizer까지

Python · loss → logit gradient → 실제 확률 갱신
공통 import·보조 함수는 전체 Python 파일에 포함
def policy_weight_steps():
    # RLHF: one chosen token vs all other tokens, one term of a 3-token mean.
    z = torch.nn.Parameter(torch.tensor(math.log(.55/.45)))
    opt = torch.optim.SGD([z],lr=.1)
    old_logp = torch.tensor(math.log(.5))
    a = torch.tensor(.728685677)
    rho = (F.logsigmoid(z)-old_logp).exp()
    loss = -torch.minimum(rho*a,rho.clamp(.8,1.2)*a)/3
    opt.zero_grad(); loss.backward()
    rlhf_gradient = z.grad.item()
    opt.step()
    rlhf_probability = z.sigmoid().detach().item()
    # RLVR: four fixed actions, one sample of each, [bad,good,good,bad].
    logits = torch.nn.Parameter(torch.zeros(4))
    opt = torch.optim.SGD([logits],lr=.2)
    old_lp = torch.full((4,1),-math.log(4))
    a = torch.tensor([-1.,1.,1.,-1.])
    loss = grpo_loss(logits.log_softmax(0)[:,None],old_lp,a,torch.ones(4,1))
    opt.zero_grad(); loss.backward()
    gradient = logits.grad.detach().tolist()
    opt.step()
    return dict(rlhf_logit_gradient=rlhf_gradient,rlhf_new_probability=rlhf_probability,
                rlvr_logit_gradient=gradient,rlvr_new_probability=logits.softmax(0).detach().tolist())

단계가 이해됐다면 실제 프로그램에서 무엇이 배열로 존재하는지 연결하자. B는 응답 수, T는 최대 응답 길이, Vocab은 어휘 크기다.

텐서/배열shape예 / 주의점
response_ids[B,T]실제로 sampling된 응답 token
response_mask[B,T]응답+EOS 1, padding 0
logits[B,L,Vocab]L=prompt+response 입력 길이; 응답 위치만 추출
new_logp, old_logp[B,T]선택 token만 gather한 log 확률
RM/verifier reward[B]완성 응답당 score
RLHF token reward[B,T]마지막 score + token KL 비용
critic values / target[B,T]PPO 구성에서만 필요
GRPO grouped rewards[질문 수,G]그룹 통계 계산 전 reshape
GRPO A[B] → [B,1]유효 token 위치에 broadcast
padding과 causal shift · 개념도
질문 tokenloss mask=0응답 1loss mask=1응답 2loss mask=1EOSloss mask=1PADloss mask=0

입력 위치 t의 logits는 다음 위치 t+1의 token을 예측한다.
logits[:, :-1] ↔ input_ids[:, 1:].
mask도 response_mask[:, 1:]로 label 위치에 맞춘다.

구현 그림 J. 첫 응답 token의 확률은 마지막 prompt 위치의 logits에서 나온다. prompt를 loss에서 제외한다는 말은 prompt를 모델 입력에서도 지운다는 뜻이 아니다.

현재 logp는 이미 존재하는 LLM token logp 함수로 계산한다. padding 이후의 의미 없는 값을 reward나 길이 평균에 넣으면 안 된다. 매우 큰 ratio가 나온다고 임의로 exp의 입력을 잘라 놓으면 원래 수식이 바뀌므로, dtype·정책 지연·수집 확률을 먼저 확인한다.

동작generation 시점optimization 시점
현재 actorsampling, 보통 no_grad같은 응답 scoring, gradient 있음
old logp생성 당시 값을 저장변경하지 않고 읽기
reference/RM/verifier또는 generation 후 별도 scoring고정; actor gradient 없음
criticold value 저장current value를 다시 계산
reward·advantage·targetbatch당 계산detach된 값 재사용
optimizer아직 step하지 않음zero_grad → backward → step
문장 하나를 좋게 만들기 위해 어떤 weight를 고치는가?loss가 현재 logp를 미분하고, logp가 Transformer의 모든 관련 연산을 미분하면서 gradient가 파라미터로 전달된다. 최종 token의 문자열 “12” 자체를 수정하는 것이 아니라, 다음에 비슷한 prefix에서 어떤 token을 낼지 정하는 함수가 바뀐다.
CHAPTER 20 / III · LLM 학습 루프

RLOO: 내 보상을 빼고 기준점을 계산

그룹 평균에 자기 보상까지 들어가면 내 점수가 내 기준점을 끌어올린다. REINFORCE Leave-One-Out은 같은 질문의 다른 응답들만 평균한다. critic 없이 baseline을 만드는 또 다른 방법이며, 사람 보상과 검증 보상 모두에 적용할 수 있다.

Leave-one-out baseline
b−i=1G−1∑j≠iRj,A^iRLOO=Ri−b−i=GG−1(Ri−Rˉ)b_{-i}=\frac1{G-1}\sum_{j\ne i}R_j,\quad \hat A_i^{\mathrm{RLOO}}=R_i-b_{-i}=\frac{G}{G-1}(R_i-\bar R)
G≥2. 조건부로 독립적인 on-policy 샘플이면 다른 응답만으로 만든 baseline은 내 행동과 독립이다. 표준편차 정규화는 이 식에 없다.
B내 보상 1 · 다른 보상 [0,1,0]
baseline=1/3 → A=2/3
+0.667
A내 보상 0 · 다른 보상 [1,1,0]
baseline=2/3 → A=−2/3
−0.667
그림 13. 같은 [0,1,1,0] 그룹에서 GRPO는 ±1, RLOO는 ±2/3. 척도와 추정 방식이 다르다.
Python · GRPO와 RLOO advantage를 나란히 계산
공통 import·보조 함수는 전체 Python 파일에 포함
def group_advantage(rewards, normalize=True):
    # [number_of_prompts, G]; population std, correction=0.
    centered = rewards - rewards.mean(-1, keepdim=True)
    if normalize:
        centered = centered / rewards.std(-1, keepdim=True, unbiased=False).clamp_min(1e-8)
    return centered.detach()

def rloo_advantage(rewards):
    g = rewards.shape[-1]
    assert g >= 2
    baseline = (rewards.sum(-1, keepdim=True) - rewards) / (g-1)
    return (rewards - baseline).detach()

def rloo_loss(sequence_lp, rewards):
    # [prompts, G], fresh on-policy samples; sequence_lp is a token SUM.
    return -(rloo_advantage(rewards) * sequence_lp).mean()
기본 sequence-level REINFORCE 형식
LRLOO=−1G∑isg⁡(A^iRLOO)log⁡πθ(yi∣x)L_{\mathrm{RLOO}}=-\frac1G\sum_i\operatorname{sg}(\hat A_i^{\mathrm{RLOO}})\log\pi_\theta(y_i\mid x)
이는 clipping 없는 on-policy 설명용 loss다. 재사용한 rollout에 그냥 적용하면 on-policy 추정이 아니므로 중요도 보정·PPO식 surrogate 등 별도 설계가 필요하다.

근거 · [7] RLOO

CHAPTER 21 / IV · 안정화와 효율

Dr. GRPO: 정규화가 만든 편향 줄이기

같은 오답이라도 10토큰과 100토큰 응답을 각각 평균하면 긴 응답의 토큰 하나당 페널티가 더 작아진다. 그룹 표준편차로 나누면 질문의 성공률에 따라 같은 보상 차이의 척도도 달라진다. Dr. GRPO는 이 두 정규화를 제거한다.

일정한 분모, std 없는 advantage
A^i=Ri−Rˉ,JDr.GRPO=E[1GTmax⁡∑i∑t=1Timin⁡(ρi,tA^i,clip⁡(ρi,t,1−ϵ,1+ϵ)A^i)]\hat A_i=R_i-\bar R,\qquad J_{\mathrm{Dr.GRPO}}=\mathbb E\left[\frac1{G T_{\max}}\sum_i\sum_{t=1}^{T_i}\min(\rho_{i,t}\hat A_i,\operatorname{clip}(\rho_{i,t},1-\epsilon,1+\epsilon)\hat A_i)\right]
Tmax는 고정 생성 예산과 같은 상수다. 응답 길이에 따라 분모가 변하지 않는다. 여기서는 정책 항만 비교하며 KL을 생략했다.
두 응답, G=210토큰 응답의 토큰당 가중치100토큰 응답의 토큰당 가중치
원형 GRPO: 1/(G·Tᵢ)1/20 = 0.0501/200 = 0.005
Dr. GRPO: Tmax=1001/200 = 0.0051/200 = 0.005
DAPO: 1/ΣTᵢ1/110 ≈ 0.00911/110 ≈ 0.0091
그림 14. advantage와 ratio가 같다고 놓고 집계 가중치만 비교했다. DAPO의 분모는 배치마다 변하고 Dr. GRPO의 분모는 고정이다.
Python · 상수 분모로 합산하기
공통 import·보조 함수는 전체 Python 파일에 포함
def dr_grpo_loss(new_lp, old_lp, raw_a, mask, max_length, eps=0.2):
    # raw_a = R - group_mean(R), WITHOUT dividing by group std.
    ratio = (new_lp - old_lp.detach()).exp()
    a = raw_a.detach().unsqueeze(-1)
    obj = torch.minimum(ratio*a, ratio.clamp(1-eps, 1+eps)*a)
    return -(obj * mask).sum() / (new_lp.shape[0] * max_length)
“편향 제거”의 범위를 좁혀 읽기이름이 모든 통계적 편향의 제거를 뜻하지는 않는다. 자기 보상을 포함한 그룹 평균, clipping, 유한 샘플, off-policy 근사는 별도 문제다. 표준편차 정규화를 없애면 reward의 스케일도 learning rate와 함께 다시 고려해야 한다.

근거 · [9] Dr. GRPO

CHAPTER 22 / IV · 안정화와 효율

DAPO: 탐색·샘플·길이를 함께 손보기

DAPO는 단일 수식 교체를 넘어 대규모 추론 RL의 학습 구성을 개선한다. 핵심은 clip 상한 확대, 유효 그룹 재샘플링, 전역 토큰 평균, 과도한 길이에 대한 보상 처리다.

CLIP-HIGHER상한과 하한을 분리

예: [0.8,1.28]. 좋은 희귀 토큰의 확률을 올리는 유인을 더 오래 유지한다.

DYNAMIC SAMPLING전부 정답·전부 오답 그룹 제외

혼합 보상 그룹으로 배치를 채운다. 생성 비용이 들고 학습 질문 분포도 달라진다.

TOKEN MEAN + LENGTH긴 응답의 토큰도 같은 가중치

유효 토큰 전체의 평균. 잘린 응답을 단순 오답으로 취급하는 잡음도 별도로 다룬다.

DAPO 정책 항 · 명시적 KL 없음
JDAPO=E[∑i,tmin⁡(ρi,tA^i,clip⁡(ρi,t,1−ϵl,1+ϵh)A^i)∑iTi],0<∑i1[correct(yi)]<GJ_{\mathrm{DAPO}}=\mathbb E\left[\frac{\sum_{i,t}\min(\rho_{i,t}\hat A_i,\operatorname{clip}(\rho_{i,t},1-\epsilon_l,1+\epsilon_h)\hat A_i)}{\sum_i T_i}\right],\quad 0<\sum_i\mathbf1[\mathrm{correct}(y_i)]<G
원 논문은 accuracy reward에 ±1을 사용한다. 본 교재의 단순 0/1 그룹 예제는 std 정규화 시 같은 advantage를 준다. 길이 shaping을 더하면 별도 reward 설계를 따라야 한다.
①[1,1,1,1] → 분산 0교체
②[0,0,0,0] → 분산 0교체
③[1,0,0,1] → 비교 가능배치에 유지
그림 15. Dynamic sampling은 오답 개별 샘플을 전부 버리는 것이 아니다. 정답과 오답이 섞인 그룹은 함께 사용한다.
Soft overlong penalty의 선형 구간
Rlength=−clip⁡(T−(Tmax⁡−W)W,0,1)R_{\mathrm{length}}=-\operatorname{clip}\left(\frac{T-(T_{\max}-W)}{W},0,1\right)
예: Tmax=100, W=20이면 길이 80→0, 90→−0.5, 100→−1. 실제 파이프라인의 truncation 판별·overlong filtering은 별도 처리한다.
Python · 비대칭 clip·그룹 필터·길이 penalty
공통 import·보조 함수는 전체 Python 파일에 포함
def dapo_loss(new_lp, old_lp, a, mask, eps_low=0.2, eps_high=0.28):
    # Call after retaining mixed-correctness groups; no explicit KL here.
    ratio = (new_lp - old_lp.detach()).exp()
    a = a.detach().unsqueeze(-1)
    obj = torch.minimum(ratio*a, ratio.clamp(1-eps_low, 1+eps_high)*a)
    return -(obj * mask).sum() / mask.sum()   # global TOKEN mean

def useful_groups(binary_rewards):
    correct = binary_rewards.sum(-1)
    return (correct > 0) & (correct < binary_rewards.shape[-1])

def overlong_penalty(length, max_length, soft_window):
    assert 0 < soft_window <= max_length
    return -min(1.0, max(0.0, (length-(max_length-soft_window))/soft_window))
만능 기본값으로 가져오지 않기[0.8,1.28]은 논문 설정의 예다. PPO clipping 자체가 확률 상한을 보장하지는 않는다. KL 제거 역시 DAPO의 선택이며 모든 RLVR에서 바람직하다는 뜻은 아니다.

근거 · [10] DAPO

CHAPTER 23 / IV · 안정화와 효율

GSPO: 토큰 대신 응답 전체의 변화량

GRPO는 같은 응답 안에서도 토큰별 ratio로 각각 clip한다. GSPO는 응답 전체의 길이 정규화 likelihood ratio를 사용해 응답 단위로 clip한다. 긴 응답과 MoE 학습에서 불안정한 토큰별 비율을 다루려는 접근이다.

Sequence geometric-mean ratio
si=(πθ(yi∣x)πold(yi∣x))1/Ti=exp⁡(1Ti∑tlog⁡ρi,t)s_i=\left(\frac{\pi_\theta(y_i\mid x)}{\pi_{\mathrm{old}}(y_i\mid x)}\right)^{1/T_i}=\exp\left(\frac1{T_i}\sum_t\log\rho_{i,t}\right)
토큰 비율의 산술평균이 아니다. 길이 정규화된 s는 완성 응답의 정확한 확률비 자체도 아니다.
Sequence-level clipping
JGSPO=E[1G∑imin⁡(siA^i,clip⁡(si,1−ϵ,1+ϵ)A^i)]J_{\mathrm{GSPO}}=\mathbb E\left[\frac1G\sum_i\min(s_i\hat A_i,\operatorname{clip}(s_i,1-\epsilon,1+\epsilon)\hat A_i)\right]
토큰 1ρ=1.00토큰 2ρ=1.00토큰 3ρ=2.00토큰 4ρ=1.00

s = (1 × 1 × 2 × 1)1/4 ≈ 1.189.
양의 A, 설명용 ε=0.10: GRPO는 3번 토큰만 clip, GSPO는 전체 응답의 이 정책 항을 clip한다.

그림 16. 동일 응답에서 일부 토큰만 크게 변한 상황. ε=0.10은 차이를 크게 보이기 위한 가상값이다. 실제 GSPO clip 폭은 훨씬 작게 설정할 수 있다.
Python · 평균 log ratio를 exp한 뒤 clipping
공통 import·보조 함수는 전체 Python 파일에 포함
def gspo_loss(new_lp, old_lp, a, mask, eps=3e-4):
    # Full sequence geometric-mean ratio, not arithmetic token mean.
    mean_logratio = ((new_lp-old_lp.detach())*mask).sum(-1) / mask.sum(-1)
    ratio = mean_logratio.exp()
    a = a.detach()
    obj = torch.minimum(ratio*a, ratio.clamp(1-eps, 1+eps)*a)
    return -obj.mean()   # differentiation through ratio gives 1/length
GRPO의 ε를 그대로 옮기지 않기토큰 ratio와 길이 정규화 응답 ratio는 척도가 다르다. GSPO 수식을 미분하면 각 토큰 logp에 1/Tᵢ가 생긴다. 이를 구현하면서 또 길이로 나누면 의도와 다른 가중치가 된다.

근거 · [11] GSPO

CHAPTER 24 / IV · 안정화와 효율

SAPO: 딱 잘라 끊는 대신 부드럽게 감쇠

SAPO는 토큰별 확률비의 hard clipping을 sigmoid 기반 smooth surrogate로 바꾼다. 오래된 정책에서 너무 멀어진 토큰은 완만하게 약화하고, 비교적 가까운 토큰의 신호는 유지한다.

SAPO · τ는 advantage 부호에 따라 선택
fτ(ρ)=4τσ(τ(ρ−1)),JSAPO=E[1G∑i1Ti∑tfτi(ρi,t)A^i]f_\tau(\rho)=\frac4\tau\sigma(\tau(\rho-1)),\quad J_{\mathrm{SAPO}}=\mathbb E\left[\frac1G\sum_i\frac1{T_i}\sum_t f_{\tau_i}(\rho_{i,t})\hat A_i\right]
τpos 또는 τneg를 사용한다. 원 논문은 음의 advantage 쪽을 더 강하게 감쇠하는 비대칭 설정을 다룬다. τ가 클수록 감쇠가 빠르다.
실제 gradient의 계수
p=σ(τ(ρ−1)),w=4p(1−p),∇θfτ(ρ)A^=wρA^∇θlog⁡πθp=\sigma(\tau(\rho-1)),\quad w=4p(1-p),\quad \nabla_\theta f_\tau(\rho)\hat A=w\rho\hat A\nabla_\theta\log\pi_\theta
w는 추가 gate이며 전체 계수는 wρA다. ρ=1에서 w=1이므로 on-policy gradient와 맞는다. sigmoid gate를 곱한 PPO와 동일한 손실은 아니다.
실험 06 · hard gate와 soft gate
10012
SAPO의 추가 gate wA>0인 PPO/GRPO gate, ε=0.2
가로축 ρ / 세로축 gate · ρA를 포함한 전체 gradient 크기를 그린 것은 아니다.
Python · smooth surrogate를 그대로 미분하기
공통 import·보조 함수는 전체 Python 파일에 포함
def sapo_loss(new_lp, old_lp, a, mask, tau_pos=1.0, tau_neg=1.05):
    ratio = (new_lp-old_lp.detach()).exp()
    a = a.detach().unsqueeze(-1)
    tau = torch.where(a > 0, tau_pos, tau_neg)
    surrogate = (4/tau) * torch.sigmoid(tau*(ratio-1))
    return -seq_mean(surrogate*a, mask)
새 기법이 해결하지 않는 것SAPO도 검증기의 오류, reward hacking, 데이터 부족을 자동으로 해결하지 않는다. 논문의 안정성 개선은 평가된 모델·과제·학습 예산 조건에 대한 결과다.

근거 · [12] SAPO

CHAPTER 25 / V · 최신 연구

2026 · PNPO: 지금까지의 prefix로 가중하기

2026-08-02 공개 · 연구 단계. 같은 rollout을 더 많이 재사용하면 생성 비용은 줄지만 정책 지연(policy lag)이 커진다. PNPO는 토큰 하나나 완성 응답 전체 대신 현재 위치까지의 누적 log ratio를 길이 정규화한다.

Prefix-normalized weight
Ct=∏k=1tρk,wtPN=Ct1/t=exp⁡(1t∑k=1tlog⁡ρk)C_t=\prod_{k=1}^{t}\rho_k,\quad w_t^{\mathrm{PN}}=C_t^{1/t}=\exp\left(\frac1t\sum_{k=1}^{t}\log\rho_k\right)
t=1에서는 local ratio, 마지막 위치에서는 GSPO와 같은 응답 기하평균이 된다. 중간 위치의 weight에는 미래 suffix 변화가 들어가지 않는다.
토큰 1ρ=1 → w₁=1토큰 2ρ=1 → w₂=1토큰 3ρ=2 → w₃=1.260토큰 4ρ=1 → w₄=1.189
그림 17. GSPO에서는 s=1.189가 응답 전체에 연결된다. PNPO의 앞 두 위치 weight는 뒤에 나타난 ρ=2의 영향을 받지 않는다.
위치별 acceptance gate
Mi,t=1[1−ϵ−Ti/t≤wi,tPN≤1+ϵ+Ti/t]M_{i,t}=\mathbf1\left[1-\epsilon_-\sqrt{T_i/t}\le w_{i,t}^{\mathrm{PN}}\le1+\epsilon_+\sqrt{T_i/t}\right]
Detached score surrogate
JPNPO=E[1G∑i1Ti∑tsg⁡(Mi,twi,tPNA^i)log⁡πθ(yi,t∣x,yi,<t)]J_{\mathrm{PNPO}}=\mathbb E\left[\frac1G\sum_i\frac1{T_i}\sum_t\operatorname{sg}(M_{i,t}w_{i,t}^{\mathrm{PN}}\hat A_i)\log\pi_\theta(y_{i,t}\mid x,y_{i,<t})\right]
gate와 weight는 고정 계수로 취급한다. gate는 weight를 경계값으로 clip하는 대신 해당 위치의 항을 0으로 만든다. 분모는 통과한 토큰 수가 아니라 원래 응답 길이다.
Python · prefix weight·위치별 gate·detach
공통 import·보조 함수는 전체 Python 파일에 포함
def pnpo_loss(new_lp, behavior_lp, a, mask, eps_low=7e-4, eps_high=9.5e-4):
    # Assumes valid response tokens followed by RIGHT padding.
    t = torch.arange(1, new_lp.shape[-1]+1, device=new_lp.device)
    with torch.no_grad():
        d = (new_lp-behavior_lp) * mask
        weight = (d.cumsum(-1) / t).exp()
        h = (mask.sum(-1, keepdim=True) / t).sqrt()
        gate = (weight >= 1-eps_low*h) & (weight <= 1+eps_high*h)
        coefficient = gate * weight * a.unsqueeze(-1)
    # Stop gradient through weights; do NOT divide by accepted-token count.
    return -seq_mean(coefficient * new_lp, mask)
정확한 importance sampling과는 다르다t>1에서 Cₜ의 1/t제곱은 정확한 density ratio가 아니다. 논문도 완성된 PNPO 목적을 편향된 proximal surrogate로 명시한다. 재사용 가능성을 무제한 보장하지 않으며, 초기 결과는 제한된 모델·수학 벤치마크 조건이다.

근거 · [13] PNPO

CHAPTER 26 / V · 최신 연구

2026 · GAPO: 드문 정답에 더 넓은 clip

2026-08-31 제출 · 연구 단계. 논문 제목은 Group Adaptive Clipping Policy Optimization, 본문 약칭은 GAPO다. 그룹에서 정답이 드물수록 그 정답에 더 넓은 upper clip을 부여한다. DAPO의 고정 상한을 그룹별로 적응시키는 방향이다.

c는 그룹의 정답 개수
ϵhi(c)=ϵlo+(ϵhimax⁡−ϵlo)G−cG−1,1≤c≤G\epsilon_{\mathrm{hi}}(c)=\epsilon_{\mathrm{lo}}+(\epsilon_{\mathrm{hi}}^{\max}-\epsilon_{\mathrm{lo}})\frac{G-c}{G-1},\quad 1\le c\le G
정답 1개이면 최대 폭, 전부 정답이면 최소 폭이다. 음의 advantage에는 고정 lower clip을 사용한다. 이 연구는 std 정규화를 하지 않는 A=R−평균을 사용한다.
8개 중 1개 정답
ε=.005
8개 중 7개 정답
ε≈.00329

상한: 1.005 대 1.003286. 희귀한 성공을 강화하는 항을 조금 더 오래 유지한다.

그림 18. εlow=.003, εhigh,max=.005, G=8인 sequence ratio 예제. 막대 길이는 ε/.005이다. 모두 정답인 그룹은 advantage=0이라 신호가 없다.
실험 07 · 그룹의 난도와 clip 상한
최소 폭 대비 추가 폭
Python · 적응형 upper clip과 sequence surrogate
공통 import·보조 함수는 전체 Python 파일에 포함
def gapo_upper(correct, group_size, low=0.003, high_max=0.005):
    assert group_size > 1 and 1 <= correct <= group_size
    return low + (high_max-low)*(group_size-correct)/(group_size-1)

def gapo_group_loss(new_lp, old_lp, rewards, mask, low=0.003, high_max=0.005):
    # One prompt's G responses; main sequence-ratio surrogate (Eq.12).
    # Token-aggregation variants in experiments are a separate choice.
    c, g = int(rewards.sum().item()), len(rewards)
    a = (rewards-rewards.mean()).detach()   # no std normalization
    if c == 0 or c == g:
        return new_lp.sum()*0              # no group reward signal
    high = gapo_upper(c, g, low, high_max)
    s = (((new_lp-old_lp.detach())*mask).sum(-1)/mask.sum(-1)).exp()
    return -torch.minimum(s*a, s.clamp(1-low, 1+high)*a).mean()
비교할 때 통제해야 하는 것ratio를 token/sequence 중 무엇으로 계산하는지, std 정규화 여부, loss 집계까지 맞춰야 adaptive clip 자체를 평가할 수 있다. 코드의 기본형은 논문 Eq.12를 보여주며 token 집계 실험 변형과 구분한다.

근거 · [14] GAPO

CHAPTER 27 / V · 최신 연구

2026 · Cliff: 처음 틀린 지점부터 구분하기

2026-09-02 공개 · 연구 단계. 결과가 틀린 응답 전체에 같은 음의 advantage를 주면 앞부분의 올바른 계산도 함께 불이익을 받는다. Cliff는 더 강한 teacher가 첫 오류 위치를 찾고, 올바른 prefix와 오류 suffix의 advantage를 구분한다.

Teacher 풀이teacher가 먼저 풀이
검증기로 정답 확인
첫 오류 판단student 풀이의
pitfall 위치 찾기
토큰별 조정정상 prefix / 오류 suffix
서로 다른 advantage
재중심화전체 토큰 평균 b를 빼고
GRPO형 loss 적용
그림 19. teacher 자체가 틀리면 기본 GRPO로 되돌아간다. teacher의 최종 정답 검증이 오류 위치 판정까지 증명하는 것은 아니다.
Cliff advantage
A~i,t={Acor,Ri=1λAcor,Ri=0, t<piAinc,Ri=0, t≥pi,Ai,t=A~i,t−∑j,kA~j,k∑jTj\tilde A_{i,t}=\begin{cases}A_{\mathrm{cor}},&R_i=1\\\lambda A_{\mathrm{cor}},&R_i=0,\ t<p_i\\A_{\mathrm{inc}},&R_i=0,\ t\ge p_i\end{cases},\quad A_{i,t}=\tilde A_{i,t}-\frac{\sum_{j,k}\tilde A_{j,k}}{\sum_jT_j}
Acor=(1−μ)/σ, Ainc=−μ/σ. pᵢ는 첫 오류 토큰 위치. 논문의 실험 기본 λ=0은 틀린 응답의 올바른 prefix를 재중심화 전에 0으로 놓는다.
정답 1개·오답 1개, 각 3토큰인 축소 예제 · μ=.5, σ=.5, λ=0
3 × 4raw +1 → +.833= 12raw +1 → +.833답 12raw +1 → +.833
3 × 4raw 0 → −.167= 14raw −1 → −1.167답 14raw −1 → −1.167

raw 합계 = 3 − 2 = 1 → b=1/6≈0.167. 재중심화 후 합계는 0이다. 올바른 prefix는 오류 suffix보다 덜 불이익을 받지만, 반드시 양수가 되는 것은 아니다.

그림 20. 첫 오류는 두 번째 토큰이다. “오류 이전은 무조건 양의 보상”이라고 읽으면 안 된다. 표시는 소수 셋째 자리 반올림이다.
Python · pitfall의 토큰 경계와 전체 토큰 재중심화
공통 import·보조 함수는 전체 Python 파일에 포함
def cliff_advantage(rewards, pitfall_token, mask, teacher_ok=True, lam=0.0):
    # One prompt. pitfall_token is first wrong token, ZERO-based.
    # A teacher identifies steps; preprocessing maps step boundaries to tokens.
    base = group_advantage(rewards[None])[0]
    if not teacher_ok:
        return base[:, None].expand_as(mask).detach()
    mu = rewards.mean()
    if mu <= 0 or mu >= 1:
        return torch.zeros_like(mask)  # explicit toy handling: skip zero variance
    sd = (mu*(1-mu)).sqrt()
    a_good, a_bad = (1-mu)/sd, -mu/sd
    t = torch.arange(mask.shape[-1], device=mask.device)[None, :]
    raw = torch.where(rewards[:,None] == 1, a_good,
                      torch.where(t < pitfall_token[:,None], lam*a_good, a_bad))
    offset = (raw*mask).sum()/mask.sum()
    return ((raw-offset)*mask).detach()
순수한 규칙 보상만 쓰는 방법은 아니다teacher의 과정 판단을 섞는 확장이다. 추가 추론 비용, teacher 오류, 오류 위치를 토큰에 매핑하는 문제가 남는다. 최종 정답뿐 아니라 과정의 어느 부분을 강화할지 다루는 방향으로 읽어야 한다.

근거 · [15] Cliff

CHAPTER 28 / VI · 정리와 실습

같은 질문으로 모든 방식 비교하기

방법을 선택할 때는 먼저 데이터 형태를 보고, 그다음 rollout 비용과 학습 안정성을 본다. 아래 표는 기본형의 차이다. 최신 라이브러리의 동명 trainer는 여러 변형을 조합할 수 있다.

방법학습 입력/보상별도 모델새 응답 생성핵심 갱신 신호
SFT좋은 응답 C없음기본형: 필요 없음C의 token likelihood
RLHF + PPO사람 비교 C≻B≻A → RMRM + critic + 보통 reference필요RM reward − 기준 가치; ratio clip
DPO(x, C, A) 비교쌍reference기본형: 필요 없음C와 A의 reference 보정 log ratio 차이
KTO(x, C, 좋음) 등 단일 라벨reference기본형: 필요 없음각 응답의 log ratio − KL 기준점
RLVR + PPO정답/테스트 보상critic; reference는 구성별필요검증 reward로 계산한 GAE
RLVR + GRPO같은 질문의 G개 검증 결과critic 없음; reference는 구성별G개씩 필요그룹 표준화 advantage와 token clip
RLOO같은 질문의 G개 보상critic 없음G개씩 필요다른 응답 평균을 뺀 return
RLVR 기법바꾼 핵심ratio / 갱신 단위주로 줄이려는 문제
GRPOcritic → 그룹 상대 baselinetoken ratio / 응답별 평균critic 메모리·학습 비용
Dr. GRPOstd 제거 + 일정 길이 분모token ratio / 일정 분모 합질문 난도·응답 길이 관련 정규화 편향
DAPO상한 확대·동적 sampling·token mean·길이 처리token ratio / 전체 토큰 평균탐색 붕괴·무신호 그룹·긴 응답 문제
GSPO길이 정규화 sequence ratiosequence clip토큰 ratio 변동성과 응답 단위 신호 불일치
SAPOhard clip → smooth sigmoidtoken별 부드러운 gate경계에서 신호가 갑자기 소실되는 문제
PNPO · 2026prefix weight + 위치별 gateprefix weight / detached token score오래된 rollout 재사용 시 정책 지연
GAPO · 2026그룹 정답 개수별 clip 상한기본 sequence; token 변형도 있음드문 성공의 업데이트가 일찍 막히는 문제
Cliff · 2026첫 오류 전후 advantage 구분teacher가 정한 token 경계전체 오답에 동일 불이익을 주는 거친 credit assignment
취향·설명·말투사람 선호가 필요하다

A/B/C의 유용성까지 구별하려면 선호 데이터가 필요하다. 정답 verifier는 B와 C 모두를 1로 볼 수 있다.

수학·코드·형식 증명검증 가능한 결과를 활용한다

결과 검증이 신뢰 가능하다면 RLVR를 고려한다. PPO와 그룹 방식 중 선택은 메모리·rollout 예산·분산을 함께 본다.

대부분의 실제 과제목표를 조합할 수도 있다

정확도와 선호를 함께 학습할 수 있다. R=Rverify+αRpreference 같은 혼합은 reward 간 척도와 충돌을 새로 설계하는 일이다.

CHAPTER 29 / VI · 정리와 실습

Python 실습과 실제 LLM으로의 연결

전체 Python 파일은 CPU에서 실행되는 PyTorch 예제다. 모델 다운로드·API 키 없이 계산과 gradient 방향을 확인한다. 그림의 가상 수치와 실제 학습 출력은 구별한다.

실행 준비
python3 -m venv .venv
# macOS / Linux
source .venv/bin/activate
# Windows: .venv\Scriptsctivate
python -m pip install torch==2.8.0 numpy==2.0.2
python rl_examples.py

검증 환경: Python 3.9 / PyTorch 2.8.0 / CPU. 수치 함수, clipping의 gradient 방향, padding 제외, 0분산 그룹 처리를 점검한다. 난수 기반 최종 확률은 환경에 따라 조금 달라질 수 있다.

Python · 검증기와 GRPO를 연결한 완결된 학습 루프
공통 import·보조 함수는 전체 Python 파일에 포함
def tiny_rlvr_grpo():
    # A complete RLVR loop over four fixed strings: teaches probabilities,
    # NOT arithmetic generalization or language generation.
    logits = torch.nn.Parameter(torch.zeros(4))
    opt = torch.optim.Adam([logits], lr=0.04)
    for _ in range(100):
        with torch.no_grad():
            ids = torch.distributions.Categorical(logits=logits).sample((16,))
            rewards = torch.tensor([verify_integer(samples[i]) for i in ids.tolist()])
            a = group_advantage(rewards[None])[0]
            old_lp = logits.log_softmax(0)[ids, None].clone()
        for _ in range(2):
            new_lp = logits.log_softmax(0)[ids, None]
            loss = grpo_loss(new_lp, old_lp, a, torch.ones_like(new_lp))
            opt.zero_grad()
            loss.backward()
            opt.step()
    probs = logits.softmax(0).detach()
    return probs.tolist(), float(probs[1]+probs[2])
이 예제가 배우는 것과 배우지 않는 것고정 문자열 4개 중 정답 문자열을 선택할 확률만 학습한다. 3×4의 일반적인 연산 법칙을 배우거나 새로운 문장을 생성하지 않는다. 실제 LLM에서는 logits를 Transformer가 출력하고 autoregressive rollout으로 문장을 만들어야 한다.

실행 결과 예시

동일 seed, CPU에서 확인한 출력
Q update: 0.464
REINFORCE P(correct): 0.5000 → 0.9775
Reward model scores A/B/C: [-1.8876, 0.0000, 1.8876]
RLHF + PPO probabilities A/B/C: [0.0073, 0.0049, 0.9878]
DPO loss: 0.182322
RLVR total correct probability: 0.9909

RLHF 예제는 설명까지 선호한 C에 질량을 모으고, RLVR 예제는 두 정답 문자열을 함께 강화한다. 서로 다른 후보 집합을 사용한 데모이므로 숫자를 알고리즘 성능 비교로 읽지 않는다.

실제 LLM 학습 시 대응되는 단계

  1. 질문 데이터와 verifier를 준비한다. 학습 질문과 held-out 평가 질문을 분리한다. 코드 문제는 격리 환경에서 실행한다.
  2. behavior policy로 응답을 sampling한다. 질문마다 G개 응답, EOS와 truncation 정보, 생성 당시 token logp를 저장한다.
  3. reward와 advantage를 계산한다. 같은 질문의 그룹 경계를 보존한다. 정답률 0/1 그룹 처리와 표준편차 정의를 명시한다.
  4. 현재 정책으로 해당 응답의 logp를 다시 계산한다. causal shift, prompt mask, padding mask를 적용한다. reference와 critic은 선택한 알고리즘에 맞춰 사용한다.
  5. loss → backward → optimizer step. rollout의 old logp는 고정한다. 한 배치의 재사용 횟수와 정책 지연을 제한한다.
  6. 새 rollout을 만든다. 성능·길이·다양성·KL 등을 비교하며 학습을 반복한다.
관측 지표무엇을 뜻하는가해석할 때 주의할 점
Held-out pass@1한 번 생성해서 정답일 비율학습 reward 상승과 일반화는 다르다
pass@kk개 후보 중 적어도 하나 정답여러 샘플 비용을 포함해 비교; 단순 정확도와 다름
정답률 0/1 그룹 비율그룹 reward advantage가 0인 빈도샘플 수 G와 질문 난도에 따라 바뀜
응답 길이·truncation 비율생성 예산과 끝내기 행동장문 자체가 올바른 추론의 증거는 아님
Entropy·서로 다른 풀이 수탐색과 다양성entropy가 높아도 무의미한 토큰일 수 있음
KL·clip/gate 비율·ratio 분포정책 이동과 신호 차단 정도token/sequence/prefix 정의가 다르면 직접 비교 불가
전체 생성 토큰·wall time실제 학습 비용step 수만 같다고 계산량이 같은 것은 아님
동일 질문에서 n개 중 c개가 정답일 때의 표본 추정량
pass@k^=1−(n−ck)(nk)\widehat{\mathrm{pass@}k}=1-\frac{\binom{n-c}{k}}{\binom nk}
n≥k인 후보 집합에서 k개를 뽑아 하나 이상 성공할 확률. 독립적인 동일 성공확률 p라는 단순 모형에서는 1−(1−p)ᵏ다. 질문별 난도가 다르면 평균 p를 이 식에 먼저 넣지 않는다.
알고리즘별 성능 순위를 외우는 대신같은 base model, 데이터, verifier, 생성 토큰 예산, sampling 설정과 평가셋으로 비교한다. 최신 연구의 제한된 실험 결과를 모든 LLM에서의 우열로 일반화하지 않는다.
CHAPTER 30 / VI · 정리와 실습

이해 점검: 설명할 수 있으면 배운 것

각 질문에 먼저 답한 뒤 펼쳐서 확인하자.

1. RLHF와 PPO는 왜 같은 층위의 비교가 아닌가?

RLHF는 사람 피드백에서 학습 신호를 얻는 방식이고, PPO는 정책 업데이트 알고리즘이다. RLHF + PPO도 가능하고 RLVR + PPO도 가능하다.

2. PPO에서 πold와 πref를 바꾸어 쓰면 무엇이 잘못되는가?

πold는 rollout을 생성한 확률로 정책 변화량을 계산한다. πref는 장기적인 기준 분포다. 둘을 혼동하면 배치 재사용 보정과 기준 정책에 대한 정규화가 뒤섞인다.

3. A=−2, ρ=0.5, ε=0.2일 때 PPO 항은?

min(−1.0, −1.6)=−1.6. 나쁜 행동의 확률을 이미 크게 줄였으므로 이 샘플의 추가 개선 방향 gradient는 0이다.

4. GRPO 그룹이 [1,1,1,0]일 때 정답과 오답 A는?

μ=.75, σ=√(.75×.25)≈.433. 정답 A≈+.577, 오답 A≈−1.732. 전부 같은 보상이면 centered advantage=0이다.

5. DPO의 chosen을 KTO의 좋음 라벨로 바로 바꿔도 되는가?

데이터 가정이 추가된다. “둘 중 낫다”는 “좋다”를 뜻하지 않는다. 모두 나쁜 응답일 수도 있고 모두 좋은 응답일 수도 있다.

6. 정답 보상 1을 받았으면 모든 추론 토큰이 올바른가?

아니다. 틀린 중간 식 뒤에 우연히 정답을 쓸 수도 있다. Outcome reward는 최종 결과를 검사한다. 과정 supervision은 별도 신호와 신뢰도 문제를 갖는다.

7. GSPO의 ratio를 토큰 ratio의 산술평균으로 계산해도 되는가?

아니다. exp(mean(log ratio)), 즉 기하평균이다. [1,1,2,1]의 산술평균은 1.25이고 기하평균은 약 1.189다.

8. PNPO에서 weight를 detach하지 않으면?

logp뿐 아니라 prefix weight를 통해서도 gradient가 흐르므로 원 논문의 detached score surrogate와 다른 업데이트가 된다.

9. 최신 방법들이 각각 무엇을 고치는가?

Dr. GRPO: 정규화. DAPO: clip·샘플·길이. GSPO: ratio 단위. SAPO: gate의 부드러움. PNPO: prefix와 rollout 재사용. GAPO: 그룹별 clip 폭. Cliff: 과정별 advantage.

10. 이 자료를 읽고 직접 해볼 최소 실험은?

rl_examples.py를 실행하고, GRPO 실험을 전부 정답 그룹으로 바꿔 A가 0이 되는지 확인한다. 다음으로 PPO의 A 부호를 반대로 바꾸어 평평해지는 방향이 바뀌는지 본다.

추천 학습 순서

첫 회독: 배송 로봇 → 상태·rollout → V·Q → advantage → 정책 gradient → actor–critic → GAE → TRPO → PPO → LLM → RLHF/RLVR batch 추적.
두 번째 회독: Python을 실행하며 수식과 텐서 shape를 연결한다.
심화: 정규화·ratio·gate·credit assignment라는 네 질문으로 최신 논문을 읽는다.

CHAPTER 31 / VI · 정리와 실습

부록: 기호와 최소 수학 사전

미적분을 깊이 몰라도 확률, 로그, 평균, 미분의 부호를 이해하면 손실함수의 방향을 읽을 수 있다. 이 교재는 정책 목적함수 J를 최대화하며, 코드에서는 대개 loss = −J를 최소화한다.

표기뜻사과 예제
x, y, yₜ질문, 완성된 응답, t번째 토큰x = 사과 질문; y = “12개입니다”
πθ(y|x)파라미터 θ인 모델의 조건부 생성 확률πθ(B|x)=0.30
E[X]가능한 값의 확률 가중 평균P(정답) × 1 + P(오답) × 0
log, exp자연로그와 그 역함수log(0.5)≈−0.693; exp(−0.693)≈0.5
σ(z)sigmoid, 1/(1+exp(−z))σ(0)=0.5, σ(log 5)=5/6
∇θ, detach / sgθ에 대한 미분; 역전파를 끊고 상수 취급advantage를 actor loss에서 detach
R, Gₜ, Aₜ보상, 할인 누적 보상, 기준보다 나은 정도정답 R=1; A=1−평균
β, ε, λ문맥별 하이퍼파라미터β: KL/DPO 척도; ε: clip 폭; λ: GAE 등
기대값
E[R]=∑yπθ(y∣x)R(x,y)=0.7⋅0+0.3⋅1=0.3\mathbb E[R]=\sum_y\pi_\theta(y\mid x)R(x,y)=0.7\cdot0+0.3\cdot1=0.3
여기서는 가능한 답변이 “7”과 “12” 둘뿐이다. 12를 낼 확률을 높이면 기대 보상도 높아진다.
로그와 gradient descent
log⁡∏t=1Tpt=∑t=1Tlog⁡pt,θ←θ−η∇θL\log\prod_{t=1}^{T}p_t=\sum_{t=1}^{T}\log p_t,\qquad \theta\leftarrow\theta-\eta\nabla_\theta L
아주 작은 토큰 확률을 계속 곱하면 수치가 0에 가까워진다. 로그 확률을 더하면 안정적이며 미분하기도 쉽다. η는 한 번에 움직이는 크기다.
한 개 샘플의 log ratio와 KL은 다르다log(πθ(y)/πref(y))는 음수가 될 수도 있다. KL은 이 값을 πθ 분포 전체에 대해 평균한 것이어서 0 이상이다. log ratio 한 개가 음수라고 “KL이 음수”라고 해석하면 안 된다.
KL divergence
DKL(πθ∥πref)=∑yπθ(y∣x)log⁡πθ(y∣x)πref(y∣x)D_{\mathrm{KL}}(\pi_\theta\|\pi_{\mathrm{ref}})=\sum_y\pi_\theta(y\mid x)\log\frac{\pi_\theta(y\mid x)}{\pi_{\mathrm{ref}}(y\mid x)}
KL은 방향에 따라 값이 달라지는 분포 차이이다. πref=0인 곳에 πθ>0이면 이 방향의 KL은 무한대가 될 수 있다.
CHAPTER 32 / VI · 정리와 실습

논문과 읽기 범위

자료 확인 기준: 2026-09-10. 공개된 원 논문을 중심으로 선별했다. “최신”은 아래 2026년 연구까지 포함한다는 뜻이며, 모든 후속 논문이나 비공개 학습 기법을 망라한다는 뜻은 아니다. 2026년 항목은 초기 연구 결과로 구분했다. 날짜는 최초 제출 기준이며 arXiv 번호의 월과 다를 수 있다.

  1. [1] PPO2017-07-20
    Clipped surrogate objective
  2. [2] GAE2015-06-08
    TD residual과 advantage 추정
  3. [3] InstructGPT / RLHF2022-03-04
    사람의 비교 → 보상 모델 → PPO
  4. [4] DPO2023-05-29
    KL 정규화 최적 정책과 직접 선호 손실
  5. [5] KTO2024-02-02
    단일 응답의 좋음/나쁨, v4의 기준점 및 손실
  6. [6] DeepSeekMath / GRPO2024-02-05
    그룹 상대 advantage와 critic 제거
  7. [7] RLOO2024-02-22
    REINFORCE와 leave-one-out baseline
  8. [8] DeepSeek-R12025-01-22
    검증 가능한 보상으로 추론 학습; R1-Zero와 R1의 구분
  9. [9] Dr. GRPO2025-03-26
    길이·표준편차 정규화의 편향 분석
  10. [10] DAPO2025-03-18
    비대칭 clip, dynamic sampling, token mean, overlong shaping
  11. [11] GSPO2025-07-24
    길이 정규화 sequence ratio와 sequence clipping
  12. [12] SAPO2025-11-25
    v2 Eq. 5–8의 smooth surrogate와 gradient gate
  13. [13] PNPO2026-08-02
    v1 Eq. 7–11: prefix weight, gate, detached score surrogate
  14. [14] GAPO2026-08-31
    Group Adaptive Clipping Policy Optimization, v1 Eq. 11–13
  15. [15] Cliff2026-09-02
    v1 Eq. 3–5: 첫 오류 위치와 token advantage 재중심화
  16. [16] TRL GRPO 구현 문서2026-09-10 확인
    loss 집계·reward scaling·KL 설정의 구현상 구분
  17. [17] TRL DPO 구현 문서2026-09-10 확인
    비교쌍 데이터와 직접 선호 학습 구현
  18. [18] TRL KTO 구현 문서2026-09-10 확인
    좋음/나쁨 라벨과 KTO 설정
  19. [19] TRPO2015-02-19
    정책 개선 surrogate, KL 제약, 자연 gradient와 line search
  20. [20] RLHF PPO 구현 세부사항2026-09-10 확인
    rollout, reward·KL, value 및 응답 mask 구현

그림·계산 예제·Python 코드는 개념 설명을 위해 새로 작성했다. 원 논문의 그래프나 실험 수치를 복제하지 않았다. 수식은 교육 목적에 맞게 기호를 통일했으며, 생략한 항과 구현 가정은 해당 절에 표시했다.