GAM & MARS Methods¶
The cca_zoo.gam module provides two spline-based nonlinear multiview CCA methods, each modelled
on the R package its users will know: GAMCCA follows mgcv, and MARSCCA (see
below) follows earth. Both are built on scikit-learn's SplineTransformer and
SciPy, with no optional dependency.
GAMCCA¶
GAMCCA gives each view a generalized additive model — \(f_i(x) = \sum_j s_{ij}(x_j)\), one smooth
per input feature — where each smooth is mgcv's P-spline, s(x, bs="ps", k=k, m=m): a
k-dimensional B-spline basis on evenly spaced knots with a difference penalty on neighbouring
coefficients (Eilers and Marx, 1996). The embeddings minimise the Eckart-Young (EY) objective
shared with CCAEY, DCCAEY and TreeCCA,
(\(C\) the mean pairwise cross-covariance including \(i = j\) terms, \(V\) the mean auto-covariance), plus each view's smoothing penalty \(\tfrac12\,\mathrm{sp}_i \sum_j \beta_{ij}^\top D^\top D \beta_{ij}\). The penalty shrinks every smooth towards a polynomial — a straight line for the default second-difference penalty — rather than towards zero.
With the basis fixed, the whole fit is one generalized eigenproblem, solved in closed form at its
global optimum: no iterations, no tolerance, no random start. It is the same solver MARSCCA
uses. A P-spline basis is rank-deficient by design — splines with no data under them are kept
for the penalty to fill in, and every feature's splines sum to a constant — so, as mgcv
resolves identifiability by reparametrisation, the problem is first rewritten exactly on the
basis's row space.
from cca_zoo.gam import GAMCCA
model = GAMCCA(n_components=2).fit([X1, X2])
z1, z2 = model.transform([X1, X2])
corr = model.score([X1, X2]) # mean canonical correlation
# mgcv-style controls, per view where useful
model = GAMCCA(k=[10, 30], m=(2, 1), sp=[0.1, 1.0]).fit([X1, X2])
Choosing the smoothing parameter¶
mgcv estimates each sp by GCV or REML, both likelihood or residual criteria with no EY-loss
counterpart, so here sp is chosen by cross-validation:
from cca_zoo.model_selection import GridSearchCV
gs = GridSearchCV(GAMCCA(), {"sp": [1e-3, 1e-2, 1e-1, 1, 10, 100]}).fit([X1, X2])
To prefer the smoothest fit whose score is statistically indistinguishable from the best, pass
a custom refit rule that picks the largest sp.
Inspecting fitted smooths¶
GAMCCA has no linear weight matrices. feature_importances_per_view_ is each smooth's variance over the
training data (normalised to sum to 1 per view), and shape_function evaluates one feature's
fitted smooth, plot.gam's partial effect:
import numpy as np
x_grid = np.linspace(X1[:, 0].min(), X1[:, 0].max(), 200)
shape = model.shape_function(view=0, feature=0, x=x_grid) # (200, n_components)
Summing every feature's shape_function at the training values reproduces
model.encoders_[view].predict().
Parameters¶
Parameters share mgcv's names and meanings for bs="ps" smooths.
| Parameter | mgcv |
Description |
|---|---|---|
k |
k |
Basis dimension: B-splines per feature. Default 10, mgcv's; raise it when a relationship needs more wiggles, and the penalty keeps the larger basis in check. Cost grows with the cube of the total basis size. Scalar or per-view list. |
m |
m |
(order, penalty order): B-splines of degree order + 1 and a penalty order-th difference penalty; a single value sets both. Default 2 (cubic, second differences), mgcv's. An int or tuple, or a list of per-view values. |
sp |
sp |
Smoothing parameter; larger is smoother. Default 0.01. mgcv estimates it; here choose it by cross-validation (above). Scalar or per-view list. |
When to use: nonlinear multiview CCA where each feature's relationship is expected to be
smooth. A GAM is additive, so it cannot represent an interaction between two features of the
same view (e.g. \(x_1 x_2\)); MARSCCA with degree >= 2, TreeCCA or GaussianProcessCCA can.
MARSCCA¶
MARSCCA swaps GAMCCA's fixed B-spline basis for a multivariate adaptive regression spline
(Friedman, 1991): each view's encoder is a linear combination of basis functions, each a product
of up to degree hinges \(\max(0, \pm(x_j - t))\), and the basis is grown rather than fixed.
Every forward step scores each candidate reflected hinge pair — any existing term (or the
constant) as parent, any feature not already in that parent, any knot earth's minspan and
endspan rules allow within the parent's support — by how much of the current EY gradient the
pair can absorb once orthogonalised against the current basis: classical MARS's
residual-sum-of-squares criterion with the residual replaced by the EY loss's negative gradient.
The best ten of those are then re-ranked by their exact loss after a refit, the criterion
earth applies to every candidate (scoring all of them exactly would take an eigenproblem
each). The best pair is added to each view in
turn, then every view's coefficients are refit jointly. On a fixed basis the ridge-EY fit is a
generalized eigenproblem — the one ridge-regularised MCCA solves — so each refit is its exact
global optimum in closed form. Knots therefore land only where the cross-view signal needs them.
With degree=1 (the default, as in R's earth) the encoder is additive, like GAMCCA but
with adaptive knots. With degree=2 a term can represent a within-view interaction such as
\(x_1 x_2\) — exactly the case the note above says GAMCCA cannot handle — while staying
inspectable term by term.
from cca_zoo.gam import MARSCCA
model = MARSCCA(n_components=1, degree=2, nk=20).fit([X1, X2])
terms = model.basis_functions(0) # e.g. ['h(x1 - 0.41)', 'h(0.41 - x1)', ...]
coefs = model.encoders_[0].coef_ # (len(terms), n_components), row m ↔ terms[m]
Products appear as e.g. 'h(x1 - 0.41) * h(x0 + 0.2)', with h(u) = max(0, u) and knots in the
raw feature units.
Pruning¶
The forward pass deliberately overshoots, so, as in R's earth, a backward pass prunes it: from
every term the forward pass added, it repeatedly deletes the term (from whichever view) whose
removal raises the refit training EY loss least, down to nprune terms in total. Every refit
being a closed-form eigenproblem, each deletion is exact: removing a term restricts that
eigenproblem by one linear constraint, so every candidate's new eigenvalues follow from one
eigendecomposition per step, the eigenvalue analogue of earth's least-squares downdates.
Unlike truncating the forward sequence, the backward pass can drop a stepping-stone term — a
lone hinge in \(x_1\), say — once the interaction it led to has taken over its job. The forward
pass itself also stops early, as earth's does, once a round lowers the loss by less than a
fraction thresh (default 0.001).
earth then picks the size by GCV, a squared-error quantity with no EY-loss counterpart. Its
alternative, choosing the size along the backward sequence by cross-validation
(pmethod="cv"), which carries over exactly as a search over nprune and, like earth, takes
the best mean score:
from cca_zoo.model_selection import GridSearchCV
gs = GridSearchCV(
MARSCCA(degree=2, nk=40), {"nprune": [2, 4, 8, 12, 16, 24, 32, 48, 80]}
).fit([X1, X2])
Without GCV's per-knot penalty the best mean can keep a few noise terms; a custom refit rule such as the one-standard-error rule trades them for a smaller model.
Variable importance¶
feature_importances_per_view_ is earth's evimp with its rss criterion, computed over the backward
pass's nested subsets from the fitted model down: each subset's decrease in EY loss over the next
smaller one is credited to every feature it uses. As for every model in the package, it is one
array per view, normalised to sum to 1 (where evimp scales the largest to 100):
Parameters share earth's names and defaults wherever earth has one. earth counts an
intercept in nk and nprune; views here are centred, so neither counts one. The one default
that differs from earth's is minspan (see the table). As elsewhere in
the package, every parameter that configures one view's basis takes a single value or a list
with one entry per view (None entries take that view's default). thresh and nprune are
global: both are judged on the joint fit across views.
| Parameter | earth |
Description |
|---|---|---|
degree |
degree |
Maximum hinge factors per term: 1 is additive (default), 2 allows pairwise interactions. Scalar or per-view list. |
nk |
nk |
Maximum terms per view in the forward pass. Default min(200, max(20, 2 * n_features)), earth's less its intercept. Scalar or per-view list. |
nprune |
nprune |
Total terms, across views, kept by the backward pass. None keeps the whole forward pass, since earth's default of choosing it by GCV has no EY counterpart — search it by cross-validation instead. |
thresh |
thresh |
Forward-pass stopping threshold (default 0.001): stop once a round lowers the loss by less than thresh times its magnitude. |
minspan, endspan |
minspan, endspan |
Knot rules within each parent's support: at least minspan points between knots, none within endspan points of either end (doubled for interaction terms, as Adjust.endspan=2). endspan=None is Friedman's formula, earth's default. minspan=0 is Friedman's spacing exactly, as in earth; the default None widens it to leave at most 20 knots per feature, which markedly helps this forward pass find interactions (held-out 0.94 against 0.61 on a pure three-way interaction). Raising minspan is also the lever for speed on large data. Scalar or per-view list. |
alpha |
— | Ridge penalty on every basis function's coefficient at unit variance, so it ignores each feature's units (CCA needs it; the EY fit is otherwise unregularised). Default 0.01. Scalar or per-view list. |