diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 17a6c39..fe752d8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -28,7 +28,7 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip setuptools wheel - python -m pip install "qunfold @ git+https://github.com/mirkobunse/qunfold@v0.1.4" + python -m pip install "qunfold @ git+https://github.com/mirkobunse/qunfold@main" python -m pip install -e .[bayes,tests] - name: Test with unittest run: python -m unittest @@ -47,7 +47,7 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip setuptools wheel "jax[cpu]" - python -m pip install "qunfold @ git+https://github.com/mirkobunse/qunfold@v0.1.4" + python -m pip install "qunfold @ git+https://github.com/mirkobunse/qunfold@main" python -m pip install -e .[neural,docs] - name: Build documentation run: sphinx-build -M html docs/source docs/build diff --git a/quapy/method/composable.py b/quapy/method/composable.py index 5d40aad..d4a7956 100644 --- a/quapy/method/composable.py +++ b/quapy/method/composable.py @@ -1,19 +1,26 @@ """This module allows the composition of quantification methods from loss functions and feature transformations. This functionality is realized through an integration of the qunfold package: https://github.com/mirkobunse/qunfold.""" -_import_error_message = """qunfold, the back-end of quapy.method.composable, is not properly installed. +from dataclasses import dataclass +from .base import BaseQuantifier + +# what to display when an ImportError is thrown +_IMPORT_ERROR_MESSAGE = """qunfold, the back-end of quapy.method.composable, is not properly installed. To fix this error, call: pip install --upgrade pip setuptools wheel pip install "jax[cpu]" - pip install "qunfold @ git+https://github.com/mirkobunse/qunfold@v0.1.4" + pip install "qunfold @ git+https://github.com/mirkobunse/qunfold@v0.1.5" """ +# try to import members of qunfold as members of this module try: import qunfold - from qunfold.quapy import QuaPyWrapper + from qunfold.base import BaseMixin + from qunfold.methods import AbstractMethod from qunfold.sklearn import CVClassifier from qunfold import ( + LinearMethod, # methods LeastSquaresLoss, # losses BlobelLoss, EnergyLoss, @@ -21,46 +28,81 @@ try: CombinedLoss, TikhonovRegularization, TikhonovRegularized, - ClassTransformer, # transformers - HistogramTransformer, - DistanceTransformer, - KernelTransformer, - EnergyKernelTransformer, - LaplacianKernelTransformer, - GaussianKernelTransformer, - GaussianRFFKernelTransformer, + ClassRepresentation, # representations + HistogramRepresentation, + DistanceRepresentation, + KernelRepresentation, + EnergyKernelRepresentation, + LaplacianKernelRepresentation, + GaussianKernelRepresentation, + GaussianRFFKernelRepresentation, ) - - __all__ = [ # control public members, e.g., for auto-documentation in sphinx; omit QuaPyWrapper - "ComposableQuantifier", - "CVClassifier", - "LeastSquaresLoss", - "BlobelLoss", - "EnergyLoss", - "HellingerSurrogateLoss", - "CombinedLoss", - "TikhonovRegularization", - "TikhonovRegularized", - "ClassTransformer", - "HistogramTransformer", - "DistanceTransformer", - "KernelTransformer", - "EnergyKernelTransformer", - "LaplacianKernelTransformer", - "GaussianKernelTransformer", - "GaussianRFFKernelTransformer", - ] except ImportError as e: - raise ImportError(_import_error_message) from e + raise ImportError(_IMPORT_ERROR_MESSAGE) from e -def ComposableQuantifier(loss, transformer, **kwargs): +__all__ = [ # control public members, e.g., for auto-documentation in sphinx + "QUnfoldWrapper", + "ComposableQuantifier", + "CVClassifier", + "LeastSquaresLoss", + "BlobelLoss", + "EnergyLoss", + "HellingerSurrogateLoss", + "CombinedLoss", + "TikhonovRegularization", + "TikhonovRegularized", + "ClassRepresentation", + "HistogramRepresentation", + "DistanceRepresentation", + "KernelRepresentation", + "EnergyKernelRepresentation", + "LaplacianKernelRepresentation", + "GaussianKernelRepresentation", + "GaussianRFFKernelRepresentation", +] + +@dataclass +class QUnfoldWrapper(BaseQuantifier,BaseMixin): + """A thin wrapper for using qunfold methods in QuaPy. + + Args: + _method: An instance of `qunfold.methods.AbstractMethod` to wrap. + + Examples: + Here, we wrap an instance of ACC to perform a grid search with QuaPy. + + >>> from qunfold import ACC + >>> qunfold_method = QUnfoldWrapper(ACC(RandomForestClassifier(obb_score=True))) + >>> quapy.model_selection.GridSearchQ( + >>> model = qunfold_method, + >>> param_grid = { # try both splitting criteria + >>> "representation__classifier__estimator__criterion": ["gini", "entropy"], + >>> }, + >>> # ... + >>> ) + """ + _method: AbstractMethod + def fit(self, data): # data is a qp.LabelledCollection + self._method.fit(*data.Xy, data.n_classes) + return self + def quantify(self, X): + return self._method.predict(X) + def set_params(self, **params): + self._method.set_params(**params) + return self + def get_params(self, deep=True): + return self._method.get_params(deep) + def __str__(self): + return self._method.__str__() + +def ComposableQuantifier(loss, representation, **kwargs): """A generic quantification / unfolding method that solves a linear system of equations. This class represents any quantifier that can be described in terms of a loss function, a feature transformation, and a regularization term. In this implementation, the loss is minimized through unconstrained second-order minimization. Valid probability estimates are ensured through a soft-max trick by Bunse (2022). Args: loss: An instance of a loss class from `quapy.methods.composable`. - transformer: An instance of a transformer class from `quapy.methods.composable`. + representation: An instance of a representation class from `quapy.methods.composable`. solver (optional): The `method` argument in `scipy.optimize.minimize`. Defaults to `"trust-ncg"`. solver_options (optional): The `options` argument in `scipy.optimize.minimize`. Defaults to `{"gtol": 1e-8, "maxiter": 1000}`. seed (optional): A random number generator seed from which a numpy RandomState is created. Defaults to `None`. @@ -72,12 +114,12 @@ def ComposableQuantifier(loss, transformer, **kwargs): >>> ComposableQuantifier, >>> TikhonovRegularized, >>> LeastSquaresLoss, - >>> ClassTransformer, + >>> ClassRepresentation, >>> ) >>> from sklearn.ensemble import RandomForestClassifier >>> o_acc = ComposableQuantifier( >>> TikhonovRegularized(LeastSquaresLoss(), 0.01), - >>> ClassTransformer(RandomForestClassifier(oob_score=True)) + >>> ClassRepresentation(RandomForestClassifier(oob_score=True)) >>> ) Here, we perform hyper-parameter optimization with the ordinal ACC. @@ -85,7 +127,7 @@ def ComposableQuantifier(loss, transformer, **kwargs): >>> quapy.model_selection.GridSearchQ( >>> model = o_acc, >>> param_grid = { # try both splitting criteria - >>> "transformer__classifier__estimator__criterion": ["gini", "entropy"], + >>> "representation__classifier__estimator__criterion": ["gini", "entropy"], >>> }, >>> # ... >>> ) @@ -96,7 +138,7 @@ def ComposableQuantifier(loss, transformer, **kwargs): >>> from sklearn.linear_model import LogisticRegression >>> acc_lr = ComposableQuantifier( >>> LeastSquaresLoss(), - >>> ClassTransformer(CVClassifier(LogisticRegression(), 10)) + >>> ClassRepresentation(CVClassifier(LogisticRegression(), 10)) >>> ) """ - return QuaPyWrapper(qunfold.GenericMethod(loss, transformer, **kwargs)) + return QUnfoldWrapper(LinearMethod(loss, representation, **kwargs)) diff --git a/quapy/tests/test_methods.py b/quapy/tests/test_methods.py index cf5bf39..18e7f52 100644 --- a/quapy/tests/test_methods.py +++ b/quapy/tests/test_methods.py @@ -14,20 +14,20 @@ from quapy.method.composable import ( ComposableQuantifier, LeastSquaresLoss, HellingerSurrogateLoss, - ClassTransformer, - HistogramTransformer, + ClassRepresentation, + HistogramRepresentation, CVClassifier, ) COMPOSABLE_METHODS = [ ComposableQuantifier( # ACC LeastSquaresLoss(), - ClassTransformer(CVClassifier(LogisticRegression())) + ClassRepresentation(CVClassifier(LogisticRegression())) ), ComposableQuantifier( # HDy HellingerSurrogateLoss(), - HistogramTransformer( + HistogramRepresentation( 3, # 3 bins per class - preprocessor = ClassTransformer(CVClassifier(LogisticRegression())) + preprocessor = ClassRepresentation(CVClassifier(LogisticRegression())) ) ), ]