← stin.cm

Poirot Confronts Goodhart

In Part 1, we built a scorer and found a surprise: the p(CO)p(C|O) direction — “is this compression natural given the original?” — detects all three failure modes (distortion, hallucination, omission) at p<0.001 on 48 real agent trajectories. This was the opposite of what my Poirot-inspired intuition predicted; p(O|C) (“do the notes make the novel unsurprising?”) was empirically weaker as an evaluator.

The natural next step: use our strongest evaluator as an RL reward to train a compressor. If p(C|O) robustly ranks good compressions over bad ones, it should be able to teach a model to produce good compressions — right?

Enter Goodhart

“When a measure becomes a target, it ceases to be a good measure.” - Charles Goodhart

Can we use p(CO)p(C|O) as a reward signal to train a compressor with Reinforcement Learning? Because it doesn’t require human labels, this would be a scalable, self-supervised path. But does it work, or does the model learn to hack the score?

Method

Train a compressor using RL against a frozen LM scorer, using various rewards described below. For each run, track its reward trajectory over N steps and compare with blind judge scores to identify where divergence occurs.

  • Dataset: 48 curated software engineering agent trajectories from real SWE-bench problem-solving sessions. The compressor sees a truncated prefix (up to 2048 tokens) and generates a summary (up to 256 tokens).
  • Compressor: Qwen3-1.7B-Instruct + LoRA (6.4M params)
  • Scorer: Frozen Qwen3-1.7B-Base
  • Training: GRPO, group size 4, KL penalty 0.02
  • Eval: Claude Sonnet 4 as blind judge (faithfulness 1-5, coherence 1-5)
  • Baseline: Instruct model with no RL scores 2.1/5 faithfulness
  • Cost: ~$2/run on a single A10G spot instance via SkyPilot (~$25-50 total)
  • AI assistants: Claude, Gemini

Reward 1: p(C|O) - Our best evaluator as a training signal

In Part 1, p(C|O) was our strongest evaluator - 44/48 paired wins detecting failures. The natural first RL reward: “is this compression the kind of thing you’d write after reading the original?”

StepFaithfulnessCoherenceRewardChars
102/53/5-1.371194
202/52/5-1.111163
301/51/5-1.081127
501/51/5-2.44256
701/51/5-1.11277
Reward 1: p(C|O) — reward and faithfulness over training

It collapsed in 30 steps. The model discovered an exploit ladder - a progression through four classes of tokens, each more generically predictable and less informative than the last:

1. Plausible Hallucination (step 10) - Looks great. <think> blocks, correct class names. Judge says 2/5: “hallucinates error messages not in the trajectory.” The output reads like a real summary but fabricates details.

2. Identifier Repetition (step 30) - Repeats real identifiers from the trajectory: FilteringBoundLogger’s FilteringBoundLogger’s FilteringBoundLogger’s… Judge: 1/5. High p(C|O) because these tokens are contextually plausible, but zero marginal information per repetition.

3. Numeric Gibberish (step 90) - 6377900000300018010000... Digits are high-probability continuations of any technical text.

4. Terminal Delimiters (step 190) - The global optimum is of course em-dashes. Reward -0.31 (one of the best scores in the entire run):

—"—"—"—"—"—"—"—"—"—"—"—"—"—"—"—"—"—"—"—"—"—"—"—"—"—"—"—"—"

Each rung is more generic, less dependent on the specific input, and less informative. The very property that made p(C|O) a good evaluator - sensitivity to specific token choices - makes it easy to game. The model just needs to find tokens that are generically predictable given any code text.

Stronger KL (5x) doesn’t help: I tried coefficient 0.10 instead of 0.02. The output stays readable longer but faithfulness is identical (still 2/5). The leash controls drift speed, but doesn’t prevent it.

Reward 2: min(p(O|C), p(C|O)) - Score in both directions

Hypothesis: the model can’t game both directions at once.

