Evaluating Latent Variable Models of Neural Population Activity: GPFA Fitting & Multi-Metric Evaluation
This repo fits Gaussian Process Factor Analysis (GPFA) to single-unit spiking data from extracellular recordings, in order to extract low-dimensional latent trajectories that summarize population-level neural dynamics on a trial-by-trial basis, and evaluates how good those latent trajectories actually are using multiple complementary metrics.
Fitting GPFA and picking a dimensionality via leave-neuron-out cross-validation
(as in GPFA_data.ipynb) answers one specific question: which dimensionality
captures shared structure in the population's activity without overfitting to
noise? That's the approach from the original GPFA paper
(Yu et al., 2009), and it's a good
starting point — but it's only one lens on model quality.
Neural Latents Benchmark '21 (Pei, Ye, Zoltowski et al., 2021) introduces a more standardized evaluation framework for latent variable models (LVMs) of neural population activity, built around several complementary metrics rather than a single score. The table below summarizes those metrics and how each is used in this repo:
| Metric | What it tells us | Why we need it |
|---|---|---|
| Leave-neuron-out prediction error (MSE) | How well the trajectory captures correlated firing rates across the population. | A fundamental, distance-based measure of predictive ability that works for both probabilistic and non-probabilistic methods. |
| Co-smoothing (bits per spike) | How well the model fits the raw neural data. | Ensures the model isn't just "hallucinating" structure that isn't there. |
| Behavioral decoding (R²) | How well the latent signals relate to actual movement/behavior. | Shows the latents are biologically grounded, not just math that happens to fit the spikes. |
| PSTH matching | How well the model captures stereotyped, repeated response patterns. | Checks whether the model reconstructs the trial-averaged ("PSTH") response well. |
| Forward prediction | How well the model predicts future activity. | Tests whether the model has learned the dynamics governing how the brain's state evolves over time, not just its instantaneous structure. |
Metric-specific notebooks in this evaluation suite:
- Cross-validated held-out-trial neural reconstruction error (MSE) —
GPFA_data.ipynb - Co-smoothing evaluation using held-out bits per spike —
cosmoothing_bps.ipynb - Behavioral decoding from GPFA latent trajectories —
behavioral_decoding.ipynb VLMD_decoding.ipynbisn't a new metric — it reuses the behavioral-decoding metric above, applied to a different latent variable model (VLMD instead of GPFA), so its results are directly comparable tobehavioral_decoding.ipynb's.
PSTH matching and forward prediction aren't implemented in this repo yet. No single metric here is meant to stand alone as "the" measure of model quality.
Walks through the full pipeline for a single session:
- Load session data — spikes and trial event times for a recording session
- Unit selection — keep well-isolated ("good") units with enough spikes to support GPFA fitting
- Build GPFA-ready trials — convert raw spike times into per-trial, per-neuron spike trains aligned to trial onset
- Cross-validate latent dimensionality — fit GPFA across a range of candidate dimensionalities and score each by leave-neuron-out prediction error, with a held-out test set for a final unbiased evaluation
- Fit the final model at the selected dimensionality and save results
- Visualize latent trajectories — single-trial and trial-averaged dynamics, 3D trajectories, latent traces alongside spike rasters, and the GPFA loading matrix
- Relate latents to behavior — correlate each latent dimension with measured behavioral variables (e.g. licking, whisking, pupil size, respiration)
Asks a more direct question than the correlation check in step 7 above: can a simple linear decoder actually predict behavior from the GPFA latents, cross-validated across trials and swept across latent dimensionality? See Evaluating Latent Variable Models below for how this fits into the broader evaluation picture.
- Load session data, select units, build GPFA trials — same
data-preparation steps as
GPFA_data.ipynb - Bin spike trains for GPFA at the chosen
BIN_SIZE_MS - Single-behavior decoding walkthrough (lick rate) — bin lick rate into
GPFA time bins, then for each candidate
x_dim: fit GPFA on training trials, transform held-out test trials, and score a cross-validated Ridge regression decoder (R²) predicting lick rate from the latents - Multi-behavior decoding sweep — repeat the same procedure for every measured behavior (wheel velocity, whisking, pupil area/radius, respiration, face motion) at once, reusing each fold's fitted GPFA model across behaviors, and plot cross-validated R² vs. dimensionality for all behaviors together
Asks whether the fitted GPFA model explains the raw spiking data itself — specifically, whether its latents generalize to neurons it never saw during fitting, not just new trials. See Evaluating Latent Variable Models below for how this fits into the broader evaluation picture.
- Load session data, select units, build GPFA trials — same data-preparation steps as the other notebooks
- Held-in/held-out neuron split and train/test trial split — held-in neurons are used to fit GPFA and a latent→rate bridge; held-out neurons are only ever scored
- Diagnostics — firing-rate and spike-count distributions for held-in vs. held-out neurons, to check the split is reasonable and see where low-firing neurons sit
- Bridge method — fit GPFA on held-in neurons, bridge latents → held-in rates via linear regression, then bridge held-in rates → each held-out neuron's spikes via a Poisson GLM; score held-out predictions in bits/spike relative to a constant-rate null model
- Cross-validate latent dimensionality by mean held-out bits/spike, with a final unbiased score on a held-out test set
- Robustness check — how sensitive the co-smoothing score is to removing low-firing held-in neurons, used to choose a firing-rate cutoff for the final evaluation
Repeats the behavioral-decoding evaluation from behavioral_decoding.ipynb,
but on latents from a different latent variable model — Variational
Latent Mode Decomposition (VLMD) — loaded from a precomputed grid-search run
on disk, rather than fit live with GPFA. This is a model-comparison notebook:
it asks whether an alternative to GPFA produces latents that are at least as
behaviorally meaningful, using the same decoding metric as
behavioral_decoding.ipynb so the two are directly comparable.
- Setup and session loading
- Load precomputed VLMD latents — reconstruct the full latent trace from
a saved VLMD run's
modes_list.npyand related arrays - Inspect the VLMD run directory (diagnostic) — sanity-check array shapes and list sibling hyperparameter runs from the same grid search
- Align VLMD latents to trial structure — VLMD is fit on the full continuous recording, so its latent trace has to be time-aligned and cropped into per-trial segments (unlike GPFA's latents, which come pre-segmented by trial)
- Decode all behaviors from VLMD latents — same cross-validated Ridge
regression / R² procedure as
behavioral_decoding.ipynb, but at VLMD's fixed latent dimensionality rather than a swept range
- Python 3.11
elephant(spike train analysis + GPFA)neoandquantitiesnumpy,scipy,scikit-learn,matplotlib
pip install elephant neo quantities numpy scipy scikit-learn matplotlibBoth notebooks also depend on two local modules from this repo, imported via a path relative to the notebook's location:
data_paths.py— aDATA_PATHdict mapping session keys to.matfile pathsAnalysis_library/— a local package providingfile_loading.SessionData(for loading a session's spikes/trial data) andanalysis.get_good_units(for filtering to well-isolated units)
Update data_paths.py to point at your own data before running.
- Set
SESSION_KEYin the "Load Session Data" section to the session you want to analyze. - Adjust analysis parameters as needed:
BIN_SIZE_MS— GPFA time-bin width (default 30 ms; consider 10 ms for finer temporal resolution at the cost of more compute)PRE_TRIAL_S/POST_TRIAL_S— trial window around each trial eventMIN_SPIKES— minimum total spikes for a unit to be includedX_DIMS— candidate latent dimensionalities to cross-validate overN_FOLDS— number of cross-validation folds
- Run the notebook top to bottom. The cross-validation section is the slowest
step (it fits
len(X_DIMS) * N_FOLDS + 2GPFA models). - Fitted models and results are saved to
ISS/gpfa_results.pkl.
For the behavior-correlation section at the end, you'll need a binned_behaviors
dict (behavior name → array of shape [trials, time bins], aligned to the same
trial windows and bin size used above) — this notebook doesn't build it, so load
or construct it from your session's behavioral traces first.
- Set
SESSION_KEYand the binning/trial-window parameters the same way as inGPFA_data.ipynb(these are independent notebooks, so parameters aren't shared automatically — keep them consistent if you want comparable results). - Adjust
X_DIMSto the dimensionalities you want to sweep for decoding, andN_DECODE_FOLDS/ theKFoldsettings for cross-validation. - Run top to bottom. The multi-behavior sweep (Section 5) is the slowest step —
it fits
len(X_DIMS) * n_foldsGPFA models, each reused to decode every behavioral variable. - Behavioral signals are pulled directly from
s.data["behavior"]ands.data["physiology"]— if your session's data structure uses different keys, update thebehavior_signalsdict in Section 5 accordingly.
- Set
SESSION_KEYand binning/trial-window parameters the same way as in the other notebooks (again, independent notebooks — keep parameters in sync for comparable splits). - Adjust
X_DIMSandN_FOLDSfor the dimensionality cross-validation, andMIN_TEST_SPIKES(Section 6.3) if you want a different low-spike cutoff for evaluated held-out neurons. - Run top to bottom. Section 7's CV loop is the slowest step — it fits one
GPFA model, one linear regression, and one Poisson GLM per held-out neuron,
for every fold and every
x_dim. - After the CV curve and final per-neuron plot (Section 7.1), Section 8 runs
a robustness sweep at a fixed dimensionality (
FIXED_X_DIM, default10— update this if your CV picks a different bestx_dim) to check how sensitive the co-smoothing score is to low-firing held-in neurons, then applies a chosen cutoff (CHOSEN_CUTOFF_PERCENT) for a final filtered score.
- Update
STORAGE_VLMD_PATHandRUN_DIRin Section 3 to point at your own VLMD grid-search output (see Section 4's diagnostic cell for listing other available runs/hyperparameter configurations to swap in). - Set
SESSION_KEYand keepPRE_TRIAL_S/POST_TRIAL_S/BIN_SIZE_MSin Section 5 consistent with the run's parameters (VLMD's ownbin_sizehyperparameter should matchBIN_SIZE_MShere for the trial alignment to be meaningful). - Run top to bottom. This notebook is much faster than the other three — there's no model fitting, just loading, alignment, and one decoding pass per behavior.
- Compare the printed
vlmd_resultsR² scores againstbehavioral_decoding.ipynb's per-behavior results at a comparable dimensionality to see how VLMD stacks up against GPFA.
- The final GPFA model in
GPFA_data.ipynbisn't currently saved in a form that supports inference on new data without refitting. - Raster plots aren't yet shown alongside every latent trajectory plot.
behavioral_decoding.ipynbandGPFA_data.ipynbeach rebuildgpfa_dataandbinned_trialsindependently — if you're evaluating the same session across metrics, this means the two notebooks may not be using an identical train/test trial split unlessRANDOM_SEEDand fold settings are kept in sync.cosmoothing_bps.ipynb'sFIXED_X_DIM(used for the robustness sweep in Section 8) is hardcoded rather than automatically pulled from the CV result in Section 7 — update it manually if cross-validation picks a different best dimensionality.
ISS/gpfa_results.pkl(fromGPFA_data.ipynb) — pickled dict containing the fitted GPFA model, estimated parameters, selectedx_dim, cross-validation errors, unit selections, and the analysis settings used to produce them.behavioral_decoding.ipynbdoesn't currently save results to disk — CV R² scores per behavior/dimensionality live in thebehavior_scoresandall_resultsdicts in-notebook, and are surfaced via the summary plots.cosmoothing_bps.ipynbdoesn't currently save results to disk either — CV bits/spike per dimensionality lives incv_results, final per-neuron scores infinal_bps_per_neuron, and the robustness sweep inpercentile_results, all surfaced via the in-notebook plots.VLMD_decoding.ipynbdoesn't save results to disk — per-behavior CV R² scores live in thevlmd_resultsdict and are printed at the end of the notebook.





