Aluode/PerceptionLabPortable
0
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 