Skip to content

Add Kernel PCA decomposition - #337

Merged
josevalim merged 5 commits into
elixir-nx:mainfrom
RicardoSantos-99:add-kernel-pca
Jul 12, 2026
Merged

Add Kernel PCA decomposition#337
josevalim merged 5 commits into
elixir-nx:mainfrom
RicardoSantos-99:add-kernel-pca

Conversation

@RicardoSantos-99

Copy link
Copy Markdown
Contributor

Adds Scholar.Decomposition.KernelPCA, the kernel extension of PCA that runs
the decomposition in a reproducing kernel Hilbert space, capturing non-linear
structure that ordinary PCA cannot. Addresses the Kernel PCA item of #246.

Supports the :linear, :poly, :rbf, :sigmoid and :cosine kernels,
with fit/2, transform/2 and fit_transform/2 mirroring the existing PCA
API. The kernel matrix is double-centered, decomposed with Nx.LinAlg.eigh,
and the eigenvectors are sign-flipped for deterministic output.

A few numerical decisions worth calling out:

  • The centered kernel is explicitly symmetrized before the decomposition,
    since centering can leave asymmetries of a few ULPs and eigh requires
    exact symmetry.
  • eigh can stop before the eigenvalues fully converge while the
    eigenvectors are already accurate, so the eigenvalues are recomputed as
    the Rayleigh quotient of the renormalized eigenvectors. On a 25-sample
    dataset this brings them from a few percent off to within 1.0e-10 of
    scikit-learn (covered by a regression test).
  • The eigh tolerance follows the input precision (f32/f64): a tolerance
    below the floating-point resolution makes the iteration accumulate
    rounding noise past convergence.
  • Zero and negative eigenvalues (floating-point noise, or indefinite
    kernels such as sigmoid) are clipped and their components project to
    zero, as in scikit-learn.

Verification

Results match sklearn.decomposition.KernelPCA on all five kernels for
eigenvalues, fit_transform and transform, including custom
gamma/degree, zero-norm rows with the cosine kernel, and
num_components == num_samples. f32 agrees to roughly 1.0e-4 and f64 to
roughly 1.0e-5 or better. The reference values in the tests were generated
with scikit-learn 1.6.1.

Implements Scholar.Decomposition.KernelPCA, the kernel extension of PCA that
runs the decomposition in a reproducing kernel Hilbert space, capturing
non-linear structure that ordinary PCA cannot.

Supports the linear, poly, rbf, sigmoid and cosine kernels, with fit/2,
transform/2 and fit_transform/2 mirroring the existing PCA API. The kernel
matrix is double-centered, decomposed with Nx.LinAlg.eigh, and the
eigenvectors are sign-flipped for deterministic output.

Results match scikit-learn's KernelPCA on all five kernels (covered by the
tests). Addresses the Kernel PCA item of elixir-nx#246.
Three issues found while reviewing the initial implementation:

  * transform/2 now validates the input has the same number of features used
    to fit the model, matching PCA's own validation, instead of raising an
    Nx-internal shape error.

  * The cosine kernel no longer divides by zero for all-zero rows; it treats
    them as zero similarity, matching scikit-learn's normalize().

  * Centering the kernel matrix can leave it asymmetric by a few ULPs, which
    Nx.LinAlg.eigh rejects outright since it requires exact symmetry (this
    could crash fit/2 depending on kernel/gamma/dataset). The centered
    kernel is now explicitly symmetrized before the decomposition.

Also expands test coverage: transform/2 and eigenvalues for poly/sigmoid/
cosine (previously only checked via fit_transform/2), custom gamma and
degree, the num_components == num_samples boundary, default kernel/gamma,
eigenvalue ordering, and regression tests for the three fixes above.
Verifying every output against scikit-learn surfaced two more issues:

  * With num_components close to num_samples, the smallest eigenvalue of the
    centered kernel is zero up to floating-point noise (the centering forces
    it), and could come out slightly negative, crashing Nx.sqrt in
    fit_transform/2. Indefinite kernels such as sigmoid can also produce
    genuinely negative eigenvalues. They are now clipped to zero and their
    components project to zero, as in scikit-learn.

  * The eps previously passed to Nx.LinAlg.eigh (1.0e-8) stopped the
    iteration too early for f64 inputs, leaving eigenpair residuals around
    1.0e-4 where LAPACK reaches 1.0e-16, while tighter tolerances made f32
    accumulate rounding noise past convergence. The tolerance now matches
    the input precision (1.0e-8 for f32, 1.0e-11 for f64), which brings the
    f64 projections within 1.0e-5 of the SciPy reference in the
    worst-conditioned case measured (500x better than before).

Also validates gamma as a positive number and accepts integers for gamma
and coef0, following the option style used elsewhere in Scholar.
Verifying against scikit-learn on a larger dataset (25 samples) exposed
that Nx.LinAlg.eigh can stop before the eigenvalues converge: the leading
eigenvalue was exact, but the following ones came out up to a few percent
too small (their eigenpair residuals were around 1.0e-2 where the leading
one was at 1.0e-6), throwing fit_transform/2 off by the same margin. The
6-sample data used by the other tests was too small to show this.

The eigenvectors themselves were accurate, so the eigenvalues are now
recomputed as the Rayleigh quotient of the renormalized eigenvectors,
whose error is quadratic in the eigenvector error. On the 25-sample data
this brings the eigenvalues from an absolute error of 2.7 (poly kernel)
to 1.0e-10 against scikit-learn, and fit_transform from 0.3 to 1.0e-5.

Adds a regression test with the 25-sample dataset and refreshes the
doctest values, which moved by a few ULPs.
The component order comes from sorting the raw eigh eigenvalues, but the
final values are the refined ones, so two eigenvalues that eigh resolved
within its error could end up out of decreasing order, breaking the
documented invariant. They are sorted again after the refinement.

Also documents the kernel option fields stored in the struct.
@josevalim
josevalim merged commit 0e3d37d into elixir-nx:main Jul 12, 2026
2 checks passed
@josevalim

Copy link
Copy Markdown
Contributor

💚 💙 💜 💛 ❤️

@RicardoSantos-99 RicardoSantos-99 mentioned this pull request Jul 16, 2026
2 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants