Source code for qsarkit.models._gaussian_process

"""Gaussian-process regressor with a Tanimoto kernel default, for QSAR."""

from __future__ import annotations

from typing import Any, Optional

import numpy.typing as npt
from sklearn.gaussian_process import GaussianProcessRegressor

from qsarkit.models._tanimoto_kernel import TanimotoKernel

__all__ = ["GaussianProcessQSAR"]


[docs] class GaussianProcessQSAR(GaussianProcessRegressor): """Gaussian-process regressor defaulting to a Tanimoto fingerprint kernel. A thin subclass of :class:`sklearn.gaussian_process.GaussianProcessRegressor` whose only behavioural change is the default kernel: when ``kernel=None`` (the constructor default, exactly as in the parent class), :meth:`fit` builds a :class:`~qsarkit.models.TanimotoKernel` rather than scikit-learn's own RBF default — mirroring the parent class's own convention of resolving ``kernel=None`` to a concrete kernel lazily inside ``fit()``, so ``__init__`` keeps storing exactly what was passed to it (sklearn's ``get_params``/``clone`` contract). The QSAR motivation for a Gaussian process at all is uncertainty quantification: unlike a point-estimate regressor, a fitted GP returns both a predictive mean and a predictive standard deviation (``predict(X, return_std=True)``), and that per-compound standard deviation is a natural, model-native applicability-domain signal — it grows exactly where the model is extrapolating away from the training fingerprints. Using the Tanimoto kernel rather than RBF means that "far from training data" is measured in the similarity metric fingerprints were designed for, not in a Euclidean geometry that is not meaningful for sparse binary vectors. Parameters ---------- kernel : Kernel, optional Covariance function. If None (default), a :class:`~qsarkit.models.TanimotoKernel` is constructed inside :meth:`fit`. alpha : float or array-like, default 1e-6 Value added to the diagonal of the kernel matrix during fitting, interpreted as observation noise variance. The default is larger than scikit-learn's ``1e-10`` to better tolerate the assay noise typical of biological QSAR endpoints. optimizer : str or callable, default "fmin_l_bfgs_b" Optimizer used to find the kernel hyperparameters maximizing the log-marginal-likelihood, or None to keep the initial hyperparameters fixed. n_restarts_optimizer : int, default 0 Number of restarts of the optimizer from hyperparameters sampled log-uniformly from their bounds, in addition to the initial run. normalize_y : bool, default True Whether to subtract the mean and scale the training targets to unit variance before fitting. True by default here (unlike scikit-learn's False) since QSAR endpoints (pIC50, logS, ...) are rarely already zero-mean. copy_X_train : bool, default True Whether a copy of the training data is stored. n_targets : int, optional Number of targets expected when fitting multi-output data. random_state : int, optional Seed controlling the optimizer's random restarts. Examples -------- >>> import numpy as np >>> from sklearn.datasets import make_regression >>> X, y = make_regression(n_samples=30, n_features=6, random_state=0) >>> X_binary = (X > np.median(X, axis=0)).astype(float) >>> model = GaussianProcessQSAR(random_state=0).fit(X_binary, y) >>> mean, std = model.predict(X_binary, return_std=True) >>> mean.shape (30,) >>> bool((std >= 0).all()) True References ---------- - Ralaivola, L., Swamidass, S. J., Saigo, H. & Baldi, P. (2005). "Graph Kernels for Chemical Informatics." Neural Networks, 18(8), 1093-1110. https://doi.org/10.1016/j.neunet.2005.07.009 - Rasmussen, C. E. & Williams, C. K. I. (2006). "Gaussian Processes for Machine Learning." MIT Press. ISBN 0-262-18253-X. Freely available at http://gaussianprocess.org/gpml/ """ # Declared explicitly because it is set by scikit-learn's __init__, # which mypy sees as untyped - without this, `self.kernel` has no # inferable type and the None-resolution in fit() cannot type-check. kernel: Optional[Any] def __init__( self, kernel: Optional[Any] = None, *, alpha: npt.ArrayLike = 1e-6, optimizer: Optional[Any] = "fmin_l_bfgs_b", n_restarts_optimizer: int = 0, normalize_y: bool = True, copy_X_train: bool = True, n_targets: Optional[int] = None, random_state: Optional[int] = None, ) -> None: super().__init__( kernel=kernel, alpha=alpha, optimizer=optimizer, n_restarts_optimizer=n_restarts_optimizer, normalize_y=normalize_y, copy_X_train=copy_X_train, n_targets=n_targets, random_state=random_state, )
[docs] def fit(self, X: npt.ArrayLike, y: npt.ArrayLike) -> "GaussianProcessQSAR": """Fit the Gaussian process, defaulting to a Tanimoto kernel. Parameters ---------- X : array-like of shape (n_samples, n_features) Training fingerprints or descriptors. y : array-like of shape (n_samples,) or (n_samples, n_targets) Training target values. Returns ------- GaussianProcessQSAR The fitted estimator. """ if self.kernel is None: # Resolve the QSAR default lazily, exactly where scikit-learn's # own GaussianProcessRegressor.fit() resolves kernel=None to an # RBF default - only choosing by what the data actually is. # # Tanimoto is the right kernel for fingerprints, but it is only # positive semi-definite on non-negative inputs, so on signed # descriptors (the norm after standardization) it produces an # indefinite Gram matrix and Cholesky fails. Pick RBF there. # # self.kernel is restored to None afterwards so __init__'s # "store exactly what was passed" contract still holds for # get_params()/clone() on the fitted estimator. import numpy as np from sklearn.gaussian_process.kernels import RBF, WhiteKernel X_arr = np.asarray(X, dtype=np.float64) self.kernel = ( TanimotoKernel() if X_arr.size and np.all(X_arr >= 0) else RBF() + WhiteKernel() ) try: super().fit(X, y) finally: self.kernel = None return self super().fit(X, y) return self