좋은 답변은
어떻게 학습되는가?
배송 로봇의 가치·advantage·actor–critic부터 TRPO·PPO까지.
이어서 RLHF와 RLVR의 한 batch를 생성부터 역전파까지 추적합니다.
첫 수업: 배송 로봇은 경험에서 무엇을 배울까?
먼저 LLM을 잠시 내려놓고, 두 번까지 행동할 수 있는 배송 로봇을 생각하자. 로봇은 안전하게 작은 보상을 받을 수도 있고, 비용을 내고 다리에 들어가 더 큰 보상을 노릴 수도 있다. 우리는 이 환경 하나로 가치, advantage, actor–critic, TRPO, PPO까지 연결한다. 지금은 이 이름들을 외울 필요가 없다.
안전한 길은 2점을 확실히 얻고 끝난다. 다리에 들어가면 먼저 1점을 잃는다. 그 다음 천천히 가면 8점, 서두르면 운에 따라 10점 또는 −6점을 받는다. 한 번 서둘러서 10점을 받았다고 해서 서두르기가 좋은 전략이라는 뜻은 아니다. 반복했을 때의 결과를 배워야 한다.
로봇이 다리로 들어가면 상태가 바뀌고 −1점을 받는다. 서두른 결과의 동전 던지기도 환경이 결정한다.
처음에는 출발점에서 40% 확률로 다리에 간다. 다리 위에서는 75% 확률로 천천히 간다.
좋은 결정을 더 자주 고르도록 확률을 바꾸고, 각 위치에서 기대할 점수의 예측도 함께 고친다.
- 상태·행동·보상과 실제 경험 기록을 구별한다.
- 한 번 받은 총점과 반복했을 때의 기대값을 구별한다.
- 기대값을 기준으로 행동의 장점을 계산한다.
- 행동을 고르는 모듈과 점수를 예측하는 모듈을 따로 학습한다.
- 확률을 너무 크게 바꿨을 때 생기는 문제를 TRPO와 PPO로 다룬다.
상태·정책·rollout: 실제로 모으는 데이터
상태(state)는 의사결정에 필요한 현재 상황이다. 여기서는 출발점 S₀인지 다리 위 S₁인지만 알면 된다. 행동(action)은 그 상태에서 로봇이 고르는 선택이다. 보상(reward)은 그 행동 직후 환경이 주는 점수다. 상태를 보고 행동을 선택하는 확률 규칙이 정책(policy)이다.
| 시각 t | 현재 상태 sₜ | 선택한 행동 aₜ | 받은 보상 rₜ | 다음 상태 | 종료? |
|---|---|---|---|---|---|
| 0 | S₀ | 다리 진입 | −1 | S₁ | 아니요 |
| 1 | S₁ | 천천히 | +8 | 종료 | 예 |
이렇게 시작부터 종료까지 실제로 경험한 기록 한 줄기를 trajectory 또는 rollout이라고 한다. 에피소드(episode)는 그 한 번의 시도다. batch는 학습을 위해 모은 여러 기록의 묶음이다. 이 용어들은 생성형 모델만의 용어가 아니다.
선택 확률 0.4
다음 상태 S₁
선택 확률 0.75
trajectory 저장
| 가능한 전체 경로 | 확률 | 보상 기록 |
|---|---|---|
| 안전 배송 | 0.60 | [+2] |
| 다리 → 천천히 | 0.30 | [−1, +8] |
| 다리 → 서두르기 → 성공 | 0.05 | [−1, +10] |
| 다리 → 서두르기 → 실패 | 0.05 | [−1, −6] |
Return, V, Q: 점수 세 가지를 구별하기
1. Return: 이번 한 번의 경험에서 얻은 누적 점수
즉시 보상은 −1이지만, 나중에 +8을 받는다면 다리에 들어가는 행동은 가치가 있을 수 있다. 미래 점수를 얼마나 반영할지 정하는 수가 할인율 γ다. 0.9라면 한 단계 뒤 보상은 90%, 두 단계 뒤는 81%만 반영한다.
γ=0.9: G₁=8, G₀=−1+0.9×8=6.2.
서둘러 실패한 다른 경로라면 G₀=−1+0.9×(−6)=−6.4.
2. V: 지금 전략을 계속 따를 때 이 상태의 평균 전망
같은 상태에서 현재 정책으로 무한히 반복했을 때 return의 평균을 상태 가치 Vπ(s)라고 한다. “현재 정책”이 바뀌면 가치도 바뀐다. 다리 위에서는 천천히 가는 선택의 평균 점수가 8, 서두르기의 평균은 0.5×10+0.5×(−6)=2다.
3. Q: 첫 행동을 지정했을 때의 평균 전망
행동 가치 Qπ(s,a)는 “이 상태에서 처음에는 a를 선택하고, 이후에는 현재 정책을 따르면?”의 기대 return이다. 출발점에서 다리로 가겠다고 정하면, 다리 위 이후의 전망은 여전히 V(S₁)=6.5다.
| 값 | 계산 | 질문에 대한 답 |
|---|---|---|
| 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 식: 지금 받은 점수와 다음 상태의 전망을 합친다
출발점에서 “다리로 들어가기”를 선택한 순간으로 돌아가 보자. 지금은 통행 비용으로 −1점을 받는다. 그 대신 다리 위에 도착하고, 그곳부터의 미래 전망은 6.5점이다. 시간은 다음 그림에서 오른쪽으로 흐른다.
행동을 고르기 전
지금 보상 r=−1
행동한 뒤 도착
미래 누적 점수의 평균
5. 다음 상태의 가치 6.5점은 어디에서 왔을까?
다리 위에서 현재 정책 π는 75% 확률로 천천히 가고 25% 확률로 서두른다. 천천히 가면 확실히 +8점을 받고 종료한다. 서두르면 성공·실패가 반반이다. 행동을 고르는 정책의 확률과, 선택 후 결과를 정하는 환경의 확률을 구별하자.
6. 왜 다음 상태의 가치에 γ를 한 번 곱할까?
가치를 평가하는 기준 시점이 한 단계 다르기 때문이다. 다리 위에 이미 도착한 입장에서 천천히 가서 받는 8점은 첫 보상이다. 출발점 입장에서는 통행료 −1점을 받은 다음 단계의 보상이다.
| 어디에서 계산하는가? | “다리 → 천천히” 경로의 남은 보상 | γ=.9인 return |
|---|---|---|
| 다리 위 s′에서 | [+8] | 8 |
| 출발점 s에서 | [−1, +8] | −1+.9×8=6.2 |
γ=.9는 “한 단계 뒤의 보상을 지금 평가할 때 90% 반영한다”는 설정이다. 환경이 실제로 8점 대신 7.2점을 지급한다는 뜻은 아니다. γ=1이면 미래 보상을 줄이지 않고 전부 반영한다.
7. 이제 expectation을 포함한 식을 읽어 보자
𝔼는 평균을 내라는 뜻이다. 같은 행동도 확률적으로 보상이나 다음 상태가 달라질 수 있다. 여기서는 다리 진입의 보상 −1과 도착 상태 s′가 확정되어 있어 −1+.9×6.5로 바로 계산했다. 그 이후의 행동과 결과에 대한 평균은 Vπ(s′)=6.5 안에 들어 있다. 진짜 종료 상태에 도착했다면 V=0이므로 Q는 그 행동의 즉시 보상의 기대값만 남는다.
8. Q를 평균하면 왜 V가 될까?
| 기호 | 답하려는 질문 |
|---|---|
| Qπ(s,a) | 지금 이 행동 a를 선택하고, 이후에는 현재 정책대로 하면 얼마나 벌까? |
| Vπ(s) | 지금부터 행동 선택을 현재 정책에 맡기면 얼마나 벌까? |
확인: 식의 s′는 과거인가, 미래인가?
미래다. 현재 s에서 a를 한 뒤 도착한 다음 상태다. 그 상태의 가치에 γ를 곱해 현재 시점의 평가로 가져온다. V는 다음 보상 하나가 아니라 그 상태부터의 할인 누적 보상 전체의 기대값이다.
다리 위에서 천천히 갈 확률은 0.75로 고정한다.
Python · 모든 V·Q·A와 return을 직접 계산
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은 보충으로 둔다.
Python · 단일 Q 갱신
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)Advantage: 평균보다 얼마나 나은 선택인가?
advantage를 처음에는 “그 상태에서 평소보다 얼마나 유리한 행동인가”라고 읽자. 출발점의 현재 평균 전망은 3.14다. 다리를 선택한 전망은 4.85이므로 다리는 현재 평균보다 1.71 좋다. 안전 배송의 전망은 2이므로 평균보다 1.14 낮다.
| 상태 | 행동 | Q | 그 상태의 V | A=Q−V |
|---|---|---|---|---|
| S₀ | 다리 | 4.85 | 3.14 | +1.71 |
| S₀ | 안전 배송 | 2.00 | 3.14 | −1.14 |
| S₁ | 천천히 | 8.00 | 6.50 | +1.50 |
| S₁ | 서두르기 | 2.00 | 6.50 | −4.50 |
이 상대 평가를 배우면 “평균보다 좋은 선택의 확률을 높이고, 평균보다 못한 선택의 확률을 낮추자”라는 갱신 규칙을 만들 수 있다. 성공이 쉬운 상태의 +8과 성공이 어려운 상태의 +8을 같은 강도로 칭찬하지 않도록 해 준다.
하지만 실제 경험에서는 Q를 정확히 모른다
환경의 평균을 모르므로 관측한 return에서 가치 예측을 빼는 추정 advantage Â를 사용한다. 모자 표시는 “정확한 값 대신 추정치”라는 뜻이다.
| S₁에서 서두른 실제 경험 | 관측 return | G−V(S₁) |
|---|---|---|
| 운 좋게 성공 | +10 | +3.5 |
| 실패 | −6 | −12.5 |
| 두 결과의 확률 평균 | 0.5×10+0.5×(−6)=2 | 0.5×3.5+0.5×(−12.5)=−4.5 |
내 말로 확인하기
“다리 선택의 advantage가 +1.71”은 다리에 들어가면 즉시 +1.71점을 준다는 뜻인가? 아니다. 즉시 보상은 −1이고, 이후까지 고려한 기대 성과가 현재 전략 평균보다 1.71 높다는 뜻이다.
Policy gradient와 REINFORCE: 확률이 바뀌는 과정을 눈으로 보기
움직이는 수업 · 경험이 어떻게 다음 선택을 바꿀까?
먼저 재생을 누르세요. 한 장면마다 설명이 바뀝니다. 계산이 낯설면 멈춘 뒤 다음 단계로 따라가세요.
- 확률표
- 행동 선택
- 보상 관측
- 평가 신호
- 방향 계산
- 확률 갱신
갈림길
다음번 행동의 선택 확률
현재 평균: 5.000점
막대는 보상이 아니라 선택 확률입니다.손잡이 z가 움직이면 평균 점수도 변한다
흰 점: 학습 전 · 초록 점: 현재 정책곡선은 환경을 아는 독자에게 보여주는 이론값입니다.
평가 점수 대기
평가 점수 대기
γ=0.9 · 기본 REINFORCE · baseline=0. 여기서 계산한 G는 이번에 실제로 경험한 경로의 점수이며, 여러 가능한 경로의 평균인 V·Q와 구별합니다.
왼쪽과 오른쪽을 각각 50%로 선택하는 정책입니다. 손잡이 z=0에서 시작합니다.
무엇을 눈여겨봐야 하나요?
①에서는 8점을 본 뒤에만 왼쪽 확률이 올라갑니다. ②에서는 +2점도 양수라 오른쪽을 강화합니다. ④에서는 2−5=−3이 되어 같은 오른쪽 경험으로 오른쪽 확률을 줄입니다. ⑤에서는 행동과 보상 관측은 앞으로 진행하지만, 종료 후 return 계산은 뒤에서 앞으로 진행합니다.
Policy gradient는 행동을 고르는 확률을, 평균 점수가 높아지는 방향으로 조정하는 방법이다. REINFORCE는 그 방향을 실제 경험으로 추정하는 방법이다. 먼저 advantage와 critic 없이 시작하자. 이 둘은 기본 REINFORCE에 반드시 필요한 것이 아니다.
1. 정책은 “왼쪽을 얼마나 자주 고를까?”라는 확률표다
이번 절에서는 앞의 다리 환경을 잠시 단순화한다. 왼쪽으로 가면 8점, 오른쪽으로 가면 2점을 받고 즉시 종료한다. 한 번만 행동하므로 미래 보상도 할인도 필요 없다. 정책은 처음에 두 길을 반반 선택한다. 학습자는 처음부터 두 길의 점수를 안다고 가정하지 않는다. 아래 점수표는 이해를 돕기 위해 독자에게 공개한 환경의 규칙이다.
2. 우리가 높이고 싶은 것은 한 번의 점수가 아니라 평균 점수다
왼쪽 확률을 p라고 쓰면 오른쪽 확률은 1−p다. p=0.5로 여러 번 시도하면 평균 점수는 5점이다. 왼쪽 확률을 0.8로 높이면 평균 점수는 6.8점이 된다. 환경이 주는 8점·2점은 그대로이고, 우리가 바꾸는 것은 선택 빈도다.
오른쪽 50% · 평균 5.0점
오른쪽 20% · 평균 6.8점
3. Gradient는 “확률을 만드는 손잡이를 어느 쪽으로 돌릴까?”다
실제 학습에서는 확률 p를 그대로 더하면 1을 넘을 수 있다. 그래서 제한 없는 숫자 z를 조절하고, sigmoid라는 변환으로 0~1 사이 확률 p를 만든다. z=0이면 반반, z가 양수면 왼쪽을 더 선호한다. 여기서는 손잡이가 하나이고, 신경망에서는 수많은 가중치가 이 손잡이 역할을 한다. 그 전체를 θ라고 쓴다.
미분/gradient는 현재 위치의 기울기다. 여기서는 z=0 근처에서 z를 아주 조금 높이면 J도 높아진다. 기울기 +1.5는 “z를 0.01 높이면 평균 점수가 약 0.015 높아진다”는 국소적인 뜻이다. 크게 움직였을 때도 같은 직선이라는 뜻은 아니다.
4. 그런데 안 가 본 길의 점수는 모른다 → 경험으로 방향을 추정한다
이번 rollout에서 왼쪽을 선택해 8점을 받았다면, 우리는 “그때 선택했던 왼쪽의 확률을 높이는 방향”에 8점만큼의 가중치를 준다. 이를 수식으로 쓰면 아래와 같다. 한 번의 경험으로 계산한 방향이므로 g에 모자(추정 표시)를 붙였다.
| 수식의 조각 | 쉬운 말 | 이번 왼쪽 경험 |
|---|---|---|
| G | 이번 결과를 얼마나 강하게 반영할까? | 8 |
| ∂ log π(a|s) / ∂z | 선택했던 행동이 더 잘 나오려면 손잡이를 어느 방향으로? | 왼쪽이면 1−p = +0.5 |
| ĝ | 두 가지를 곱한 이번 경험의 갱신 방향 | 8 × 0.5 = +4 |
| ηĝ | 실제로 손잡이를 움직일 양 | 학습률 0.1이면 +0.4 |
G=8
5. 오른쪽도 +2점인데, 그 확률도 높아지는가?
기본 REINFORCE에서는 그렇다. 오른쪽을 뽑아 +2점을 받으면, 이번 경험만으로는 오른쪽 확률이 올라간다. “나쁜 쪽인데 왜?”라는 의문이 자연스럽다. 원형은 평균과 비교하지 않고 양의 return을 받은 행동을 강화한다. 하지만 여러 경험을 평균하면 왼쪽의 강화 효과가 더 크다.
| 처음 p=0.5에서 가능한 경험 | 그 행동의 log 확률 기울기 | G × 기울기 | η=0.1일 때 새 왼쪽 확률 |
|---|---|---|---|
| 왼쪽 → 8점 | 1−p=+0.5 | +4 | 59.87% |
| 오른쪽 → 2점 | −p=−0.5 | −1 | 47.50% |
이것이 policy gradient의 핵심이다. 환경 전체를 미분하지 않아도, 내가 선택한 행동의 확률을 미분하고 관측 점수를 곱하면, 반복 평균에서 원하는 방향을 얻을 수 있다. 실제로는 여러 rollout의 ĝ를 평균해 한 번 갱신하기도 한다.
6. 직접 바꿔 보기: 경험 한 번이 확률을 어떻게 움직이는가?
하나의 관측을 이용한 가상 업데이트다. 슬라이더를 움직이면 선택한 초기 조건에서 다시 계산한다. 업데이트를 누적하지 않는다.
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을 써도 잡음이 남는다.
앞 장의 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다.
8. log는 왜 나오고, loss에는 왜 마이너스가 붙을까?
log는 외워야 하는 장식이 아니다. 선택 확률로 가중해 평균할 때 필요한 수학적 보정이다. 실제로 ∇log π = (∇π)/π이므로, 경험이 뽑힐 확률 π와 평균에서 곱해져 π가 약분된다. 그러면 기대 점수의 미분이 남는다. 아래 유도는 이해한 뒤 펼쳐도 된다.
두 행동만으로 보는 log의 유도
학습 라이브러리는 보통 loss를 줄이는 방향으로 움직인다. 우리는 점수를 높이고 싶으므로 마이너스를 붙여 방향을 맞춘다. G나 baseline을 고쳐서 loss를 줄이는 것이 아니라, 확률을 만드는 z만 고쳐야 한다.
Python · 8점/2점 예제: 관측 하나에서 확률 갱신까지
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₀로 첫 선택을 평가한다.
보상 [−1, +8]
G₀=6.2
천천히 log 확률 ↔ G₁
새 정책으로 다시 rollout
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
Actor–Critic: 행동 담당과 예측 담당의 협업
Actor는 정책 πθ다. 상태를 보고 어떤 행동을 얼마나 자주 고를지 결정한다. Critic은 가치 예측기 Vψ다. 같은 상태를 보고 앞으로 받을 누적 보상을 예상한다. “평가자”라는 이름 때문에 critic이 사람의 취향을 판정한다고 생각하기 쉽지만, 여기서는 환경 보상으로 정의된 미래 return을 예측한다.
출력: 안전 0.60 / 다리 0.40
다리를 sampling → 환경으로 보냄
배울 것: 더 나은 행동을 고르는 확률
출력: 예상 누적 보상 3.14
관측된 결과와 비교할 기준점
배울 것: 미래 누적 보상의 정확한 예측
Critic의 다음 상태 예측: V(S₁)=6.5
TD target=−1+.9×6.5=4.85 → 예측 오차 δ=4.85−3.14=+1.71
| 갱신 대상 | 현재 값 | target/신호 | 학습률 0.1로 한 번 갱신 |
|---|---|---|---|
| Actor의 logit z | −0.4055 | Â=+1.71 | z≈−0.3029 → P(다리)≈0.4249 |
| Critic의 V(S₀) | 3.1400 | target=4.8500 | 3.14+0.1×(4.85−3.14)=3.3110 |
| Critic의 V(S₁) | 6.5000 | 이번 target에서 고정 | 6.5000 유지 |
Python · 서로 다른 파라미터에 흐르는 두 gradient
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은 변하는 목표를 계속 따라간다.
실제 신경망에서는?
actor와 critic을 완전히 별도 네트워크로 만들 수도 있고, 같은 특징 추출기를 공유한 뒤 정책 head와 가치 head를 나눌 수도 있다. 공유한다면 두 loss가 공통 파라미터에 함께 영향을 준다. 위 코드는 역할을 명확하게 보이기 위해 파라미터를 분리했다.
MC·TD·GAE: advantage를 어떻게 추정할까?
우리가 알고 싶은 것은 Q−V지만, 경험에서는 Q를 직접 볼 수 없다. 그래서 얼마나 오래 실제 보상을 보고, 어디부터 critic의 예측으로 대신할지를 결정한다.
| 방법 | 사용하는 값 | 장점 | 어려운 점 |
|---|---|---|---|
| Monte Carlo | 종료까지 실제 보상 | 부정확한 미래 bootstrap에 덜 의존 | 환경·행동의 운이 많이 섞임; 종료까지 대기 |
| 1-step TD | 보상 1개 + 다음 V | 짧은 경험에서도 갱신 가능 | critic이 틀리면 target도 편향 |
| n-step | n개 보상 + n단계 뒤 V | 두 방식 사이를 절충 | n을 정해야 함 |
| GAE | 여러 길이 TD 정보를 λ로 혼합 | 편향·분산 조절 | γ, λ, 종료 처리와 value 정확도가 중요 |
같은 “다리 → 천천히” 경험을 끝까지 계산
보상 [−1,8], 값 예측 [3.14,6.5,0], γ=.9를 고정한다.
| 위치 | TD residual δ | MC advantage |
|---|---|---|
| t=1 · 천천히 | 8+.9×0−6.5 = 1.5 | 8−6.5 = 1.5 |
| t=0 · 다리 | −1+.9×6.5−3.14 = 1.71 | 6.2−3.14 = 3.06 |
λ=0 → 첫 위치 Â=1.71 (TD).
λ=.95 → Â=2.9925.
λ=1 → Â=3.06 (이 종료 경로에서는 MC−V).
Python · 배열을 뒤에서 앞으로 도는 GAE 함수
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()왜 정책을 조금씩 바꿔야 하는가?
이전 actor로 배달 경험 100개를 모았다고 하자. 평균적으로 40개는 다리로, 60개는 안전한 길로 갔을 것이다. 이 자료만 반복해서 학습하면 현재 actor는 계속 바뀌지만, 자료 속 행동 비율은 옛 actor의 비율로 남아 있다.
| 분포 | 다리 선택 | 안전 선택 | 의미 |
|---|---|---|---|
| 데이터를 만든 πold | 40% | 60% | 저장한 batch의 출처 |
| 현재 πθ | 50% | 50% | 이제 학습하려는 정책 |
| 비율 ρ=πθ/πold | 0.5/0.4=1.25 | 0.5/0.6≈0.8333 | 고정된 상태에서 행동별 중요도 조정 |
출발점에서 새 다리 확률을 .50으로 바꾸면:
.4×1.25×1.71 + .6×.8333×(−1.14) = .285.
다리 위의 정책을 고정한 이 특수 예제는 실제 출발점 가치도
3.14 → .5×4.85+.5×2 = 3.425, 즉 +.285 변한다.
하지만 일반적인 신경망 갱신은 여러 상태의 행동을 동시에 바꾸므로 이렇게 깔끔하게 일치하지 않을 수 있다.
On-policy와 off-policy를 이 문맥에서 읽기
현재 정책으로 갓 만든 경험을 사용하는 것이 on-policy의 기본 상황이다. 다른 정책이 만든 경험을 사용하는 것이 off-policy다. PPO도 한 rollout batch 안에서 몇 번 갱신하면 old와 current가 달라진다. 그렇다고 임의로 오래된 모든 경험을 계속 재사용해도 되는 off-policy 알고리즘이 되는 것은 아니다.
TRPO: 정책 변화에 KL 예산을 걸기
파라미터가 0.1만큼 바뀌었다는 말만으로 정책이 얼마나 바뀌었는지 알기 어렵다. 어떤 파라미터는 확률에 거의 영향을 주지 않고, 다른 파라미터는 행동 분포를 크게 바꾼다. TRPO는 행동 확률 분포의 변화를 KL divergence로 측정한다.
숫자로 보는 허용 범위
다리 위 전략은 고정하고, 출발점의 다리 확률 p만 바꾼다. old p=.4이고 KL 예산은 .01이다.
큰 신경망에서 이 제약을 푸는 방법
새 파라미터를 전부 탐색할 수 없으므로 현재 지점 근처에서 목적함수는 1차, KL은 2차로 근사한다. g는 surrogate의 gradient, F는 옛 정책에서 계산한 KL의 곡률 행렬이다. F가 이 문맥의 Fisher information matrix다.
- 옛 정책으로 rollout을 모으고 advantage를 추정한다.
- 고정된 batch에서 surrogate gradient g를 계산한다.
- KL의 곡률을 이용해 Fx=g를 푼다. 수치 안정화를 위해 damping을 넣을 수 있다.
- KL 예산 δ에 맞춘 후보 step을 만든다.
- 후보 파라미터에서 실제 batch KL과 surrogate 개선을 다시 계산한다.
- 통과하지 못하면 step을 1/2, 1/4처럼 줄여 다시 검사한다. 이것이 backtracking line search다.
- 허용되는 후보만 actor에 반영한다. critic은 별도의 회귀 loss로 학습한다.
Python · Fisher·step scaling·line search를 모두 보는 1차원 TRPO
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 | 판정 |
|---|---|---|---|
| 전체 step | 0.470836 | 0.010161 | 예산 .01 초과 → 거절 |
| 절반 step | 0.435087 | 0.002522 | 예산 충족·surrogate 개선 → 수용 |
근거 · [19] TRPO
PPO: clipping으로 rollout을 여러 번 학습하기
TRPO의 곡률 계산과 line search를 매번 수행하는 대신, PPO-Clip은 좋은 방향으로 너무 많이 바뀐 샘플에서 더 이상 보너스를 주지 않는 surrogate를 사용한다. 보통 Adam 같은 1차 optimizer로 여러 번 학습한다. critic과 advantage가 사라지는 것은 아니다.
같은 rollout을 여러 번 학습하면 현재 정책이 데이터를 만든 정책에서 멀어진다. PPO는 현재 확률 / 이전 확률과 advantage를 사용하되, 유리한 방향으로 너무 크게 바뀐 샘플의 추가 유인을 줄인다.
| A와 확률비 | PPO가 보는 값 | 의미 |
|---|---|---|
| A=+1, ρ=1.5 | min(1.5, 1.2)=1.2 | 좋은 행동을 이미 많이 올렸으므로 추가 유인 차단 |
| A=−1, ρ=0.6 | min(−0.6, −0.8)=−0.8 | 나쁜 행동을 이미 많이 내렸으므로 추가 유인 차단 |
| A=−1, ρ=1.5 | min(−1.5, −1.2)=−1.5 | 나쁜 행동을 더 자주 하는 방향은 계속 교정 |
advantage는 어디서 오는가? Critic과 GAE
critic은 prefix의 가치를 Vψ(sₜ)로 예측한다. GAE는 한 단계 예측 오차를 뒤에서 앞으로 모아, 긴 return의 분산과 짧은 bootstrap의 편향을 절충한다.
구현은 앞의 GAE 장의 같은 함수를 사용한다.
Python · clipped actor loss와 전체 loss
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 search | Fisher-vector product, conjugate gradient |
| PPO-Clip | 일부 샘플의 과한 개선 유인을 objective에서 차단 | 보통 Adam, 여러 epoch·minibatch |
배송 로봇의 PPO 학습 한 바퀴
- Collect: 현재 actor를 그대로 두고 128회 배송한다. 상태·행동·보상·종료와 old logp, old value를 저장한다.
- Prepare: 각 에피소드에서 GAE와 critic target을 뒤로 계산한다. episode 경계를 넘겨 섞지 않는다.
- Freeze batch: old logp·advantage·target을 detach한다. 이것들은 이 batch를 재사용하는 동안 바꾸지 않는다.
- Recompute: 현재 actor로 저장된 상태의 행동 logp를 다시 계산하고 ratio를 만든다. critic도 현재 value를 다시 예측한다.
- Optimize: clipped actor loss + value loss − entropy bonus를 미분해 파라미터를 바꾼다.
- Repeat locally: 같은 batch로 몇 번만 반복한다. 보통 shuffle한 minibatch로 수행하며 KL이 너무 크면 일찍 멈추기도 한다.
- Refresh: 새 actor로 새 배송 경험을 수집한다. 이때 새 old logp를 만든다.
batch B₀ 생성
current=θ₁
current=θ₂
새 데이터 수집
Python · 외부 환경 라이브러리 없이 실행하는 배송 로봇 PPO
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 historyLLM으로 넘어가기: 방법론의 전체 지도
강화학습은 행동의 결과를 점수로 평가하고, 좋은 결과를 낳은 행동을 더 자주 선택하도록 확률을 바꾸는 학습이다. LLM에서는 “행동”이 다음 토큰을 생성하는 일이다. 이제부터 같은 문제를 따라가며 보상, 확률, gradient가 어떻게 연결되는지 살펴본다.
사과는 모두 몇 개인가요?”
3 + 3 + 3 + 3 = 3 × 4 = 12
| 구분할 축 | 질문 | 해당 이름 |
|---|---|---|
| 보상의 출처 | 누가 좋은 답이라고 판단하는가? | RLHF: 사람 피드백 · RLVR: 검증기 |
| 정책 갱신 규칙 | 생성한 답변의 확률을 어떻게 바꾸는가? | PPO, REINFORCE/RLOO, GRPO, GSPO, SAPO 등 |
| 직접 선호 학습 | 고정된 선호 데이터를 어떤 손실로 학습하는가? | DPO: 비교쌍 · KTO: 단일 응답 라벨 |
기대값 → MDP → 가치 → 정책 gradient → LLM 토큰
RLHF 파이프라인 → PPO → DPO → KTO
RLVR → GRPO → 안정화·효율 개선 → 2026 연구
근거 · [1] PPO · [3] InstructGPT / RLHF · [4] DPO · [5] KTO · [6] DeepSeekMath / GRPO
LLM을 강화학습 문제로 바꾸기
사전학습 → SFT → 후속 정렬을 먼저 구분하자. 사전학습은 대량 텍스트에서 다음 토큰을 예측하며 언어와 지식을 학습한다. SFT는 질문에 답하는 좋은 예제를 모방한다. 선호 학습과 RLVR는 이미 이런 능력을 가진 모델의 응답 분포를 추가로 조정하는 단계이며, 반드시 하나의 고정 순서로만 결합하는 것은 아니다.
질문 x와 지금까지 생성한 토큰 y<ₜ를 합친 것이 상태다. 다음 토큰 yₜ가 행동이다. EOS 토큰이나 생성 한도에 도달하면 에피소드가 끝난다. 분명한 최종 정답이 있는 유한 길이 LLM 예제에서는 γ=1을 자주 사용한다.
응답 확률 = 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
Python · causal shift, response mask, sequence log probability
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 | 학습하는 LLM | optimizer step마다 |
| πold / behavior | 이번 rollout을 생성한 모델의 확률 | 새 rollout 배치를 만들 때; 배치 재사용 동안 고정 |
| πref · reference | 너무 멀리 벗어나지 않을 기준 | 보통 SFT 모델로 고정 |
| Vψ · critic | 현재 prefix의 미래 성과 예측 | PPO에서 value loss로 학습 |
RLHF: 사람의 비교를 보상으로 변환하기
사람에게 모든 새 응답을 매번 평가해 달라고 할 수는 없다. 전형적인 RLHF는 사람의 비교를 모아 보상 모델을 학습하고, 이 모델이 새 응답에 부여하는 점수를 높이도록 정책을 학습한다. InstructGPT는 SFT → 보상 모델 → PPO의 대표 사례다.
모방하는 LLM
C ≻ B ≻ A
Rφ(x,y)
PPO로 확률 갱신
Python · C ≻ B ≻ A 비교로 점수 학습
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()같은 질문에 대한 기대 RM 점수가 2.00, KL=0.50, β=0.50인 가상 정책이면 J=2.00−0.25=1.75.
Python · RM → sampling → critic → PPO, 완결된 작은 학습 루프
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()RLHF 실전 추적: 한 batch가 가중치를 바꾸기까지
이제 RLHF + PPO의 한 rollout batch를 처음부터 끝까지 추적하자. RLHF라는 이름만으로 모든 구현이 같지는 않다. 아래는 고정 보상 모델 + 고정 reference + actor + value critic, 토큰별 KL 표본 비용, GAE, PPO를 사용하는 명시적인 교육용 구성이다. 데이터 수치와 토큰 분할은 가상이다.
온라인 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 |
질문 batch를 뽑고 actor로 답변 생성
질문을 2개 뽑는다고 하자. 각 질문에서 actor가 다음 토큰을 뽑고, prefix에 붙이고, 다시 다음 토큰을 뽑는다. EOS 또는 길이 한도까지 반복한다. 이 generation 단계에서는 계산 그래프를 저장하지 않는다. discrete token sampling 자체에 역전파하는 것도 아니다.
4봉지
“3×4” sampling
“=12” sampling
EOS sampling
무엇을 저장하는가?
| 데이터 | 응답 한 개의 예 | 저장 이유 |
|---|---|---|
| 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 mask | terminal=True, [1,1,1] | 보상 위치·유효 토큰 구분 |
실제로는 prompt token, response token, attention mask, truncation 정보도 필요하다. GPU generation engine과 learner를 분리한다면 generation 당시 확률의 정의를 맞춰야 한다. temperature·top-p 변환 뒤의 sampling 분포와 학습에 쓰는 모델 확률이 다르면 단순 ratio가 정확한 behavior 보정은 아니다. 이 추적 예제는 온도 1, 분포 절단 없음으로 일치시킨다.
보상 모델 점수와 reference logp 계산
고정 RM이 완성 응답에 1.0점을 준다고 하자. 이것은 선호 점수이며 100% 정답 확률이라는 뜻은 아니다. reference는 같은 응답의 토큰 확률 [.40,.80,.90]을 준다. β=.1로 기준 정책을 벗어난 표본 비용을 만든다.
| 가상 token | old p | ref p | KL 표본 비용 | RM 점수 | 최종 rₜ |
|---|---|---|---|---|---|
| 계산 | .50 | .40 | −.1 log(1.25)=−.022314 | 0 | −.022314 |
| 정답 | .80 | .80 | 0 | 0 | 0 |
| EOS | .90 | .90 | 0 | +1.0 | 1.0 |
RM 점수는 마지막 유효 응답 token에 한 번만 더한다. 모든 token에 1점을 주면 긴 응답이 더 많은 보상을 받는 다른 과제가 된다. 이 표의 log ratio 비용 한 개는 정확한 전체 KL이 아니며 음의 비용 또는 양의 보너스가 나올 수도 있다.
critic으로 GAE와 target을 계산
γ=1, λ=.95, 마지막 상태 V=0이다. 위 reward와 old value를 사용한다.
| t | rₜ | Vold(sₜ) | δₜ=rₜ+Vnext−V | Âₜ=δₜ+.95Ânext | target=Â+Vold |
|---|---|---|---|---|---|
| 2 | 1.000000 | .600000 | .400000 | .400000 | 1.000000 |
| 1 | 0.000000 | .400000 | .200000 | .580000 | .980000 |
| 0 | −.022314 | .200000 | .177686 | .728686 | .928686 |
완성 응답에 한 번 붙인 점수가 GAE를 통해 앞의 token에도 연결됐다. 하지만 이것은 각 token의 인과적 정답 여부를 판정한 것이 아니다. 계산된 Â와 target은 detach한다. 이 예제에서는 숫자를 그대로 추적하기 위해 advantage whitening을 하지 않는다.
같은 응답을 현재 actor에 다시 넣기
저장된 질문과 응답을 입력하고 선택됐던 token의 logp를 다시 계산한다. 이 단계는 teacher forcing 방식의 scoring이다. 이번에는 actor에 gradient가 필요하다. 새 응답을 생성해서 기존 reward와 짝지으면 안 된다.
| 같은 저장 token | old p · 고정 | 현재 p · 재계산 | ρ | Â |
|---|---|---|---|---|
| 계산 | .50 | .55 | 1.10 | .728686 |
| 정답 | .80 | .84 | 1.05 | .580000 |
| EOS | .90 | .90 | 1.00 | .400000 |
rollout 직후 아직 업데이트 전이면 current=old이므로 ρ=1이다. 표는 그 배치로 한 번 이상 갱신한 뒤를 보여주는 가상 중간 시점이다. current가 바뀌어도 기존 old p, RM reward, Â, target은 그대로 둔다.
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다. 정확한 값은 아래 코드가 출력한다.
loss = actor loss + critic loss로 역전파한다. 예제에서는 entropy를 0으로 두고 value coefficient는 ½ 제곱오차에 이미 포함했다. reward에 KL을 넣었으므로 같은 비용을 loss에 또 더하지 않는다. 실무의 value clipping·entropy·reward scaling 등은 별도로 명시해야 한다.
Python · reward부터 actor·critic gradient까지 수치 재현
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())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로 갱신된다.
·target 저장
PPO + value loss
새 응답을 시도
RM·ref는 그대로
다음 batch에서 확인할 것
RM 점수와 별도로 held-out 사람 선호 평가, 응답 길이, reference KL, clip 비율, critic error를 본다. RM 점수가 오르는데 실제 유용성이 떨어지면 보상 모델의 허점을 학습했을 수 있다. EOS 없이 잘린 응답을 어떻게 점수화할지와 bootstrap 여부는 reward 계약으로 정한다.
DPO: 보상 모델 없이 비교쌍을 직접 학습
DPO는 질문 하나에 대한 선호 응답 yw와 비선호 응답 yl을 사용한다. 보상 모델을 별도로 맞춘 뒤 RL을 돌리는 대신, 정책의 log probability 비율로 선호 모형을 직접 학습한다.
chosen=C, rejected=A
각 답의 logp 계산
A보다 크도록 학습
| 가상 생성 확률 | chosen C | rejected A |
|---|---|---|
| 기준 정책 πref | 0.25 | 0.25 |
| 현재 정책 πθ | 0.50 | 0.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다. 나머지 응답에도 확률 질량이 남아 있다.
Python · sequence logp 차이로 계산하는 DPO
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와 PPO가 유한 데이터에서 같은 결과를 내는 것은 아니다. 기본 DPO는 주어진 비교쌍의 범위를 벗어나는 탐색을 직접 수행하지 않는다.
근거 · [4] DPO · [17] TRL DPO 구현 문서
KTO: 짝이 없는 좋음/나쁨 피드백
어떤 서비스에는 같은 질문의 비교쌍 없이 “이 답변이 좋았나요?”라는 피드백만 쌓인다. KTO는 (질문, 응답, 좋음/나쁨)으로 학습한다. 기준 정책 대비 log ratio가 기준점 z보다 높은지 낮은지를 sigmoid utility로 평가한다.
두 응답은 같은 질문의 비교쌍일 필요가 없다.
예: 좋은 응답의 현재 확률 0.50, 기준 0.25이면 r=log 2≈0.693. z=0.20, β=λD=λU=1에서 좋은 라벨의 loss≈0.379, 나쁜 라벨의 loss≈0.621이다.
Python · mismatched pair 기준점 추정과 stop-gradient
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()근거 · [5] KTO · [18] TRL KTO 구현 문서
RLVR: 사람의 취향 대신 결과를 검증하기
Reinforcement Learning with Verifiable Rewards는 정답 비교, 코드 테스트, 형식 증명 검사처럼 결과를 검증할 수 있는 보상을 사용한다. 정답 텍스트를 그대로 모방하는 SFT와 달리, 모델이 직접 시도한 응답에 점수를 준 뒤 학습한다.
gold = 12
“3×4=12”
0 또는 1
정답 경로의 확률 ↑
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
GRPO: 같은 질문의 답변끼리 비교하기
GRPO는 한 질문에 G개의 응답을 생성한 뒤 그 그룹의 평균을 기준점으로 삼는다. PPO에서 별도로 학습하던 critic을 없애는 대신 여러 rollout의 비용을 지불한다. 여기서는 최종 보상을 모든 응답 토큰에 공유하는 outcome-reward 형식을 다룬다.
초기에는 [0,1,1,0]이다. 정답을 하나만 남기거나 전부 정답으로 바꾸어 보자. 버튼의 문구는 후보 이름이며, 클릭은 가상 검증 결과를 바꾸는 실험이다.
Python · 원형 집계와 선택적 KL 근사
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)RLVR 실전 추적: 검증 결과가 gradient가 되는 과정
이번에는 RLVR + GRPO를 같은 깊이로 추적한다. 비교를 명확히 하기 위해 이 구성은 규칙 기반 정답 보상, critic 없음, 명시적 KL 없음(β=0), 원형 GRPO의 응답별 평균을 사용한다. RLVR를 PPO와 결합하면 앞 장의 critic·GAE 단계가 그대로 돌아온다.
질문과 verifier의 계약 준비
학습 질문에는 사과 수의 gold answer 12가 함께 있다. actor는 질문만 받고, verifier는 질문·출력·gold를 받는다. 정답을 어느 태그에서 읽을지, 파싱 실패·여러 정답 태그·길이 제한·시간 초과를 어떻게 처리할지 먼저 고정한다. 학습을 보고 임의로 정답 판정을 바꾸지 않는다.
질문마다 여러 응답 rollout 생성
질문 2개, 질문당 G=4개를 뽑으면 응답은 8개다. 같은 actor에서 각 응답을 독립적으로 sampling한다. 배치의 질문 경계를 보존해야 한다. 아래는 사과 질문의 4개만 펼쳤다.
| 그룹 | rollout | 생성 응답 | 정답 보상 |
|---|---|---|---|
| 사과 질문 | A | 3+4=7 → <final>7</final> | 0 |
| 사과 질문 | B | 3×4=12 → <final>12</final> | 1 |
| 사과 질문 | C | 3+3+3+3=12 → <final>12</final> | 1 |
| 사과 질문 | D | 3×4=14 → <final>14</final> | 0 |
각 응답의 token IDs, old token logp, 유효 response mask, 그룹 ID, EOS/truncation을 저장한다. generation에서는 gradient를 저장하지 않는다. 샘플링 분포와 old logp의 정의는 RLHF 때와 마찬가지로 일치시킨다.
검증기로 reward 계산
문자열에서 최종값을 읽고 12와 비교하면 R=[0,1,1,0]이다. verifier는 미분 가능한 신경망일 필요가 없다. Python의 숫자 비교, 테스트 실행, 증명 검사도 가능하다. 검증기를 미분해서 actor를 학습하는 것이 아니다. 계산한 점수가 다음 단계의 계수가 된다.
그룹 기준점과 advantage 계산
같은 질문 4개의 평균 μ=.5, 모집단 표준편차 σ=.5이므로 Â=[−1,+1,+1,−1]이다. 다른 질문의 답과 섞어 평균내면 “같은 난도의 다른 시도”라는 기준점이 사라진다.
“이 prefix의 예측 return V와 비교하면?”
별도의 critic과 GAE를 사용한다.
“같은 질문에서 나온 다른 응답들과 비교하면?”
그룹 통계가 기준점이며 critic을 학습하지 않는다.
전부 0 또는 전부 1이면 centered advantage는 0이다. 수치 분모를 안전하게 처리하고 그 그룹의 보상 기반 정책 항을 0으로 두거나, DAPO처럼 새 그룹을 모으는 정책을 택한다. 없는 정답 신호를 0 나누기로 만들어 낼 수는 없다.
응답의 A를 토큰에 나눠 주기
여기서는 최종 outcome만 보므로 B 응답의 모든 유효 token에 +1, A 응답의 모든 token에 −1을 복제한다. 보상 1을 token 수만큼 누적하는 것이 아니라, policy-gradient의 계수를 공유하는 것이다. prompt와 padding token의 mask는 0, 실제 생성 EOS의 mask는 1이다.
현재 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가 살아 있다.
mask·길이 평균·loss·역전파
각 응답의 평균 objective는 A=−.85, B=1.15, C=1.10, D=−.95. 이를 4개 평균하면 .1125이므로 loss=−.1125다. 이 예제는 길이가 모두 같아 전역 token mean과 값이 같지만, 길이가 다르면 원형 GRPO와 DAPO의 가중치가 달라진다.
actor의 현재 logp만 미분한다. reward, old logp, group mean/std, advantage는 고정 계수다. critic이 없으므로 value loss도 없다. reference KL을 넣는 변형이면 reference scoring과 별도 KL 항이 추가된다.
Python · 4×2 tensor로 loss와 gradient 확인
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())업데이트가 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 표와 다른 단순화임을 구분해서 보자.
새 rollout에서 다시 검증하고 반복
몇 번의 optimizer step 이후 새 actor로 다음 4개 응답을 생성한다. 이번에는 [0,1,1,1]이 나올 수도 있고 모두 정답일 수도 있다. reward와 group 통계는 매 rollout에서 다시 계산한다. 과거 정답 문자열을 영원히 반복 학습하는 SFT와 달리, 정책의 변화가 다음에 관측하는 풀이 분포를 바꾼다.
backward → step
RLHF 루프와 정확히 무엇이 다른가?
| 단계 | 위 RLHF + PPO | 위 RLVR + GRPO |
|---|---|---|
| rollout | actor가 응답 생성 | actor가 질문마다 G개 생성 |
| 평가 | 사람 선호를 학습한 RM | 고정된 정답 verifier |
| 기준점 | critic의 prefix별 V | 같은 질문 응답의 평균 |
| advantage | token reward + GAE | 그룹 표준화 A를 token에 공유 |
| reference | KL 비용에 사용 | 이 구성은 β=0이라 불필요 |
| gradient | actor + critic | actor만 |
| 새 batch | 새 응답과 새 old logp | 새 응답·검증·그룹 통계 |
보상의 출처와 optimizer의 선택이 함께 달라진 비교다. RLVR + PPO는 위 RLHF 루프에서 RM 평가를 verifier로 교체하면 critic·GAE·PPO 구조를 유지할 수 있다. RLHF + GRPO는 위 그룹 루프에서 verifier 점수를 RM 점수로 교체할 수 있다.
두 루프의 공통 구현: 토큰 배열에서 optimizer까지
Python · loss → logit gradient → 실제 확률 갱신
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 |
입력 위치 t의 logits는 다음 위치 t+1의 token을 예측한다.logits[:, :-1] ↔ input_ids[:, 1:].
mask도 response_mask[:, 1:]로 label 위치에 맞춘다.
현재 logp는 이미 존재하는 LLM token logp 함수로 계산한다. padding 이후의 의미 없는 값을 reward나 길이 평균에 넣으면 안 된다. 매우 큰 ratio가 나온다고 임의로 exp의 입력을 잘라 놓으면 원래 수식이 바뀌므로, dtype·정책 지연·수집 확률을 먼저 확인한다.
| 동작 | generation 시점 | optimization 시점 |
|---|---|---|
| 현재 actor | sampling, 보통 no_grad | 같은 응답 scoring, gradient 있음 |
| old logp | 생성 당시 값을 저장 | 변경하지 않고 읽기 |
| reference/RM/verifier | 또는 generation 후 별도 scoring | 고정; actor gradient 없음 |
| critic | old value 저장 | current value를 다시 계산 |
| reward·advantage·target | batch당 계산 | detach된 값 재사용 |
| optimizer | 아직 step하지 않음 | zero_grad → backward → step |
RLOO: 내 보상을 빼고 기준점을 계산
그룹 평균에 자기 보상까지 들어가면 내 점수가 내 기준점을 끌어올린다. REINFORCE Leave-One-Out은 같은 질문의 다른 응답들만 평균한다. critic 없이 baseline을 만드는 또 다른 방법이며, 사람 보상과 검증 보상 모두에 적용할 수 있다.
baseline=1/3 → A=2/3+0.667
baseline=2/3 → A=−2/3−0.667
Python · GRPO와 RLOO advantage를 나란히 계산
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()근거 · [7] RLOO
Dr. GRPO: 정규화가 만든 편향 줄이기
같은 오답이라도 10토큰과 100토큰 응답을 각각 평균하면 긴 응답의 토큰 하나당 페널티가 더 작아진다. 그룹 표준편차로 나누면 질문의 성공률에 따라 같은 보상 차이의 척도도 달라진다. Dr. GRPO는 이 두 정규화를 제거한다.
| 두 응답, G=2 | 10토큰 응답의 토큰당 가중치 | 100토큰 응답의 토큰당 가중치 |
|---|---|---|
| 원형 GRPO: 1/(G·Tᵢ) | 1/20 = 0.050 | 1/200 = 0.005 |
| Dr. GRPO: Tmax=100 | 1/200 = 0.005 | 1/200 = 0.005 |
| DAPO: 1/ΣTᵢ | 1/110 ≈ 0.0091 | 1/110 ≈ 0.0091 |
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)근거 · [9] Dr. GRPO
DAPO: 탐색·샘플·길이를 함께 손보기
DAPO는 단일 수식 교체를 넘어 대규모 추론 RL의 학습 구성을 개선한다. 핵심은 clip 상한 확대, 유효 그룹 재샘플링, 전역 토큰 평균, 과도한 길이에 대한 보상 처리다.
예: [0.8,1.28]. 좋은 희귀 토큰의 확률을 올리는 유인을 더 오래 유지한다.
혼합 보상 그룹으로 배치를 채운다. 생성 비용이 들고 학습 질문 분포도 달라진다.
유효 토큰 전체의 평균. 잘린 응답을 단순 오답으로 취급하는 잡음도 별도로 다룬다.
Python · 비대칭 clip·그룹 필터·길이 penalty
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))근거 · [10] DAPO
GSPO: 토큰 대신 응답 전체의 변화량
GRPO는 같은 응답 안에서도 토큰별 ratio로 각각 clip한다. GSPO는 응답 전체의 길이 정규화 likelihood ratio를 사용해 응답 단위로 clip한다. 긴 응답과 MoE 학습에서 불안정한 토큰별 비율을 다루려는 접근이다.
s = (1 × 1 × 2 × 1)1/4 ≈ 1.189.
양의 A, 설명용 ε=0.10: GRPO는 3번 토큰만 clip, GSPO는 전체 응답의 이 정책 항을 clip한다.
Python · 평균 log ratio를 exp한 뒤 clipping
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근거 · [11] GSPO
SAPO: 딱 잘라 끊는 대신 부드럽게 감쇠
SAPO는 토큰별 확률비의 hard clipping을 sigmoid 기반 smooth surrogate로 바꾼다. 오래된 정책에서 너무 멀어진 토큰은 완만하게 약화하고, 비교적 가까운 토큰의 신호는 유지한다.
Python · smooth surrogate를 그대로 미분하기
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)근거 · [12] SAPO
2026 · PNPO: 지금까지의 prefix로 가중하기
2026-08-02 공개 · 연구 단계. 같은 rollout을 더 많이 재사용하면 생성 비용은 줄지만 정책 지연(policy lag)이 커진다. PNPO는 토큰 하나나 완성 응답 전체 대신 현재 위치까지의 누적 log ratio를 길이 정규화한다.
Python · prefix weight·위치별 gate·detach
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)근거 · [13] PNPO
2026 · GAPO: 드문 정답에 더 넓은 clip
2026-08-31 제출 · 연구 단계. 논문 제목은 Group Adaptive Clipping Policy Optimization, 본문 약칭은 GAPO다. 그룹에서 정답이 드물수록 그 정답에 더 넓은 upper clip을 부여한다. DAPO의 고정 상한을 그룹별로 적응시키는 방향이다.
상한: 1.005 대 1.003286. 희귀한 성공을 강화하는 항을 조금 더 오래 유지한다.
Python · 적응형 upper clip과 sequence surrogate
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()근거 · [14] GAPO
2026 · Cliff: 처음 틀린 지점부터 구분하기
2026-09-02 공개 · 연구 단계. 결과가 틀린 응답 전체에 같은 음의 advantage를 주면 앞부분의 올바른 계산도 함께 불이익을 받는다. Cliff는 더 강한 teacher가 첫 오류 위치를 찾고, 올바른 prefix와 오류 suffix의 advantage를 구분한다.
검증기로 정답 확인
pitfall 위치 찾기
서로 다른 advantage
GRPO형 loss 적용
raw 합계 = 3 − 2 = 1 → b=1/6≈0.167. 재중심화 후 합계는 0이다. 올바른 prefix는 오류 suffix보다 덜 불이익을 받지만, 반드시 양수가 되는 것은 아니다.
Python · pitfall의 토큰 경계와 전체 토큰 재중심화
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()근거 · [15] Cliff
같은 질문으로 모든 방식 비교하기
방법을 선택할 때는 먼저 데이터 형태를 보고, 그다음 rollout 비용과 학습 안정성을 본다. 아래 표는 기본형의 차이다. 최신 라이브러리의 동명 trainer는 여러 변형을 조합할 수 있다.
| 방법 | 학습 입력/보상 | 별도 모델 | 새 응답 생성 | 핵심 갱신 신호 |
|---|---|---|---|---|
| SFT | 좋은 응답 C | 없음 | 기본형: 필요 없음 | C의 token likelihood |
| RLHF + PPO | 사람 비교 C≻B≻A → RM | RM + 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 / 갱신 단위 | 주로 줄이려는 문제 |
|---|---|---|---|
| GRPO | critic → 그룹 상대 baseline | token ratio / 응답별 평균 | critic 메모리·학습 비용 |
| Dr. GRPO | std 제거 + 일정 길이 분모 | token ratio / 일정 분모 합 | 질문 난도·응답 길이 관련 정규화 편향 |
| DAPO | 상한 확대·동적 sampling·token mean·길이 처리 | token ratio / 전체 토큰 평균 | 탐색 붕괴·무신호 그룹·긴 응답 문제 |
| GSPO | 길이 정규화 sequence ratio | sequence clip | 토큰 ratio 변동성과 응답 단위 신호 불일치 |
| SAPO | hard clip → smooth sigmoid | token별 부드러운 gate | 경계에서 신호가 갑자기 소실되는 문제 |
| PNPO · 2026 | prefix weight + 위치별 gate | prefix 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 간 척도와 충돌을 새로 설계하는 일이다.
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를 연결한 완결된 학습 루프
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])실행 결과 예시
동일 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.9909RLHF 예제는 설명까지 선호한 C에 질량을 모으고, RLVR 예제는 두 정답 문자열을 함께 강화한다. 서로 다른 후보 집합을 사용한 데모이므로 숫자를 알고리즘 성능 비교로 읽지 않는다.
실제 LLM 학습 시 대응되는 단계
- 질문 데이터와 verifier를 준비한다. 학습 질문과 held-out 평가 질문을 분리한다. 코드 문제는 격리 환경에서 실행한다.
- behavior policy로 응답을 sampling한다. 질문마다 G개 응답, EOS와 truncation 정보, 생성 당시 token logp를 저장한다.
- reward와 advantage를 계산한다. 같은 질문의 그룹 경계를 보존한다. 정답률 0/1 그룹 처리와 표준편차 정의를 명시한다.
- 현재 정책으로 해당 응답의 logp를 다시 계산한다. causal shift, prompt mask, padding mask를 적용한다. reference와 critic은 선택한 알고리즘에 맞춰 사용한다.
- loss → backward → optimizer step. rollout의 old logp는 고정한다. 한 배치의 재사용 횟수와 정책 지연을 제한한다.
- 새 rollout을 만든다. 성능·길이·다양성·KL 등을 비교하며 학습을 반복한다.
| 관측 지표 | 무엇을 뜻하는가 | 해석할 때 주의할 점 |
|---|---|---|
| Held-out pass@1 | 한 번 생성해서 정답일 비율 | 학습 reward 상승과 일반화는 다르다 |
| pass@k | k개 후보 중 적어도 하나 정답 | 여러 샘플 비용을 포함해 비교; 단순 정확도와 다름 |
| 정답률 0/1 그룹 비율 | 그룹 reward advantage가 0인 빈도 | 샘플 수 G와 질문 난도에 따라 바뀜 |
| 응답 길이·truncation 비율 | 생성 예산과 끝내기 행동 | 장문 자체가 올바른 추론의 증거는 아님 |
| Entropy·서로 다른 풀이 수 | 탐색과 다양성 | entropy가 높아도 무의미한 토큰일 수 있음 |
| KL·clip/gate 비율·ratio 분포 | 정책 이동과 신호 차단 정도 | token/sequence/prefix 정의가 다르면 직접 비교 불가 |
| 전체 생성 토큰·wall time | 실제 학습 비용 | step 수만 같다고 계산량이 같은 것은 아님 |
이해 점검: 설명할 수 있으면 배운 것
각 질문에 먼저 답한 뒤 펼쳐서 확인하자.
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라는 네 질문으로 최신 논문을 읽는다.
부록: 기호와 최소 수학 사전
미적분을 깊이 몰라도 확률, 로그, 평균, 미분의 부호를 이해하면 손실함수의 방향을 읽을 수 있다. 이 교재는 정책 목적함수 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 등 |
논문과 읽기 범위
자료 확인 기준: 2026-09-10. 공개된 원 논문을 중심으로 선별했다. “최신”은 아래 2026년 연구까지 포함한다는 뜻이며, 모든 후속 논문이나 비공개 학습 기법을 망라한다는 뜻은 아니다. 2026년 항목은 초기 연구 결과로 구분했다. 날짜는 최초 제출 기준이며 arXiv 번호의 월과 다를 수 있다.
- [1] PPO2017-07-20
Clipped surrogate objective - [2] GAE2015-06-08
TD residual과 advantage 추정 - [3] InstructGPT / RLHF2022-03-04
사람의 비교 → 보상 모델 → PPO - [4] DPO2023-05-29
KL 정규화 최적 정책과 직접 선호 손실 - [5] KTO2024-02-02
단일 응답의 좋음/나쁨, v4의 기준점 및 손실 - [6] DeepSeekMath / GRPO2024-02-05
그룹 상대 advantage와 critic 제거 - [7] RLOO2024-02-22
REINFORCE와 leave-one-out baseline - [8] DeepSeek-R12025-01-22
검증 가능한 보상으로 추론 학습; R1-Zero와 R1의 구분 - [9] Dr. GRPO2025-03-26
길이·표준편차 정규화의 편향 분석 - [10] DAPO2025-03-18
비대칭 clip, dynamic sampling, token mean, overlong shaping - [11] GSPO2025-07-24
길이 정규화 sequence ratio와 sequence clipping - [12] SAPO2025-11-25
v2 Eq. 5–8의 smooth surrogate와 gradient gate - [13] PNPO2026-08-02
v1 Eq. 7–11: prefix weight, gate, detached score surrogate - [14] GAPO2026-08-31
Group Adaptive Clipping Policy Optimization, v1 Eq. 11–13 - [15] Cliff2026-09-02
v1 Eq. 3–5: 첫 오류 위치와 token advantage 재중심화 - [16] TRL GRPO 구현 문서2026-09-10 확인
loss 집계·reward scaling·KL 설정의 구현상 구분 - [17] TRL DPO 구현 문서2026-09-10 확인
비교쌍 데이터와 직접 선호 학습 구현 - [18] TRL KTO 구현 문서2026-09-10 확인
좋음/나쁨 라벨과 KTO 설정 - [19] TRPO2015-02-19
정책 개선 surrogate, KL 제약, 자연 gradient와 line search - [20] RLHF PPO 구현 세부사항2026-09-10 확인
rollout, reward·KL, value 및 응답 mask 구현
그림·계산 예제·Python 코드는 개념 설명을 위해 새로 작성했다. 원 논문의 그래프나 실험 수치를 복제하지 않았다. 수식은 교육 목적에 맞게 기호를 통일했으며, 생략한 항과 구현 가정은 해당 절에 표시했다.