CoolFace
Apppublic

Aluode/PerceptionLabPortable

sourceHugging Faceupdated 9mo agoView on Hugging Face
0likes
test_kernel_approximation.py496 linesDownload Raw Back to tests
1import re
2
3import numpy as np
4import pytest
5
6from sklearn.datasets import make_classification
7from sklearn.kernel_approximation import (
8    AdditiveChi2Sampler,
9    Nystroem,
10    PolynomialCountSketch,
11    RBFSampler,
12    SkewedChi2Sampler,
13)
14from sklearn.metrics.pairwise import (
15    chi2_kernel,
16    kernel_metrics,
17    polynomial_kernel,
18    rbf_kernel,
19)
20from sklearn.utils._testing import (
21    assert_allclose,
22    assert_array_almost_equal,
23    assert_array_equal,
24)
25from sklearn.utils.fixes import CSR_CONTAINERS
26
27# generate data
28rng = np.random.RandomState(0)
29X = rng.random_sample(size=(300, 50))
30Y = rng.random_sample(size=(300, 50))
31X /= X.sum(axis=1)[:, np.newaxis]
32Y /= Y.sum(axis=1)[:, np.newaxis]
33
34# Make sure X and Y are not writable to avoid introducing dependencies between
35# tests.
36X.flags.writeable = False
37Y.flags.writeable = False
38
39
40@pytest.mark.parametrize("gamma", [0.1, 1, 2.5])
41@pytest.mark.parametrize("degree, n_components", [(1, 500), (2, 500), (3, 5000)])
42@pytest.mark.parametrize("coef0", [0, 2.5])
43def test_polynomial_count_sketch(gamma, degree, coef0, n_components):
44    # test that PolynomialCountSketch approximates polynomial
45    # kernel on random data
46
47    # compute exact kernel
48    kernel = polynomial_kernel(X, Y, gamma=gamma, degree=degree, coef0=coef0)
49
50    # approximate kernel mapping
51    ps_transform = PolynomialCountSketch(
52        n_components=n_components,
53        gamma=gamma,
54        coef0=coef0,
55        degree=degree,
56        random_state=42,
57    )
58    X_trans = ps_transform.fit_transform(X)
59    Y_trans = ps_transform.transform(Y)
60    kernel_approx = np.dot(X_trans, Y_trans.T)
61
62    error = kernel - kernel_approx
63    assert np.abs(np.mean(error)) <= 0.05  # close to unbiased
64    np.abs(error, out=error)
65    assert np.max(error) <= 0.1  # nothing too far off
66    assert np.mean(error) <= 0.05  # mean is fairly close
67
68
69@pytest.mark.parametrize("csr_container", CSR_CONTAINERS)
70@pytest.mark.parametrize("gamma", [0.1, 1.0])
71@pytest.mark.parametrize("degree", [1, 2, 3])
72@pytest.mark.parametrize("coef0", [0, 2.5])
73def test_polynomial_count_sketch_dense_sparse(gamma, degree, coef0, csr_container):
74    """Check that PolynomialCountSketch results are the same for dense and sparse
75    input.
76    """
77    ps_dense = PolynomialCountSketch(
78        n_components=500, gamma=gamma, degree=degree, coef0=coef0, random_state=42
79    )
80    Xt_dense = ps_dense.fit_transform(X)
81    Yt_dense = ps_dense.transform(Y)
82
83    ps_sparse = PolynomialCountSketch(
84        n_components=500, gamma=gamma, degree=degree, coef0=coef0, random_state=42
85    )
86    Xt_sparse = ps_sparse.fit_transform(csr_container(X))
87    Yt_sparse = ps_sparse.transform(csr_container(Y))
88
89    assert_allclose(Xt_dense, Xt_sparse)
90    assert_allclose(Yt_dense, Yt_sparse)
91
92
93def _linear_kernel(X, Y):
94    return np.dot(X, Y.T)
95
96
97@pytest.mark.parametrize("csr_container", CSR_CONTAINERS)
98def test_additive_chi2_sampler(csr_container):
99    # test that AdditiveChi2Sampler approximates kernel on random data
100
101    # compute exact kernel
102    # abbreviations for easier formula
103    X_ = X[:, np.newaxis, :].copy()
104    Y_ = Y[np.newaxis, :, :].copy()
105
106    large_kernel = 2 * X_ * Y_ / (X_ + Y_)
107
108    # reduce to n_samples_x x n_samples_y by summing over features
109    kernel = large_kernel.sum(axis=2)
110
111    # approximate kernel mapping
112    transform = AdditiveChi2Sampler(sample_steps=3)
113    X_trans = transform.fit_transform(X)
114    Y_trans = transform.transform(Y)
115
116    kernel_approx = np.dot(X_trans, Y_trans.T)
117
118    assert_array_almost_equal(kernel, kernel_approx, 1)
119
120    X_sp_trans = transform.fit_transform(csr_container(X))
121    Y_sp_trans = transform.transform(csr_container(Y))
122
123    assert_array_equal(X_trans, X_sp_trans.toarray())
124    assert_array_equal(Y_trans, Y_sp_trans.toarray())
125
126    # test error is raised on negative input
127    Y_neg = Y.copy()
128    Y_neg[0, 0] = -1
129    msg = "Negative values in data passed to"
130    with pytest.raises(ValueError, match=msg):
131        transform.fit(Y_neg)
132
133
134@pytest.mark.parametrize("method", ["fit", "fit_transform", "transform"])
135@pytest.mark.parametrize("sample_steps", range(1, 4))
136def test_additive_chi2_sampler_sample_steps(method, sample_steps):
137    """Check that the input sample step doesn't raise an error
138    and that sample interval doesn't change after fit.
139    """
140    transformer = AdditiveChi2Sampler(sample_steps=sample_steps)
141    getattr(transformer, method)(X)
142
143    sample_interval = 0.5
144    transformer = AdditiveChi2Sampler(
145        sample_steps=sample_steps,
146        sample_interval=sample_interval,
147    )
148    getattr(transformer, method)(X)
149    assert transformer.sample_interval == sample_interval
150
151
152@pytest.mark.parametrize("method", ["fit", "fit_transform", "transform"])
153def test_additive_chi2_sampler_wrong_sample_steps(method):
154    """Check that we raise a ValueError on invalid sample_steps"""
155    transformer = AdditiveChi2Sampler(sample_steps=4)
156    msg = re.escape(
157        "If sample_steps is not in [1, 2, 3], you need to provide sample_interval"
158    )
159    with pytest.raises(ValueError, match=msg):
160        getattr(transformer, method)(X)
161
162
163def test_skewed_chi2_sampler():
164    # test that RBFSampler approximates kernel on random data
165
166    # compute exact kernel
167    c = 0.03
168    # set on negative component but greater than c to ensure that the kernel
169    # approximation is valid on the group (-c; +\infty) endowed with the skewed
170    # multiplication.
171    Y_ = Y.copy()
172    Y_[0, 0] = -c / 2.0
173
174    # abbreviations for easier formula
175    X_c = (X + c)[:, np.newaxis, :]
176    Y_c = (Y_ + c)[np.newaxis, :, :]
177
178    # we do it in log-space in the hope that it's more stable
179    # this array is n_samples_x x n_samples_y big x n_features
180    log_kernel = (
181        (np.log(X_c) / 2.0) + (np.log(Y_c) / 2.0) + np.log(2.0) - np.log(X_c + Y_c)
182    )
183    # reduce to n_samples_x x n_samples_y by summing over features in log-space
184    kernel = np.exp(log_kernel.sum(axis=2))
185
186    # approximate kernel mapping
187    transform = SkewedChi2Sampler(skewedness=c, n_components=1000, random_state=42)
188    X_trans = transform.fit_transform(X)
189    Y_trans = transform.transform(Y_)
190
191    kernel_approx = np.dot(X_trans, Y_trans.T)
192    assert_array_almost_equal(kernel, kernel_approx, 1)
193    assert np.isfinite(kernel).all(), "NaNs found in the Gram matrix"
194    assert np.isfinite(kernel_approx).all(), "NaNs found in the approximate Gram matrix"
195
196    # test error is raised on when inputs contains values smaller than -c
197    Y_neg = Y_.copy()
198    Y_neg[0, 0] = -c * 2.0
199    msg = "X may not contain entries smaller than -skewedness"
200    with pytest.raises(ValueError, match=msg):
201        transform.transform(Y_neg)
202
203
204def test_additive_chi2_sampler_exceptions():
205    """Ensures correct error message"""
206    transformer = AdditiveChi2Sampler()
207    X_neg = X.copy()
208    X_neg[0, 0] = -1
209    with pytest.raises(ValueError, match="X in AdditiveChi2Sampler"):
210        transformer.fit(X_neg)
211    with pytest.raises(ValueError, match="X in AdditiveChi2Sampler"):
212        transformer.fit(X)
213        transformer.transform(X_neg)
214
215
216def test_rbf_sampler():
217    # test that RBFSampler approximates kernel on random data
218    # compute exact kernel
219    gamma = 10.0
220    kernel = rbf_kernel(X, Y, gamma=gamma)
221
222    # approximate kernel mapping
223    rbf_transform = RBFSampler(gamma=gamma, n_components=1000, random_state=42)
224    X_trans = rbf_transform.fit_transform(X)
225    Y_trans = rbf_transform.transform(Y)
226    kernel_approx = np.dot(X_trans, Y_trans.T)
227
228    error = kernel - kernel_approx
229    assert np.abs(np.mean(error)) <= 0.01  # close to unbiased
230    np.abs(error, out=error)
231    assert np.max(error) <= 0.1  # nothing too far off
232    assert np.mean(error) <= 0.05  # mean is fairly close
233
234
235def test_rbf_sampler_fitted_attributes_dtype(global_dtype):
236    """Check that the fitted attributes are stored accordingly to the
237    data type of X."""
238    rbf = RBFSampler()
239
240    X = np.array([[1, 2], [3, 4], [5, 6]], dtype=global_dtype)
241
242    rbf.fit(X)
243
244    assert rbf.random_offset_.dtype == global_dtype
245    assert rbf.random_weights_.dtype == global_dtype
246
247
248def test_rbf_sampler_dtype_equivalence():
249    """Check the equivalence of the results with 32 and 64 bits input."""
250    rbf32 = RBFSampler(random_state=42)
251    X32 = np.array([[1, 2], [3, 4], [5, 6]], dtype=np.float32)
252    rbf32.fit(X32)
253
254    rbf64 = RBFSampler(random_state=42)
255    X64 = np.array([[1, 2], [3, 4], [5, 6]], dtype=np.float64)
256    rbf64.fit(X64)
257
258    assert_allclose(rbf32.random_offset_, rbf64.random_offset_)
259    assert_allclose(rbf32.random_weights_, rbf64.random_weights_)
260
261
262def test_rbf_sampler_gamma_scale():
263    """Check the inner value computed when `gamma='scale'`."""
264    X, y = [[0.0], [1.0]], [0, 1]
265    rbf = RBFSampler(gamma="scale")
266    rbf.fit(X, y)
267    assert rbf._gamma == pytest.approx(4)
268
269
270def test_skewed_chi2_sampler_fitted_attributes_dtype(global_dtype):
271    """Check that the fitted attributes are stored accordingly to the
272    data type of X."""
273    skewed_chi2_sampler = SkewedChi2Sampler()
274
275    X = np.array([[1, 2], [3, 4], [5, 6]], dtype=global_dtype)
276
277    skewed_chi2_sampler.fit(X)
278
279    assert skewed_chi2_sampler.random_offset_.dtype == global_dtype
280    assert skewed_chi2_sampler.random_weights_.dtype == global_dtype
281
282
283def test_skewed_chi2_sampler_dtype_equivalence():
284    """Check the equivalence of the results with 32 and 64 bits input."""
285    skewed_chi2_sampler_32 = SkewedChi2Sampler(random_state=42)
286    X_32 = np.array([[1, 2], [3, 4], [5, 6]], dtype=np.float32)
287    skewed_chi2_sampler_32.fit(X_32)
288
289    skewed_chi2_sampler_64 = SkewedChi2Sampler(random_state=42)
290    X_64 = np.array([[1, 2], [3, 4], [5, 6]], dtype=np.float64)
291    skewed_chi2_sampler_64.fit(X_64)
292
293    assert_allclose(
294        skewed_chi2_sampler_32.random_offset_, skewed_chi2_sampler_64.random_offset_
295    )
296    assert_allclose(
297        skewed_chi2_sampler_32.random_weights_, skewed_chi2_sampler_64.random_weights_
298    )
299
300
301@pytest.mark.parametrize("csr_container", CSR_CONTAINERS)
302def test_input_validation(csr_container):
303    # Regression test: kernel approx. transformers should work on lists
304    # No assertions; the old versions would simply crash
305    X = [[1, 2], [3, 4], [5, 6]]
306    AdditiveChi2Sampler().fit(X).transform(X)
307    SkewedChi2Sampler().fit(X).transform(X)
308    RBFSampler().fit(X).transform(X)
309
310    X = csr_container(X)
311    RBFSampler().fit(X).transform(X)
312
313
314def test_nystroem_approximation():
315    # some basic tests
316    rnd = np.random.RandomState(0)
317    X = rnd.uniform(size=(10, 4))
318
319    # With n_components = n_samples this is exact
320    X_transformed = Nystroem(n_components=X.shape[0]).fit_transform(X)
321    K = rbf_kernel(X)
322    assert_array_almost_equal(np.dot(X_transformed, X_transformed.T), K)
323
324    trans = Nystroem(n_components=2, random_state=rnd)
325    X_transformed = trans.fit(X).transform(X)
326    assert X_transformed.shape == (X.shape[0], 2)
327
328    # test callable kernel
329    trans = Nystroem(n_components=2, kernel=_linear_kernel, random_state=rnd)
330    X_transformed = trans.fit(X).transform(X)
331    assert X_transformed.shape == (X.shape[0], 2)
332
333    # test that available kernels fit and transform
334    kernels_available = kernel_metrics()
335    for kern in kernels_available:
336        trans = Nystroem(n_components=2, kernel=kern, random_state=rnd)
337        X_transformed = trans.fit(X).transform(X)
338        assert X_transformed.shape == (X.shape[0], 2)
339
340
341def test_nystroem_default_parameters():
342    rnd = np.random.RandomState(42)
343    X = rnd.uniform(size=(10, 4))
344
345    # rbf kernel should behave as gamma=None by default
346    # aka gamma = 1 / n_features
347    nystroem = Nystroem(n_components=10)
348    X_transformed = nystroem.fit_transform(X)
349    K = rbf_kernel(X, gamma=None)
350    K2 = np.dot(X_transformed, X_transformed.T)
351    assert_array_almost_equal(K, K2)
352
353    # chi2 kernel should behave as gamma=1 by default
354    nystroem = Nystroem(kernel="chi2", n_components=10)
355    X_transformed = nystroem.fit_transform(X)
356    K = chi2_kernel(X, gamma=1)
357    K2 = np.dot(X_transformed, X_transformed.T)
358    assert_array_almost_equal(K, K2)
359
360
361def test_nystroem_singular_kernel():
362    # test that nystroem works with singular kernel matrix
363    rng = np.random.RandomState(0)
364    X = rng.rand(10, 20)
365    X = np.vstack([X] * 2)  # duplicate samples
366
367    gamma = 100
368    N = Nystroem(gamma=gamma, n_components=X.shape[0]).fit(X)
369    X_transformed = N.transform(X)
370
371    K = rbf_kernel(X, gamma=gamma)
372
373    assert_array_almost_equal(K, np.dot(X_transformed, X_transformed.T))
374    assert np.all(np.isfinite(Y))
375
376
377def test_nystroem_poly_kernel_params():
378    # Non-regression: Nystroem should pass other parameters beside gamma.
379    rnd = np.random.RandomState(37)
380    X = rnd.uniform(size=(10, 4))
381
382    K = polynomial_kernel(X, degree=3.1, coef0=0.1)
383    nystroem = Nystroem(
384        kernel="polynomial", n_components=X.shape[0], degree=3.1, coef0=0.1
385    )
386    X_transformed = nystroem.fit_transform(X)
387    assert_array_almost_equal(np.dot(X_transformed, X_transformed.T), K)
388
389
390def test_nystroem_callable():
391    # Test Nystroem on a callable.
392    rnd = np.random.RandomState(42)
393    n_samples = 10
394    X = rnd.uniform(size=(n_samples, 4))
395
396    def logging_histogram_kernel(x, y, log):
397        """Histogram kernel that writes to a log."""
398        log.append(1)
399        return np.minimum(x, y).sum()
400
401    kernel_log = []
402    X = list(X)  # test input validation
403    Nystroem(
404        kernel=logging_histogram_kernel,
405        n_components=(n_samples - 1),
406        kernel_params={"log": kernel_log},
407    ).fit(X)
408    assert len(kernel_log) == n_samples * (n_samples - 1) / 2
409
410    # if degree, gamma or coef0 is passed, we raise a ValueError
411    msg = "Don't pass gamma, coef0 or degree to Nystroem"
412    params = ({"gamma": 1}, {"coef0": 1}, {"degree": 2})
413    for param in params:
414        ny = Nystroem(kernel=_linear_kernel, n_components=(n_samples - 1), **param)
415        with pytest.raises(ValueError, match=msg):
416            ny.fit(X)
417
418
419def test_nystroem_precomputed_kernel():
420    # Non-regression: test Nystroem on precomputed kernel.
421    # PR - 14706
422    rnd = np.random.RandomState(12)
423    X = rnd.uniform(size=(10, 4))
424
425    K = polynomial_kernel(X, degree=2, coef0=0.1)
426    nystroem = Nystroem(kernel="precomputed", n_components=X.shape[0])
427    X_transformed = nystroem.fit_transform(K)
428    assert_array_almost_equal(np.dot(X_transformed, X_transformed.T), K)
429
430    # if degree, gamma or coef0 is passed, we raise a ValueError
431    msg = "Don't pass gamma, coef0 or degree to Nystroem"
432    params = ({"gamma": 1}, {"coef0": 1}, {"degree": 2})
433    for param in params:
434        ny = Nystroem(kernel="precomputed", n_components=X.shape[0], **param)
435        with pytest.raises(ValueError, match=msg):
436            ny.fit(K)
437
438
439def test_nystroem_component_indices():
440    """Check that `component_indices_` corresponds to the subset of
441    training points used to construct the feature map.
442    Non-regression test for:
443    https://github.com/scikit-learn/scikit-learn/issues/20474
444    """
445    X, _ = make_classification(n_samples=100, n_features=20)
446    feature_map_nystroem = Nystroem(
447        n_components=10,
448        random_state=0,
449    )
450    feature_map_nystroem.fit(X)
451    assert feature_map_nystroem.component_indices_.shape == (10,)
452
453
454@pytest.mark.parametrize(
455    "Estimator", [PolynomialCountSketch, RBFSampler, SkewedChi2Sampler, Nystroem]
456)
457def test_get_feature_names_out(Estimator):
458    """Check get_feature_names_out"""
459    est = Estimator().fit(X)
460    X_trans = est.transform(X)
461
462    names_out = est.get_feature_names_out()
463    class_name = Estimator.__name__.lower()
464    expected_names = [f"{class_name}{i}" for i in range(X_trans.shape[1])]
465    assert_array_equal(names_out, expected_names)
466
467
468def test_additivechi2sampler_get_feature_names_out():
469    """Check get_feature_names_out for AdditiveChi2Sampler."""
470    rng = np.random.RandomState(0)
471    X = rng.random_sample(size=(300, 3))
472
473    chi2_sampler = AdditiveChi2Sampler(sample_steps=3).fit(X)
474    input_names = ["f0", "f1", "f2"]
475    suffixes = [
476        "f0_sqrt",
477        "f1_sqrt",
478        "f2_sqrt",
479        "f0_cos1",
480        "f1_cos1",
481        "f2_cos1",
482        "f0_sin1",
483        "f1_sin1",
484        "f2_sin1",
485        "f0_cos2",
486        "f1_cos2",
487        "f2_cos2",
488        "f0_sin2",
489        "f1_sin2",
490        "f2_sin2",
491    ]
492
493    names_out = chi2_sampler.get_feature_names_out(input_features=input_names)
494    expected_names = [f"additivechi2sampler_{suffix}" for suffix in suffixes]
495    assert_array_equal(names_out, expected_names)
496 
Aluode/PerceptionLabPortable · CoolFace