Source code for vanguard.classification.mixin

# © Crown Copyright GCHQ
#
# Licensed under the GNU General Public License, version 3 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.gnu.org/licenses/gpl-3.0.en.html
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""
Easily enable classification within a decorator.

The return classes of all classification decorators have a distinct structure
in which the standard prediction methods are unavailable. Instead, controllers
will have :meth:`~ClassificationMixin.classify_points` and
:meth:`~ClassificationMixin.classify_fuzzy_points` which should be used.
When creating new decorators, include the :class:`ClassificationMixin` as a
mixin for the inner class, and then decorate the inner class with :class:`Classification` before returning it.
"""

import warnings
from typing import NoReturn, TypeVar, Union

import numpy as np
import numpy.typing

from vanguard.base import GPController
from vanguard.decoratorutils import Decorator, wraps_class

T = TypeVar("T")


[docs] class ClassificationMixin: """Mixin that provides the base methods for classification."""
[docs] def classify_points( self, x: Union[float, numpy.typing.NDArray[np.floating]] ) -> tuple[numpy.typing.NDArray[np.integer], numpy.typing.NDArray[np.floating]]: """ Classify points. :param x: (n_predictions, n_features) The predictive inputs. :returns: (``predictions``, ``certainties``) where: * ``predictions``: (n_predictions,) The posterior predicted classes. * ``certainties``: (n_predictions,) The posterior predicted class probabilities. """ raise NotImplementedError
[docs] def classify_fuzzy_points( self, x: Union[float, numpy.typing.NDArray[np.floating]], x_std: Union[float, numpy.typing.NDArray[np.floating]] ) -> tuple[numpy.typing.NDArray[np.integer], numpy.typing.NDArray[np.floating]]: """ Classify fuzzy points. :param x: (n_predictions, n_features) The predictive inputs. :param x_std: The input noise standard deviations: * array_like[float]: (n_features,) The standard deviation per input dimension for the predictions, * float: Assume homoskedastic noise. :returns: (``predictions``, ``certainties``) where: * ``predictions``: (n_predictions,) The posterior predicted classes. * ``certainties``: (n_predictions,) The posterior predicted class probabilities. """ raise NotImplementedError
[docs] class Classification(Decorator): """ Converts a decorator class to expect a classification task. When used as a decorator for the output of classification decorators, this decorator automatically 'closes' the standard posterior methods. """
[docs] def __init__(self, **kwargs): ignore_methods = list(kwargs.pop("ignore_methods", [])) ignore_methods.extend( [ "posterior_over_point", "posterior_over_fuzzy_point", "predictive_likelihood", "fuzzy_predictive_likelihood", ] ) super().__init__(framework_class=GPController, required_decorators={}, ignore_methods=ignore_methods, **kwargs)
[docs] def verify_decorated_class(self, cls: type[T]) -> None: """ Verify that a class can be decorated by this instance. :param cls: The class to be decorated. :raises TypeError: If cls is not a subclass of the framework_class, or if another classification decorator has already been applied. """ super().verify_decorated_class(cls) if not issubclass(cls, ClassificationMixin): warnings.warn( f"Classification decorator applied to a class that doesn't " f"inherit from {ClassificationMixin.__name__}.", UserWarning, stacklevel=3, # stacklevel 2 is in BaseDecorator.__call__, so we raise this at the call site of BaseDecorator.__call__ ) for previous_decorator in cls.__decorators__: if issubclass(previous_decorator, Classification): msg = ( "This class is already decorated with a classification decorator. " "Please use only one classification decorator at once." ) raise TypeError(msg)
def _decorate_class(self, cls: type[T]) -> type[T]: """Close off the prediction methods on a GP.""" @wraps_class(cls) class InnerClass(cls): """Class that closes off the prediction methods.""" def posterior_over_point(self, x: Union[float, numpy.typing.NDArray[np.floating]]) -> NoReturn: """Use :meth:`classify_points` instead.""" raise TypeError("The 'classify_points' method should be used instead.") def posterior_over_fuzzy_point( self, x: Union[float, numpy.typing.NDArray[np.floating]], x_std: Union[float, numpy.typing.NDArray[np.floating]], ) -> NoReturn: """Use :meth:`classify_fuzzy_points` instead.""" raise TypeError("The 'classify_fuzzy_points' method should be used instead.") def predictive_likelihood(self, x: Union[float, numpy.typing.NDArray[np.floating]]) -> NoReturn: """Use :meth:`classify_points` instead.""" raise TypeError("The 'classify_points' method should be used instead.") def fuzzy_predictive_likelihood( self, x: Union[float, numpy.typing.NDArray[np.floating]], x_std: Union[float, numpy.typing.NDArray[np.floating]], ) -> NoReturn: """Use :meth:`classify_fuzzy_points` instead.""" raise TypeError("The 'classify_fuzzy_points' method should be used instead.") return InnerClass