Is there a builtin way to pass custom distance functions to be used by the kernels you could use for Gaussian Process Models? In particular, I have geographic data in lat/lon coordinates, so using Euclidean distances would not give accurate distances between points. This seems like a not-uncommon use case for GPR, so wondering if there is a standard(-ish) way of implementing this in scikit-learn.
Right now, I've written a new Kernel sub-class where I replaced the metric argument in the pdist(metric='sqeuclidean')
and cdist(metric='sqeuclidean')
calls within the RBF kernel source code with arguments that you can set when instantiating the kernel, but this seems hack-y and I'm wondering if there's a better way to do this. Ultimately, it seems like you should be able to pass an arbitrary distance function to all of these kernels but I haven't been able to figure out how to do that. The class I wrote (which is almost entirely just the same as the standard kernels.RBF class) is below. Anyone see a better way to do this? Or why what I'm doing is a bad idea?
class customDistRBF(RBF):
"""Same as sklearn.gaussian_process.kernels.RBF except that
it allows for a custom distance function in the RBF kernel.
"""
def __init__(self, length_scale=1.0, length_scale_bounds=(1e-5, 1e5), dist_func='sqeuclidean'):
RBF.__init__(self, length_scale=1.0, length_scale_bounds=length_scale_bounds)
self.dist_func = dist_func
def __call__(self, X, Y=None, eval_gradient=False):
X = np.atleast_2d(X)
length_scale = _check_length_scale(X, self.length_scale)
if Y is None:
# the below line is changed
dists = pdist(X, metric=self.dist_func) / length_scale
K = np.exp(-.5 * dists)
# convert from upper-triangular matrix to square matrix
K = squareform(K)
np.fill_diagonal(K, 1)
else:
if eval_gradient:
raise ValueError(
"Gradient can only be evaluated when Y is None.")
# the below line is changed
dists = cdist(X, Y,
metric=self.dist_func) / length_scale
K = np.exp(-.5 * dists)
if eval_gradient:
if self.hyperparameter_length_scale.fixed:
# Hyperparameter l kept fixed
return K, np.empty((X.shape[0], X.shape[0], 0))
elif not self.anisotropic or length_scale.shape[0] == 1:
K_gradient = \
(K * squareform(dists))[:, :, np.newaxis]
return K, K_gradient
elif self.anisotropic:
# We need to recompute the pairwise dimension-wise distances
K_gradient = (X[:, np.newaxis, :] - X[np.newaxis, :, :]) ** 2 \
/ (length_scale ** 2)
K_gradient *= K[..., np.newaxis]
return K, K_gradient
else:
return K