Diagnostics¶
The loss is not the metric. A collapsing encoder drives its prediction loss down, because it is predicting its own degenerate output, and a constant is very easy to predict. In the collapse comparison the run with the best loss by seven orders of magnitude had learned nothing at all.
So measure the representation:
xwm.metrics.collapse_report(z)
# {'rankme': ..., 'rank_ratio': ..., 'feature_std': ..., 'mean_cosine': ...}
What each one catches¶
| metric | collapsed when | measures |
|---|---|---|
feature_std |
→ 0 | per-dimension spread across inputs |
mean_cosine_similarity |
→ 1 | how alike two random embeddings are |
rankme |
→ 1 | effective rank of the spectrum |
effective_rank_ratio |
→ 0 | the same, as a fraction of embed_dim |
All four are reported because each misses a case the others catch:
rankmeis computed after centring, so a large constant offset is invisible to it: a representation can be rank-rich around a mean it never departs from.feature_stdis per-dimension, so it does not see a representation that varies a lot along one direction and not at all along the rest.mean_cosineis scale-invariant, so it does not see a representation whose spread is collapsing uniformly.
Read them together. feature_std → 0 and mean_cosine → 1 is the
unambiguous signature.
singular_values gives you
the spectrum itself, and
xwm.plots.plot_spectra draws it.
A probe is not a collapse detector¶
This is the trap worth internalising.
ridge_probe standardises
features before fitting, so it divides by a near-zero standard deviation and
amplifies a nearly-dead signal back to full scale.
Measured on the sprite world, the fully collapsed control run scored R² = 0.982, the highest probe score in the comparison (findings).
scores = xwm.metrics.ridge_probe(z_train, y_train, z_test, y_test)
scores["r2"] # tells you what is linearly decodable, not whether z is healthy
Use a probe for what it is good at: is the information I care about linearly
available? Then ask collapse_report whether the representation is alive.
And beware an easy target
On the synthetic sprite world the agent's position is nearly a linear function
of the red channel, so the probe saturates before training even begins:
R² = 0.999 on a randomly initialised encoder. examples/01_image_ijepa.py
prints the before-and-after side by side precisely to show that.
k-NN, for when linear is too generous¶
A k-NN probe asks whether the neighbourhood structure carries the label, which no amount of feature standardisation can fake. It is slower and it needs a sensible metric, but it is much harder to fool.
In a training loop¶
The Trainer logs whatever a model's loss returns as metrics. Most families
include an embedding-spread term (embed_std, latent_std) for exactly this
reason, so collapse is visible while it happens rather than after:
state, history = trainer.fit(
batches, steps=1000,
callbacks=[xwm.training.print_metrics(every=50, keys=["loss", "embed_std"])],
)
Then check the representation properly at the end, on held-out data:
embed = jax.jit(jax.vmap(state.model.embed))
z = xwm.core.batched_apply(embed, held_out["image"], batch_size=64)
for name, value in xwm.metrics.collapse_report(z).items():
print(f"{name:<12} {float(value):.4f}")
Diagnostics for a dynamics model¶
Representation health is only half of it. A world model also has to be right about the future, and the honest test is a free rollout against a no-op baseline: predict \(h\) steps from a single latent, consuming its own predictions:
| horizon | model L1 | no-op L1 | ratio |
|---|---|---|---|
| 1 | 0.0570 | 0.0588 | 0.97 |
| 3 | 0.0723 | 0.0998 | 0.72 |
| 7 | 0.0911 | 0.1235 | 0.74 |
A ratio below 1 means the model beats assuming nothing changes, which is a surprisingly strong baseline at short horizons, and the reason a raw L1 number is uninterpretable on its own. Full tables in Findings.