Nonparametric Methods¶
The cca_zoo.nonparametric module provides kernel- and graph-based CCA methods that can capture
nonlinear relationships between views without explicitly constructing feature maps.
Background¶
Kernel methods replace the inner product \(\mathbf{x}_i^\top \mathbf{x}_j\) with a kernel function \(k(\mathbf{x}_i, \mathbf{x}_j)\), implicitly mapping data into a potentially infinite-dimensional reproducing kernel Hilbert space (RKHS). The canonical directions are found in that RKHS and expressed via the dual (kernel coefficient) representation.
Supported kernels (passed directly to sklearn.metrics.pairwise_kernels):
| Kernel | String | Key params |
|---|---|---|
| Linear | "linear" |
— |
| Polynomial | "poly" |
degree, coef0 |
| RBF / Gaussian | "rbf" |
gamma |
| Sigmoid | "sigmoid" |
gamma, coef0 |
| Custom callable | any Callable |
via kernel_params |
KCCA — Kernel CCA¶
When to use: Nonlinear two-view CCA. The kernel generalises MCCA.
Each view's kernel \(K_i\) is centred in feature space (sklearn's KernelCenterer), and a
new row's kernel against the training rows is centred with the training statistics. KCCA
is then exactly MCCA on each view's kernel feature map: with dual variables
\(\boldsymbol{\alpha}_i\) it solves
where:
- \(A\) has off-diagonal blocks \(K_i K_j / (n-1)\), the covariances between views' scores
- \(B\) has diagonal blocks \((1-c_i) K_i^2 / (n-1) + c_i K_i\): the variance of the scores \(K_i \boldsymbol{\alpha}_i\), shrunk towards the squared norm of the feature-space direction
shrinkage (\(c_i\)) means what it does for MCCA: 0 is kernel CCA and 1 kernel PLS. With
kernel="linear", KCCA, KGCCA and KTCCA are MCCA, GCCA and TCCA, and with any
kernel KCCA equals MCCA on Nystroem features that use every training row as a
landmark. For large \(n\), a Nystroem approximation with fewer landmarks followed by the
linear model is the scalable form of the same estimator.
from cca_zoo.nonparametric import KCCA
# Linear kernel (recovers classical CCA in feature space)
model = KCCA(n_components=2, kernel="linear", shrinkage=0.1).fit([X1, X2])
# RBF kernel
model = KCCA(n_components=2, kernel="rbf", gamma=0.01, shrinkage=0.1).fit([X1, X2])
# Polynomial kernel
model = KCCA(n_components=2, kernel="poly", degree=3, shrinkage=0.1).fit([X1, X2])
# Per-view kernel parameters (list = one entry per view)
model = KCCA(
n_components=2,
kernel=["rbf", "poly"],
gamma=[0.01, None],
degree=[1, 3],
shrinkage=[0.1, 0.5],
).fit([X1, X2])
Transform¶
At test time, KCCA computes kernel matrices between test and training points and projects via the fitted dual variables:
KGCCA — Kernel Generalised CCA¶
When to use: Nonlinear extension of GCCA for three or more views.
KGCCA builds a shared kernel-space latent representation analogously to GCCA:
with centred kernels and \(B_i = (1-c_i) K_i^2 / (n-1) + c_i K_i\); it is GCCA on each view's kernel feature map.
from cca_zoo.nonparametric import KGCCA
model = KGCCA(n_components=2, kernel="rbf", gamma=0.01, shrinkage=0.1).fit([X1, X2, X3])
KTCCA — Kernel Tensor CCA¶
When to use: Captures higher-order correlations in the kernel space for three or more views.
KTCCA is TCCA on each view's kernel feature map: it whitens the feature maps, builds their
cross-moment tensor and applies PARAFAC decomposition:
from cca_zoo.nonparametric import KTCCA
model = KTCCA(
n_components=2, kernel="rbf", gamma=0.01, shrinkage=0.1, random_state=0
).fit([X1, X2, X3])
ManifoldCCA — transductive manifold CCA¶
When to use: Two views share a single underlying coordinate that is embedded nonlinearly and differently in each view's raw features (e.g. two different curved/spiral parameterisations), so no linear map and no single global kernel connects the two ambient spaces well, but each view's own local (k-nearest-neighbour) structure still respects the shared ordering.
Unlike KCCA, which replaces the inner product with a global kernel, ManifoldCCA replaces the
within-view covariance with a graph operator built from that view's own local neighbourhood
structure -- the normalised graph Laplacian (\(M = I - D^{-1/2}WD^{-1/2}\), method="laplacian",
matching sklearn.manifold.SpectralEmbedding) or the LLE reconstruction operator
(\(M = (I-W)^\top(I-W)\), method="lle"). Since there's no feature map at all here, the "weight"
the joint eigenproblem solves for is each view's training-set embedding directly:
from cca_zoo.nonparametric import ManifoldCCA
model = ManifoldCCA(method="laplacian", n_neighbors=10, n_components=1).fit([X1, X2])
train_embedding = model.embedding_ # (n_train, k) per view
Before solving, every view's operator is truncated to its own n_operator_components
smallest-eigenvalue directions (default max(4 * n_components, 40)) -- the same truncation
every spectral method already applies. Set too small, it discards real manifold structure; set
very large with shrinkage near 0, the joint eigenproblem hands each view as many free
directions as training points and, like any unregularised CCA at that dimensionality-to-sample
ratio, can fabricate cross-view correlation. On two identical views with shrinkage > 0,
ManifoldCCA reduces exactly to that view's LLE embedding, and up to centring to its spectral
embedding -- the concrete check that this is the natural multiview generalisation of
LocallyLinearEmbedding and SpectralEmbedding, not an unrelated construction that happens to
reuse their graphs.
shrinkage works as for MCCA and KCCA. Each view's constraint blends its embedding's
variance with its roughness on the view's graph, \((1-c)\,\mathrm{Cov}(Z_i, Z_i) + c\,\kappa_i Z_i^\top M_i Z_i\),
where \(\kappa_i\) rescales the roughness of the kept eigenvectors to the same trace as their
variance, so that a given shrinkage means the same for the LLE operator (whose smooth
eigenvalues are tiny) and the normalised Laplacian (whose are not):
shrinkage=0 is CCA between the views' smooth graph coordinates, and shrinkage=1 is PLS,
which maximises covariance per unit of roughness and so trades correlation for smoothness. The
fit is exactly MCCA(shrinkage=c) on each view's kept operator eigenvectors, each scaled by the
inverse square root of its rescaled eigenvalue. The default, 0.1, is KCCA's.
Transform¶
There is no feature map to apply to new data, so out-of-sample projection reuses each method's own established extension rather than a generic auxiliary model:
method="lle": a new point's barycentric reconstruction weights against itsn_neighborsnearest training points, applied to those points' rows of the fitted embedding -- exactlyLocallyLinearEmbedding.transform's own mechanism (verified directly against it in the tests).method="laplacian": the classical Nystrom extension (Bengio et al. 2003) of each kept eigenvector, using the same degree-normalised affinity rule the training graph was built from, then the same combination the joint solve used at training time. The extension divides by1 - eigenvalue, so eigenvectors with eigenvalue above 0.95, barely smoother than noise, are left out of the basis rather than amplified.
Either extension returns a training point's own embedding: transform of the training views is
embedding_.
inverse_transform/predict are not supported (same limitation as KCCA): both assume a
(n_features_i, k) weight matrix, not a (n_train_samples, k) transductive embedding.
Not implemented: Hessian-LLE and LTSA (sklearn.manifold.LocallyLinearEmbedding's other two
method options) need a local Hessian/tangent-space estimate per point, substantially more
involved to get right than the Laplacian or plain LLE operator. Isomap-flavoured (geodesic
distance) regularisation is achievable today via KCCA with a precomputed geodesic Gram matrix
in place of a standard kernel.
Hyperparameter tuning¶
Kernel hyperparameters (shrinkage, gamma, degree) are best selected by cross-validation.
Use GridSearchCV from cca_zoo.model_selection:
from cca_zoo.model_selection import GridSearchCV
from cca_zoo.nonparametric import KCCA
param_grid = {
"shrinkage": [0.01, 0.1, 1.0],
"gamma": [0.001, 0.01, 0.1],
}
gs = GridSearchCV(
KCCA(n_components=2, kernel="rbf"),
param_grid=param_grid,
cv=5,
)
gs.fit([X1, X2])
print("Best params:", gs.best_params_)
best_model = gs.best_estimator_
Custom kernels¶
Pass any callable with signature k(x, y, **params) -> float. As with sklearn's
pairwise_kernels, it is called on one pair of samples at a time, and kernel_params
supplies its keyword arguments:
import numpy as np
from cca_zoo.nonparametric import KCCA
def my_kernel(x, y, sigma=1.0):
"""Gaussian kernel of two samples, with explicit sigma."""
return np.exp(-np.sum((x - y) ** 2) / (2 * sigma**2))
model = KCCA(
n_components=2,
kernel=my_kernel,
kernel_params={"sigma": 0.5},
shrinkage=0.1,
).fit([X1, X2])
Practical notes¶
- Kernel methods store the full \(n \times n\) kernel matrices. Memory is \(O(n^2)\); be cautious with \(n > 10{,}000\).
ManifoldCCAsolves a dense \((nM) \times (nM)\) generalised eigenproblem (\(n\) = training samples, \(M\) = number of views) -- the same cost profile as the kernel methods above, intended for moderate training-set sizes rather than very large \(n\).- For large datasets, prefer the linear EY-loss methods (
CCAEY,PLSEY,StochasticCCAEY) or deep methods. - The
shrinkageparameter is crucial: too small → numerical instability; too large → loss of structure. Use cross-validation (see Model Selection).