Reinforcement Learning from Human Feedback
Explained as an A/B test with a model of the tester in the loop, because that's the only version I could hold in my head.
Download rlhf_from_scratch.py — everything below runs from it. NumPy, plus a tiny CPU PyTorch reward model.
I'm an AI Engineer. I've shipped models, done the MLOps, and I have fine-tuned
on human preference data with a library that hid every moving part behind
trainer.train(). I could not have told you what the reward model
was trained on, why the KL term is there, or why people say "reward hacking"
like a proverb. The maths wasn't the problem. Nobody told me the whole thing
is an A/B test, with a model of the tester standing in for the tester.
You can't spec it, but you can A/B test it
You know this feeling, and you know the way out of it:
def loss(answer):
# "helpful, honest, harmless"
return ??? # nobody can write this line
def which_is_better(answer_a, answer_b):
return ask_a_person() # one bit: "A" or "B". People are good at this
RLHF is what you do with those bits. Fit a model that predicts the vote: the reward model. Turn your language model's knobs to make that model happy: the policy. And keep the policy on a leash, so it can't wander off into answers the reward model has never seen: the KL penalty.
r = fit(votes) # reward model: answer -> score
policy = climb(r, start=ref, leash=beta) # optimise r, but stay near ref
That's the whole idea. Two lines, and the second is where the trouble lives.
| Term | Means | Programmer's analogy |
|---|---|---|
| Reward model r(x, y) | A learned score for answer y to prompt x, fitted to votes | A model of the A/B tester |
| Bradley-Terry | P(A beats B) = σ(rA − rB) | Elo. Two scores, one sigmoid, a probability |
| Policy π | The model being tuned. A distribution over answers | The thing you ship |
| Reference πref | The supervised model you started from. Frozen | The main branch |
| KL penalty, β | How far π has drifted from πref, charged against the reward; β is the coefficient | The size of diff you're allowed. β is the leash length, tuned by hand |
Why votes and not scores? For the reason you run A/B tests instead of surveys: people are inconsistent at absolute scores ("is this a 7?") and consistent at relative ones ("this one"). The noise doesn't vanish, it moves to the close calls, where it does the least damage. Supervised fine-tuning can only imitate; it can't learn that one good answer beats another good answer. A vote can. The three-stage pipeline that wraps all this (demonstrations, comparisons, RL) is the InstructGPT page; read it second.
The one formula
objective(π) = Ey∼π[ r(x, y) ] − β · KL( π ‖ πref )
Two lines because there are two models. Read each as pipeline stages:
| Step | Shape | What it does |
|---|---|---|
r(win) − r(lose) | scalar | The margin. Positive means the model agrees with the person |
σ(·) | (0, 1) | Margin → the probability the model gives to the vote that actually happened |
−log | scalar | Cross-entropy on one bit. Small when confident and right, huge when confident and wrong |
Ey∼π[r] | scalar | Average reward-model score of what the policy writes. This is what the optimiser climbs |
β · KL(π ‖ πref) | scalar ≥ 0 | Ey∼π[log π(y) − log πref(y)]: how far the policy has drifted, in nats. β = 0 means no leash |
Ask which, not how much
The script builds a toy world: 4 prompts, 8 candidate answers each, and an
answer is four numbers (correct, length,
hedges, polite). A hidden true_reward
says what people want: being right is most of it, and past a certain length
nobody reads it. The reward model never sees that function, and never sees
correct either: it reads text, so it gets sounds,
its own guess at correctness, wrong one time in four. The reference
policy (the supervised model) writes short-to-medium answers, like
its demonstrations did. Prompt 0:
ans correct sounds length hedges polite TRUE ref%
0 0 1 0.10 0.02 0.14 0.11 20.3%
1 1 1 0.23 0.44 0.36 2.71 20.3%
2 1 1 0.36 0.21 0.12 2.81 20.3%
3 1 1 0.49 0.06 0.65 3.56 20.3% ← the truly best answer
4 0 0 0.61 0.13 0.28 -0.04 12.2%
5 0 1 0.74 0.38 0.09 -1.62 4.3%
6 1 1 0.87 0.34 0.58 0.90 1.6%
7 1 1 1.00 0.04 0.93 0.67 0.6% ← the wall of text. Rare
Now ask a person: pick two answers the reference would actually write, show both, record the winner. The person is noisy the Bradley-Terry way, P(A wins) = σ(trueA − trueB), so a close call is a coin flip.
prompt A B true_A true_B P(A wins) label
1 5 0 2.13 -0.25 0.92 0 ← wrong. 8% chance of that
2 1 2 -0.61 2.63 0.04 2
1 2 0 -0.09 -0.25 0.54 0
1 2 1 -0.09 0.38 0.38 1
0 2 0 2.81 0.11 0.94 2
1 1 3 0.38 3.18 0.06 3
The label is one bit. No 7/10, no rubric, and nothing in the data says why one won. The reward model has to guess the why from what it can see.
The reward model is a model of the tester
A 4→16→1 network, trained on 2000 of those bits with nothing but −log
σ(r(win) − r(lose)). The loss goes from 0.6805 to 0.4591 and stops
there, because the votes themselves are noisy. The only check that matters is
whether its ordering matches the truth, and it does: rank correlation
0.877 over all 32 answers, from 2000 clicks. The scale is its own, because
Bradley-Terry only sees differences, so I normalise the way Stiennon et al. do
(reference answers score 0 on average; I also scale to unit spread so β means
the same for every model here). Only the order is comparable.
Climb it, on a leash
The policy is a softmax over the 8 answers, one row of logits per prompt, and
it starts as the reference. Each step: sample answers, score each
with the reward model, subtract the leash, nudge the logits. The reward per
sample is exactly InstructGPT's, rm(answer) − β · (log π(answer) − log
π_ref(answer)): the KL penalty folded into the reward. The papers use
PPO. I use REINFORCE, the same objective with the plainest estimator there is:
the gradient of log-softmax with respect to the logits is onehot(answer)
− probs, so the policy gradient is one line. The optimiser is Adam, as
in the papers. rm is what the optimiser sees; true
is what you never get to see.
step 0 rm 0.000 true 1.117 KL 0.000
step 50 rm 0.870 true 2.524 KL 0.682
step 100 rm 0.881 true 2.517 KL 0.688
step 150 rm 0.889 true 2.528 KL 0.701
step 200 rm 0.892 true 2.532 KL 0.706
step 250 rm 0.893 true 2.534 KL 0.707
step 300 rm 0.894 true 2.535 KL 0.709
same reward model, beta = 0: true reward peaked at step 100
(3.103), then ended at 2.704
Goodhart's law, in a table
Now the experiment I wish someone had shown me. Train the reward model on 150 comparisons instead of 2000. It is still decent where it has data (rank correlation 0.802 on the short answers the reference writes). Set β = 0. Watch the two columns.
step 0 rm 0.000 true 1.117 KL 0.000
step 25 rm 0.722 true 2.181 KL 0.475
step 50 rm 1.062 true 2.656 KL 1.039
step 75 rm 1.296 true 2.870 KL 1.745 ← peak
step 100 rm 1.390 true 2.841 KL 2.064
step 125 rm 1.695 true 2.488 KL 2.543
step 150 rm 1.759 true 2.418 KL 2.749
step 175 rm 1.815 true 2.217 KL 2.919
step 200 rm 1.930 true 1.782 KL 3.640
step 225 rm 1.937 true 1.763 KL 3.700
step 250 rm 1.939 true 1.760 KL 3.716
step 275 rm 1.940 true 1.758 KL 3.725
step 300 rm 1.941 true 1.758 KL 3.730 ← rm still climbing
Up to step 75 this looks like the happy path. Then the policy finds something the reward model loves and people don't, and the columns part ways for good. Two of the four prompts ended on the wall of text. Prompt 2, with the thin model's opinion of each answer:
ans correct sounds length hedges polite TRUE ref% rm
0 0 0 0.10 0.01 0.35 0.34 20.3% -0.16
1 0 1 0.23 0.58 0.27 -0.61 20.3% -2.49
2 1 1 0.36 0.46 0.33 2.63 20.3% 0.69
3 1 1 0.49 0.04 0.54 3.49 20.3% 1.21 ← truth's pick
4 1 1 0.61 0.44 0.32 2.55 12.2% 0.84
5 1 0 0.74 0.00 0.62 2.48 4.3% -0.42
6 1 1 0.87 0.04 0.66 1.42 1.6% -1.98
7 1 0 1.00 0.01 0.93 0.72 0.6% 1.90 ← model's pick
The reward model is fine where it has data. Off its data it is guessing: the reference almost never wrote a wall of text, so the model saw one twice in 300 answers, and its score out there is whatever a 16-unit network happens to extrapolate. The policy went looking for the highest score, and the highest score is where the guess landed high. That's the bug you will hit. Not "the reward model is wrong", which is always true, but "the optimiser is a search for where it is wrong". At scale, Gao et al. drew this exact curve (arXiv 2210.10760): reward model score up, real preference up then down, as a function of KL.
The leash length
Same thin reward model, three values of β, 300 steps each.
beta final rm final true final KL
0.0 1.941 1.758 3.730
0.5 1.237 2.360 1.143
2.0 0.385 1.605 0.091
the reference itself: true 1.117 KL 0.000
β = 0 wins on rm and loses on true. β = 2 barely
leaves the reference. The middle is the point: improve on the reference
without leaving the region where the reward model was trained. β is tuned, not
derived.
நான் ஒரு AI Engineer. Models ship செய்திருக்கிறேன், MLOps
செய்திருக்கிறேன், human preference data-வில் fine-tune கூட
செய்திருக்கிறேன் — ஆனால் அந்த library ஒவ்வொரு நகரும் பகுதியையும்
trainer.train()-க்குப் பின்னால் மறைத்து வைத்திருந்தது.
reward model எதன் மேல் train ஆனது, KL term ஏன் இருக்கிறது, "reward
hacking" என்று ஏன் எல்லோரும் ஒரு பழமொழி போலச் சொல்கிறார்கள் — எதுவும்
எனக்குத் தெரியாது. கணிதம் பிரச்சனை இல்லை. இது முழுக்க முழுக்க ஒரு A/B
test, tester-க்குப் பதிலாக tester-ஓட ஒரு model நிற்கிறது — இதை யாரும்
சொல்லவில்லை.
Spec எழுத முடியாது, ஆனால் A/B test செய்ய முடியும்
இந்த உணர்வு உங்களுக்குத் தெரியும், அதிலிருந்து வெளியேறும் வழியும் தெரியும்:
def loss(answer):
# "helpful, honest, harmless"
return ??? # இந்த வரியை யாராலும் எழுத முடியாது
def which_is_better(answer_a, answer_b):
return ask_a_person() # ஒரு bit: "A" அல்லது "B". மனிதர்கள் இதில் கெட்டி
அந்த bits-ஐ வைத்து நீங்கள் செய்வதுதான் RLHF. வாக்கை (vote) கணிக்கும் ஒரு model-ஐப் பொருத்துங்கள்: அதுதான் reward model. அந்த model-ஐ மகிழ்விக்கும்படி உங்கள் language model-ன் knobs-ஐத் திருப்புங்கள்: அதுதான் policy. பிறகு policy-ஐ ஒரு சங்கிலியில் கட்டிவையுங்கள், reward model பார்த்தே இராத பதில்களை நோக்கி அது ஓடிவிடக் கூடாது: அதுதான் KL penalty.
r = fit(votes) # reward model: answer -> score
policy = climb(r, start=ref, leash=beta) # r-ஐ optimise செய், ref அருகில் இரு
இதுதான் முழுக் கருத்து. இரண்டு வரிகள் — பிரச்சனை இரண்டாவதில் வசிக்கிறது.
Reward model = vote-ai predict panra model. Policy = adha sandhosha paduthura maadhiri thiruppura model. KL penalty = policy reference-ai vittu romba dooram poga vidaama katti vaikkura leash.
| சொல் | அர்த்தம் | Programmer-ஓட உதாரணம் |
|---|---|---|
| Reward model r(x, y) | Prompt x-க்கு answer y-ன் கற்றுக்கொண்ட score, votes-ஐ வைத்துப் பொருத்தப்பட்டது | A/B tester-ஓட ஒரு model |
| Bradley-Terry | P(A beats B) = σ(rA − rB) | Elo. இரண்டு scores, ஒரு sigmoid, ஒரு probability |
| Policy π | Tune செய்யப்படும் model. Answers மேல் ஒரு distribution | நீங்கள் ship செய்யும் பொருள் |
| Reference πref | நீங்கள் தொடங்கிய supervised model. Frozen | main branch |
| KL penalty, β | π எவ்வளவு தூரம் πref-ஐ விட்டு விலகிவிட்டது, reward-லிருந்து கழிக்கப்படுகிறது; β அதன் coefficient | உங்களுக்கு அனுமதிக்கப்பட்ட diff-ன் அளவு. β = leash-ன் நீளம், கையால் tune செய்வது |
Scores இல்லாமல் votes ஏன்? Surveys-க்குப் பதிலாக A/B tests ஏன் ஓட்டுகிறீர்களோ அதே காரணம்தான்: absolute scores-ல் ("இது 7-ஆ?") மனிதர்கள் நிலையற்றவர்கள், relative-ல் ("இதுதான்") நிலையானவர்கள். Noise மறைந்துவிடாது, close calls-க்கு நகர்கிறது — அங்கே அது செய்யும் சேதம் மிகக் குறைவு. Supervised fine-tuning-ஆல் பிரதி எடுக்க மட்டுமே முடியும்; ஒரு நல்ல பதில் இன்னொரு நல்ல பதிலை வெல்லும் என்பதை அதனால் கற்க முடியாது. ஒரு vote-ஆல் முடியும். இதையெல்லாம் சுற்றியிருக்கும் மூன்று-நிலைப் pipeline (demonstrations, comparisons, RL) InstructGPT page-ல் இருக்கிறது; அதை இரண்டாவதாகப் படியுங்கள்.
அந்த ஒரு சூத்திரம்
objective(π) = Ey∼π[ r(x, y) ] − β · KL( π ‖ πref )
இரண்டு models இருப்பதால் இரண்டு வரிகள். ஒவ்வொன்றையும் pipeline stages-ஆகப் படியுங்கள்:
| படி | Shape | என்ன செய்கிறது |
|---|---|---|
r(win) − r(lose) | scalar | Margin. Positive என்றால் model மனிதருடன் ஒத்துப்போகிறது |
σ(·) | (0, 1) | Margin → உண்மையில் நடந்த vote-க்கு model தரும் probability |
−log | scalar | ஒரு bit மேல் cross-entropy. நம்பிக்கையுடன் சரியாக இருந்தால் சிறியது, நம்பிக்கையுடன் தவறாக இருந்தால் மிகப் பெரியது |
Ey∼π[r] | scalar | Policy எழுதுவதற்கு reward model தரும் சராசரி score. Optimiser ஏறுவது இதன் மேல்தான் |
β · KL(π ‖ πref) | scalar ≥ 0 | Ey∼π[log π(y) − log πref(y)]: policy எவ்வளவு விலகியது, nats-ல். β = 0 என்றால் leash இல்லை |
எவ்வளவு என்று கேட்காதே, எது என்று கேள்
Script ஒரு பொம்மை உலகைக் கட்டுகிறது: 4 prompts, ஒவ்வொன்றுக்கும் 8 candidate
answers, ஒரு answer என்பது நான்கு எண்கள் (correct,
length, hedges, polite). மனிதர்கள் என்ன
விரும்புகிறார்கள் என்பதை ஒரு மறைவான true_reward சொல்கிறது:
சரியாக இருப்பதுதான் பெரும்பகுதி, ஒரு நீளத்திற்கு மேல் யாரும் படிப்பதில்லை.
Reward model அந்த function-ஐப் பார்ப்பதே இல்லை, correct-ஐயும்
பார்ப்பதில்லை: அது text படிக்கிறது, எனவே sounds கிடைக்கிறது —
correctness பற்றிய அதன் சொந்த யூகம், நான்கில் ஒரு முறை தவறு. Reference
policy (supervised model) குறுகிய-முதல்-நடுத்தர பதில்களை எழுதுகிறது,
அதன் demonstrations எழுதியது போல. Prompt 0:
ans correct sounds length hedges polite TRUE ref%
0 0 1 0.10 0.02 0.14 0.11 20.3%
1 1 1 0.23 0.44 0.36 2.71 20.3%
2 1 1 0.36 0.21 0.12 2.81 20.3%
3 1 1 0.49 0.06 0.65 3.56 20.3% ← மிகச் சிறந்த பதில்
4 0 0 0.61 0.13 0.28 -0.04 12.2%
5 0 1 0.74 0.38 0.09 -1.62 4.3%
6 1 1 0.87 0.34 0.58 0.90 1.6%
7 1 1 1.00 0.04 0.93 0.67 0.6% ← நீளமான சுவர். அரிது
இப்போது ஒரு மனிதரிடம் கேளுங்கள்: reference உண்மையில் எழுதக்கூடிய இரண்டு பதில்களை எடுத்து, இரண்டையும் காட்டி, வென்றதைப் பதிவு செய்யுங்கள். மனிதர் Bradley-Terry வழியில் noisy, P(A wins) = σ(trueA − trueB), எனவே ஒரு close call என்பது நாணயச் சுழற்சி.
prompt A B true_A true_B P(A wins) label
1 5 0 2.13 -0.25 0.92 0 ← தவறு. 8% வாய்ப்பு உண்டு
2 1 2 -0.61 2.63 0.04 2
1 2 0 -0.09 -0.25 0.54 0
1 2 1 -0.09 0.38 0.38 1
0 2 0 2.81 0.11 0.94 2
1 1 3 0.38 3.18 0.06 3
Label என்பது ஒரு bit. 7/10 இல்லை, rubric இல்லை, ஒன்று ஏன் வென்றது என்பது பற்றி data-வில் எதுவும் இல்லை. அந்த "ஏன்"-ஐ, தான் பார்க்கக்கூடியதிலிருந்து reward model யூகிக்க வேண்டும்.
Reward model என்பது tester-ஓட ஒரு model
ஒரு 4→16→1 network, அந்த 2000 bits மேல், −log σ(r(win) −
r(lose)) தவிர வேறொன்றுமில்லாமல் train ஆகிறது. Loss 0.6805-லிருந்து
0.4591-க்குப் போய் அங்கேயே நிற்கிறது, ஏனெனில் votes-ஏ noisy. முக்கியமான ஒரே
சோதனை: அதன் வரிசை உண்மையுடன் பொருந்துகிறதா — பொருந்துகிறது: 32
answers மேல் rank correlation 0.877, 2000 clicks-லிருந்து. Scale அதற்குச்
சொந்தமானது, ஏனெனில் Bradley-Terry வித்தியாசங்களை மட்டுமே பார்க்கிறது, எனவே
Stiennon et al. செய்வது போல நான் normalise செய்கிறேன் (reference answers
சராசரியாக 0 score; β எல்லா model-க்கும் ஒரே பொருள் தரும்படி unit spread-க்கும்
scale செய்கிறேன்). வரிசை மட்டுமே ஒப்பிடத்தக்கது.
சங்கிலியுடன் ஏறு
Policy என்பது 8 answers மேல் ஒரு softmax, prompt-க்கு ஒரு வரி logits, மேலும்
அது reference-ஆகவே தொடங்குகிறது. ஒவ்வொரு step-லும்: answers sample
செய், ஒவ்வொன்றையும் reward model-ஆல் score செய், leash-ஐக் கழி, logits-ஐ
லேசாகத் தள்ளு. Sample-க்கான reward சரியாக InstructGPT-ஓடதுதான்,
rm(answer) − β · (log π(answer) − log π_ref(answer)): KL penalty
reward-க்குள் மடிக்கப்பட்டது. Papers PPO பயன்படுத்துகின்றன. நான் REINFORCE
பயன்படுத்துகிறேன் — அதே objective, இருக்கும் மிக எளிய estimator-உடன்:
logits-ஐப் பொறுத்து log-softmax-ன் gradient onehot(answer) −
probs, எனவே policy gradient ஒரே வரி. Optimiser Adam, papers-ல்
இருப்பது போல. Optimiser பார்ப்பது rm; true என்பது
நீங்கள் ஒருபோதும் பார்க்க முடியாதது.
step 0 rm 0.000 true 1.117 KL 0.000
step 50 rm 0.870 true 2.524 KL 0.682
step 100 rm 0.881 true 2.517 KL 0.688
step 150 rm 0.889 true 2.528 KL 0.701
step 200 rm 0.892 true 2.532 KL 0.706
step 250 rm 0.893 true 2.534 KL 0.707
step 300 rm 0.894 true 2.535 KL 0.709
same reward model, beta = 0: true reward peaked at step 100
(3.103), then ended at 2.704
Beta = 0 vachaa rm score merike merike poidum, aana true reward peak aagi apram keela iranga aarambichidum. Adha thaan reward hacking-nu solraanga.
Goodhart-ன் விதி, ஒரு table-ல்
இப்போது யாராவது எனக்குக் காட்டியிருக்க வேண்டும் என்று நினைக்கும் சோதனை. 2000-க்குப் பதிலாக 150 comparisons மேல் reward model-ஐ train செய்யுங்கள். Data இருக்கும் இடத்தில் அது இன்னும் ஓரளவு நல்லது (reference எழுதும் குறுகிய பதில்கள் மேல் rank correlation 0.802). β = 0 வையுங்கள். இரண்டு columns-ஐயும் பாருங்கள்.
step 0 rm 0.000 true 1.117 KL 0.000
step 25 rm 0.722 true 2.181 KL 0.475
step 50 rm 1.062 true 2.656 KL 1.039
step 75 rm 1.296 true 2.870 KL 1.745 ← உச்சம்
step 100 rm 1.390 true 2.841 KL 2.064
step 125 rm 1.695 true 2.488 KL 2.543
step 150 rm 1.759 true 2.418 KL 2.749
step 175 rm 1.815 true 2.217 KL 2.919
step 200 rm 1.930 true 1.782 KL 3.640
step 225 rm 1.937 true 1.763 KL 3.700
step 250 rm 1.939 true 1.760 KL 3.716
step 275 rm 1.940 true 1.758 KL 3.725
step 300 rm 1.941 true 1.758 KL 3.730 ← rm இன்னும் ஏறுகிறது
Step 75 வரை இது மகிழ்ச்சியான பாதை போலவே தெரிகிறது. பிறகு reward model விரும்பும், மனிதர்கள் விரும்பாத ஒன்றை policy கண்டுபிடிக்கிறது, அதன் பிறகு columns இரண்டும் நிரந்தரமாகப் பிரிந்துவிடுகின்றன. நான்கு prompts-ல் இரண்டு நீளமான சுவரில் முடிந்தன. Prompt 2, மெலிந்த model ஒவ்வொரு பதிலைப் பற்றி நினைப்பதுடன்:
ans correct sounds length hedges polite TRUE ref% rm
0 0 0 0.10 0.01 0.35 0.34 20.3% -0.16
1 0 1 0.23 0.58 0.27 -0.61 20.3% -2.49
2 1 1 0.36 0.46 0.33 2.63 20.3% 0.69
3 1 1 0.49 0.04 0.54 3.49 20.3% 1.21 ← உண்மை தேர்வு
4 1 1 0.61 0.44 0.32 2.55 12.2% 0.84
5 1 0 0.74 0.00 0.62 2.48 4.3% -0.42
6 1 1 0.87 0.04 0.66 1.42 1.6% -1.98
7 1 0 1.00 0.01 0.93 0.72 0.6% 1.90 ← model-ஓட தேர்வு
Data இருக்கும் இடத்தில் reward model நன்றாகவே இருக்கிறது. Data-க்கு வெளியே அது யூகிக்கிறது: reference கிட்டத்தட்ட ஒருபோதும் நீளமான சுவரை எழுதியதில்லை, எனவே 300 answers-ல் model அதை இரண்டு முறை மட்டுமே பார்த்தது, அங்கே அதன் score என்பது ஒரு 16-unit network தற்செயலாக extrapolate செய்வது. Policy மிக உயர்ந்த score-ஐத் தேடிப் போனது, அந்த உயர்ந்த score இருந்தது யூகம் உயரமாக விழுந்த இடத்தில்தான். அதுதான் நீங்கள் சந்திக்கப் போகும் bug. "reward model தவறு" என்பது அல்ல — அது எப்போதும் உண்மைதான் — "optimiser என்பது அது எங்கே தவறு என்பதைத் தேடும் ஒரு search" என்பதுதான். பெரிய அளவில் Gao et al. இதே வளைவை வரைந்தார்கள் (arXiv 2210.10760): reward model score மேலே, உண்மையான preference மேலே பிறகு கீழே, KL-ன் function-ஆக.
Reward model data irukkura idathula nallaa dhaan irukku. Veliya pona adhu yoosikkuthu, optimiser andha thappa dhaan thedi pogudhu.
Leash-ன் நீளம்
அதே மெலிந்த reward model, β-ன் மூன்று மதிப்புகள், ஒவ்வொன்றுக்கும் 300 steps.
beta final rm final true final KL
0.0 1.941 1.758 3.730
0.5 1.237 2.360 1.143
2.0 0.385 1.605 0.091
the reference itself: true 1.117 KL 0.000
β = 0, rm-ல் வெல்கிறது, true-ல் தோற்கிறது. β = 2
reference-ஐ விட்டு அரிதாகவே நகர்கிறது. நடுவில் இருப்பதுதான் கருத்து: reward
model எந்தப் பகுதியில் train ஆனதோ அதை விட்டு வெளியேறாமல் reference-ஐ விட
மேம்படுத்துங்கள். β derive செய்யப்படுவதில்லை, tune செய்யப்படுகிறது.
मैं एक AI Engineer हूँ। मैंने models ship किए हैं, MLOps किया है, और human
preference data पर fine-tune भी किया है — पर उस library ने हर चलता-फिरता
पुर्ज़ा trainer.train() के पीछे छिपा रखा था। reward model किस
चीज़ पर train हुआ, KL term वहाँ क्यों है, और लोग "reward hacking" को
कहावत की तरह क्यों बोलते हैं — मैं आपको कुछ नहीं बता सकता था। समस्या गणित
नहीं थी। किसी ने यह नहीं बताया कि यह पूरी चीज़ एक A/B test है, जिसमें
tester की जगह tester का एक model खड़ा है।
Spec नहीं लिख सकते, पर A/B test कर सकते हैं
यह एहसास आप जानते हैं, और इससे निकलने का रास्ता भी:
def loss(answer):
# "helpful, honest, harmless"
return ??? # यह लाइन कोई नहीं लिख सकता
def which_is_better(answer_a, answer_b):
return ask_a_person() # एक bit: "A" या "B". लोग इसमें अच्छे हैं
उन्हीं bits के साथ जो आप करते हैं, वही RLHF है। एक model fit कीजिए जो vote की भविष्यवाणी करे: वह है reward model। अपने language model के knobs इस तरह घुमाइए कि वह model ख़ुश हो: वह है policy। और policy को पट्टे पर रखिए, ताकि वह ऐसे जवाबों की तरफ़ न भटक जाए जो reward model ने कभी देखे ही नहीं: वह है KL penalty।
r = fit(votes) # reward model: answer -> score
policy = climb(r, start=ref, leash=beta) # r को optimise करो, पर ref के पास रहो
पूरा विचार यही है। दो लाइनें, और मुसीबत दूसरी में रहती है।
| शब्द | मतलब | Programmer की उपमा |
|---|---|---|
| Reward model r(x, y) | Prompt x के जवाब y का सीखा हुआ score, votes पर fit किया गया | A/B tester का एक model |
| Bradley-Terry | P(A beats B) = σ(rA − rB) | Elo. दो scores, एक sigmoid, एक probability |
| Policy π | वह model जिसे tune किया जा रहा है। जवाबों पर एक distribution | जो चीज़ आप ship करते हैं |
| Reference πref | वह supervised model जहाँ से आपने शुरू किया। Frozen | main branch |
| KL penalty, β | π, πref से कितना खिसक चुका है, reward में से घटाया जाता है; β उसका coefficient है | कितने बड़े diff की इजाज़त है। β पट्टे की लंबाई है, हाथ से tune होती है |
Scores क्यों नहीं, votes क्यों? उसी वजह से जिस वजह से आप surveys की जगह A/B tests चलाते हैं: absolute scores में ("क्या यह 7 है?") लोग अस्थिर होते हैं और relative में ("यह वाला") स्थिर। Noise ग़ायब नहीं होता, वह close calls में चला जाता है, जहाँ उससे सबसे कम नुक़सान होता है। Supervised fine-tuning सिर्फ़ नक़ल कर सकती है; वह यह नहीं सीख सकती कि एक अच्छा जवाब दूसरे अच्छे जवाब को हरा देता है। एक vote यह सीख सकता है। इस सब को लपेटने वाली तीन-चरणों की pipeline (demonstrations, comparisons, RL) InstructGPT page है; उसे दूसरे नंबर पर पढ़िए।
वह एक formula
objective(π) = Ey∼π[ r(x, y) ] − β · KL( π ‖ πref )
दो लाइनें क्योंकि दो models हैं। हर एक को pipeline stages की तरह पढ़िए:
| चरण | Shape | क्या करता है |
|---|---|---|
r(win) − r(lose) | scalar | Margin. Positive का मतलब model इंसान से सहमत है |
σ(·) | (0, 1) | Margin → जो vote सचमुच हुआ, उसे model जो probability देता है |
−log | scalar | एक bit पर cross-entropy. आश्वस्त और सही हो तो छोटा, आश्वस्त और ग़लत हो तो बहुत बड़ा |
Ey∼π[r] | scalar | Policy जो लिखती है उसका औसत reward-model score. Optimiser इसी पर चढ़ता है |
β · KL(π ‖ πref) | scalar ≥ 0 | Ey∼π[log π(y) − log πref(y)]: policy कितनी खिसकी, nats में। β = 0 यानी कोई पट्टा नहीं |
कितना नहीं, कौन-सा पूछिए
Script एक खिलौना दुनिया बनाती है: 4 prompts, हर एक के 8 candidate जवाब, और
एक जवाब यानी चार संख्याएँ (correct, length,
hedges, polite)। एक छिपा हुआ
true_reward बताता है कि लोग क्या चाहते हैं: सही होना ही
ज़्यादातर है, और एक लंबाई के बाद कोई पढ़ता नहीं। Reward model वह function
कभी नहीं देखता, और correct भी नहीं देखता: वह text पढ़ता है,
इसलिए उसे sounds मिलता है — correctness का उसका अपना अंदाज़ा,
चार में से एक बार ग़लत। Reference policy (supervised
model) छोटे-से-मँझोले जवाब लिखती है, जैसे उसके demonstrations लिखते थे।
Prompt 0:
ans correct sounds length hedges polite TRUE ref%
0 0 1 0.10 0.02 0.14 0.11 20.3%
1 1 1 0.23 0.44 0.36 2.71 20.3%
2 1 1 0.36 0.21 0.12 2.81 20.3%
3 1 1 0.49 0.06 0.65 3.56 20.3% ← सचमुच सबसे अच्छा जवाब
4 0 0 0.61 0.13 0.28 -0.04 12.2%
5 0 1 0.74 0.38 0.09 -1.62 4.3%
6 1 1 0.87 0.34 0.58 0.90 1.6%
7 1 1 1.00 0.04 0.93 0.67 0.6% ← शब्दों की दीवार. दुर्लभ
अब किसी इंसान से पूछिए: दो ऐसे जवाब चुनिए जो reference सचमुच लिखती, दोनों दिखाइए, और जीतने वाला दर्ज कीजिए। इंसान Bradley-Terry तरीक़े से noisy है, P(A wins) = σ(trueA − trueB), इसलिए क़रीबी मुक़ाबला सिक्के का उछाल है।
prompt A B true_A true_B P(A wins) label
1 5 0 2.13 -0.25 0.92 0 ← ग़लत. 8% मौक़ा था
2 1 2 -0.61 2.63 0.04 2
1 2 0 -0.09 -0.25 0.54 0
1 2 1 -0.09 0.38 0.38 1
0 2 0 2.81 0.11 0.94 2
1 1 3 0.38 3.18 0.06 3
Label एक bit है। कोई 7/10 नहीं, कोई rubric नहीं, और data में कहीं यह नहीं लिखा कि एक क्यों जीता। वह "क्यों" reward model को उसी से अंदाज़ा लगाना है जो वह देख सकता है।
Reward model, tester का ही एक model है
एक 4→16→1 network, उन्हीं 2000 bits पर train किया गया, सिर्फ़ −log
σ(r(win) − r(lose)) के सहारे। Loss 0.6805 से 0.4591 तक जाता है और
वहीं रुक जाता है, क्योंकि votes ख़ुद noisy हैं। असल जाँच सिर्फ़ यह है कि
उसका क्रम सच्चाई से मिलता है या नहीं, और मिलता है: सभी 32 जवाबों
पर rank correlation 0.877, 2000 clicks से। Scale उसका अपना है, क्योंकि
Bradley-Terry सिर्फ़ अंतर देखता है, इसलिए मैं वैसे ही normalise करता हूँ
जैसे Stiennon et al. करते हैं (reference जवाबों का औसत score 0; साथ ही
unit spread पर scale, ताकि β का मतलब हर model के लिए एक ही रहे)। सिर्फ़
क्रम की तुलना की जा सकती है।
पट्टे के साथ चढ़िए
Policy 8 जवाबों पर एक softmax है, हर prompt के लिए logits की एक पंक्ति, और
वह reference से ही शुरू होती है। हर step: जवाब sample कीजिए, हर
एक को reward model से score कीजिए, पट्टा घटाइए, logits को हल्का धकेलिए।
हर sample का reward ठीक InstructGPT वाला है, rm(answer) − β · (log
π(answer) − log π_ref(answer)): KL penalty reward के भीतर मोड़ दिया
गया। Papers PPO इस्तेमाल करते हैं। मैं REINFORCE इस्तेमाल करता हूँ, वही
objective सबसे सीधे estimator के साथ: logits के सापेक्ष log-softmax का
gradient onehot(answer) − probs है, इसलिए policy gradient एक
लाइन है। Optimiser Adam है, जैसे papers में। Optimiser को rm
दिखता है; true वह है जो आपको कभी नहीं दिखता।
step 0 rm 0.000 true 1.117 KL 0.000
step 50 rm 0.870 true 2.524 KL 0.682
step 100 rm 0.881 true 2.517 KL 0.688
step 150 rm 0.889 true 2.528 KL 0.701
step 200 rm 0.892 true 2.532 KL 0.706
step 250 rm 0.893 true 2.534 KL 0.707
step 300 rm 0.894 true 2.535 KL 0.709
same reward model, beta = 0: true reward peaked at step 100
(3.103), then ended at 2.704
Goodhart का नियम, एक table में
अब वह प्रयोग जो काश किसी ने मुझे दिखाया होता। Reward model को 2000 के बजाय 150 comparisons पर train कीजिए। जहाँ उसके पास data है वहाँ वह अब भी ठीक है (reference जो छोटे जवाब लिखती है उन पर rank correlation 0.802)। β = 0 रखिए। अब दोनों columns देखिए।
step 0 rm 0.000 true 1.117 KL 0.000
step 25 rm 0.722 true 2.181 KL 0.475
step 50 rm 1.062 true 2.656 KL 1.039
step 75 rm 1.296 true 2.870 KL 1.745 ← चोटी
step 100 rm 1.390 true 2.841 KL 2.064
step 125 rm 1.695 true 2.488 KL 2.543
step 150 rm 1.759 true 2.418 KL 2.749
step 175 rm 1.815 true 2.217 KL 2.919
step 200 rm 1.930 true 1.782 KL 3.640
step 225 rm 1.937 true 1.763 KL 3.700
step 250 rm 1.939 true 1.760 KL 3.716
step 275 rm 1.940 true 1.758 KL 3.725
step 300 rm 1.941 true 1.758 KL 3.730 ← rm अब भी चढ़ रहा है
Step 75 तक यह सुखद रास्ता लगता है। फिर policy को कुछ ऐसा मिल जाता है जो reward model को बहुत पसंद है और लोगों को बिल्कुल नहीं, और उसके बाद दोनों columns हमेशा के लिए अलग हो जाते हैं। चार में से दो prompts शब्दों की दीवार पर ख़त्म हुए। Prompt 2, हर जवाब पर पतले model की राय के साथ:
ans correct sounds length hedges polite TRUE ref% rm
0 0 0 0.10 0.01 0.35 0.34 20.3% -0.16
1 0 1 0.23 0.58 0.27 -0.61 20.3% -2.49
2 1 1 0.36 0.46 0.33 2.63 20.3% 0.69
3 1 1 0.49 0.04 0.54 3.49 20.3% 1.21 ← सच्चाई की पसंद
4 1 1 0.61 0.44 0.32 2.55 12.2% 0.84
5 1 0 0.74 0.00 0.62 2.48 4.3% -0.42
6 1 1 0.87 0.04 0.66 1.42 1.6% -1.98
7 1 0 1.00 0.01 0.93 0.72 0.6% 1.90 ← model की पसंद
जहाँ data है वहाँ reward model ठीक है। अपने data के बाहर वह अंदाज़ा लगा रहा है: reference ने शब्दों की दीवार लगभग कभी नहीं लिखी, इसलिए model ने 300 जवाबों में उसे दो बार देखा, और वहाँ उसका score वही है जो एक 16-unit network संयोग से extrapolate कर देता है। Policy सबसे ऊँचे score की तलाश में निकली, और सबसे ऊँचा score वहीं था जहाँ अंदाज़ा ऊपर गिरा। यही वह bug है जो आपको मिलेगा। "reward model ग़लत है" नहीं, जो हमेशा सच है, बल्कि "optimiser एक search है, ठीक उन्हीं जगहों की जहाँ वह ग़लत है"। बड़े पैमाने पर Gao et al. ने यही वक्र खींचा (arXiv 2210.10760): reward model score ऊपर, असली preference पहले ऊपर फिर नीचे, KL के फलन के रूप में।
पट्टे की लंबाई
वही पतला reward model, β के तीन मान, हर एक पर 300 steps।
beta final rm final true final KL
0.0 1.941 1.758 3.730
0.5 1.237 2.360 1.143
2.0 0.385 1.605 0.091
the reference itself: true 1.117 KL 0.000
β = 0, rm पर जीतता है और true पर हारता है।
β = 2 reference से मुश्किल से हटता है। बीच वाला ही असल बात है: reference
से बेहतर होइए, पर उस इलाक़े से बाहर जाए बिना जहाँ reward model train हुआ
था। β निकाला नहीं जाता, tune किया जाता है।
The code
NumPy for the mechanism: the world, the votes, the policy and its gradient. A 4→16→1 PyTorch model for the one part that needs backprop, fitting the reward model. CPU, about four seconds. No function over 33 lines. Below, the two functions that are the paper; the toy world, the votes, the reward-model fit and seven lines of Adam are in the script.
Download the full script — 444 lines including six runnable demos. Then:
python3 rlhf_from_scratch.py
def bradley_terry_loss(r_win, r_lose):
return -F.logsigmoid(r_win - r_lose).mean()
def train_policy(rm_table, R_true, ref_logp, beta, steps=300, lr=0.05,
batch=64, log_every=25, verbose=False, moving_reference=False,
plain_sgd=False, seed=0):
rng = np.random.default_rng(seed)
logits = ref_logp.copy() # start AT the reference
penalty_ref = ref_logp
state = [np.zeros_like(logits), np.zeros_like(logits)]
history = []
for step in range(0, steps + 1):
logp = log_softmax(logits)
probs = np.exp(logp)
if moving_reference:
penalty_ref = logp.copy() # the break: leash tied to self
if step % log_every == 0:
rm_now = (probs * rm_table).sum(-1).mean() # optimiser's view
true_now = (probs * R_true).sum(-1).mean() # what you never see
row = (step, rm_now, true_now, kl_to_reference(logp, ref_logp))
history.append(row)
if verbose:
print(" step {:4d} rm {:6.3f} true {:6.3f} KL {:6.3f}"
.format(*row))
grad = np.zeros_like(logits)
for p in range(N_PROMPTS):
a = rng.choice(N_RESP, size=batch, p=probs[p]) # sample answers
R = rm_table[p, a] - beta * (logp[p, a] - penalty_ref[p, a])
adv = R - R.mean() # baseline
grad[p] = (adv[:, None] * (np.eye(N_RESP)[a] - probs[p])).mean(0)
if plain_sgd:
logits += lr * grad # the break
else:
logits += adam_step(grad, state, lr, step + 1) # ascent
return np.exp(log_softmax(logits)), history
Now break it
This is the part that actually makes it stick. You debug for a living — reverse-engineering from broken behaviour is your native mode. Demo 6 runs most of these and prints them side by side.
| Break this | Watch for |
|---|---|
Set BETA = 0.0 | Demo 3's true reward peaks at 3.103 and ends at 2.704; demo 5's row reads true 1.758 at KL 3.730. rm never stops climbing. That's reward hacking. |
Change collect_comparisons(2000, …) in demo 2 to 150 | Rank correlation 0.877 → 0.580. The wall of text was shown 2 times instead of 23. The model now has a blind spot, and a policy with β = 0 will find it. |
Swap to r_lose - r_win in bradley_terry_loss | Rank correlation −0.893. The model learned the opposite ordering, confidently, and the policy will climb straight down. |
Pass moving_reference=True (the reference is the current policy every step) | The penalty reads exactly 0 forever. Final true 1.758 at KL 3.730, identical to β = 0. A leash tied to yourself is not a leash. |
Pass plain_sgd=True and 3000 steps | True reward 3.194 and no hack at all. Nothing is fixed: the reward model is exactly as wrong. A rare answer's probability grows as its own square under plain SGD, so the policy never explores far enough to ask. A real language model has a million ways to be longer, so it always asks. |
Train on the truth: train_policy(R_true, R_true, …) | True 3.187 of a possible 3.196 in 300 steps, no reward model needed. You will never be able to do this. R_true is the thing you were trying to build. |
On the maths anxiety
Honestly: a sigmoid, a log, and a weighted average. The log is there so a product of probabilities becomes a sum. The "expectation" in the objective is a weighted average over what the policy would say, and the KL is a weighted average of log-ratios. That is the whole formula sheet.
The one derivative worth knowing by heart: the gradient of log-softmax with
respect to the logits is onehot − probs. It is one line of
train_policy and it is the entire policy gradient. PPO's clipping
is engineering on top of it; loss.backward() handles the reward
model. What separates AI engineers here is not the maths. It is knowing that the
reward model is a model, that models are wrong off their data, and that an
optimiser is a search for exactly the places where it is wrong.
Where to go next
This page gave you one thing: the reward model as a model of the tester, and the leash as the only thing between you and Goodhart. These fill in what I skipped. The second paper, Stiennon et al., 2020, is the same recipe applied to a language model: the reward model becomes a language model with one output unit, the policy becomes GPT, and the KL penalty is written the way I wrote it. Its over-optimisation figure is my demo 4, measured with real people.
-
Deep Reinforcement Learning from Human Preferences
Read §2.2 with the script open: it is the code above, with an Atari agent where I have a softmax over eight answers. They also choose which pairs to ask about, a part I skipped entirely.
-
Illustrating Reinforcement Learning from Human Feedback
The three-stage pipeline as pictures, and PPO named properly. Where I said "the papers use PPO, this is REINFORCE", this shows what the swap involves. Best first read if diagrams land better than a printed table.
-
Reward Hacking in Reinforcement Learning
My demo 4 is one instance. This is the taxonomy: why it happens, what has been tried against it, and what it looks like in deployed language models. Go here when "it found the blind spot" stops being enough.