StepFaithfulnessCoherenceRewardChars
102/53/5-1.681261
201/51/5-1.75758
301/51/5-1.84768
501/51/5-2.101792
701/51/5-1.36999
Reward 2: min(both) — faster collapse than either direction alone

Result: same collapse, different tokens. The model finds “universal tokens” ([dir, serial, @qq) that are generically probable in both directions. Collapsed at step 20 - faster than p(C|O).

Why faster? When optimizing min(A, B), gradients only flow through the smaller value. To improve the overall reward, the model must improve whichever direction is currently worse. But pushing for quality in one direction can penalize the other - genuine compression tokens that score well on p(O|C) might score poorly on p(C|O) or vice versa. The path of least resistance is to find “universal tokens” that are equally mediocre in both directions. The model settles into this saddle point because any step toward actual compression hurts one of the two metrics.

Reward 3: P(C|O) with a Self-perplexity gate

The exploits in Rewards 1-2 all use high-frequency tokens. What if we keep p(C|O) as the reward but block the obvious exploits? If unconditional p(C) is too high, replace the reward with -10.

StepFaithfulnessCoherenceRewardChars
102/52/5-3.851370
202/53/5-3.381173
304/54/5-3.661342
402/51/5-3.98986
501/51/5-3.31724
701/51/5-8.031032
Reward 3: Self-perplexity gate — brief success at step 30

This unlocked our highest judge score so far. Step 30 scores 4/5 faithfulness - “accurately captures the key issue, correct file location, correct fix.” But by step 50 the model learns to stay just below the threshold while still gaming the conditional scorer.

The gate is the right idea, wrong implementation - a binary threshold when you need a continuous signal.

Reward 4: PMI

The gate’s partial success pointed to a potential fix: don’t gate generic tokens with a strict binary threshold, instead we can continuously penalize them.

reward = log p(C|O) - log p(C)

If a token is equally probable with or without the original, it scores zero. Only tokens specifically predictable because of this original score positively.

This is Pointwise Mutual Information (PMI). Li et al. (NAACL 2016) popularized it as the “Maximum Mutual Information” (MMI) objective. Li, Monroe et al. (EMNLP 2016) used it as an RL reward. I reimplemented a decade-old idea.

Results across 4 seeds (75-126 steps each)

StepMean faithfulness (n=4)RangeReward
Baseline2.11-3-
103.02-4+0.67
202.02-2+0.50
303.02-4+1.00
401.51-2+0.44
501.51-2+0.87
601.81-2+0.81
701.51-2+1.67
Reward 4: PMI — no adversarial collapse, but slow drift

What PMI gets right: Zero adversarial collapse in any seed. No identifier repetition, no gibberish, no delimiter spam. Compression length stays healthy (850-1260 chars). Steps 10 and 30 above baseline across all seeds.

What PMI doesn’t solve: Quality drops below baseline by step 40. Reward keeps climbing (+0.67 → +1.67) while quality drops (3.0 → 1.5) - reward hacking in slow motion. The model drifts toward meta-commentary and broken formatting. Real words, not adversarial, but not useful.

Caveat: Under GRPO’s within-group normalization, the log p(C) subtraction may be partially absorbed. I haven’t run the ablation that would isolate PMI’s contribution.

Reward 5: p(O|C) - Back to Poirot’s intuition

After seeing four variations of p(C|O) all Goodhart in different ways, I went back to the direction Poirot suggested all along: “do the notes make the novel unsurprising?”

Results across 3 seeds

StepMean faithfulness (n=3)RangeMean reward
Baseline2.11-3-
102.72-3-0.96
202.72-4-1.43
303.73-4-1.14
401.71-2-1.14
502.01-3-1.59
601.01-1-1.24
701.31-2-1.14
Reward 5: p(O|C) — honest signal, chaotic optimization

Something different happens here. The reward doesn’t smoothly increase while quality collapses. Instead, reward roughly correlates with quality across all 3 seeds - better rewards correspond to better compressions, worse rewards to worse ones.

p(O|C) is the only reward where the signal is honest across multiple seeds. It doesn’t mislead - consistent with Part 1’s finding that p(O|C) is an informative evaluator (32/48, p=0.015).

Step 30 replicates at 3.7/5 mean - every seed produces quality well above baseline here. But all three seeds also collapse to 1/5 by step 60.

The problem is the model can’t consistently optimize it. There’s no easy exploit - repeating a class name doesn’t help predict 2048 tokens of original. The model thrashes rather than converging. Training dynamics are chaotic (KL spikes to 12.9, and the character count oscillates wildly). The noisy gradients (group size 4, 1 trajectory per step) aren’t enough signal to solve this hard optimization problem.

But this seems like a different kind of failure - not Goodhart (reward deceiving you) but underfitting (reward too hard to optimize with this setup).

The fundamental asymmetry: in p(C|O), the model controls the tokens being evaluated (C) - it can tweak them to directly manipulate the score. In p(O|C), the model is evaluated on how well its output predicts a massive, fixed 2048-token target (O). Unless the summary is already highly accurate, the gradient signal pointing toward “how to fix this summary to better predict token 1,402 of the original” is effectively noise. Again we find continuation is easier than compression.

This means SFT warmup may be mandatory. The model must start inside the attractor basin of “decent compressions” for the p(O|C) gradients to provide directional guidance rather than chaotic thrashing.

Summary

Faithfulness across all reward formulations
RewardSeedsPeak qualityCollapse to 1/5Honest signal?Failure mode
p(C|O)12/5 (step 10)Step 30No (reward misleads)Exploit ladder
p(C|O) + 5x KL12/5 (step 10)SlowerNo (same pattern)Same hallucination
min(both)12/5 (step 10)Step 20No (same pattern)Universal tokens
Self-perp gate14/5 (step 30)Step 50PartiallyBelow-threshold gaming
PMI43.0/5 meanNeverNo (drifts smoothly)Semantic drift (never gibberish)
p(O|C)33.7/5 mean (step 30)Step 60Yes (replicates)Underfitting

PMI vs p(O|C): Complementary properties

These two rewards represent opposite tradeoffs. Looking at them side-by-side:

StepPMI faithfulness (4 seeds)p(O|C) faithfulness (3 seeds)
103.02.7
202.02.7
303.03.7
401.51.7
501.52.0
601.81.0
701.51.3

PMI provides stability. It never collapses to 1/5 gibberish — the worst it produces is 1.5/5 (fragmented real words). But its reward keeps climbing (+0.67 → +1.67) while quality drops (3.0 → 1.5). You can’t trust the reward curve.

p(O|C) provides honesty. The reward roughly tracks quality — when output is good (step 30, reward -1.14), it scores well; when output is bad (step 60, reward -1.24), it scores poorly. But it collapses to actual gibberish by step 60.

PropertyPMIp(O|C)
Peak quality3.0/53.7/5
Worst quality1.5/5 (never gibberish)1.0/5 (gibberish)
Reward honest?No (climbs while quality drops)Yes (tracks quality)
Reward improves?Yes (+0.67 → +1.67)No (flat, thrashes)

What’s next

PMI gives stable training but drifts silently; p(O|C) gives honest signal but is hard to optimize from a cold start. To address underfitting, we can experiment with:

  • SFT warmup → light RL with p(O|C). Start from a model that already generates decent compressions (trained on frontier-model outputs). From inside that basin, p(O|C)‘s honest gradient can refine quality without thrashing.
  • Larger group size (16-64). The noisy gradient problem (group size 4, 1 trajectory/step) hamstrings all reward formulations equally. More samples = better signal regardless of which reward you use.
  • Iterative distillation. Avoid online RL entirely. Generate N=16, score with p(O|C), filter, retrain. Each round is a fresh SFT — no optimization dynamics to fight, no Goodhart drift to worry about.