DeepSeek-R1: reasoning nobody taught it
Nobody wrote the reasoning examples. Nobody rewarded length. The working-out showed up anyway, and that is the whole story.
Download deepseek_r1_from_scratch.py — everything below runs from it. NumPy for the environment, plus a tiny CPU PyTorch policy for the GRPO training loop. About 20 seconds.
Blunt about the limits first, the same way the chain-of-thought page and the RAG page are about their stand-ins. There is no language model in my script. A four-input MLP cannot demonstrate emergent reasoning in a 671-billion-parameter model, and nothing it prints is evidence about what R1 does on a maths olympiad problem.
What it can show, honestly, is the mechanism the result stands on: when a correctness-only reward is paired with a task where more steps genuinely help, a policy gradient discovers longer solutions without anybody telling it to. That claim fits on a laptop. Every number below is from that analogy, and I say so each time.
I had this paper filed under "another RLHF variant" for months. It isn't. The interesting model in it is R1-Zero, and what makes it interesting is a deletion.
You paid for correct answers and got the working-out for free
You have shipped this function:
def solve(question):
return model(question) # one forward pass. no locals, no loop.
And you know the fix from chain-of-thought: give it somewhere to put intermediate values, because every token it emits becomes part of its own next input. Emitted tokens are serial compute. So you write exemplars that show the working, and the continuation shows its working too.
R1-Zero is what happens when you refuse to write those exemplars:
def solve(question):
scratch = []
while keep_going(scratch): # nobody wrote this loop
scratch.append(step(question, scratch))
return final(scratch)
reward = 1.0 if answer_is_right else 0.0 # this is all you paid for
Take a base model. Skip supervised fine-tuning entirely. Score its output with a rule — is the answer right, is it in the required tags — and run reinforcement learning. Long chains of reasoning appear on their own, including the model stopping to re-check its own working. The paper's framing is that the reasoning was incentivised, not taught.
It's the contractor you pay per correct answer with no time limit. They start showing their working, not because you asked for working, but because checking themselves is the cheapest route to the thing you're paying for. The optimiser found the same trade.
| Term | Means | Programmer's analogy |
|---|---|---|
| R1-Zero | RL straight onto the base model, no SFT at all | Shipping with only an integration test and no unit tests written by hand |
| Rule-based reward | A grader you can read: answer correct, tags present | assert, not a learned classifier |
| GRPO | Score a group of answers to the same prompt, compare them to each other | Ranking within a batch instead of keeping a second model to predict scores |
| Cold start | A small SFT pass before RL, added in R1 proper | A style guide applied before the optimiser is let loose |
| Distillation | Train small models on the big model's good outputs | Recording a working session and replaying it, instead of rediscovering it |
What it deleted from the InstructGPT pipeline
InstructGPT is three stages: supervised fine-tuning on human demonstrations, then a reward model trained on human comparisons, then PPO against that reward model. Two of those three stages need people, and the second one has the failure mode the RLHF page is entirely about — the optimiser hunting for the soft spots in a learned model of the tester.
R1-Zero deletes both.
| Stage | InstructGPT | R1-Zero |
|---|---|---|
| Demonstrations | Humans write ideal answers, SFT on them | Skipped |
| Reward | Reward model learned from human comparisons | A rule. Is the answer right, are the tags there |
| Optimisation | PPO with a KL penalty to the SFT model | GRPO with a KL penalty to the reference |
You can only delete them because maths and code answers are checkable. There is a right answer and a script can tell. That is the boundary of the method, and it is worth saying out loud in an interview: rule-based rewards work where a unit test works.
The one formula
Two sentences of GRPO, because the GRPO page derives it properly and I'm not re-deriving it here. Sample a group of G answers to the same prompt, score them all, and use each answer's score minus the group's mean, over the group's standard deviation, as its advantage — so the group is the baseline and there is no value network to train. Then it is the ordinary policy gradient, with a KL leash back to a frozen reference.
Read it as pipeline stages:
| Step | Shape | What it does |
|---|---|---|
o1..G | G answers, one prompt | The group. G=8 in my script. Sampled, so they differ |
ri | scalar | The rule. Correct answer + right tags. No learned model anywhere |
Âi | scalar | "Better or worse than my siblings?" A z-score inside the group |
log πθ(oi) | sum over steps | Make the above-average answers more likely, the below-average ones less |
β · KL | nats | The leash. Distance from the frozen reference policy |
Notice what the reward does not contain. There is no term for length, no term for "reasoning quality", no term for style. If length is going to appear, it has to appear because of the correctness term.
First, prove that thinking longer actually helps
If it doesn't, nothing else on this page means anything — so my toy task is built so that it genuinely does. Each problem is one true digit and six noisy readings of it, each reading correct 55% of the time. The policy chooses, step by step, whether to read once more or stop and answer with the majority of what it has read. There are only six real readings; step seven onwards re-reads one it already has, which is not new information and double-counts whatever noise it lands on.
Don't take my word for it. Here's the actual output:
k accuracy (hard, p=0.55) accuracy (control, p=1.00)
1 0.546 1.000
2 0.539 1.000
3 0.666 1.000
4 0.764 1.000
5 0.829 1.000
6 0.871 1.000
8 0.802 1.000
12 0.752 1.000
16 0.734 1.000
On the hard task, more steps buy real accuracy up to the sixth, then stop and start costing. On the control task one read is already perfect, so extra steps buy exactly nothing. Same code, one parameter apart. That pair is what makes the next section honest instead of a coincidence.
The centrepiece: length rises although nothing rewards length
Now train it. Reward is 0.2 × format + 1.0 × correct. GRPO,
β = 0.1, 150 iterations, eight prompts by a group of eight. Each row is 300
held-out problems so the curve is signal rather than one noisy batch.
HARD TASK (p=0.55, extra steps genuinely help)
iter mean length accuracy
0 0.47 0.137
15 8.52 0.717
30 8.12 0.767
45 5.52 0.777
60 6.11 0.800
75 3.76 0.800
90 4.46 0.783
105 6.60 0.833
120 4.89 0.833
135 3.41 0.780
150 6.15 0.860
Length goes from half a step to six, accuracy from 0.137 to 0.860, and the reward function never once mentioned length. Now the control — identical in every respect except that a single read is already correct:
CONTROL TASK (p=1.00, one read is already enough)
iter mean length accuracy
0 0.68 0.297
15 2.06 0.987
30 2.63 1.000
45 2.18 1.000
60 2.34 1.000
75 2.31 1.000
90 2.15 1.000
105 2.12 1.000
120 2.11 1.000
135 2.19 1.000
150 2.05 1.000
hard task length 0.47 -> 6.15 accuracy 0.137 -> 0.860
control task length 0.68 -> 2.05 accuracy 0.297 -> 1.000
Reward design, which is where you will actually go wrong
Three reward functions, everything else identical. This is the section I would put on a whiteboard if someone asked me what I learned from R1.
reward function accuracy mean length format
correct + format 0.850 5.12 1.00
format ONLY 0.360 1.12 1.00
correct + format + 0.05*length 0.743 16.00 1.00
Format only gets you perfectly structured output with the accuracy of a ten-sided coin. It never learned to look at the answer, because nothing ever paid it to. Every field validates. Nothing is true.
Length in the reward gets you padding. It runs to the context window and accuracy falls — 0.850 down to 0.743 — because the extra steps are re-reads, and re-reads amplify noise. You bought tokens and called it thought. This is why R1's reward has no length term: length is a symptom you want, not a target you set.
The tags, the parser, and the bug you will ship
R1's template is thinking between <think> tags and the
final answer between <answer> tags, so a rule can find the
answer without guessing. Mine copies that:
<think>
step 1: read -> 9
step 2: read -> 8
step 3: read -> 9
step 4: read -> 5
step 5: read -> 9
step 6: read -> 9
</think>
<answer>9</answer>
truth : 9
parse_answer (tag-anchored) : 9
parse_answer_broken (first : 1 <- that's the step index
number anywhere)
Now break the parser the way everyone breaks it once — match the first number anywhere in the output instead of the one inside the answer tags. Same model, same rollouts, two graders:
scored with the correct parser : 0.850
scored with the broken parser : 0.090
trained AGAINST the broken parser -> real accuracy 0.330, length 1.17
The second number is the parser's opinion of the working-out, not the answer. Grade with it and you have not measured a worse model, you have measured a different string. Train against it and the reward signal is noise, so the policy learns to stop early and stops learning anything at all. A regex is a component of your training objective. Treat it like one.
The leash, swept
Same reward, same task, four values of β. KL is per trajectory, in nats, measured against the frozen reference the run started from.
beta accuracy mean length KL from reference (nats/traj)
0.00 0.743 16.00 16.498
0.02 0.745 15.96 16.495
0.10 0.850 5.12 5.113
0.50 0.588 2.81 1.659
With no leash it runs to the context window and stays there. It still scores well — the answer is in the right tags and is often right — and the output is this:
<think>
step 1: read -> 8
step 2: read -> 6
step 3: read -> 0
step 4: read -> 0
step 5: read -> 8
step 6: read -> 4
step 7: read -> 8
step 8: read -> 6
step 9: read -> 6
step 10: read -> 6
step 11: read -> 6
step 12: read -> 8
step 13: read -> 4
step 14: read -> 6
step 15: read -> 8
step 16: read -> 8
</think>
<answer>6</answer>
Sixteen steps of which six carry information, and a wrong answer at the end of them. β = 0.5 is the opposite failure: it never leaves the reference and never learns to think at all. β is tuned, not derived, and the number you put on a dashboard is the KL, not β — see the RLHF page, which is entirely about what an optimiser does when you take the leash off.
Distillation, and why R1 needed a cold start
The paper's other finding is that distilling R1's outputs into smaller dense models worked better than running large-scale RL on those small models directly. My version of that, matched on rollouts of the environment rather than on gradient steps, because rollouts are the expensive part:
teacher (hidden=16, 150 GRPO iters) acc 0.850 length 5.12
kept 1051/1200 teacher rollouts -> 6446 labelled decisions
small network, hidden=4:
distilled from teacher, 0 new rollouts acc 0.850 length 5.02
GRPO directly, 18 iters (1152 rollouts) acc 0.485 length 2.02
GRPO directly, 150 iters (9600 rollouts) acc 0.823 length 5.47
Every successful teacher trajectory hands the student a label for every decision inside it. A reward hands it one scalar for the whole trajectory. That is the entire difference. Give the RL student eight times the rollouts and it catches up — which is exactly the trade the paper is pointing at. Matched on gradient steps instead, my two students are within noise of each other, so treat this as an analogy for their result rather than evidence of it.
முதலில் வரம்புகளைத் தெளிவாகச் சொல்லிவிடுகிறேன் — chain-of-thought page-ம் RAG page-ம் தங்கள் stand-in-களைப் பற்றி சொல்வது போலவே. என் script-ல் எந்த language model-ம் இல்லை. நான்கு input கொண்ட ஒரு சிறு MLP-ஆல், 671 பில்லியன் parameter உள்ள ஒரு model-ல் reasoning தானாக வெளிப்படுவதை நிரூபிக்க முடியாது. அது அச்சிடும் எதுவும், ஒரு maths olympiad கேள்வியில் R1 என்ன செய்கிறது என்பதற்கு ஆதாரம் அல்ல.
ஆனால் அது நேர்மையாகக் காட்டக்கூடியது ஒன்று உண்டு — இந்த முடிவு எந்த இயந்திரத்தின் மேல் நிற்கிறதோ அது: correctness மட்டுமே பார்க்கும் ஒரு reward-ஐ, அதிக steps உண்மையிலேயே உதவும் ஒரு task-உடன் இணைத்தால், policy gradient தானாகவே நீளமான தீர்வுகளைக் கண்டுபிடிக்கிறது — யாரும் அதைச் சொல்லாமலேயே. அந்தக் கூற்று ஒரு laptop-ல் ஓடும். கீழே வரும் ஒவ்வொரு எண்ணும் அந்த ஒப்புமையிலிருந்து வருவது; ஒவ்வொரு முறையும் அதை நான் சொல்லிவிடுகிறேன்.
இந்த paper-ஐ "இன்னொரு RLHF variant" என்று பல மாதங்கள் ஒதுக்கி வைத்திருந்தேன். அப்படி இல்லை. இதில் சுவாரஸ்யமான model R1-Zero. அதை சுவாரஸ்யமாக்குவது ஒரு நீக்கம்.
சரியான விடைக்குப் பணம் கொடுத்தீர்கள்; வேலைக் காட்டல் இலவசமாக வந்தது
இந்த function-ஐ நீங்கள் ship செய்திருக்கிறீர்கள்:
def solve(question):
return model(question) # ஒரே forward pass. locals இல்லை, loop இல்லை.
chain-of-thought-லிருந்து இதற்கான திருத்தம் உங்களுக்குத் தெரியும்: இடைநிலை மதிப்புகளை வைக்க ஓர் இடம் கொடுங்கள். ஏனெனில் அது வெளியிடும் ஒவ்வொரு token-ம் அதன் அடுத்த input-ன் பகுதியாகிறது. வெளியிடப்பட்ட tokens தான் serial compute. எனவே வேலையைக் காட்டும் exemplars எழுதுகிறீர்கள்; பின் அதன் தொடர்ச்சியும் தன் வேலையைக் காட்டுகிறது.
அந்த exemplars எழுத மறுத்தால் என்ன ஆகும் — அதுதான் R1-Zero:
def solve(question):
scratch = []
while keep_going(scratch): # இந்த loop-ஐ யாரும் எழுதவில்லை
scratch.append(step(question, scratch))
return final(scratch)
reward = 1.0 if answer_is_right else 0.0 # நீங்கள் பணம் கொடுத்தது இதற்கே
ஒரு base model எடுங்கள். supervised fine-tuning-ஐ முற்றிலும் தவிர்த்து விடுங்கள். ஒரு விதியால் அதன் output-ஐ score செய்யுங்கள் — விடை சரியா, கேட்ட tags-ல் இருக்கிறதா — பிறகு reinforcement learning ஓட்டுங்கள். நீளமான reasoning சங்கிலிகள் தானாகவே தோன்றுகின்றன; தன் சொந்த வேலையை நிறுத்தி மறுபரிசோதனை செய்வதும் உட்பட. Paper-ன் வாதம்: reasoning ஊக்குவிக்கப்பட்டது, கற்பிக்கப்படவில்லை.
சரியான விடைக்கு மட்டும் பணம், நேர வரம்பே இல்லை — அப்படி ஒரு ஒப்பந்தக்காரர். அவர் தன் வேலையைக் காட்டத் தொடங்குகிறார்; நீங்கள் கேட்டதால் அல்ல, தன்னைத்தானே சரிபார்ப்பதுதான் நீங்கள் பணம் கொடுக்கும் விஷயத்தை அடைய மலிவான வழி என்பதால். Optimiser-ம் அதே பேரத்தைக் கண்டுபிடித்தது.
Reward-la "sari-yaana answer" mattum dhaan iruku; length-kku oru point-um illa. Aana thanna check panradhu dhaan sari-ya irukka cheap-aana vazhi — adhanaala neelam thaana valarudhu.
| சொல் | பொருள் | Programmer-க்கான ஒப்புமை |
|---|---|---|
| R1-Zero | SFT-யே இல்லாமல், base model மேல் நேரடி RL | கையால் எழுதிய unit test இல்லாமல், integration test மட்டும் வைத்து ship செய்வது |
| Rule-based reward | நீங்களே படிக்கக்கூடிய grader: விடை சரி, tags இருக்கு | கற்றுக்கொண்ட classifier அல்ல — assert |
| GRPO | ஒரே prompt-க்கு வரும் ஒரு குழு விடைகளை score செய்து, அவற்றை ஒன்றோடொன்று ஒப்பிடுவது | score கணிக்க இரண்டாவது model வைப்பதற்குப் பதிலாக, batch-க்குள்ளேயே ranking |
| Cold start | RL-க்கு முன் ஒரு சிறிய SFT pass; R1-ல் சேர்க்கப்பட்டது | optimiser-ஐ அவிழ்த்து விடுவதற்கு முன் ஒரு style guide |
| Distillation | பெரிய model-ன் நல்ல outputs மேல் சிறிய models-ஐ train செய்வது | ஒரு வேலை session-ஐ record செய்து மறுபடி ஓட்டுவது; மீண்டும் கண்டுபிடிப்பதற்குப் பதிலாக |
InstructGPT pipeline-லிருந்து அது நீக்கியது என்ன
InstructGPT மூன்று நிலைகள்: மனிதர்களின் demonstrations மேல் supervised fine-tuning, பிறகு மனித ஒப்பீடுகளிலிருந்து கற்ற ஒரு reward model, பிறகு அந்த reward model-க்கு எதிராக PPO. அந்த மூன்றில் இரண்டுக்கு மனிதர்கள் தேவை; இரண்டாவதற்கு RLHF page முழுவதுமாகப் பேசும் அந்த failure mode இருக்கிறது — தேர்வாளரின் கற்ற model-ல் உள்ள பலவீனமான இடங்களை optimiser தேடிப் பிடிப்பது.
R1-Zero இரண்டையும் நீக்குகிறது.
| நிலை | InstructGPT | R1-Zero |
|---|---|---|
| Demonstrations | மனிதர்கள் சிறந்த விடைகளை எழுத, அவற்றின் மேல் SFT | தவிர்க்கப்பட்டது |
| Reward | மனித ஒப்பீடுகளிலிருந்து கற்ற reward model | ஒரு விதி. விடை சரியா, tags இருக்கா |
| Optimisation | SFT model-க்கு KL penalty வைத்த PPO | reference-க்கு KL penalty வைத்த GRPO |
இவற்றை நீக்க முடிவதற்குக் காரணம், maths மற்றும் code விடைகளை சரிபார்க்க முடியும் என்பதுதான். ஒரு சரியான விடை உண்டு, ஒரு script அதைச் சொல்ல முடியும். அதுதான் இந்த முறையின் எல்லை; ஒரு interview-ல் இதை வெளிப்படையாகச் சொல்வது மதிப்புள்ளது: unit test வேலை செய்யும் இடத்தில் rule-based rewards வேலை செய்யும்.
அந்த ஒரே சூத்திரம்
GRPO-வை இரண்டே வாக்கியங்களில் சொல்கிறேன்; ஏனெனில் GRPO page அதை முறையாக வருவிக்கிறது, நான் இங்கே மீண்டும் வருவிக்கப் போவதில்லை. ஒரே prompt-க்கு G விடைகள் கொண்ட ஒரு குழுவை sample செய்யுங்கள், எல்லாவற்றையும் score செய்யுங்கள், பிறகு ஒவ்வொரு விடையின் score-ல் இருந்து குழுவின் mean-ஐக் கழித்து, குழுவின் standard deviation-ஆல் வகுத்து, அதை advantage-ஆகப் பயன்படுத்துங்கள் — அப்போது குழுவே baseline; train செய்ய value network தேவையில்லை. மீதி வழக்கமான policy gradient, ஒரு உறைந்த reference-க்குத் திரும்பும் KL சங்கிலியுடன்.
இதை pipeline stages ஆகப் படியுங்கள்:
| Step | Shape | என்ன செய்கிறது |
|---|---|---|
o1..G | G விடைகள், ஒரு prompt | அந்தக் குழு. என் script-ல் G=8. Sample செய்யப்படுவதால் அவை வேறுபடும் |
ri | scalar | அந்த விதி. சரியான விடை + சரியான tags. எங்கும் கற்ற model இல்லை |
Âi | scalar | "என் உடன்பிறப்புகளை விட நல்லதா, கெட்டதா?" குழுவுக்குள் ஒரு z-score |
log πθ(oi) | steps மேல் கூட்டல் | சராசரிக்கு மேல் உள்ள விடைகளை அதிக வாய்ப்புள்ளதாகவும், கீழ் உள்ளவற்றைக் குறைவாகவும் ஆக்குகிறது |
β · KL | nats | அந்தச் சங்கிலி. உறைந்த reference policy-லிருந்து உள்ள தூரம் |
Reward-ல் இல்லாதவற்றைக் கவனியுங்கள். length-க்கு எந்தச் சொல்லும் இல்லை, "reasoning quality"-க்கு இல்லை, style-க்கு இல்லை. நீளம் தோன்றப்போகிறது என்றால், அது correctness சொல்லின் காரணமாகவே தோன்ற வேண்டும்.
முதலில், நீண்ட நேரம் யோசிப்பது உண்மையிலேயே உதவுகிறது என நிரூபியுங்கள்
அது உதவவில்லை என்றால், இந்தப் பக்கத்தில் வேறெதற்கும் அர்த்தமில்லை — எனவே என் toy task அது உண்மையாகவே உதவும்படி கட்டப்பட்டிருக்கிறது. ஒவ்வொரு problem-ம் ஓர் உண்மையான இலக்கம், அதன் ஆறு noisy readings; ஒவ்வொரு reading-ம் 55% நேரம் சரியாக இருக்கும். Policy ஒவ்வொரு step-ஆக முடிவெடுக்கிறது — இன்னொரு முறை படிக்கவா, அல்லது நிறுத்தி, இதுவரை படித்ததின் பெரும்பான்மை விடையைச் சொல்லவா. உண்மையான readings ஆறு மட்டுமே; ஏழாவது step-லிருந்து ஏற்கனவே உள்ள ஒன்றையே மறுபடி படிக்கிறது — அது புதிய தகவல் அல்ல, அது சிக்கிக்கொள்ளும் noise-ஐ இரட்டிப்பாக எண்ணுகிறது.
என்னை நம்ப வேண்டாம். இதோ உண்மையான output:
k accuracy (hard, p=0.55) accuracy (control, p=1.00)
1 0.546 1.000
2 0.539 1.000
3 0.666 1.000
4 0.764 1.000
5 0.829 1.000
6 0.871 1.000
8 0.802 1.000
12 0.752 1.000
16 0.734 1.000
Hard task-ல், ஆறாவது வரை அதிக steps உண்மையான accuracy-ஐ வாங்கித் தருகின்றன; பிறகு நின்று, செலவாகத் தொடங்குகின்றன. Control task-ல் ஒரே read-ஏ ஏற்கனவே சரியானது, எனவே கூடுதல் steps சரியாக ஒன்றுமே வாங்குவதில்லை. ஒரே code, ஒரே parameter வித்தியாசம். அந்த ஜோடிதான் அடுத்த பகுதியை தற்செயல் அல்ல, நேர்மையான ஒன்றாக ஆக்குகிறது.
மையப் பகுதி: length-க்கு எந்த reward-ம் இல்லாமலேயே length உயர்கிறது
இப்போது train செய்யுங்கள். Reward என்பது
0.2 × format + 1.0 × correct. GRPO, β = 0.1, 150 iterations,
எட்டு prompts × எட்டு பேர் கொண்ட ஒரு குழு. ஒவ்வொரு வரியும் 300 held-out
problems — எனவே இந்த curve ஒரு noisy batch அல்ல, signal.
HARD TASK (p=0.55, extra steps genuinely help)
iter mean length accuracy
0 0.47 0.137
15 8.52 0.717
30 8.12 0.767
45 5.52 0.777
60 6.11 0.800
75 3.76 0.800
90 4.46 0.783
105 6.60 0.833
120 4.89 0.833
135 3.41 0.780
150 6.15 0.860
Length அரை step-லிருந்து ஆறாக, accuracy 0.137-லிருந்து 0.860-ஆக ஏறுகிறது; reward function ஒரு முறை கூட length-ஐப் பற்றிப் பேசவில்லை. இப்போது control — ஒரே ஒரு read ஏற்கனவே சரியானது என்பதைத் தவிர எல்லா வகையிலும் ஒரே மாதிரி:
CONTROL TASK (p=1.00, one read is already enough)
iter mean length accuracy
0 0.68 0.297
15 2.06 0.987
30 2.63 1.000
45 2.18 1.000
60 2.34 1.000
75 2.31 1.000
90 2.15 1.000
105 2.12 1.000
120 2.11 1.000
135 2.19 1.000
150 2.05 1.000
hard task length 0.47 -> 6.15 accuracy 0.137 -> 0.860
control task length 0.68 -> 2.05 accuracy 0.297 -> 1.000
Control task-la one read podhum, adhanaala length 2-la nikkardhu. Hard task-la mattum dhaan neelam valarudhu. Ondre reward, ondre code — task dhaan vithyasam.
Reward design — நீங்கள் உண்மையில் தவறு செய்யப் போகும் இடம் இதுதான்
மூன்று reward functions, மற்ற எல்லாம் ஒரே மாதிரி. R1-லிருந்து நான் என்ன கற்றேன் என்று யாராவது கேட்டால், whiteboard-ல் நான் எழுதப்போகும் பகுதி இது.
reward function accuracy mean length format
correct + format 0.850 5.12 1.00
format ONLY 0.360 1.12 1.00
correct + format + 0.05*length 0.743 16.00 1.00
Format மட்டும் வைத்தால், கட்டமைப்பில் குற்றமற்ற output கிடைக்கும் — accuracy-யோ பத்து பக்க நாணயத்தினுடையது. அது விடையைப் பார்க்கவே கற்றுக்கொள்ளவில்லை, ஏனெனில் அதற்காக யாரும் பணம் கொடுக்கவில்லை. ஒவ்வொரு field-ம் validate ஆகிறது. எதுவும் உண்மையல்ல.
Reward-ல் length சேர்த்தால் padding கிடைக்கும். அது context window வரை ஓடுகிறது, accuracy விழுகிறது — 0.850-லிருந்து 0.743-க்கு — ஏனெனில் கூடுதல் steps என்பவை மறுவாசிப்புகள், மறுவாசிப்புகள் noise-ஐப் பெருக்குகின்றன. நீங்கள் tokens-ஐ வாங்கிவிட்டு அதை சிந்தனை என்று அழைத்தீர்கள். R1-ன் reward-ல் length term இல்லாததற்குக் காரணம் இதுதான்: length என்பது நீங்கள் விரும்பும் ஓர் அறிகுறி, நீங்கள் வைக்கும் இலக்கு அல்ல.
Tags, parser, மற்றும் நீங்கள் ship செய்யப்போகும் அந்த bug
R1-ன் template: சிந்தனை <think> tags-க்கு இடையே,
இறுதி விடை <answer> tags-க்கு இடையே — அப்போது ஒரு விதி
ஊகிக்காமலேயே விடையைக் கண்டுபிடிக்க முடியும். என்னுடையது அதையே நகலெடுக்கிறது:
<think>
step 1: read -> 9
step 2: read -> 8
step 3: read -> 9
step 4: read -> 5
step 5: read -> 9
step 6: read -> 9
</think>
<answer>9</answer>
truth : 9
parse_answer (tag-anchored) : 9
parse_answer_broken (first : 1 <- அது step index
number anywhere)
இப்போது எல்லாரும் ஒரு முறை உடைப்பது போல parser-ஐ உடையுங்கள் — answer tags-க்கு உள்ளே உள்ள எண்ணுக்குப் பதிலாக, output-ல் எங்கு வேண்டுமானாலும் வரும் முதல் எண்ணை எடுத்துக்கொள்ளுங்கள். ஒரே model, ஒரே rollouts, இரண்டு graders:
scored with the correct parser : 0.850
scored with the broken parser : 0.090
trained AGAINST the broken parser -> real accuracy 0.330, length 1.17
இரண்டாவது எண், விடையைப் பற்றிய parser-ன் கருத்து அல்ல — வேலைக் காட்டலைப் பற்றியது. அதை வைத்து grade செய்தால், நீங்கள் மோசமான ஒரு model-ஐ அளக்கவில்லை, வேறொரு string-ஐ அளந்திருக்கிறீர்கள். அதற்கு எதிராக train செய்தால் reward signal வெறும் noise; எனவே policy சீக்கிரமே நிறுத்தக் கற்றுக்கொண்டு, பிறகு எதையுமே கற்பதில்லை. ஒரு regex என்பது உங்கள் training objective-ன் ஒரு component. அப்படியே நடத்துங்கள்.
Parser thappaana, reward-um thappu — model illa, string dhaan maarudhu. Regex-um training objective-oda oru pagudhi; adha oru component maadhiri test pannunga.
அந்தச் சங்கிலி, sweep செய்யப்பட்டது
ஒரே reward, ஒரே task, β-வின் நான்கு மதிப்புகள். KL என்பது ஒரு trajectory-க்கு, nats-ல், ஓட்டம் தொடங்கிய உறைந்த reference-க்கு எதிராக அளக்கப்பட்டது.
beta accuracy mean length KL from reference (nats/traj)
0.00 0.743 16.00 16.498
0.02 0.745 15.96 16.495
0.10 0.850 5.12 5.113
0.50 0.588 2.81 1.659
சங்கிலியே இல்லாமல் அது context window வரை ஓடி அங்கேயே தங்கிவிடுகிறது. அப்போதும் நல்ல score வாங்குகிறது — விடை சரியான tags-ல் இருக்கிறது, பல நேரம் சரியாகவும் இருக்கிறது — output இப்படி இருக்கிறது:
<think>
step 1: read -> 8
step 2: read -> 6
step 3: read -> 0
step 4: read -> 0
step 5: read -> 8
step 6: read -> 4
step 7: read -> 8
step 8: read -> 6
step 9: read -> 6
step 10: read -> 6
step 11: read -> 6
step 12: read -> 8
step 13: read -> 4
step 14: read -> 6
step 15: read -> 8
step 16: read -> 8
</think>
<answer>6</answer>
பதினாறு steps, அவற்றில் ஆறு மட்டுமே தகவல் சுமக்கின்றன; இறுதியில் தவறான விடை. β = 0.5 என்பது எதிர் திசை தோல்வி: அது reference-ஐ விட்டு வெளியேறவே இல்லை, யோசிக்கவும் கற்றுக்கொள்வதில்லை. β என்பது tune செய்யப்படுவது, வருவிக்கப்படுவது அல்ல; dashboard-ல் நீங்கள் வைக்க வேண்டிய எண் KL, β அல்ல — சங்கிலியை அவிழ்த்து விட்டால் optimiser என்ன செய்யும் என்பதைப் பற்றியே முழுவதுமாகப் பேசும் RLHF page-ஐப் பாருங்கள்.
Distillation, மற்றும் R1-க்கு ஏன் cold start தேவைப்பட்டது
Paper-ன் மற்றொரு கண்டுபிடிப்பு: R1-ன் outputs-ஐ சிறிய dense models-க்குள் distil செய்வது, அந்தச் சிறிய models மேல் நேரடியாக பெரிய அளவில் RL ஓட்டுவதை விட நன்றாக வேலை செய்தது. அதன் என் பதிப்பு — gradient steps அடிப்படையில் அல்ல, environment-ன் rollouts அடிப்படையில் ஒப்பிட்டது, ஏனெனில் விலை உயர்ந்த பகுதி rollouts தான்:
teacher (hidden=16, 150 GRPO iters) acc 0.850 length 5.12
kept 1051/1200 teacher rollouts -> 6446 labelled decisions
small network, hidden=4:
distilled from teacher, 0 new rollouts acc 0.850 length 5.02
GRPO directly, 18 iters (1152 rollouts) acc 0.485 length 2.02
GRPO directly, 150 iters (9600 rollouts) acc 0.823 length 5.47
வெற்றி பெற்ற ஒவ்வொரு teacher trajectory-ம், அதற்குள் உள்ள ஒவ்வொரு முடிவுக்கும் ஒரு label-ஐ student-க்குக் கொடுக்கிறது. ஒரு reward, முழு trajectory-க்கும் சேர்த்து ஒரே ஒரு scalar-ஐத்தான் கொடுக்கிறது. முழு வித்தியாசமும் அதுதான். RL student-க்கு எட்டு மடங்கு rollouts கொடுத்தால் அது பிடித்துவிடுகிறது — paper சுட்டிக்காட்டும் பேரம் சரியாக அதுதான். Gradient steps அடிப்படையில் ஒப்பிட்டால், என் இரண்டு students-ம் noise-க்குள் ஒரே மாதிரி; எனவே இதை அவர்களுடைய முடிவுக்கான ஆதாரமாக அல்ல, ஒரு ஒப்புமையாக மட்டுமே எடுத்துக்கொள்ளுங்கள்.
सीमाएँ पहले, बिना लाग-लपेट के — ठीक वैसे ही जैसे chain-of-thought page और RAG page अपने stand-in के बारे में कहते हैं। मेरी script में कोई language model है ही नहीं। चार input वाला एक छोटा MLP यह नहीं दिखा सकता कि 671 अरब parameter वाले model में reasoning अपने आप उभरती है, और वह जो कुछ भी print करता है वह इस बात का सबूत नहीं है कि R1 किसी maths olympiad सवाल पर क्या करता है।
लेकिन ईमानदारी से वह एक चीज़ दिखा सकती है — वह तंत्र जिस पर यह नतीजा टिका है: जब सिर्फ़ correctness देखने वाले reward को ऐसे task के साथ जोड़ा जाए जहाँ ज़्यादा steps सचमुच मदद करते हैं, तो policy gradient बिना किसी के कहे लंबे हल खोज निकालता है। यह दावा एक laptop पर चल जाता है। नीचे का हर आँकड़ा उसी analogy से आया है, और हर बार मैं यह कह भी देता हूँ।
महीनों तक यह paper मेरे लिए "एक और RLHF variant" था। है नहीं। इसमें दिलचस्प model R1-Zero है, और उसे दिलचस्प बनाती है एक कटौती।
आपने सही जवाब का पैसा दिया और काम दिखाना मुफ़्त में मिल गया
यह function आप ship कर चुके हैं:
def solve(question):
return model(question) # एक forward pass. न locals, न loop.
और इसका इलाज आपको chain-of-thought से पता है: उसे बीच के मान रखने के लिए जगह दीजिए, क्योंकि वह जो भी token निकालता है वह उसके अगले input का हिस्सा बन जाता है। निकाले गए tokens ही serial compute हैं। तो आप ऐसे exemplars लिखते हैं जो काम करके दिखाते हैं, और फिर उसका continuation भी अपना काम दिखाने लगता है।
R1-Zero वही है जो तब होता है जब आप वे exemplars लिखने से मना कर दें:
def solve(question):
scratch = []
while keep_going(scratch): # यह loop किसी ने नहीं लिखा
scratch.append(step(question, scratch))
return final(scratch)
reward = 1.0 if answer_is_right else 0.0 # पैसा सिर्फ़ इसी का दिया
एक base model लीजिए। supervised fine-tuning पूरी तरह छोड़ दीजिए। उसके output को एक नियम से score कीजिए — जवाब सही है क्या, ज़रूरी tags में है क्या — और reinforcement learning चलाइए। लंबी reasoning शृंखलाएँ अपने आप आ जाती हैं, यहाँ तक कि model रुककर अपना ही काम दोबारा जाँचने लगता है। Paper का कहना है कि reasoning सिखाई नहीं गई, उसे प्रोत्साहित किया गया।
वह ठेकेदार सोचिए जिसे आप हर सही जवाब का पैसा देते हैं और समय की कोई सीमा नहीं रखते। वह अपना काम दिखाने लगता है — इसलिए नहीं कि आपने काम दिखाने को कहा, बल्कि इसलिए कि ख़ुद को जाँचना उस चीज़ तक पहुँचने का सबसे सस्ता रास्ता है जिसका आप पैसा दे रहे हैं। Optimiser ने भी वही सौदा ढूँढ़ लिया।
| शब्द | मतलब | Programmer वाली analogy |
|---|---|---|
| R1-Zero | बिना किसी SFT के, सीधे base model पर RL | हाथ से लिखे unit tests के बिना, सिर्फ़ एक integration test के भरोसे ship करना |
| Rule-based reward | ऐसा grader जिसे आप पढ़ सकें: जवाब सही, tags मौजूद | सीखा हुआ classifier नहीं — assert |
| GRPO | एक ही prompt के जवाबों के समूह को score करके उन्हें आपस में तुलना करना | score का अनुमान लगाने के लिए दूसरा model रखने की जगह, batch के भीतर ही ranking |
| Cold start | RL से पहले एक छोटा SFT pass, जो असली R1 में जोड़ा गया | optimiser को खुला छोड़ने से पहले लागू किया गया style guide |
| Distillation | बड़े model के अच्छे outputs पर छोटे models को train करना | काम की एक session record करके दोबारा चलाना, बजाय उसे फिर से खोजने के |
उसने InstructGPT pipeline से क्या हटाया
InstructGPT तीन चरण हैं: इंसानी demonstrations पर supervised fine-tuning, फिर इंसानी तुलनाओं से सीखा गया एक reward model, फिर उस reward model के ख़िलाफ़ PPO। इन तीन में से दो में लोग चाहिए, और दूसरे चरण में वही failure mode है जिस पर RLHF page पूरी तरह टिका है — optimiser का जाकर परखने वाले के सीखे हुए model की कमज़ोर जगहें ढूँढ़ना।
R1-Zero दोनों हटा देता है।
| चरण | InstructGPT | R1-Zero |
|---|---|---|
| Demonstrations | इंसान आदर्श जवाब लिखते हैं, उन पर SFT | छोड़ दिया गया |
| Reward | इंसानी तुलनाओं से सीखा गया reward model | एक नियम। जवाब सही है क्या, tags हैं क्या |
| Optimisation | SFT model पर KL penalty के साथ PPO | reference पर KL penalty के साथ GRPO |
इन्हें हटा पाना सिर्फ़ इसलिए मुमकिन है कि maths और code के जवाब जाँचे जा सकते हैं। एक सही जवाब होता है और एक script बता सकती है। यही इस तरीक़े की सीमा है, और interview में इसे खुलकर कहना अच्छा रहता है: rule-based rewards वहीं काम करते हैं जहाँ एक unit test काम करता है।
वह इकलौता सूत्र
GRPO दो वाक्यों में, क्योंकि GRPO page उसे ठीक से व्युत्पन्न करता है और मैं यहाँ दोबारा नहीं कर रहा। एक ही prompt के लिए G जवाबों का एक समूह sample कीजिए, सबको score कीजिए, और हर जवाब के score में से समूह का mean घटाकर, समूह के standard deviation से भाग देकर उसे उसका advantage मान लीजिए — तब समूह ही baseline है और train करने के लिए कोई value network नहीं चाहिए। इसके बाद यह साधारण policy gradient है, एक जमे हुए reference तक वापस बाँधने वाली KL डोरी के साथ।
इसे pipeline stages की तरह पढ़िए:
| Step | Shape | यह क्या करता है |
|---|---|---|
o1..G | G जवाब, एक prompt | वह समूह। मेरी script में G=8. Sample किए गए हैं, इसलिए अलग-अलग हैं |
ri | scalar | वह नियम। सही जवाब + सही tags. कहीं कोई सीखा हुआ model नहीं |
Âi | scalar | "अपने भाई-बहनों से बेहतर या बदतर?" समूह के भीतर एक z-score |
log πθ(oi) | steps पर योग | औसत से अच्छे जवाबों की संभावना बढ़ाओ, औसत से ख़राब की घटाओ |
β · KL | nats | वह डोरी। जमी हुई reference policy से दूरी |
ध्यान दीजिए कि reward में क्या नहीं है। length के लिए कोई term नहीं, "reasoning quality" के लिए नहीं, style के लिए नहीं। अगर लंबाई आनी है, तो उसे correctness वाले term की वजह से ही आना होगा।
पहले यह साबित कीजिए कि ज़्यादा देर सोचना सचमुच मदद करता है
अगर नहीं करता, तो इस पेज की बाक़ी हर बात बेमानी है — इसलिए मेरा toy task इस तरह बनाया गया है कि वहाँ यह सचमुच मदद करे। हर problem में एक सही अंक है और उसके छह noisy readings, हर reading 55% बार सही। Policy हर step पर तय करती है कि एक बार और पढ़ना है या रुककर अब तक पढ़े गए में से बहुमत वाला जवाब दे देना है। असली readings सिर्फ़ छह हैं; सातवें step से आगे वह पहले से मौजूद किसी reading को दोबारा पढ़ता है, जो नई जानकारी नहीं है और जिस भी noise पर पड़े उसे दोगुना गिन लेता है।
मेरी बात मत मानिए। यह रहा असली output:
k accuracy (hard, p=0.55) accuracy (control, p=1.00)
1 0.546 1.000
2 0.539 1.000
3 0.666 1.000
4 0.764 1.000
5 0.829 1.000
6 0.871 1.000
8 0.802 1.000
12 0.752 1.000
16 0.734 1.000
Hard task पर छठे step तक ज़्यादा steps सचमुच accuracy ख़रीदते हैं, फिर रुक जाते हैं और नुक़सान करने लगते हैं। Control task पर एक read ही पहले से सही है, इसलिए अतिरिक्त steps बिल्कुल कुछ नहीं ख़रीदते। वही code, बस एक parameter का फ़र्क़। यही जोड़ी अगले हिस्से को संयोग नहीं, ईमानदार बनाती है।
मुख्य बात: length को कोई reward नहीं, फिर भी length बढ़ती है
अब इसे train कीजिए। Reward है 0.2 × format + 1.0 × correct.
GRPO, β = 0.1, 150 iterations, आठ prompts × आठ का समूह। हर पंक्ति 300
held-out problems पर है, ताकि यह curve एक शोर भरे batch की जगह signal हो।
HARD TASK (p=0.55, extra steps genuinely help)
iter mean length accuracy
0 0.47 0.137
15 8.52 0.717
30 8.12 0.767
45 5.52 0.777
60 6.11 0.800
75 3.76 0.800
90 4.46 0.783
105 6.60 0.833
120 4.89 0.833
135 3.41 0.780
150 6.15 0.860
Length आधे step से छह तक जाती है, accuracy 0.137 से 0.860 तक, और reward function ने एक बार भी length का ज़िक्र नहीं किया। अब control — हर बात में वही, सिवाय इसके कि यहाँ एक read ही पहले से सही है:
CONTROL TASK (p=1.00, one read is already enough)
iter mean length accuracy
0 0.68 0.297
15 2.06 0.987
30 2.63 1.000
45 2.18 1.000
60 2.34 1.000
75 2.31 1.000
90 2.15 1.000
105 2.12 1.000
120 2.11 1.000
135 2.19 1.000
150 2.05 1.000
hard task length 0.47 -> 6.15 accuracy 0.137 -> 0.860
control task length 0.68 -> 2.05 accuracy 0.297 -> 1.000
Reward design, जहाँ आप असल में ग़लती करेंगे
तीन reward functions, बाक़ी सब कुछ एक जैसा। अगर कोई पूछे कि R1 से मैंने क्या सीखा, तो whiteboard पर मैं यही हिस्सा लिखूँगा।
reward function accuracy mean length format
correct + format 0.850 5.12 1.00
format ONLY 0.360 1.12 1.00
correct + format + 0.05*length 0.743 16.00 1.00
सिर्फ़ format से आपको बिल्कुल सही ढाँचे वाला output मिलता है, accuracy दस-पहलू वाले सिक्के जितनी। उसने जवाब देखना सीखा ही नहीं, क्योंकि उसका पैसा कभी किसी ने नहीं दिया। हर field validate होता है। कुछ भी सच नहीं है।
Reward में length डालिए तो padding मिलेगी। वह context window तक चला जाता है और accuracy गिरती है — 0.850 से 0.743 — क्योंकि अतिरिक्त steps दोबारा पढ़ना हैं, और दोबारा पढ़ना noise को बढ़ाता है। आपने tokens ख़रीदे और उसे सोच कह दिया। इसीलिए R1 के reward में कोई length term नहीं है: length एक लक्षण है जो आप चाहते हैं, वह लक्ष्य नहीं जो आप तय करते हैं।
Tags, parser, और वह bug जो आप ship करेंगे
R1 का template है — सोच <think> tags के बीच और आख़िरी
जवाब <answer> tags के बीच, ताकि एक नियम बिना अंदाज़ा
लगाए जवाब ढूँढ़ सके। मेरा वही नक़ल करता है:
<think>
step 1: read -> 9
step 2: read -> 8
step 3: read -> 9
step 4: read -> 5
step 5: read -> 9
step 6: read -> 9
</think>
<answer>9</answer>
truth : 9
parse_answer (tag-anchored) : 9
parse_answer_broken (first : 1 <- यह step index है
number anywhere)
अब parser को उसी तरह तोड़िए जैसे हर कोई एक बार तोड़ता है — answer tags के भीतर वाले के बजाय output में कहीं भी मिलने वाला पहला number ले लीजिए। वही model, वही rollouts, दो graders:
scored with the correct parser : 0.850
scored with the broken parser : 0.090
trained AGAINST the broken parser -> real accuracy 0.330, length 1.17
दूसरा आँकड़ा जवाब के बारे में नहीं, काम दिखाने वाले हिस्से के बारे में parser की राय है। उससे grade कीजिए तो आपने कोई ख़राब model नहीं नापा, आपने एक अलग string नापी है। उसके ख़िलाफ़ train कीजिए तो reward signal शोर है, इसलिए policy जल्दी रुकना सीख लेती है और फिर कुछ भी सीखना बंद कर देती है। एक regex आपके training objective का component है। उसे वैसा ही मानिए।
वह डोरी, sweep करके
वही reward, वही task, β के चार मान। KL प्रति trajectory है, nats में, और उस जमे हुए reference के सापेक्ष नापा गया है जहाँ से run शुरू हुआ था।
beta accuracy mean length KL from reference (nats/traj)
0.00 0.743 16.00 16.498
0.02 0.745 15.96 16.495
0.10 0.850 5.12 5.113
0.50 0.588 2.81 1.659
बिना डोरी के वह context window तक भागता है और वहीं जमा रहता है। फिर भी score अच्छा आता है — जवाब सही tags में है और अक्सर सही भी होता है — और output ऐसा दिखता है:
<think>
step 1: read -> 8
step 2: read -> 6
step 3: read -> 0
step 4: read -> 0
step 5: read -> 8
step 6: read -> 4
step 7: read -> 8
step 8: read -> 6
step 9: read -> 6
step 10: read -> 6
step 11: read -> 6
step 12: read -> 8
step 13: read -> 4
step 14: read -> 6
step 15: read -> 8
step 16: read -> 8
</think>
<answer>6</answer>
सोलह steps, जिनमें से छह ही जानकारी रखते हैं, और आख़िर में जवाब भी ग़लत। β = 0.5 उलटी नाकामी है: वह reference से हटता ही नहीं और सोचना कभी सीखता ही नहीं। β tune किया जाता है, व्युत्पन्न नहीं, और dashboard पर रखने वाला आँकड़ा KL है, β नहीं — देखिए RLHF page, जो पूरी तरह इसी पर है कि डोरी हटा देने पर optimiser क्या करता है।
Distillation, और R1 को cold start क्यों चाहिए था
Paper की दूसरी खोज यह है कि R1 के outputs को छोटे dense models में distil करना, उन्हीं छोटे models पर सीधे बड़े पैमाने की RL चलाने से बेहतर रहा। इसका मेरा संस्करण, gradient steps के बजाय environment के rollouts पर मिलाया गया, क्योंकि महँगा हिस्सा rollouts ही हैं:
teacher (hidden=16, 150 GRPO iters) acc 0.850 length 5.12
kept 1051/1200 teacher rollouts -> 6446 labelled decisions
small network, hidden=4:
distilled from teacher, 0 new rollouts acc 0.850 length 5.02
GRPO directly, 18 iters (1152 rollouts) acc 0.485 length 2.02
GRPO directly, 150 iters (9600 rollouts) acc 0.823 length 5.47
teacher की हर सफल trajectory student को उसके भीतर के हर फ़ैसले का एक label दे देती है। एक reward पूरी trajectory के लिए बस एक scalar देता है। पूरा फ़र्क़ यही है। RL वाले student को आठ गुना rollouts दीजिए और वह बराबरी कर लेता है — और paper ठीक इसी सौदे की ओर इशारा कर रहा है। Gradient steps पर मिलाने पर मेरे दोनों students शोर के भीतर एक जैसे हैं, इसलिए इसे उनके नतीजे का सबूत नहीं, उसकी analogy मानिए।
The code
NumPy for the environment, the majority vote and the parsers. A tiny PyTorch policy — four inputs, sixteen hidden units, three actions — CPU only, for the GRPO loop, because hand-writing backprop through 150 iterations would bury the one thing worth looking at. Every function under 25 lines.
Download the full script — about 500 lines plus 6 runnable demos. Then:
python3 deepseek_r1_from_scratch.py
N_DIGITS = 10 # answers are digits 0-9
N_READINGS = 6 # how much genuine evidence a problem contains
K_MAX = 16 # the context window: a hard stop, not a penalty
P_CORRECT = 0.55 # per-reading accuracy on the hard task
BETA = 0.1 # default KL leash (swept in demo 5)
CONTINUE, STOP_TAGGED, STOP_RAW = 0, 1, 2
# STAGE 0 -- the environment. A problem is a truth plus six noisy readings.
# Reading k for k < 6 is fresh evidence. Reading 7, 8, 9... is a re-read of
# something already seen: no new information, and it double-counts whatever
# noise it happens to land on.
def make_problem(rng, p_correct=P_CORRECT):
truth = int(rng.integers(N_DIGITS))
readings = []
for _ in range(N_READINGS):
if rng.random() < p_correct:
readings.append(truth)
else:
step = 1 + int(rng.integers(N_DIGITS - 1))
readings.append((truth + step) % N_DIGITS)
return truth, readings
def next_reading(rng, readings, seen):
"""Fresh evidence while there is any left, otherwise a re-read."""
if len(seen) < N_READINGS:
return readings[len(seen)]
return seen[int(rng.integers(len(seen)))]
def majority(seen):
"""The answer readout. NOT learned -- the policy only decides when to stop."""
if not seen:
return -1
return int(np.bincount(seen, minlength=N_DIGITS).argmax())
# STAGE 1 -- what the policy sees. Four numbers: how much context is spent, how
# decisive the current vote is, how big the leading count is, and a bias term.
# Deliberately tiny, so nothing interesting can hide inside the network.
def features(seen):
k = len(seen)
if k == 0:
return [0.0, 0.0, 0.0, 1.0]
counts = np.sort(np.bincount(seen, minlength=N_DIGITS))[::-1]
return [k / K_MAX, (counts[0] - counts[1]) / k, counts[0] / k, 1.0]
# STAGE 2 -- the output format. R1 asks for thinking between <think> tags and
# the final answer between <answer> tags, so that a rule -- not a learned
# reward model -- can grade it. STOP_RAW emits the same content with no tags,
# which is the degree of freedom the format reward exists to remove.
def render(seen, answer, tagged):
steps = "\n".join(f"step {i + 1}: read -> {v}" for i, v in enumerate(seen))
if tagged:
return f"<think>\n{steps}\n</think>\n<answer>{answer}</answer>"
return f"{steps}\nthe answer is {answer}"
ANSWER_RE = re.compile(r"<answer>\s*(\d+)\s*</answer>")
FORMAT_RE = re.compile(r"<think>.*</think>\s*<answer>.*</answer>", re.S)
def parse_answer(text):
"""Pull the final answer out of the tags. Returns None if it isn't there."""
m = ANSWER_RE.search(text)
return int(m.group(1)) if m else None
def parse_answer_broken(text):
"""The bug everyone ships once: first number ANYWHERE. Grabs the working."""
m = re.search(r"(\d+)", text)
return int(m.group(1)) if m else None
# STAGE 3 -- the reward functions. R1-Zero's is rule-based: accuracy plus
# format, no learned reward model, because a learned one invites the optimiser
# to go hunting for its soft spots (see the RLHF page). Note what is NOT here:
# any term that mentions length.
def reward_correct_format(text, truth, n_steps, parser=parse_answer):
return 0.2 * format_ok(text) + 1.0 * (parser(text) == truth)
def reward_format_only(text, truth, n_steps, parser=parse_answer):
return 0.2 * format_ok(text) # never mentions the answer
def reward_with_length(text, truth, n_steps, parser=parse_answer):
return reward_correct_format(text, truth, n_steps, parser) + 0.05 * n_steps
# STAGE 5 -- GRPO, in one function. Two sentences: sample a group of G answers
# for each prompt, score them, and use (r - mean of the group) / (std of the
# group) as the advantage, so no value network is needed -- the group is the
# baseline. Then the usual policy gradient, plus a KL leash back to the
# reference policy. Full derivation on the GRPO page; this is the same
# estimator, in 20 lines.
def grpo_step(policy, ref, opt, roll, rewards, group, beta):
r = torch.tensor(rewards, dtype=torch.float32).view(-1, group)
adv = (r - r.mean(1, keepdim=True)) / (r.std(1, keepdim=True) + 1e-6)
adv = adv.view(-1)
x = torch.tensor(roll["feats"], dtype=torch.float32)
logp = torch.log_softmax(policy(x), dim=-1)
chosen = logp[torch.arange(len(roll["acts"])), torch.tensor(roll["acts"])]
# sum the log-probs of each trajectory, weight by that trajectory's advantage
per_traj = torch.zeros(roll["n"]).index_add_(
0, torch.tensor(roll["owner"]), chosen)
pg = -(per_traj * adv).mean()
# the leash: KL(policy || reference) at every state actually visited
with torch.no_grad():
ref_logp = torch.log_softmax(ref(x), dim=-1)
kl = (logp.exp() * (logp - ref_logp)).sum(-1)
kl_traj = torch.zeros(roll["n"]).index_add_(
0, torch.tensor(roll["owner"]), kl)
opt.zero_grad()
(pg + beta * kl_traj.mean()).backward()
opt.step()
return float(kl_traj.mean())
Now break it
This is the part that actually makes it stick. Reading won't do it. You debug for a living — reverse-engineering from broken behaviour is your native mode. Every failure below is one my script prints today.
| Break this | Watch for |
|---|---|
Train with reward_format_only — pay for the tags, never for the answer |
Format score 1.00, accuracy 0.360 against 0.850. Perfectly structured output that is wrong two times in three. Every field validates, nothing is true |
Train with reward_with_length — add 0.05 * n_steps to the reward |
Mean length jumps to 16.00, the context window, and accuracy drops to 0.743. Nineteen-line traces of which six lines carry information |
Pass parser=parse_answer_broken — match the first number anywhere instead of inside <answer> |
The same rollouts score 0.090 instead of 0.850, because it is grading the step index in the working. Train against it and real accuracy lands at 0.330 |
Set BETA = 0.0 — take the KL leash off |
Length pins at 16.00, KL from the reference reaches 16.498 nats, accuracy falls to 0.743. Output that still scores well and reads as padding |
Run the whole thing with p_correct=1.0, so extra steps cannot help |
Length stays at 2.05 from iteration 15 to 150 while accuracy sits at 1.000. Nothing emerges, because there is nothing for length to buy |
Set N_READINGS = 16 so every step is fresh evidence |
The plateau in demo 1 disappears and the accuracy column keeps rising to k=16 — and the length-in-reward run stops looking like padding, because now it isn't |
On the maths anxiety
There is less here than the notation suggests. The advantage is a z-score: subtract the mean of eight numbers, divide by their standard deviation. You have written that in a monitoring dashboard. It exists so the policy is told "better or worse than your siblings" rather than "your score was 0.83", which is what removes the need for a separate value network.
The policy gradient is one derivative you can take on trust: nudge up the
log-probability of things that went better than average, nudge it down for
things that went worse. The KL term is a distance between two probability
distributions, measured in nats, and in code it is literally
(p * (log p − log q)).sum(). That's it. Nothing in this paper
needs a proof to be used correctly.
What does need care is not maths at all — it's the reward function and the parser that feeds it. Those are ordinary software, they are wrong in ordinary ways, and they are where the whole method succeeds or quietly fails. The question you will be asked in an interview is not how GRPO's advantage is derived. It's "how did you know the model got better, and not just better at your grader?"
Where to go next
This page gave you one angle — the emergence mechanism and the reward-design traps. These fill in the parts I skipped: the real training runs, the algorithm's derivation, and the wider family of reasoning models.
-
DeepSeek-R1: Incentivizing Reasoning Capability in LLMs via Reinforcement Learning
Read the R1-Zero half properly: the rule-based reward, the template, the response-length curve, and the readability and language-mixing problems that made the cold start necessary. A revised version also appeared in Nature.
-
DeepSeekMath: GRPO, where the algorithm comes from
The algorithm section R1 assumes you already know. Read it for why the group baseline replaces a value network, which my 20-line version shows but does not justify.
-
Open-R1: a fully open reproduction of DeepSeek-R1
What this page most obviously lacks: an actual language model being trained this way, with the datasets, the recipe and the things that went wrong in public.
-
Understanding Reasoning LLMs
Puts R1 next to the other ways of buying reasoning — inference-time scaling, SFT, distillation — and is honest about which is worth its cost.