docs: add mnist rgm notebook#414
Conversation
|
Thanks for this @Arun-Niranjan and sorry for the late review. I pulled this down and ran the training path on the full 10k images. I got roughly 83% on the first 1,000 official test images and 86.3% on the SPM-style My main thoughts would be: maybe don't spend the next phase tuning performance to 95.1% quite yet. The SPM reference fits its SVD bases and bin ranges on all 11,000 images before splitting 10k/1k, and its “test” set is drawn from MNIST’s training split. Your implementation fits the basis from only the 130 structure exemplars and evaluates the official test set, so it's not entirely surprising that the reported accuracies are not comparable. I’d suggest defining two modes:
Before further optimisation, I think we should validate several algorithmic details against small exported SPM fixtures:
I’d keep the implementation under Actions or the Dove/Atari extensions do not need to be in scope for this PR; establishing a correct static MNIST RGM is already a substantial and useful milestone. |
| # --------------------------------------------------------------------------- | ||
|
|
||
|
|
||
| def _mi_from_pA(pA_list: list[jnp.ndarray]) -> jnp.ndarray: |
There was a problem hiding this comment.
from what i can tell, this is summing across every patch and modality, whereas SPM gates each likelihood mapping independently. With
| data = data_packed[:N, :] # (N, n_pixels) | ||
| L = data_packed[N, 0] # scalar L for this tile | ||
|
|
||
| _, S_full, Vh = jnp.linalg.svd(data, full_matrices=False) |
There was a problem hiding this comment.
it looks like you first truncate to the top nksingular values here before calculating the 'retention denominator', whereas SPM thresholds using the full spectrum (more than nk vlaues) and then only caps them post-threshold at 16: https://github.com/spm/spm/blob/23a9359a60bb13eec207591e238ec24b367f42ad/spm_svd.m#L35-L82
|
|
||
| mode_mask = (S > 0).astype(jnp.float32) | ||
|
|
||
| def symmetric_bin_centres(v): |
There was a problem hiding this comment.
| self.levels = levels | ||
| self.n_classes = n_classes | ||
|
|
||
| @classmethod |
There was a problem hiding this comment.
maybe i'm confused here, but doesn't this conflate the fitting of the SVD frnot-end with the fitting of the structuer learning algorithm? I thought they should be separate sets of inputs --one dataset for fitting the front end and then 13-per-class exemplars for fast structure learning itself, using the separately-learned SVD basis frontend
| num_states_flat = stats.num_states.flatten() # (n_patches,) | ||
| max_states = int(num_states_flat.max()) | ||
|
|
||
| # Max child states across entire child grid — observation dimension |
There was a problem hiding this comment.
The code from here seems to suggest that probability can leak into nonexistent states.
The batched JAX representation pads every local mapping to the largest child-state vocabulary. Concentration is then added to those padded rows, while only hidden-state masks (not observation mask) are consistently carried through inference and generation, see also
pymdp/examples/renormalising_generative_models/rgm_hierarchy.py
Lines 370 to 385 in 2618449
| valid_mask: jnp.ndarray, | ||
| num_iter: int = 16, | ||
| ) -> tuple[list[jnp.ndarray], jnp.ndarray]: | ||
| """Interleaved Q-A EM matching MATLAB's spm_VBX within-level solver. |
There was a problem hiding this comment.
i don't think this matches the code path the demo actually runs: the 16-iteration qa = pa; qa += cross(O,Q) scheme lives in spm_backwards, which is only entered when OPTIONS.B=1 -- and the MNIST demo sets OPTIONS.B = 0. spm_VBX itself is explicitly non-iterative (one belief-propagation pass with A held fixed), so the demo's update is a single inference under the prior A followed by a single Dirichlet accumulation. iterating Q against an A refit to the same image is self-reinforcing and will sharpen the counts towards one-hot relative to SPM -- worth trying num_iter=1 and comparing before attributing the accuracy gap elsewhere
| new_levels = [] | ||
| for lv_idx in range(n_hier): | ||
| level = levels[lv_idx] | ||
| D_em = refined_soft[lv_idx] # top-down refined prior over states |
There was a problem hiding this comment.
refined_soft[lv_idx] is a posterior that has already absorbed this level's observations (the refinement pass re-ran infer_states on the same obs list), and the E-step in _interleaved_em then multiplies the same observations in again -- so the evidence gets counted twice. in SPM the prior for the learning posterior is the top-down message alone (mdp.D{f} = spm_dot(A, Q_parent) folded into the child's D), with the observation entering once. i think passing the _top_down_D_hierarchical / _top_down_D_from_cls output here instead of the refined posterior would be closer to the reference
| n_obs_l1 = level.pA[0].shape[1] # n_levels (SVD bin outcomes) | ||
| obs_soft = [jax.nn.one_hot(o, n_obs_l1) for o in level_obs_lists[0]] | ||
| else: | ||
| obs_soft = level_obs_lists[lv_idx] # already (n_patches, max_child_states) |
There was a problem hiding this comment.
the outcome side of the cross(O, Q) accumulation here (and obs_soft_cls = [child_beliefs] below for the classification level) comes from the unsupervised bottom-up pass. in SPM the supervised one-hot D conditions the whole downward sweep before inference happens, so the child posteriors that come back up as the outcomes O are label-conditioned at every level by the time qa += cross(O,Q) runs. early in training this matters: SPM's counts get pulled toward the correct class's exemplar states, whereas these reinforce whatever the unsupervised parse said (only the Q side is label-corrected). using the refined (post-top-down) beliefs as the observation side would be closer to the reference
Initial attempt post v1 release to implement scale free active inference MNIST classifier as per this paper and this demo script in SPM.
I could not achieve the accuracy levels of ~95% mentioned in the paper (this implementation gets to ~87% in the test set), and there are quite a few deviations between the paper and the demo script, or underspecified methods in general.
Of particular note:
etahyperparameterBecause this PR required quite a lot of extra infrastructure to get going, I recommend checking out the branch, running the
mnist_classifier.ipynbnotebook and then clicking back into the various functions to get a better idea of the implementationDisclaimer: The work on this branch was done with heavy use of Claude code, especially for trying to compare against the SPM demo and also to make siginificant performance improvements with JAX optimisations
THIS PR SHOULD NOT BE MERGED AS IT IS. I would like to get some broad feedback on direction and algorithmic choices, and once we can get decent accuracy we can decide on:
Then I will split code from this branch into sensible chunks that can be properly reviewed.