CoolFace
Apppublic

Aluode/PerceptionLabPortable

sourceHugging Faceupdated 9mo agoView on Hugging Face
0likes
test_kernel_pca.py567 linesDownload Raw Back to tests
1import warnings
2
3import numpy as np
4import pytest
5
6import sklearn
7from sklearn.datasets import load_iris, make_blobs, make_circles
8from sklearn.decomposition import PCA, KernelPCA
9from sklearn.exceptions import NotFittedError
10from sklearn.linear_model import Perceptron
11from sklearn.metrics.pairwise import rbf_kernel
12from sklearn.model_selection import GridSearchCV
13from sklearn.pipeline import Pipeline
14from sklearn.preprocessing import StandardScaler
15from sklearn.utils._testing import (
16    assert_allclose,
17    assert_array_almost_equal,
18    assert_array_equal,
19)
20from sklearn.utils.fixes import CSR_CONTAINERS
21from sklearn.utils.validation import _check_psd_eigenvalues
22
23
24def test_kernel_pca(global_random_seed):
25    """Nominal test for all solvers and all known kernels + a custom one
26
27    It tests
28     - that fit_transform is equivalent to fit+transform
29     - that the shapes of transforms and inverse transforms are correct
30    """
31    rng = np.random.RandomState(global_random_seed)
32    X_fit = rng.random_sample((5, 4))
33    X_pred = rng.random_sample((2, 4))
34
35    def histogram(x, y, **kwargs):
36        # Histogram kernel implemented as a callable.
37        assert kwargs == {}  # no kernel_params that we didn't ask for
38        return np.minimum(x, y).sum()
39
40    for eigen_solver in ("auto", "dense", "arpack", "randomized"):
41        for kernel in ("linear", "rbf", "poly", histogram):
42            # histogram kernel produces singular matrix inside linalg.solve
43            # XXX use a least-squares approximation?
44            inv = not callable(kernel)
45
46            # transform fit data
47            kpca = KernelPCA(
48                4, kernel=kernel, eigen_solver=eigen_solver, fit_inverse_transform=inv
49            )
50            X_fit_transformed = kpca.fit_transform(X_fit)
51            X_fit_transformed2 = kpca.fit(X_fit).transform(X_fit)
52            assert_array_almost_equal(
53                np.abs(X_fit_transformed), np.abs(X_fit_transformed2)
54            )
55
56            # non-regression test: previously, gamma would be 0 by default,
57            # forcing all eigenvalues to 0 under the poly kernel
58            assert X_fit_transformed.size != 0
59
60            # transform new data
61            X_pred_transformed = kpca.transform(X_pred)
62            assert X_pred_transformed.shape[1] == X_fit_transformed.shape[1]
63
64            # inverse transform
65            if inv:
66                X_pred2 = kpca.inverse_transform(X_pred_transformed)
67                assert X_pred2.shape == X_pred.shape
68
69
70def test_kernel_pca_invalid_parameters():
71    """Check that kPCA raises an error if the parameters are invalid
72
73    Tests fitting inverse transform with a precomputed kernel raises a
74    ValueError.
75    """
76    estimator = KernelPCA(
77        n_components=10, fit_inverse_transform=True, kernel="precomputed"
78    )
79    err_ms = "Cannot fit_inverse_transform with a precomputed kernel"
80    with pytest.raises(ValueError, match=err_ms):
81        estimator.fit(np.random.randn(10, 10))
82
83
84def test_kernel_pca_consistent_transform(global_random_seed):
85    """Check robustness to mutations in the original training array
86
87    Test that after fitting a kPCA model, it stays independent of any
88    mutation of the values of the original data object by relying on an
89    internal copy.
90    """
91    # X_fit_ needs to retain the old, unmodified copy of X
92    state = np.random.RandomState(global_random_seed)
93    X = state.rand(10, 10)
94    kpca = KernelPCA(random_state=state).fit(X)
95    transformed1 = kpca.transform(X)
96
97    X_copy = X.copy()
98    X[:, 0] = 666
99    transformed2 = kpca.transform(X_copy)
100    assert_array_almost_equal(transformed1, transformed2)
101
102
103def test_kernel_pca_deterministic_output(global_random_seed):
104    """Test that Kernel PCA produces deterministic output
105
106    Tests that the same inputs and random state produce the same output.
107    """
108    rng = np.random.RandomState(global_random_seed)
109    X = rng.rand(10, 10)
110    eigen_solver = ("arpack", "dense")
111
112    for solver in eigen_solver:
113        transformed_X = np.zeros((20, 2))
114        for i in range(20):
115            kpca = KernelPCA(n_components=2, eigen_solver=solver, random_state=rng)
116            transformed_X[i, :] = kpca.fit_transform(X)[0]
117        assert_allclose(transformed_X, np.tile(transformed_X[0, :], 20).reshape(20, 2))
118
119
120@pytest.mark.parametrize("csr_container", CSR_CONTAINERS)
121def test_kernel_pca_sparse(csr_container, global_random_seed):
122    """Test that kPCA works on a sparse data input.
123
124    Same test as ``test_kernel_pca except inverse_transform`` since it's not
125    implemented for sparse matrices.
126    """
127    rng = np.random.RandomState(global_random_seed)
128    X_fit = csr_container(rng.random_sample((5, 4)))
129    X_pred = csr_container(rng.random_sample((2, 4)))
130
131    for eigen_solver in ("auto", "arpack", "randomized"):
132        for kernel in ("linear", "rbf", "poly"):
133            # transform fit data
134            kpca = KernelPCA(
135                4,
136                kernel=kernel,
137                eigen_solver=eigen_solver,
138                fit_inverse_transform=False,
139                random_state=0,
140            )
141            X_fit_transformed = kpca.fit_transform(X_fit)
142            X_fit_transformed2 = kpca.fit(X_fit).transform(X_fit)
143            assert_array_almost_equal(
144                np.abs(X_fit_transformed), np.abs(X_fit_transformed2)
145            )
146
147            # transform new data
148            X_pred_transformed = kpca.transform(X_pred)
149            assert X_pred_transformed.shape[1] == X_fit_transformed.shape[1]
150
151            # inverse transform: not available for sparse matrices
152            # XXX: should we raise another exception type here? For instance:
153            # NotImplementedError.
154            with pytest.raises(NotFittedError):
155                kpca.inverse_transform(X_pred_transformed)
156
157
158@pytest.mark.parametrize("solver", ["auto", "dense", "arpack", "randomized"])
159@pytest.mark.parametrize("n_features", [4, 10])
160def test_kernel_pca_linear_kernel(solver, n_features, global_random_seed):
161    """Test that kPCA with linear kernel is equivalent to PCA for all solvers.
162
163    KernelPCA with linear kernel should produce the same output as PCA.
164    """
165    rng = np.random.RandomState(global_random_seed)
166    X_fit = rng.random_sample((5, n_features))
167    X_pred = rng.random_sample((2, n_features))
168
169    # for a linear kernel, kernel PCA should find the same projection as PCA
170    # modulo the sign (direction)
171    # fit only the first four components: fifth is near zero eigenvalue, so
172    # can be trimmed due to roundoff error
173    n_comps = 3 if solver == "arpack" else 4
174    assert_array_almost_equal(
175        np.abs(KernelPCA(n_comps, eigen_solver=solver).fit(X_fit).transform(X_pred)),
176        np.abs(
177            PCA(n_comps, svd_solver=solver if solver != "dense" else "full")
178            .fit(X_fit)
179            .transform(X_pred)
180        ),
181    )
182
183
184def test_kernel_pca_n_components():
185    """Test that `n_components` is correctly taken into account for projections
186
187    For all solvers this tests that the output has the correct shape depending
188    on the selected number of components.
189    """
190    rng = np.random.RandomState(0)
191    X_fit = rng.random_sample((5, 4))
192    X_pred = rng.random_sample((2, 4))
193
194    for eigen_solver in ("dense", "arpack", "randomized"):
195        for c in [1, 2, 4]:
196            kpca = KernelPCA(n_components=c, eigen_solver=eigen_solver)
197            shape = kpca.fit(X_fit).transform(X_pred).shape
198
199            assert shape == (2, c)
200
201
202def test_remove_zero_eig():
203    """Check that the ``remove_zero_eig`` parameter works correctly.
204
205    Tests that the null-space (Zero) eigenvalues are removed when
206    remove_zero_eig=True, whereas they are not by default.
207    """
208    X = np.array([[1 - 1e-30, 1], [1, 1], [1, 1 - 1e-20]])
209
210    # n_components=None (default) => remove_zero_eig is True
211    kpca = KernelPCA()
212    Xt = kpca.fit_transform(X)
213    assert Xt.shape == (3, 0)
214
215    kpca = KernelPCA(n_components=2)
216    Xt = kpca.fit_transform(X)
217    assert Xt.shape == (3, 2)
218
219    kpca = KernelPCA(n_components=2, remove_zero_eig=True)
220    Xt = kpca.fit_transform(X)
221    assert Xt.shape == (3, 0)
222
223
224def test_leave_zero_eig():
225    """Non-regression test for issue #12141 (PR #12143)
226
227    This test checks that fit().transform() returns the same result as
228    fit_transform() in case of non-removed zero eigenvalue.
229    """
230    X_fit = np.array([[1, 1], [0, 0]])
231
232    # Assert that even with all np warnings on, there is no div by zero warning
233    with warnings.catch_warnings():
234        # There might be warnings about the kernel being badly conditioned,
235        # but there should not be warnings about division by zero.
236        # (Numpy division by zero warning can have many message variants, but
237        # at least we know that it is a RuntimeWarning so lets check only this)
238        warnings.simplefilter("error", RuntimeWarning)
239        with np.errstate(all="warn"):
240            k = KernelPCA(n_components=2, remove_zero_eig=False, eigen_solver="dense")
241            # Fit, then transform
242            A = k.fit(X_fit).transform(X_fit)
243            # Do both at once
244            B = k.fit_transform(X_fit)
245            # Compare
246            assert_array_almost_equal(np.abs(A), np.abs(B))
247
248
249def test_kernel_pca_precomputed(global_random_seed):
250    """Test that kPCA works with a precomputed kernel, for all solvers"""
251    rng = np.random.RandomState(global_random_seed)
252    X_fit = rng.random_sample((5, 4))
253    X_pred = rng.random_sample((2, 4))
254
255    for eigen_solver in ("dense", "arpack", "randomized"):
256        X_kpca = (
257            KernelPCA(4, eigen_solver=eigen_solver, random_state=0)
258            .fit(X_fit)
259            .transform(X_pred)
260        )
261
262        X_kpca2 = (
263            KernelPCA(
264                4, eigen_solver=eigen_solver, kernel="precomputed", random_state=0
265            )
266            .fit(np.dot(X_fit, X_fit.T))
267            .transform(np.dot(X_pred, X_fit.T))
268        )
269
270        X_kpca_train = KernelPCA(
271            4, eigen_solver=eigen_solver, kernel="precomputed", random_state=0
272        ).fit_transform(np.dot(X_fit, X_fit.T))
273
274        X_kpca_train2 = (
275            KernelPCA(
276                4, eigen_solver=eigen_solver, kernel="precomputed", random_state=0
277            )
278            .fit(np.dot(X_fit, X_fit.T))
279            .transform(np.dot(X_fit, X_fit.T))
280        )
281
282        assert_array_almost_equal(np.abs(X_kpca), np.abs(X_kpca2))
283
284        assert_array_almost_equal(np.abs(X_kpca_train), np.abs(X_kpca_train2))
285
286
287@pytest.mark.parametrize("solver", ["auto", "dense", "arpack", "randomized"])
288def test_kernel_pca_precomputed_non_symmetric(solver):
289    """Check that the kernel centerer works.
290
291    Tests that a non symmetric precomputed kernel is actually accepted
292    because the kernel centerer does its job correctly.
293    """
294
295    # a non symmetric gram matrix
296    K = [[1, 2], [3, 40]]
297    kpca = KernelPCA(
298        kernel="precomputed", eigen_solver=solver, n_components=1, random_state=0
299    )
300    kpca.fit(K)  # no error
301
302    # same test with centered kernel
303    Kc = [[9, -9], [-9, 9]]
304    kpca_c = KernelPCA(
305        kernel="precomputed", eigen_solver=solver, n_components=1, random_state=0
306    )
307    kpca_c.fit(Kc)
308
309    # comparison between the non-centered and centered versions
310    assert_array_equal(kpca.eigenvectors_, kpca_c.eigenvectors_)
311    assert_array_equal(kpca.eigenvalues_, kpca_c.eigenvalues_)
312
313
314def test_gridsearch_pipeline():
315    """Check that kPCA works as expected in a grid search pipeline
316
317    Test if we can do a grid-search to find parameters to separate
318    circles with a perceptron model.
319    """
320    X, y = make_circles(n_samples=400, factor=0.3, noise=0.05, random_state=0)
321    kpca = KernelPCA(kernel="rbf", n_components=2)
322    pipeline = Pipeline([("kernel_pca", kpca), ("Perceptron", Perceptron(max_iter=5))])
323    param_grid = dict(kernel_pca__gamma=2.0 ** np.arange(-2, 2))
324    grid_search = GridSearchCV(pipeline, cv=3, param_grid=param_grid)
325    grid_search.fit(X, y)
326    assert grid_search.best_score_ == 1
327
328
329def test_gridsearch_pipeline_precomputed():
330    """Check that kPCA works as expected in a grid search pipeline (2)
331
332    Test if we can do a grid-search to find parameters to separate
333    circles with a perceptron model. This test uses a precomputed kernel.
334    """
335    X, y = make_circles(n_samples=400, factor=0.3, noise=0.05, random_state=0)
336    kpca = KernelPCA(kernel="precomputed", n_components=2)
337    pipeline = Pipeline([("kernel_pca", kpca), ("Perceptron", Perceptron(max_iter=5))])
338    param_grid = dict(Perceptron__max_iter=np.arange(1, 5))
339    grid_search = GridSearchCV(pipeline, cv=3, param_grid=param_grid)
340    X_kernel = rbf_kernel(X, gamma=2.0)
341    grid_search.fit(X_kernel, y)
342    assert grid_search.best_score_ == 1
343
344
345def test_nested_circles():
346    """Check that kPCA projects in a space where nested circles are separable
347
348    Tests that 2D nested circles become separable with a perceptron when
349    projected in the first 2 kPCA using an RBF kernel, while raw samples
350    are not directly separable in the original space.
351    """
352    X, y = make_circles(n_samples=400, factor=0.3, noise=0.05, random_state=0)
353
354    # 2D nested circles are not linearly separable
355    train_score = Perceptron(max_iter=5).fit(X, y).score(X, y)
356    assert train_score < 0.8
357
358    # Project the circles data into the first 2 components of a RBF Kernel
359    # PCA model.
360    # Note that the gamma value is data dependent. If this test breaks
361    # and the gamma value has to be updated, the Kernel PCA example will
362    # have to be updated too.
363    kpca = KernelPCA(
364        kernel="rbf", n_components=2, fit_inverse_transform=True, gamma=2.0
365    )
366    X_kpca = kpca.fit_transform(X)
367
368    # The data is perfectly linearly separable in that space
369    train_score = Perceptron(max_iter=5).fit(X_kpca, y).score(X_kpca, y)
370    assert train_score == 1.0
371
372
373def test_kernel_conditioning():
374    """Check that ``_check_psd_eigenvalues`` is correctly called in kPCA
375
376    Non-regression test for issue #12140 (PR #12145).
377    """
378
379    # create a pathological X leading to small non-zero eigenvalue
380    X = [[5, 1], [5 + 1e-8, 1e-8], [5 + 1e-8, 0]]
381    kpca = KernelPCA(kernel="linear", n_components=2, fit_inverse_transform=True)
382    kpca.fit(X)
383
384    # check that the small non-zero eigenvalue was correctly set to zero
385    assert kpca.eigenvalues_.min() == 0
386    assert np.all(kpca.eigenvalues_ == _check_psd_eigenvalues(kpca.eigenvalues_))
387
388
389@pytest.mark.parametrize("solver", ["auto", "dense", "arpack", "randomized"])
390def test_precomputed_kernel_not_psd(solver):
391    """Check how KernelPCA works with non-PSD kernels depending on n_components
392
393    Tests for all methods what happens with a non PSD gram matrix (this
394    can happen in an isomap scenario, or with custom kernel functions, or
395    maybe with ill-posed datasets).
396
397    When ``n_component`` is large enough to capture a negative eigenvalue, an
398    error should be raised. Otherwise, KernelPCA should run without error
399    since the negative eigenvalues are not selected.
400    """
401
402    # a non PSD kernel with large eigenvalues, already centered
403    # it was captured from an isomap call and multiplied by 100 for compacity
404    K = [
405        [4.48, -1.0, 8.07, 2.33, 2.33, 2.33, -5.76, -12.78],
406        [-1.0, -6.48, 4.5, -1.24, -1.24, -1.24, -0.81, 7.49],
407        [8.07, 4.5, 15.48, 2.09, 2.09, 2.09, -11.1, -23.23],
408        [2.33, -1.24, 2.09, 4.0, -3.65, -3.65, 1.02, -0.9],
409        [2.33, -1.24, 2.09, -3.65, 4.0, -3.65, 1.02, -0.9],
410        [2.33, -1.24, 2.09, -3.65, -3.65, 4.0, 1.02, -0.9],
411        [-5.76, -0.81, -11.1, 1.02, 1.02, 1.02, 4.86, 9.75],
412        [-12.78, 7.49, -23.23, -0.9, -0.9, -0.9, 9.75, 21.46],
413    ]
414    # this gram matrix has 5 positive eigenvalues and 3 negative ones
415    # [ 52.72,   7.65,   7.65,   5.02,   0.  ,  -0.  ,  -6.13, -15.11]
416
417    # 1. ask for enough components to get a significant negative one
418    kpca = KernelPCA(kernel="precomputed", eigen_solver=solver, n_components=7)
419    # make sure that the appropriate error is raised
420    with pytest.raises(ValueError, match="There are significant negative eigenvalues"):
421        kpca.fit(K)
422
423    # 2. ask for a small enough n_components to get only positive ones
424    kpca = KernelPCA(kernel="precomputed", eigen_solver=solver, n_components=2)
425    if solver == "randomized":
426        # the randomized method is still inconsistent with the others on this
427        # since it selects the eigenvalues based on the largest 2 modules, not
428        # on the largest 2 values.
429        #
430        # At least we can ensure that we return an error instead of returning
431        # the wrong eigenvalues
432        with pytest.raises(
433            ValueError, match="There are significant negative eigenvalues"
434        ):
435            kpca.fit(K)
436    else:
437        # general case: make sure that it works
438        kpca.fit(K)
439
440
441@pytest.mark.parametrize("n_components", [4, 10, 20])
442def test_kernel_pca_solvers_equivalence(n_components):
443    """Check that 'dense' 'arpack' & 'randomized' solvers give similar results"""
444
445    # Generate random data
446    n_train, n_test = 1_000, 100
447    X, _ = make_circles(
448        n_samples=(n_train + n_test), factor=0.3, noise=0.05, random_state=0
449    )
450    X_fit, X_pred = X[:n_train, :], X[n_train:, :]
451
452    # reference (full)
453    ref_pred = (
454        KernelPCA(n_components, eigen_solver="dense", random_state=0)
455        .fit(X_fit)
456        .transform(X_pred)
457    )
458
459    # arpack
460    a_pred = (
461        KernelPCA(n_components, eigen_solver="arpack", random_state=0)
462        .fit(X_fit)
463        .transform(X_pred)
464    )
465    # check that the result is still correct despite the approx
466    assert_array_almost_equal(np.abs(a_pred), np.abs(ref_pred))
467
468    # randomized
469    r_pred = (
470        KernelPCA(n_components, eigen_solver="randomized", random_state=0)
471        .fit(X_fit)
472        .transform(X_pred)
473    )
474    # check that the result is still correct despite the approximation
475    assert_array_almost_equal(np.abs(r_pred), np.abs(ref_pred))
476
477
478def test_kernel_pca_inverse_transform_reconstruction():
479    """Test if the reconstruction is a good approximation.
480
481    Note that in general it is not possible to get an arbitrarily good
482    reconstruction because of kernel centering that does not
483    preserve all the information of the original data.
484    """
485    X, *_ = make_blobs(n_samples=100, n_features=4, random_state=0)
486
487    kpca = KernelPCA(
488        n_components=20, kernel="rbf", fit_inverse_transform=True, alpha=1e-3
489    )
490    X_trans = kpca.fit_transform(X)
491    X_reconst = kpca.inverse_transform(X_trans)
492    assert np.linalg.norm(X - X_reconst) / np.linalg.norm(X) < 1e-1
493
494
495def test_kernel_pca_raise_not_fitted_error():
496    X = np.random.randn(15).reshape(5, 3)
497    kpca = KernelPCA()
498    kpca.fit(X)
499    with pytest.raises(NotFittedError):
500        kpca.inverse_transform(X)
501
502
503def test_32_64_decomposition_shape():
504    """Test that the decomposition is similar for 32 and 64 bits data
505
506    Non regression test for
507    https://github.com/scikit-learn/scikit-learn/issues/18146
508    """
509    X, y = make_blobs(
510        n_samples=30, centers=[[0, 0, 0], [1, 1, 1]], random_state=0, cluster_std=0.1
511    )
512    X = StandardScaler().fit_transform(X)
513    X -= X.min()
514
515    # Compare the shapes (corresponds to the number of non-zero eigenvalues)
516    kpca = KernelPCA()
517    assert kpca.fit_transform(X).shape == kpca.fit_transform(X.astype(np.float32)).shape
518
519
520def test_kernel_pca_feature_names_out():
521    """Check feature names out for KernelPCA."""
522    X, *_ = make_blobs(n_samples=100, n_features=4, random_state=0)
523    kpca = KernelPCA(n_components=2).fit(X)
524
525    names = kpca.get_feature_names_out()
526    assert_array_equal([f"kernelpca{i}" for i in range(2)], names)
527
528
529def test_kernel_pca_inverse_correct_gamma(global_random_seed):
530    """Check that gamma is set correctly when not provided.
531
532    Non-regression test for #26280
533    """
534    rng = np.random.RandomState(global_random_seed)
535    X = rng.random_sample((5, 4))
536
537    kwargs = {
538        "n_components": 2,
539        "random_state": rng,
540        "fit_inverse_transform": True,
541        "kernel": "rbf",
542    }
543
544    expected_gamma = 1 / X.shape[1]
545    kpca1 = KernelPCA(gamma=None, **kwargs).fit(X)
546    kpca2 = KernelPCA(gamma=expected_gamma, **kwargs).fit(X)
547
548    assert kpca1.gamma_ == expected_gamma
549    assert kpca2.gamma_ == expected_gamma
550
551    X1_recon = kpca1.inverse_transform(kpca1.transform(X))
552    X2_recon = kpca2.inverse_transform(kpca1.transform(X))
553
554    assert_allclose(X1_recon, X2_recon)
555
556
557def test_kernel_pca_pandas_output():
558    """Check that KernelPCA works with pandas output when the solver is arpack.
559
560    Non-regression test for:
561    https://github.com/scikit-learn/scikit-learn/issues/27579
562    """
563    pytest.importorskip("pandas")
564    X, _ = load_iris(as_frame=True, return_X_y=True)
565    with sklearn.config_context(transform_output="pandas"):
566        KernelPCA(n_components=2, eigen_solver="arpack").fit_transform(X)
567 
Aluode/PerceptionLabPortable · CoolFace