test_kernels.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388
  1. """Testing for kernels for Gaussian processes."""
  2. # Author: Jan Hendrik Metzen <jhm@informatik.uni-bremen.de>
  3. # License: BSD 3 clause
  4. from inspect import signature
  5. import numpy as np
  6. import pytest
  7. from sklearn.base import clone
  8. from sklearn.gaussian_process.kernels import (
  9. RBF,
  10. CompoundKernel,
  11. ConstantKernel,
  12. DotProduct,
  13. Exponentiation,
  14. ExpSineSquared,
  15. KernelOperator,
  16. Matern,
  17. PairwiseKernel,
  18. RationalQuadratic,
  19. WhiteKernel,
  20. _approx_fprime,
  21. )
  22. from sklearn.metrics.pairwise import (
  23. PAIRWISE_KERNEL_FUNCTIONS,
  24. euclidean_distances,
  25. pairwise_kernels,
  26. )
  27. from sklearn.utils._testing import (
  28. assert_allclose,
  29. assert_almost_equal,
  30. assert_array_almost_equal,
  31. assert_array_equal,
  32. )
  33. X = np.random.RandomState(0).normal(0, 1, (5, 2))
  34. Y = np.random.RandomState(0).normal(0, 1, (6, 2))
  35. kernel_rbf_plus_white = RBF(length_scale=2.0) + WhiteKernel(noise_level=3.0)
  36. kernels = [
  37. RBF(length_scale=2.0),
  38. RBF(length_scale_bounds=(0.5, 2.0)),
  39. ConstantKernel(constant_value=10.0),
  40. 2.0 * RBF(length_scale=0.33, length_scale_bounds="fixed"),
  41. 2.0 * RBF(length_scale=0.5),
  42. kernel_rbf_plus_white,
  43. 2.0 * RBF(length_scale=[0.5, 2.0]),
  44. 2.0 * Matern(length_scale=0.33, length_scale_bounds="fixed"),
  45. 2.0 * Matern(length_scale=0.5, nu=0.5),
  46. 2.0 * Matern(length_scale=1.5, nu=1.5),
  47. 2.0 * Matern(length_scale=2.5, nu=2.5),
  48. 2.0 * Matern(length_scale=[0.5, 2.0], nu=0.5),
  49. 3.0 * Matern(length_scale=[2.0, 0.5], nu=1.5),
  50. 4.0 * Matern(length_scale=[0.5, 0.5], nu=2.5),
  51. RationalQuadratic(length_scale=0.5, alpha=1.5),
  52. ExpSineSquared(length_scale=0.5, periodicity=1.5),
  53. DotProduct(sigma_0=2.0),
  54. DotProduct(sigma_0=2.0) ** 2,
  55. RBF(length_scale=[2.0]),
  56. Matern(length_scale=[2.0]),
  57. ]
  58. for metric in PAIRWISE_KERNEL_FUNCTIONS:
  59. if metric in ["additive_chi2", "chi2"]:
  60. continue
  61. kernels.append(PairwiseKernel(gamma=1.0, metric=metric))
  62. @pytest.mark.parametrize("kernel", kernels)
  63. def test_kernel_gradient(kernel):
  64. # Compare analytic and numeric gradient of kernels.
  65. K, K_gradient = kernel(X, eval_gradient=True)
  66. assert K_gradient.shape[0] == X.shape[0]
  67. assert K_gradient.shape[1] == X.shape[0]
  68. assert K_gradient.shape[2] == kernel.theta.shape[0]
  69. def eval_kernel_for_theta(theta):
  70. kernel_clone = kernel.clone_with_theta(theta)
  71. K = kernel_clone(X, eval_gradient=False)
  72. return K
  73. K_gradient_approx = _approx_fprime(kernel.theta, eval_kernel_for_theta, 1e-10)
  74. assert_almost_equal(K_gradient, K_gradient_approx, 4)
  75. @pytest.mark.parametrize(
  76. "kernel",
  77. [
  78. kernel
  79. for kernel in kernels
  80. # skip non-basic kernels
  81. if not (isinstance(kernel, (KernelOperator, Exponentiation)))
  82. ],
  83. )
  84. def test_kernel_theta(kernel):
  85. # Check that parameter vector theta of kernel is set correctly.
  86. theta = kernel.theta
  87. _, K_gradient = kernel(X, eval_gradient=True)
  88. # Determine kernel parameters that contribute to theta
  89. init_sign = signature(kernel.__class__.__init__).parameters.values()
  90. args = [p.name for p in init_sign if p.name != "self"]
  91. theta_vars = map(
  92. lambda s: s[0 : -len("_bounds")], filter(lambda s: s.endswith("_bounds"), args)
  93. )
  94. assert set(hyperparameter.name for hyperparameter in kernel.hyperparameters) == set(
  95. theta_vars
  96. )
  97. # Check that values returned in theta are consistent with
  98. # hyperparameter values (being their logarithms)
  99. for i, hyperparameter in enumerate(kernel.hyperparameters):
  100. assert theta[i] == np.log(getattr(kernel, hyperparameter.name))
  101. # Fixed kernel parameters must be excluded from theta and gradient.
  102. for i, hyperparameter in enumerate(kernel.hyperparameters):
  103. # create copy with certain hyperparameter fixed
  104. params = kernel.get_params()
  105. params[hyperparameter.name + "_bounds"] = "fixed"
  106. kernel_class = kernel.__class__
  107. new_kernel = kernel_class(**params)
  108. # Check that theta and K_gradient are identical with the fixed
  109. # dimension left out
  110. _, K_gradient_new = new_kernel(X, eval_gradient=True)
  111. assert theta.shape[0] == new_kernel.theta.shape[0] + 1
  112. assert K_gradient.shape[2] == K_gradient_new.shape[2] + 1
  113. if i > 0:
  114. assert theta[:i] == new_kernel.theta[:i]
  115. assert_array_equal(K_gradient[..., :i], K_gradient_new[..., :i])
  116. if i + 1 < len(kernel.hyperparameters):
  117. assert theta[i + 1 :] == new_kernel.theta[i:]
  118. assert_array_equal(K_gradient[..., i + 1 :], K_gradient_new[..., i:])
  119. # Check that values of theta are modified correctly
  120. for i, hyperparameter in enumerate(kernel.hyperparameters):
  121. theta[i] = np.log(42)
  122. kernel.theta = theta
  123. assert_almost_equal(getattr(kernel, hyperparameter.name), 42)
  124. setattr(kernel, hyperparameter.name, 43)
  125. assert_almost_equal(kernel.theta[i], np.log(43))
  126. @pytest.mark.parametrize(
  127. "kernel",
  128. [
  129. kernel
  130. for kernel in kernels
  131. # Identity is not satisfied on diagonal
  132. if kernel != kernel_rbf_plus_white
  133. ],
  134. )
  135. def test_auto_vs_cross(kernel):
  136. # Auto-correlation and cross-correlation should be consistent.
  137. K_auto = kernel(X)
  138. K_cross = kernel(X, X)
  139. assert_almost_equal(K_auto, K_cross, 5)
  140. @pytest.mark.parametrize("kernel", kernels)
  141. def test_kernel_diag(kernel):
  142. # Test that diag method of kernel returns consistent results.
  143. K_call_diag = np.diag(kernel(X))
  144. K_diag = kernel.diag(X)
  145. assert_almost_equal(K_call_diag, K_diag, 5)
  146. def test_kernel_operator_commutative():
  147. # Adding kernels and multiplying kernels should be commutative.
  148. # Check addition
  149. assert_almost_equal((RBF(2.0) + 1.0)(X), (1.0 + RBF(2.0))(X))
  150. # Check multiplication
  151. assert_almost_equal((3.0 * RBF(2.0))(X), (RBF(2.0) * 3.0)(X))
  152. def test_kernel_anisotropic():
  153. # Anisotropic kernel should be consistent with isotropic kernels.
  154. kernel = 3.0 * RBF([0.5, 2.0])
  155. K = kernel(X)
  156. X1 = np.array(X)
  157. X1[:, 0] *= 4
  158. K1 = 3.0 * RBF(2.0)(X1)
  159. assert_almost_equal(K, K1)
  160. X2 = np.array(X)
  161. X2[:, 1] /= 4
  162. K2 = 3.0 * RBF(0.5)(X2)
  163. assert_almost_equal(K, K2)
  164. # Check getting and setting via theta
  165. kernel.theta = kernel.theta + np.log(2)
  166. assert_array_equal(kernel.theta, np.log([6.0, 1.0, 4.0]))
  167. assert_array_equal(kernel.k2.length_scale, [1.0, 4.0])
  168. @pytest.mark.parametrize(
  169. "kernel", [kernel for kernel in kernels if kernel.is_stationary()]
  170. )
  171. def test_kernel_stationary(kernel):
  172. # Test stationarity of kernels.
  173. K = kernel(X, X + 1)
  174. assert_almost_equal(K[0, 0], np.diag(K))
  175. @pytest.mark.parametrize("kernel", kernels)
  176. def test_kernel_input_type(kernel):
  177. # Test whether kernels is for vectors or structured data
  178. if isinstance(kernel, Exponentiation):
  179. assert kernel.requires_vector_input == kernel.kernel.requires_vector_input
  180. if isinstance(kernel, KernelOperator):
  181. assert kernel.requires_vector_input == (
  182. kernel.k1.requires_vector_input or kernel.k2.requires_vector_input
  183. )
  184. def test_compound_kernel_input_type():
  185. kernel = CompoundKernel([WhiteKernel(noise_level=3.0)])
  186. assert not kernel.requires_vector_input
  187. kernel = CompoundKernel([WhiteKernel(noise_level=3.0), RBF(length_scale=2.0)])
  188. assert kernel.requires_vector_input
  189. def check_hyperparameters_equal(kernel1, kernel2):
  190. # Check that hyperparameters of two kernels are equal
  191. for attr in set(dir(kernel1) + dir(kernel2)):
  192. if attr.startswith("hyperparameter_"):
  193. attr_value1 = getattr(kernel1, attr)
  194. attr_value2 = getattr(kernel2, attr)
  195. assert attr_value1 == attr_value2
  196. @pytest.mark.parametrize("kernel", kernels)
  197. def test_kernel_clone(kernel):
  198. # Test that sklearn's clone works correctly on kernels.
  199. kernel_cloned = clone(kernel)
  200. # XXX: Should this be fixed?
  201. # This differs from the sklearn's estimators equality check.
  202. assert kernel == kernel_cloned
  203. assert id(kernel) != id(kernel_cloned)
  204. # Check that all constructor parameters are equal.
  205. assert kernel.get_params() == kernel_cloned.get_params()
  206. # Check that all hyperparameters are equal.
  207. check_hyperparameters_equal(kernel, kernel_cloned)
  208. @pytest.mark.parametrize("kernel", kernels)
  209. def test_kernel_clone_after_set_params(kernel):
  210. # This test is to verify that using set_params does not
  211. # break clone on kernels.
  212. # This used to break because in kernels such as the RBF, non-trivial
  213. # logic that modified the length scale used to be in the constructor
  214. # See https://github.com/scikit-learn/scikit-learn/issues/6961
  215. # for more details.
  216. bounds = (1e-5, 1e5)
  217. kernel_cloned = clone(kernel)
  218. params = kernel.get_params()
  219. # RationalQuadratic kernel is isotropic.
  220. isotropic_kernels = (ExpSineSquared, RationalQuadratic)
  221. if "length_scale" in params and not isinstance(kernel, isotropic_kernels):
  222. length_scale = params["length_scale"]
  223. if np.iterable(length_scale):
  224. # XXX unreached code as of v0.22
  225. params["length_scale"] = length_scale[0]
  226. params["length_scale_bounds"] = bounds
  227. else:
  228. params["length_scale"] = [length_scale] * 2
  229. params["length_scale_bounds"] = bounds * 2
  230. kernel_cloned.set_params(**params)
  231. kernel_cloned_clone = clone(kernel_cloned)
  232. assert kernel_cloned_clone.get_params() == kernel_cloned.get_params()
  233. assert id(kernel_cloned_clone) != id(kernel_cloned)
  234. check_hyperparameters_equal(kernel_cloned, kernel_cloned_clone)
  235. def test_matern_kernel():
  236. # Test consistency of Matern kernel for special values of nu.
  237. K = Matern(nu=1.5, length_scale=1.0)(X)
  238. # the diagonal elements of a matern kernel are 1
  239. assert_array_almost_equal(np.diag(K), np.ones(X.shape[0]))
  240. # matern kernel for coef0==0.5 is equal to absolute exponential kernel
  241. K_absexp = np.exp(-euclidean_distances(X, X, squared=False))
  242. K = Matern(nu=0.5, length_scale=1.0)(X)
  243. assert_array_almost_equal(K, K_absexp)
  244. # matern kernel with coef0==inf is equal to RBF kernel
  245. K_rbf = RBF(length_scale=1.0)(X)
  246. K = Matern(nu=np.inf, length_scale=1.0)(X)
  247. assert_array_almost_equal(K, K_rbf)
  248. assert_allclose(K, K_rbf)
  249. # test that special cases of matern kernel (coef0 in [0.5, 1.5, 2.5])
  250. # result in nearly identical results as the general case for coef0 in
  251. # [0.5 + tiny, 1.5 + tiny, 2.5 + tiny]
  252. tiny = 1e-10
  253. for nu in [0.5, 1.5, 2.5]:
  254. K1 = Matern(nu=nu, length_scale=1.0)(X)
  255. K2 = Matern(nu=nu + tiny, length_scale=1.0)(X)
  256. assert_array_almost_equal(K1, K2)
  257. # test that coef0==large is close to RBF
  258. large = 100
  259. K1 = Matern(nu=large, length_scale=1.0)(X)
  260. K2 = RBF(length_scale=1.0)(X)
  261. assert_array_almost_equal(K1, K2, decimal=2)
  262. @pytest.mark.parametrize("kernel", kernels)
  263. def test_kernel_versus_pairwise(kernel):
  264. # Check that GP kernels can also be used as pairwise kernels.
  265. # Test auto-kernel
  266. if kernel != kernel_rbf_plus_white:
  267. # For WhiteKernel: k(X) != k(X,X). This is assumed by
  268. # pairwise_kernels
  269. K1 = kernel(X)
  270. K2 = pairwise_kernels(X, metric=kernel)
  271. assert_array_almost_equal(K1, K2)
  272. # Test cross-kernel
  273. K1 = kernel(X, Y)
  274. K2 = pairwise_kernels(X, Y, metric=kernel)
  275. assert_array_almost_equal(K1, K2)
  276. @pytest.mark.parametrize("kernel", kernels)
  277. def test_set_get_params(kernel):
  278. # Check that set_params()/get_params() is consistent with kernel.theta.
  279. # Test get_params()
  280. index = 0
  281. params = kernel.get_params()
  282. for hyperparameter in kernel.hyperparameters:
  283. if isinstance("string", type(hyperparameter.bounds)):
  284. if hyperparameter.bounds == "fixed":
  285. continue
  286. size = hyperparameter.n_elements
  287. if size > 1: # anisotropic kernels
  288. assert_almost_equal(
  289. np.exp(kernel.theta[index : index + size]), params[hyperparameter.name]
  290. )
  291. index += size
  292. else:
  293. assert_almost_equal(
  294. np.exp(kernel.theta[index]), params[hyperparameter.name]
  295. )
  296. index += 1
  297. # Test set_params()
  298. index = 0
  299. value = 10 # arbitrary value
  300. for hyperparameter in kernel.hyperparameters:
  301. if isinstance("string", type(hyperparameter.bounds)):
  302. if hyperparameter.bounds == "fixed":
  303. continue
  304. size = hyperparameter.n_elements
  305. if size > 1: # anisotropic kernels
  306. kernel.set_params(**{hyperparameter.name: [value] * size})
  307. assert_almost_equal(
  308. np.exp(kernel.theta[index : index + size]), [value] * size
  309. )
  310. index += size
  311. else:
  312. kernel.set_params(**{hyperparameter.name: value})
  313. assert_almost_equal(np.exp(kernel.theta[index]), value)
  314. index += 1
  315. @pytest.mark.parametrize("kernel", kernels)
  316. def test_repr_kernels(kernel):
  317. # Smoke-test for repr in kernels.
  318. repr(kernel)
  319. def test_rational_quadratic_kernel():
  320. kernel = RationalQuadratic(length_scale=[1.0, 1.0])
  321. message = (
  322. "RationalQuadratic kernel only supports isotropic "
  323. "version, please use a single "
  324. "scalar for length_scale"
  325. )
  326. with pytest.raises(AttributeError, match=message):
  327. kernel(X)