Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
29 changes: 18 additions & 11 deletions docs/source/manuals/methods.md
Original file line number Diff line number Diff line change
Expand Up @@ -447,17 +447,24 @@ The [](quapy.method.composable) module allows the composition of quantification
```sh
pip install --upgrade pip setuptools wheel
pip install "jax[cpu]"
pip install "qunfold @ git+https://github.com/mirkobunse/[email protected].4"
pip install "qunfold @ git+https://github.com/mirkobunse/[email protected].5"
```

### Basics

The composition of a method is implemented through the [](quapy.method.composable.ComposableQuantifier) class. Its documentation also features an example to get you started in composing your own methods.

```python
from quapy.method.composable import (
ComposableQuantifier,
TikhonovRegularized,
LeastSquaresLoss,
ClassRepresentation,
)

ComposableQuantifier( # ordinal ACC, as proposed by Bunse et al., 2022
TikhonovRegularized(LeastSquaresLoss(), 0.01),
ClassTransformer(RandomForestClassifier(oob_score=True))
TikhonovRegularized(LeastSquaresLoss(), 0.01),
ClassRepresentation(RandomForestClassifier(oob_score=True))
)
```

Expand All @@ -484,16 +491,16 @@ You can use the [](quapy.method.composable.CombinedLoss) to create arbitrary, we

### Feature transformations

- [](quapy.method.composable.ClassTransformer)
- [](quapy.method.composable.DistanceTransformer)
- [](quapy.method.composable.HistogramTransformer)
- [](quapy.method.composable.EnergyKernelTransformer)
- [](quapy.method.composable.GaussianKernelTransformer)
- [](quapy.method.composable.LaplacianKernelTransformer)
- [](quapy.method.composable.GaussianRFFKernelTransformer)
- [](quapy.method.composable.ClassRepresentation)
- [](quapy.method.composable.DistanceRepresentation)
- [](quapy.method.composable.HistogramRepresentation)
- [](quapy.method.composable.EnergyKernelRepresentation)
- [](quapy.method.composable.GaussianKernelRepresentation)
- [](quapy.method.composable.LaplacianKernelRepresentation)
- [](quapy.method.composable.GaussianRFFKernelRepresentation)

```{hint}
The [](quapy.method.composable.ClassTransformer) requires the classifier to have a property `oob_score==True` and to produce a property `oob_decision_function` during fitting. In [scikit-learn](https://scikit-learn.org/), this requirement is fulfilled by any bagging classifier, such as random forests. Any other classifier needs to be cross-validated through the [](quapy.method.composable.CVClassifier).
The [](quapy.method.composable.ClassRepresentation) requires the classifier to have a property `oob_score==True` and to produce a property `oob_decision_function` during fitting. In [scikit-learn](https://scikit-learn.org/), this requirement is fulfilled by any bagging classifier, such as random forests. Any other classifier needs to be cross-validated through the [](quapy.method.composable.CVClassifier).
```


Expand Down
26 changes: 13 additions & 13 deletions examples/14.composable_methods.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
"""
This example illustrates the composition of quantification methods from
arbitrary loss functions and feature transformations. It will extend the basic
arbitrary loss functions and feature representations. It will extend the basic
example on the usage of quapy with this composition.

This example requires the installation of qunfold, the back-end of QuaPy's
composition module:

pip install --upgrade pip setuptools wheel
pip install "jax[cpu]"
pip install "qunfold @ git+https://github.com/mirkobunse/[email protected].4"
pip install "qunfold @ git+https://github.com/mirkobunse/[email protected].5"
"""

import numpy as np
Expand All @@ -24,20 +24,20 @@
training, testing = data.train_test

# We start by recovering PACC from its building blocks, a LeastSquaresLoss and
# a probabilistic ClassTransformer. A 5-fold cross-validation is implemented
# a probabilistic ClassRepresentation. A 5-fold cross-validation is implemented
# through a CVClassifier.

from quapy.method.composable import (
ComposableQuantifier,
LeastSquaresLoss,
ClassTransformer,
ClassRepresentation,
CVClassifier,
)
from sklearn.linear_model import LogisticRegression

pacc = ComposableQuantifier(
LeastSquaresLoss(),
ClassTransformer(
ClassRepresentation(
CVClassifier(LogisticRegression(random_state=0), 5),
is_probabilistic = True
),
Expand All @@ -63,7 +63,7 @@

model = ComposableQuantifier(
HellingerSurrogateLoss(), # the loss is different from before
ClassTransformer( # we use the same transformer
ClassRepresentation( # we use the same representation
CVClassifier(LogisticRegression(random_state=0), 5),
is_probabilistic = True
),
Expand All @@ -79,7 +79,7 @@
print(f"MAE = {np.mean(absolute_errors):.4f}+-{np.std(absolute_errors):.4f}")

# In general, any composed method solves a linear system of equations by
# minimizing the loss after transforming the data. Methods of this kind include
# minimizing the loss after representing the data. Methods of this kind include
# ACC, PACC, HDx, HDy, and many other well-known methods, as well as an
# unlimited number of re-combinations of their building blocks.

Expand All @@ -93,18 +93,18 @@

model = ComposableQuantifier(
CombinedLoss(HellingerSurrogateLoss(), LeastSquaresLoss()),
ClassTransformer(
ClassRepresentation(
CVClassifier(LogisticRegression(random_state=0), 5),
is_probabilistic = True
),
)

from qunfold.quapy import QuaPyWrapper
from qunfold import GenericMethod
from quapy.method.composable import QUnfoldWrapper
from qunfold import LinearMethod

model = QuaPyWrapper(GenericMethod(
model = QUnfoldWrapper(LinearMethod(
CombinedLoss(HellingerSurrogateLoss(), LeastSquaresLoss()),
ClassTransformer(
ClassRepresentation(
CVClassifier(LogisticRegression(random_state=0), 5),
is_probabilistic = True
),
Expand All @@ -115,7 +115,7 @@

param_grid = {
"loss__weights": [ (w, 1-w) for w in [.1, .5, .9] ],
"transformer__classifier__estimator__C": [1e-1, 1e1],
"representation__classifier__estimator__C": [1e-1, 1e1],
}

grid_search = qp.model_selection.GridSearchQ(
Expand Down
120 changes: 81 additions & 39 deletions quapy/method/composable.py
Original file line number Diff line number Diff line change
@@ -1,66 +1,108 @@
"""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/[email protected].4"
pip install "qunfold @ git+https://github.com/mirkobunse/[email protected].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,
HellingerSurrogateLoss,
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

__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, transformer, **kwargs):
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`.
Expand All @@ -72,20 +114,20 @@ 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.

>>> 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"],
>>> },
>>> # ...
>>> )
Expand All @@ -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))
10 changes: 5 additions & 5 deletions quapy/tests/test_methods.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,20 +14,20 @@
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()))
)
),
]
Expand Down