Frequently Asked Questions#
This page answers common questions about SPD Learn and working with SPD matrices.
General Questions#
What is an SPD matrix?#
A Symmetric Positive Definite (SPD) matrix is a square matrix \(X\) that satisfies:
Symmetry: \(X = X^\top\)
Positive definiteness: \(z^\top X z > 0\) for all non-zero vectors \(z\)
Equivalently, all eigenvalues of an SPD matrix are strictly positive.
Common examples:
Covariance matrices of multivariate data
Correlation matrices
Diffusion tensors in medical imaging
Kernel matrices in machine learning
Why use SPD-specific neural networks?#
SPD matrices lie on a Riemannian manifold, not a flat Euclidean space. Standard neural network operations (like addition, scaling) can produce matrices that are no longer SPD, leading to:
Numerical instability (negative eigenvalues)
Loss of geometric structure
Suboptimal learning
SPD Learn’s layers (BiMap, ReEig, LogEig) are designed to:
Preserve positive definiteness throughout the network
Respect the manifold geometry for more principled learning
Enable stable gradient computation through eigenvalue operations
How is SPD Learn different from pyRiemann?#
pyRiemann focuses on classical Riemannian methods:
Riemannian classifiers (MDM, FgMDM)
Tangent space projections
Geodesic operations
scikit-learn compatible transformers
SPD Learn focuses on deep learning:
Neural network layers for SPD manifolds
End-to-end differentiable architectures
GPU acceleration via PyTorch
Integration with modern DL frameworks (Braindecode, skorch)
Since pyRiemann (>= 0.12) adopted the Python Array API, the two are no longer
merely complementary — SPD Learn builds on pyRiemann. pyRiemann is a core
dependency that runs natively on PyTorch tensors with autograd; SPD Learn
delegates the geometry it does not need to keep numerically special — geodesics,
parallel transport, and Fréchet derivatives — to it, and also re-exports
pyRiemann’s broader geometry toolkit from spd_learn.functional. On top
of that shared backend SPD Learn adds:
nn.Modulelayers (BiMap, ReEig, LogEig, SPD batch norm, LieBN, …)Numerically-stable, custom-autograd matrix-function primitives, distances, and means that keep gradients well-behaved (and forwards finite) near clustered or near-singular eigenvalues — where the generic
eighbackward and unclampedlogare unstable — essential for training SPDNets.
So pyRiemann remains the place to look for the broader classical toolkit
(MDM/FgMDM classifiers, tangent-space pipelines, pyriemann.datasets,
estimators, and many more metrics) — all of which interoperate with SPD Learn
on the same PyTorch tensors.
Technical Questions#
How do I ensure my input is SPD?#
If starting from raw covariance estimates, they may not be strictly SPD due to:
Numerical precision issues
Insufficient samples
Rank deficiency
Solutions:
Add regularization (Ledoit-Wolf shrinkage):
from spd_learn.modules import Shrinkage shrinkage = Shrinkage(n_chans=64, init_shrinkage=0.1, learnable=True) X_reg = shrinkage(X)
Use ReEig to clamp eigenvalues:
from spd_learn.modules import ReEig reeig = ReEig(threshold=1e-4) X_spd = reeig(X)
Add small identity matrix:
X_spd = X + 1e-5 * torch.eye(X.shape[-1])
Why am I getting NaN values during training?#
NaN values typically occur due to:
Negative eigenvalues: Use
ReEigwith appropriate thresholdreeig = ReEig(threshold=1e-4) # Increase threshold if still getting NaNs
Eigenvalue decomposition instability: Enable autograd mode
logeig = LogEig(autograd=True) # LogEig with more stable backward pass
Learning rate too high: Reduce learning rate
optimizer = torch.optim.Adam(model.parameters(), lr=1e-4)
Poorly conditioned matrices: Add regularization or shrinkage
How do I handle different matrix sizes?#
If you have matrices of varying sizes (e.g., different channel counts):
Pad smaller matrices:
import torch.nn.functional as F # Pad to max_size X_padded = F.pad(X, (0, max_size - X.shape[-1], 0, max_size - X.shape[-2]))
Use
BiMapIncreaseDimto expand dimensions:from spd_learn.modules import BiMapIncreaseDim expand = BiMapIncreaseDim(in_features=16, out_features=32)
Create separate models for different sizes
What’s the difference between LogEig and tangent space projection?#
Both map SPD matrices to Euclidean space, but:
LogEig (matrix logarithm):
Maps to the space of symmetric matrices
\(\logeig(X) = U \log(\Lambda) U^\top\)
Preserves more structure, but requires eigendecomposition
Tangent space projection (at identity or reference):
Projects to tangent space at a reference point
\(\Log{\I}(X) = \log(X)\) (at identity)
\(\Log{\frechet}(X) = \frechet^{-1/2} \log(\frechet^{-1/2} X \frechet^{-1/2}) \frechet^{-1/2}\) (at \(\frechet\))
In SPD Learn, LogEig implements the matrix logarithm. For tangent space
at a reference (like batch mean), use SPDBatchNormMeanVar first.
Model-Specific Questions#
When should I use filter banks vs. raw EEG?#
Use filter banks (TensorCSPNet) when:
You know relevant frequency bands (e.g., mu/beta for motor imagery)
You want explicit frequency decomposition
You have sufficient data for the larger model
Use raw EEG (EEGSPDNet, GREEN, MAtt) when:
You want the model to learn frequency representations
You prefer end-to-end learning
You have limited prior knowledge about discriminative frequencies
How do I choose BiMap dimensions?#
The BiMap layer reduces dimensionality: (n, n) → (m, m) where m < n.
Guidelines:
Start with
m = n // 2(50% reduction)For small datasets: more aggressive reduction (prevent overfitting)
For large datasets: preserve more dimensions
Multiple BiMap layers: gradual reduction (e.g., 64 → 32 → 16)
# Single layer
model = SPDNet(n_chans=64, subspacedim=32)
# Progressive reduction in EEGSPDNet
model = EEGSPDNet(n_chans=22, bimap_sizes=(2, 3)) # 220→110→55→27
How does SPDBatchNormMeanVar enable domain adaptation?#
SPDBatchNormMeanVar maintains running statistics (Fréchet mean) that can be
domain-specific:
Training: Learns normalization from source domain
Adaptation: Updates running mean on target domain (unlabeled)
Inference: Uses adapted statistics
This enables Source-Free Unsupervised Domain Adaptation (SFUDA) where you adapt to a new domain without source data.
Performance Questions#
How can I speed up training?#
Use GPU:
model = model.cuda() X = X.cuda()
Increase batch size (if memory allows):
batch_size = 64 # or higher
Use autograd=False for
LogEig(faster but less stable):logeig = LogEig(autograd=False)
Reduce model complexity:
model = SPDNet(n_chans=64, subspacedim=16) # Smaller subspace
How much GPU memory do SPD models need?#
Memory scales with:
Matrix size: \(O(n^2)\) for
n x nmatricesBatch size: Linear scaling
Eigendecomposition: Requires temporary storage
Typical requirements (22-channel EEG, batch_size=32):
SPDNet: ~500 MB
TensorCSPNet: ~1 GB
MAtt: ~1.5 GB
Reduce memory:
# Gradient checkpointing
from torch.utils.checkpoint import checkpoint
# Mixed precision (PyTorch 2.0+)
with torch.autocast("cuda"):
output = model(X)
Why is my model not learning?#
Common issues:
Data not SPD: Verify inputs are positive definite
eigvals = torch.linalg.eigvalsh(X) assert (eigvals > 0).all(), "Not SPD!"
Learning rate: Try different rates (1e-3, 1e-4, 1e-5)
Initialization: Check
BiMapweights are orthogonalRegularization: Add dropout or weight decay
optimizer = torch.optim.AdamW(model.parameters(), weight_decay=1e-4)
Data preprocessing: Normalize/standardize inputs
Integration Questions#
How do I use SPD Learn with MOABB?#
from moabb.datasets import BNCI2014_001
from moabb.paradigms import MotorImagery
from braindecode import EEGClassifier
from spd_learn.models import SPDNet
# Load data
dataset = BNCI2014_001()
paradigm = MotorImagery()
X, y, meta = paradigm.get_data(dataset, subjects=[1])
# Create classifier
clf = EEGClassifier(
SPDNet(n_chans=22, n_outputs=4),
criterion=torch.nn.CrossEntropyLoss,
optimizer=torch.optim.Adam,
batch_size=32,
max_epochs=100,
)
clf.fit(X, y)
How do I use SPD Learn with Nilearn?#
from nilearn.connectome import ConnectivityMeasure
from spd_learn.models import SPDNet
# Compute connectivity matrices
conn = ConnectivityMeasure(kind="covariance")
X = conn.fit_transform(time_series_list) # (n_subjects, n_rois, n_rois)
# Convert to tensor
X_tensor = torch.tensor(X, dtype=torch.float32)
# Create model for pre-computed covariance
model = SPDNet(n_chans=X.shape[1], n_outputs=2, input_type="cov")
Can I use SPD Learn with scikit-learn pipelines?#
Yes, via Braindecode’s EEGClassifier or custom wrappers:
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
from braindecode import EEGClassifier
pipe = Pipeline([("clf", EEGClassifier(SPDNet(n_chans=22, n_outputs=4)))])
# Cross-validation
from sklearn.model_selection import cross_val_score
scores = cross_val_score(pipe, X, y, cv=5)
Getting Help#
If your question isn’t answered here:
Check the User Guide for conceptual explanations
Check the API Reference for detailed function documentation
Browse the Applied Examples for code examples
Open an issue on GitHub