and is a separate concern from making the SOM a default — see the note at the
end.
phasecurvefit.som is vmap-able over its initial prototypes, which was the
guarantee #56 set out to make (pure functions, explicit init and key, static
shapes, lax.scan over epochs, no host callbacks). tests/test_som_ensemble.py
makes that executable and it holds. But it is necessary, not sufficient, for
a Statistically Combined Ensemble of SOMs.
The finding
Batch Kohonen is strongly contractive. Each epoch replaces every prototype with
a neighbourhood-weighted mean of the data; the previous prototype value
enters only through which prototype currently wins each datum. So once two
ensemble members' best-matching-unit assignments coincide — which happens within
a few epochs on a smooth, well-sampled curve — every subsequent epoch is
byte-identical and the members stay merged forever.
Measured while building the guarantee test: with a shared PCA init jittered per
member at 0.05, ensemble members converged bit-for-bit identical. A sweep
over 5 seeds × jitter magnitudes found only ~20× that jitter reliably keeps
members apart. So the obvious construction — map over perturbed initializations —
yields a degenerate posterior: zero spread, no uncertainty information, and
nothing to combine.
Switching to genuinely independent initializations (a per-member random
ordering into init_prototypes, rather than perturbing one shared init) does
separate them: pairwise separations of 0.44–1.64 on fitted prototypes and
4.7–6.6 on chords, seven to eight orders of magnitude above jnp.allclose
tolerances. That is what _ensemble_inits in tests/test_som_ensemble.py now
does, with a comment explaining why the diversity has to be large so a future
tidy-up does not silently re-merge the ensemble.
What a real posterior will need
Independent initializations are enough to make members differ, but an ensemble
whose only diversity source is the starting lattice is measuring initialization
sensitivity, not data uncertainty. A meaningful posterior of orderings probably
wants one of:
- bootstrap-resampling the data per member — the standard choice, and it
propagates sampling noise into the spread;
- per-member subsampling — cheaper, similar effect;
- a stochastic (online) update — closer to the paper's own (A9)/(A10), where
presentation order injects randomness that batch Kohonen has removed by
construction.
Worth deciding deliberately, because the choice determines what the resulting
spread actually means.
Already documented
docs/guides/som.md, "A note on ensembles" — states plainly that vmap-ability
is a necessary building block but not a sufficient one.
tests/test_som_ensemble.py::_ensemble_inits — carries the contractivity
explanation and a "do not tidy this back down" warning.
This is arguably a distinct follow-up from "make the SOM part of the default
pipeline": that one is about the order() default and its compatibility
surface, this one is about designing the SCESOM feature. Happy to split it into
its own issue if that is cleaner — say the word.
Relationship to #57
Split out of a comment on #57 (make the SOM the default pipeline). That issue is
about the order() default and its compatibility surface — minimum-N, return
type, gamma_range, migration. This one is about designing the SCESOM feature
itself. Related only in that both build on the SOM stage added in #56.
🤖 Generated with Claude Code
and is a separate concern from making the SOM a default — see the note at the
end.
phasecurvefit.somisvmap-able over its initial prototypes, which was theguarantee #56 set out to make (pure functions, explicit init and key, static
shapes,
lax.scanover epochs, no host callbacks).tests/test_som_ensemble.pymakes that executable and it holds. But it is necessary, not sufficient, for
a Statistically Combined Ensemble of SOMs.
The finding
Batch Kohonen is strongly contractive. Each epoch replaces every prototype with
a neighbourhood-weighted mean of the data; the previous prototype value
enters only through which prototype currently wins each datum. So once two
ensemble members' best-matching-unit assignments coincide — which happens within
a few epochs on a smooth, well-sampled curve — every subsequent epoch is
byte-identical and the members stay merged forever.
Measured while building the guarantee test: with a shared PCA init jittered per
member at 0.05, ensemble members converged bit-for-bit identical. A sweep
over 5 seeds × jitter magnitudes found only ~20× that jitter reliably keeps
members apart. So the obvious construction — map over perturbed initializations —
yields a degenerate posterior: zero spread, no uncertainty information, and
nothing to combine.
Switching to genuinely independent initializations (a per-member random
orderingintoinit_prototypes, rather than perturbing one shared init) doesseparate them: pairwise separations of 0.44–1.64 on fitted prototypes and
4.7–6.6 on chords, seven to eight orders of magnitude above
jnp.allclosetolerances. That is what
_ensemble_initsintests/test_som_ensemble.pynowdoes, with a comment explaining why the diversity has to be large so a future
tidy-up does not silently re-merge the ensemble.
What a real posterior will need
Independent initializations are enough to make members differ, but an ensemble
whose only diversity source is the starting lattice is measuring initialization
sensitivity, not data uncertainty. A meaningful posterior of orderings probably
wants one of:
propagates sampling noise into the spread;
presentation order injects randomness that batch Kohonen has removed by
construction.
Worth deciding deliberately, because the choice determines what the resulting
spread actually means.
Already documented
docs/guides/som.md, "A note on ensembles" — states plainly thatvmap-abilityis a necessary building block but not a sufficient one.
tests/test_som_ensemble.py::_ensemble_inits— carries the contractivityexplanation and a "do not tidy this back down" warning.
This is arguably a distinct follow-up from "make the SOM part of the default
pipeline": that one is about the
order()default and its compatibilitysurface, this one is about designing the SCESOM feature. Happy to split it into
its own issue if that is cleaner — say the word.
Relationship to #57
Split out of a comment on #57 (make the SOM the default pipeline). That issue is
about the
order()default and its compatibility surface — minimum-N, returntype,
gamma_range, migration. This one is about designing the SCESOM featureitself. Related only in that both build on the SOM stage added in #56.
🤖 Generated with Claude Code