Skip to content

lachlanmcalpine/rung4

Repository files navigation

Rung 4 — amortized personality reading

A meta-learner that reads a stranger. Given the first k rounds against a player it has never seen, it emits an embedding of who they are — and predicts their next moves better than a Bayesian model handed the simulator's own source of truth.

Headline: the read curve is real. Prediction accuracy climbs monotonically with observation (.454 → .496 over k=1…15) while a no-embedding baseline stays flat. The learned reader beats a frozen hand-built Bayes at every k, biggest edge at k=1–2.


Why I built it

I wanted to know whether an individual's psychology can be quantified — not described, but reduced to something you can measure and then predict from.

That question doesn't stop at one person. If you can read individuals, you can say something about crowds — including how much randomness a crowd actually supplies, which is the assumption sitting underneath a lot of mass events. Rock-Paper-Scissors is the smallest game where reading your opponent is the entire skill, so it's where I started.


The problem

Reading a person is normally treated as an inference problem you solve from scratch, per person, with a posterior that needs a lot of rounds to sharpen. That's fine when you have hundreds of rounds. It's useless in the regime that actually matters — the first handful, where you have almost nothing and still have to act.

So the question isn't "can I model this player." It's: can reading a stranger fast be learned as a skill, in advance, across a population?


What went wrong, and what I changed

Run 1 failed, and the failure is the finding.

The first population was homogeneous — agents drawn from near-identical trait priors. The encoder trained fine, the loss went down, and the person embedding z was worth nothing. Ablating it changed no predictions. I had built a reader for a room where everybody was the same person.

The fix wasn't architectural. It was the population. Rebuilt sim.py so agents carry genuinely varied latent traits — program mixture, habit bias, frustration threshold, tilt style, boredom clock, stickiness, latency profile — and z immediately started carrying signal.

That reframed the whole project:

Heterogeneity is not a nuisance parameter. It is the resource. The value of a personality read is bounded above by stable between-person variance. No amount of model capacity buys you a read of a population that doesn't vary.

I kept run 1 in the repo rather than deleting it, because the negative result is the part that explains why the positive one works.

I also gave the baseline an unfair advantage on purpose. The comparison Bayes model gets the simulator's true program library — it knows the exact generative family the agents are drawn from, which no real reader ever would. Beating it means something precisely because of that handicap.


Architecture, and the calls I made

  • GRU encoder → variational z, reactive head conditioned on (last round, z). Keeping the head reactive and small was deliberate: if the head is expressive enough to model the opponent by itself, z stops having to carry anything and the experiment measures nothing.
  • Freeze the read at k, then predict the next 10 moves. Freezing is what makes it a test of reading rather than a test of tracking. It's also why online Bayes is allowed to win — see below.
  • Trait priors live in sim.py, and they are the theory. The simulator isn't scaffolding around the model; it's the hypothesis about what varies between people. Everything else is downstream of that file.

Claude wrote most of the implementation. The population design, the decision to freeze the read at k, and the call to keep run 1 in the repo are mine — and the first of those is the one the entire result depends on.


Results: v1 → final

population does z carry person-information? verdict
v1 homogeneous no — ablating z changes nothing dead end, kept for the record
final heterogeneous latent traits yes — monotonic read curve the result below

Accuracy predicting the next 10 moves after freezing the read at k rounds (4,000 training agents, 500 held out, 60 rounds each; chance = .333):

k 1 2 3 5 8 12 15
meta (z) .454 .466 .475 .483 .489 .489 .496
reactive (no z) .456 .455 .453 .449 .447 .442 .441
Bayes, frozen .408 .430 .461 .476 .487 .483 .486
Bayes, online .512 .520 .526 .528 .536 .534 .534

Three things to read off it:

  1. The read curve exists. meta climbs with support; reactive declines. z is doing work.
  2. Fast reading is learnable. meta beats frozen Bayes at every k, by +3–5 points at k=1–2 — despite Bayes holding the true program library. The embedding captures traits (habit bias, stickiness, θ) that a program posterior structurally cannot represent.
  3. Online Bayes still wins by ~4 points, and I'm reporting that. It keeps updating through the query zone while my read is frozen. That gap isn't a defeat — it's a measurement of exactly what continuous re-reading is worth, which is the next thing to build.

What I learned, and what's next

The lesson that generalised beyond this project: when an embedding turns out worthless, check the data-generating process before the model. I spent time on architecture when the problem was that my synthetic population had no variance to read.

Next, in order:

  • Amortized filtering — re-emit z every round instead of freezing it, to close the ~4-point online gap.
  • Active probing — let the reader choose moves for information gain on z, rather than only reacting.
  • Two-timescale splitz_trait (stable across sessions) vs z_state (within a session). Tilt and boredom are not personality.
  • Human data via the v2 widget, as a sim2real validation set.

Run it

pip install -r requirements.txt
python sim.py        # generate the synthetic population
python meta2.py      # train + evaluate, writes results2.json and read_curve.png
python play.py       # play it yourself

model2.pt / model3.pt are the trained weights; selfplay.py hardens against bait-and-switch opponents.

About

Amortized personality reading: a meta-learner that reads an unseen Rock-Paper-Scissors opponent from k rounds and beats a hand-built Bayesian model at every k.

Topics

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages