+ + Source code for quapy.classification.calibration +from copy import deepcopy + +from sklearn.base import BaseEstimator, clone +from sklearn.model_selection import cross_val_predict, train_test_split +from sklearn.preprocessing import LabelEncoder +from sklearn.utils.validation import check_X_y +import numpy as np # Wrappers of calibration defined by Alexandari et al. in paper <http://proceedings.mlr.press/v119/alexandari20a.html> @@ -88,20 +384,20 @@ # see https://github.com/kundajelab/abstention -def _require_abstention_calibration(): +def _require_abstention_calibration(): try: - from abstention.calibration import NoBiasVectorScaling, TempScaling, VectorScaling + from abstention.calibration import NoBiasVectorScaling, TempScaling, VectorScaling except ImportError as exc: raise ImportError( "Calibration methods in quapy.classification.calibration require the optional " "'abstention' package." - ) from exc + ) from exc return NoBiasVectorScaling, TempScaling, VectorScaling [docs] -class RecalibratedProbabilisticClassifier: +class RecalibratedProbabilisticClassifier: """ Abstract class for (re)calibration method from `abstention.calibration`, as defined in `Alexandari, A., Kundaje, A., & Shrikumar, A. (2020, November). Maximum likelihood with bias-corrected calibration @@ -114,7 +410,7 @@ [docs] -class RecalibratedProbabilisticClassifierBase(BaseEstimator, RecalibratedProbabilisticClassifier): +class RecalibratedProbabilisticClassifierBase(BaseEstimator, RecalibratedProbabilisticClassifier): """ Applies a (re)calibration method from `abstention.calibration`, as defined in `Alexandari et al. paper <http://proceedings.mlr.press/v119/alexandari20a.html>`_. @@ -130,7 +426,7 @@ :param verbose: whether or not to display information in the standard output """ - def __init__(self, classifier, calibrator, val_split=5, n_jobs=None, verbose=False): + def __init__(self, classifier, calibrator, val_split=5, n_jobs=None, verbose=False): self.classifier = classifier self.calibrator = calibrator self.val_split = val_split @@ -139,7 +435,7 @@ [docs] - def fit(self, X, y): + def fit(self, X, y): """ Fits the calibration for the probabilistic classifier. @@ -160,7 +456,7 @@ [docs] - def fit_cv(self, X, y): + def fit_cv(self, X, y): """ Fits the calibration in a cross-validation manner, i.e., it generates posterior probabilities for all training instances via cross-validation, and then retrains the classifier on all training instances. @@ -181,7 +477,7 @@ [docs] - def fit_tr_val(self, X, y): + def fit_tr_val(self, X, y): """ Fits the calibration in a train/val-split manner, i.e.t, it partitions the training instances into a training and a validation set, and then uses the training samples to learn classifier which is then used @@ -202,7 +498,7 @@ [docs] - def predict(self, X): + def predict(self, X): """ Predicts class labels for the data instances in `X` @@ -214,7 +510,7 @@ [docs] - def predict_proba(self, X): + def predict_proba(self, X): """ Generates posterior probabilities for the data instances in `X` @@ -226,7 +522,7 @@ @property - def classes_(self): + def classes_(self): """ Returns the classes on which the classifier has been trained on @@ -238,7 +534,7 @@ [docs] -class NBVSCalibration(RecalibratedProbabilisticClassifierBase): +class NBVSCalibration(RecalibratedProbabilisticClassifierBase): """ Applies the No-Bias Vector Scaling (NBVS) calibration method from `abstention.calibration`, as defined in `Alexandari et al. paper <http://proceedings.mlr.press/v119/alexandari20a.html>`_: @@ -252,7 +548,7 @@ :param verbose: whether or not to display information in the standard output """ - def __init__(self, classifier, val_split=5, n_jobs=None, verbose=False): + def __init__(self, classifier, val_split=5, n_jobs=None, verbose=False): NoBiasVectorScaling, _, _ = _require_abstention_calibration() self.classifier = classifier self.calibrator = NoBiasVectorScaling(verbose=verbose) @@ -264,7 +560,7 @@ [docs] -class BCTSCalibration(RecalibratedProbabilisticClassifierBase): +class BCTSCalibration(RecalibratedProbabilisticClassifierBase): """ Applies the Bias-Corrected Temperature Scaling (BCTS) calibration method from `abstention.calibration`, as defined in `Alexandari et al. paper <http://proceedings.mlr.press/v119/alexandari20a.html>`_: @@ -278,7 +574,7 @@ :param verbose: whether or not to display information in the standard output """ - def __init__(self, classifier, val_split=5, n_jobs=None, verbose=False): + def __init__(self, classifier, val_split=5, n_jobs=None, verbose=False): _, TempScaling, _ = _require_abstention_calibration() self.classifier = classifier self.calibrator = TempScaling(verbose=verbose, bias_positions='all') @@ -290,7 +586,7 @@ [docs] -class TSCalibration(RecalibratedProbabilisticClassifierBase): +class TSCalibration(RecalibratedProbabilisticClassifierBase): """ Applies the Temperature Scaling (TS) calibration method from `abstention.calibration`, as defined in `Alexandari et al. paper <http://proceedings.mlr.press/v119/alexandari20a.html>`_: @@ -304,7 +600,7 @@ :param verbose: whether or not to display information in the standard output """ - def __init__(self, classifier, val_split=5, n_jobs=None, verbose=False): + def __init__(self, classifier, val_split=5, n_jobs=None, verbose=False): _, TempScaling, _ = _require_abstention_calibration() self.classifier = classifier self.calibrator = TempScaling(verbose=verbose) @@ -316,7 +612,7 @@ [docs] -class VSCalibration(RecalibratedProbabilisticClassifierBase): +class VSCalibration(RecalibratedProbabilisticClassifierBase): """ Applies the Vector Scaling (VS) calibration method from `abstention.calibration`, as defined in `Alexandari et al. paper <http://proceedings.mlr.press/v119/alexandari20a.html>`_: @@ -330,7 +626,7 @@ :param verbose: whether or not to display information in the standard output """ - def __init__(self, classifier, val_split=5, n_jobs=None, verbose=False): + def __init__(self, classifier, val_split=5, n_jobs=None, verbose=False): _, _, VectorScaling = _require_abstention_calibration() self.classifier = classifier self.calibrator = VectorScaling(verbose=verbose) @@ -342,7 +638,7 @@ [docs] -class TemperatureScalingFromLogits(BaseEstimator): +class TemperatureScalingFromLogits(BaseEstimator): """ Calibrates a matrix of logits by learning a temperature-scaling mapping with the calibration methods from `abstention.calibration`. @@ -357,13 +653,13 @@ information """ - def __init__(self, bias_corrected=False, verbose=False): + def __init__(self, bias_corrected=False, verbose=False): self.bias_corrected = bias_corrected self.verbose = verbose [docs] - def fit(self, X, y): + def fit(self, X, y): """ Fits the logits calibrator. @@ -398,7 +694,7 @@ [docs] - def predict_proba(self, X): + def predict_proba(self, X): """ Converts logits into calibrated posterior probabilities. @@ -412,7 +708,7 @@ [docs] - def predict(self, X): + def predict(self, X): """ Predicts class labels after calibration. @@ -427,31 +723,75 @@ - + + + + + + + + + + + + + - + + + + - Built with Sphinx using a - theme - provided by Read the Docs. - + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/build/html/_modules/quapy/classification/methods.html b/docs/build/html/_modules/quapy/classification/methods.html index d921a91..08e9987 100644 --- a/docs/build/html/_modules/quapy/classification/methods.html +++ b/docs/build/html/_modules/quapy/classification/methods.html @@ -1,88 +1,384 @@ - - - - - - quapy.classification.methods — QuaPy: A Python-based open-source framework for quantification 0.2.1 documentation - - + + + + + + + + quapy.classification.methods — QuaPy: A Python-based open-source framework for quantification 0.2.1 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Skip to main content - - - - - - - - - + + + + Back to top + + + + + + + + + + Ctrl+K + + - - - - - - - - - - QuaPy: A Python-based open-source framework for quantification - - - - - - - + + - - -Quickstart - - -Manuals - - -API - - - - + + + + + + + + + + + - - - QuaPy: A Python-based open-source framework for quantification - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Home + + + + + + + Manuals + + + + + + + API + + - - - - - - Module code - quapy.classification.methods - - - + + + + + + + + + + + + + Search + Ctrl+K + + + + + + + + + + + + + + System Settings + Light + Dark + + + + + + + + + + + + + + GitHub + + + + + + + + + + + + + Search + Ctrl+K + + + + + - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Home + + + + + + + Manuals + + + + + + + API + + + + + + + + + + + + + + + + + + + + + + + System Settings + Light + Dark + + + + + + + + + + + + + + GitHub + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Module code + + quapy.classification.methods + + + + + + + + + + + + + + + + Source code for quapy.classification.methods -import numpy as np -from sklearn.base import BaseEstimator -from sklearn.decomposition import TruncatedSVD -from sklearn.linear_model import LogisticRegression +import numpy as np +from sklearn.base import BaseEstimator +from sklearn.decomposition import TruncatedSVD +from sklearn.linear_model import LogisticRegression [docs] -class LowRankLogisticRegression(BaseEstimator): +class LowRankLogisticRegression(BaseEstimator): """ An example of a classification method (i.e., an object that implements `fit`, `predict`, and `predict_proba`) that also generates embedded inputs (i.e., that implements `transform`), as those required for @@ -96,13 +392,13 @@ `Logistic Regression <https://scikit-learn.org/stable/modules/generated/sklearn.linear_model.LogisticRegression.html>`__ classifier """ - def __init__(self, n_components=100, **kwargs): + def __init__(self, n_components=100, **kwargs): self.n_components = n_components self.classifier = LogisticRegression(**kwargs) [docs] - def get_params(self): + def get_params(self): """ Get hyper-parameters for this estimator. @@ -115,7 +411,7 @@ [docs] - def set_params(self, **params): + def set_params(self, **params): """ Set the parameters of this estimator. @@ -132,7 +428,7 @@ [docs] - def fit(self, X, y): + def fit(self, X, y): """ Fit the model according to the given training data. The fit consists of fitting `TruncatedSVD` and then `LogisticRegression` on the low-rank representation. @@ -153,7 +449,7 @@ [docs] - def predict(self, X): + def predict(self, X): """ Predicts labels for the instances `X` embedded into the low-rank space. @@ -167,7 +463,7 @@ [docs] - def predict_proba(self, X): + def predict_proba(self, X): """ Predicts posterior probabilities for the instances `X` embedded into the low-rank space. @@ -180,7 +476,7 @@ [docs] - def transform(self, X): + def transform(self, X): """ Returns the low-rank approximation of `X` with `n_components` dimensions, or `X` unaltered if `n_components` >= `X.shape[1]`. @@ -197,7 +493,7 @@ [docs] -class MockClassifierFromPosteriors(BaseEstimator): +class MockClassifierFromPosteriors(BaseEstimator): """ Mock classifier that bypasses classifier training when the input instances are already posterior probabilities produced by a pretrained probabilistic @@ -208,50 +504,94 @@ [docs] - def fit(self, X, y): + def fit(self, X, y): self.classes_ = np.sort(np.unique(y)) return self [docs] - def predict(self, X): + def predict(self, X): return np.argmax(X, axis=1) [docs] - def predict_proba(self, X): + def predict_proba(self, X): return X - + + + + + + + + + + + + + - + + + + - Built with Sphinx using a - theme - provided by Read the Docs. - + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/build/html/_modules/quapy/classification/neural.html b/docs/build/html/_modules/quapy/classification/neural.html index e5788fa..871d391 100644 --- a/docs/build/html/_modules/quapy/classification/neural.html +++ b/docs/build/html/_modules/quapy/classification/neural.html @@ -29,7 +29,7 @@ - + @@ -370,26 +370,27 @@ Source code for quapy.classification.neural -import os -from abc import ABCMeta, abstractmethod -from pathlib import Path +import logging +import os +from abc import ABCMeta, abstractmethod +from pathlib import Path -import numpy as np -import torch -import torch.nn as nn -import torch.nn.functional as F -from sklearn.metrics import accuracy_score, f1_score -from torch.nn.utils.rnn import pad_sequence -from tqdm import tqdm +import numpy as np +import torch +import torch.nn as nn +import torch.nn.functional as F +from sklearn.metrics import accuracy_score, f1_score +from torch.nn.utils.rnn import pad_sequence +from tqdm import tqdm -import quapy as qp -from quapy.data import LabelledCollection -from quapy.util import EarlyStop +import quapy as qp +from quapy.data import LabelledCollection +from quapy.util import EarlyStop [docs] -class NeuralClassifierTrainer: +class NeuralClassifierTrainer: """ Trains a neural network for text classification. @@ -407,7 +408,7 @@ according to the evaluation in the held-out validation split (default '../checkpoint/classifier_net.dat') """ - def __init__(self, + def __init__(self, net: 'TextClassifierNet', lr=1e-3, weight_decay=0, @@ -416,7 +417,7 @@ batch_size=64, batch_size_test=512, padding_length=300, - device='cuda', + device='cpu', checkpointpath='../checkpoint/classifier_net.dat'): super().__init__() @@ -437,12 +438,12 @@ self.learner_hyperparams = self.net.get_params() self.checkpointpath = checkpointpath - print(f'[NeuralNetwork running on {device}]') + logging.getLogger(__name__).info(f'NeuralNetwork running on {device}') os.makedirs(Path(checkpointpath).parent, exist_ok=True) [docs] - def reset_net_params(self, vocab_size, n_classes): + def reset_net_params(self, vocab_size, n_classes): """Reinitialize the network parameters :param vocab_size: the size of the vocabulary @@ -455,7 +456,7 @@ [docs] - def get_params(self): + def get_params(self): """Get hyper-parameters for this estimator :return: a dictionary with parameter names mapped to their values @@ -465,7 +466,7 @@ [docs] - def set_params(self, **params): + def set_params(self, **params): """Set the parameters of this trainer and the learner it is training. In this current version, parameter names for the trainer and learner should be disjoint. @@ -491,14 +492,14 @@ @property - def device(self): + def device(self): """ Gets the device in which the network is allocated :return: device """ return next(self.net.parameters()).device - def _train_epoch(self, data, status, pbar, epoch): + def _train_epoch(self, data, status, pbar, epoch): self.net.train() criterion = torch.nn.CrossEntropyLoss() losses, predictions, true_labels = [], [], [] @@ -518,7 +519,7 @@ status["f1"] = f1_score(true_labels, predictions, average='macro') self.__update_progress_bar(pbar, epoch) - def _test_epoch(self, data, status, pbar, epoch): + def _test_epoch(self, data, status, pbar, epoch): self.net.eval() criterion = torch.nn.CrossEntropyLoss() losses, predictions, true_labels = [], [], [] @@ -536,7 +537,7 @@ status["f1"] = f1_score(true_labels, predictions, average='macro') self.__update_progress_bar(pbar, epoch) - def __update_progress_bar(self, pbar, epoch): + def __update_progress_bar(self, pbar, epoch): pbar.set_description(f'[{self.net.__class__.__name__}] training epoch={epoch} ' f'tr-loss={self.status["tr"]["loss"]:.5f} ' f'tr-acc={100 * self.status["tr"]["acc"]:.2f}% ' @@ -548,7 +549,7 @@ [docs] - def fit(self, instances, labels, val_split=0.3): + def fit(self, instances, labels, val_split=0.3): """ Fits the model according to the given training data. @@ -583,21 +584,22 @@ if self.early_stop.IMPROVED: torch.save(self.net.state_dict(), checkpoint) elif self.early_stop.STOP: - print(f'training ended by patience exhasted; loading best model parameters in {checkpoint} ' - f'for epoch {self.early_stop.best_epoch}') + logging.getLogger(__name__).info( + f'training ended by patience exhausted; loading best model parameters in {checkpoint} ' + f'for epoch {self.early_stop.best_epoch}') self.net.load_state_dict(torch.load(checkpoint)) break - print('performing one training pass over the validation set...') + logging.getLogger(__name__).info('performing one training pass over the validation set...') self._train_epoch(valid_generator, self.status['tr'], pbar, epoch=0) - print('[done]') + logging.getLogger(__name__).info('done') return self [docs] - def predict(self, instances): + def predict(self, instances): """ Predicts labels for the instances @@ -610,7 +612,7 @@ [docs] - def predict_proba(self, instances): + def predict_proba(self, instances): """ Predicts posterior probabilities for the instances @@ -629,7 +631,7 @@ [docs] - def transform(self, instances): + def transform(self, instances): """ Returns the embeddings of the instances @@ -651,7 +653,7 @@ [docs] -class TorchDataset(torch.utils.data.Dataset): +class TorchDataset(torch.utils.data.Dataset): """ Transforms labelled instances into a Torch's :class:`torch.utils.data.DataLoader` object @@ -659,19 +661,19 @@ :param labels: array-like of shape `(n_samples, n_classes)` with the class labels """ - def __init__(self, instances, labels=None): + def __init__(self, instances, labels=None): self.instances = instances self.labels = labels - def __len__(self): + def __len__(self): return len(self.instances) - def __getitem__(self, index): + def __getitem__(self, index): return {'doc': self.instances[index], 'label': self.labels[index] if self.labels is not None else None} [docs] - def asDataloader(self, batch_size, shuffle, pad_length, device): + def asDataloader(self, batch_size, shuffle, pad_length, device): """ Converts the labelled collection into a Torch DataLoader with dynamic padding for the batch @@ -684,7 +686,7 @@ :param device: whether to allocate tensors in cpu or in cuda :return: a :class:`torch.utils.data.DataLoader` object """ - def collate(batch): + def collate(batch): data = [torch.LongTensor(item['doc'][:pad_length]) for item in batch] data = pad_sequence(data, batch_first=True, padding_value=qp.environ['PAD_INDEX']).to(device) targets = [item['label'] for item in batch] @@ -702,7 +704,7 @@ [docs] -class TextClassifierNet(torch.nn.Module, metaclass=ABCMeta): +class TextClassifierNet(torch.nn.Module, metaclass=ABCMeta): """ Abstract Text classifier (`torch.nn.Module`) """ @@ -710,7 +712,7 @@ [docs] @abstractmethod - def document_embedding(self, x): + def document_embedding(self, x): """Embeds documents (i.e., performs the forward pass up to the next-to-last layer). @@ -725,7 +727,7 @@ [docs] - def forward(self, x): + def forward(self, x): """Performs the forward pass. :param x: a batch of instances, typically generated by a torch's `DataLoader` @@ -739,7 +741,7 @@ [docs] - def dimensions(self): + def dimensions(self): """Gets the number of dimensions of the embedding space :return: integer @@ -749,7 +751,7 @@ [docs] - def predict_proba(self, x): + def predict_proba(self, x): """ Predicts posterior probabilities for the instances in `x` @@ -764,7 +766,7 @@ [docs] - def xavier_uniform(self): + def xavier_uniform(self): """ Performs Xavier initialization of the network parameters """ @@ -776,7 +778,7 @@ [docs] @abstractmethod - def get_params(self): + def get_params(self): """ Get hyper-parameters for this estimator @@ -786,7 +788,7 @@ @property - def vocabulary_size(self): + def vocabulary_size(self): """ Return the size of the vocabulary @@ -798,7 +800,7 @@ [docs] -class LSTMnet(TextClassifierNet): +class LSTMnet(TextClassifierNet): """ An implementation of :class:`quapy.classification.neural.TextClassifierNet` based on Long Short Term Memory networks. @@ -812,7 +814,7 @@ :param drop_p: drop probability for dropout (default 0.5) """ - def __init__(self, vocabulary_size, n_classes, embedding_size=100, hidden_size=256, repr_size=100, lstm_class_nlayers=1, + def __init__(self, vocabulary_size, n_classes, embedding_size=100, hidden_size=256, repr_size=100, lstm_class_nlayers=1, drop_p=0.5): super().__init__() @@ -834,7 +836,7 @@ self.doc_embedder = torch.nn.Linear(hidden_size, self.dim) self.output = torch.nn.Linear(self.dim, n_classes) - def __init_hidden(self, set_size): + def __init_hidden(self, set_size): opt = self.hyperparams var_hidden = torch.zeros(opt['lstm_class_nlayers'], set_size, opt['hidden_size']) var_cell = torch.zeros(opt['lstm_class_nlayers'], set_size, opt['hidden_size']) @@ -844,7 +846,7 @@ [docs] - def document_embedding(self, x): + def document_embedding(self, x): """Embeds documents (i.e., performs the forward pass up to the next-to-last layer). @@ -863,7 +865,7 @@ [docs] - def get_params(self): + def get_params(self): """ Get hyper-parameters for this estimator @@ -873,7 +875,7 @@ @property - def vocabulary_size(self): + def vocabulary_size(self): """ Return the size of the vocabulary @@ -885,7 +887,7 @@ [docs] -class CNNnet(TextClassifierNet): +class CNNnet(TextClassifierNet): """ An implementation of :class:`quapy.classification.neural.TextClassifierNet` based on Convolutional Neural Networks. @@ -902,7 +904,7 @@ :param drop_p: drop probability for dropout (default 0.5) """ - def __init__(self, vocabulary_size, n_classes, embedding_size=100, hidden_size=256, repr_size=100, + def __init__(self, vocabulary_size, n_classes, embedding_size=100, hidden_size=256, repr_size=100, kernel_heights=[3, 5, 7], stride=1, padding=0, drop_p=0.5): super(CNNnet, self).__init__() @@ -927,7 +929,7 @@ self.doc_embedder = torch.nn.Linear(len(kernel_heights) * hidden_size, self.dim) self.output = nn.Linear(self.dim, n_classes) - def __conv_block(self, input, conv_layer): + def __conv_block(self, input, conv_layer): conv_out = conv_layer(input) # conv_out.size() = (batch_size, out_channels, dim, 1) activation = F.relu(conv_out.squeeze(3)) # activation.size() = (batch_size, out_channels, dim1) max_out = F.max_pool1d(activation, activation.size()[2]).squeeze(2) # maxpool_out.size() = (batch_size, out_channels) @@ -935,7 +937,7 @@ [docs] - def document_embedding(self, input): + def document_embedding(self, input): """Embeds documents (i.e., performs the forward pass up to the next-to-last layer). @@ -960,7 +962,7 @@ [docs] - def get_params(self): + def get_params(self): """ Get hyper-parameters for this estimator @@ -970,7 +972,7 @@ @property - def vocabulary_size(self): + def vocabulary_size(self): """ Return the size of the vocabulary diff --git a/docs/build/html/_modules/quapy/classification/svmperf.html b/docs/build/html/_modules/quapy/classification/svmperf.html index 9b60c1a..e855546 100644 --- a/docs/build/html/_modules/quapy/classification/svmperf.html +++ b/docs/build/html/_modules/quapy/classification/svmperf.html @@ -1,94 +1,391 @@ - - - - - - quapy.classification.svmperf — QuaPy: A Python-based open-source framework for quantification 0.2.1 documentation - - + + + + + + + + quapy.classification.svmperf — QuaPy: A Python-based open-source framework for quantification 0.2.1 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Skip to main content - - - - - - - - - + + + + Back to top + + + + + + + + + + Ctrl+K + + - - - - - - - - - - QuaPy: A Python-based open-source framework for quantification - - - - - - - + + - - -Quickstart - - -Manuals - - -API - - - - + + + + + + + + + + + - - - QuaPy: A Python-based open-source framework for quantification - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Home + + + + + + + Manuals + + + + + + + API + + - - - - - - Module code - quapy.classification.svmperf - - - + + + + + + + + + + + + + Search + Ctrl+K + + + + + + + + + + + + + + System Settings + Light + Dark + + + + + + + + + + + + + + GitHub + + + + + + + + + + + + + Search + Ctrl+K + + + + + - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Home + + + + + + + Manuals + + + + + + + API + + + + + + + + + + + + + + + + + + + + + + + System Settings + Light + Dark + + + + + + + + + + + + + + GitHub + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Module code + + quapy.classification.svmperf + + + + + + + + + + + + + + + + Source code for quapy.classification.svmperf -import random -import shutil -import subprocess -import tempfile -from os import remove, makedirs -from os.path import join, exists -from subprocess import PIPE, STDOUT -import numpy as np -from sklearn.base import BaseEstimator, ClassifierMixin -from sklearn.datasets import dump_svmlight_file +import logging +import random +import shutil +import subprocess +import tempfile +from os import remove, makedirs +from os.path import join, exists +from subprocess import PIPE, STDOUT +import numpy as np +from sklearn.base import BaseEstimator, ClassifierMixin +from sklearn.datasets import dump_svmlight_file [docs] -class SVMperf(BaseEstimator, ClassifierMixin): +class SVMperf(BaseEstimator, ClassifierMixin): """A wrapper for the `SVM-perf package <https://www.cs.cornell.edu/people/tj/svm_light/svm_perf.html>`__ by Thorsten Joachims. When using losses for quantification, the source code has to be patched. See the `installation documentation <https://hlt-isti.github.io/QuaPy/build/html/Installation.html#svm-perf-with-quantification-oriented-losses>`__ @@ -110,7 +407,7 @@ # losses with their respective codes in svm_perf implementation valid_losses = {'01':0, 'f1':1, 'kld':12, 'nkld':13, 'q':22, 'qacc':23, 'qf1':24, 'qgm':25, 'mae':26, 'mrae':27} - def __init__(self, svmperf_base, C=0.01, verbose=False, loss='01', host_folder=None): + def __init__(self, svmperf_base, C=0.01, verbose=False, loss='01', host_folder=None): assert exists(svmperf_base), \ (f'path {svmperf_base} does not seem to point to a valid path;' f'did you install svm-perf? ' @@ -123,7 +420,7 @@ [docs] - def fit(self, X, y): + def fit(self, X, y): """ Trains the SVM for the multivariate performance loss @@ -146,8 +443,7 @@ # this would allow to run parallel instances of predict random_code = 'svmperfprocess'+'-'.join(str(local_random.randint(0, 1000000)) for _ in range(5)) if self.host_folder is None: - # tmp dir are removed after the fit terminates in multiprocessing... - self.tmpdir = tempfile.TemporaryDirectory(suffix=random_code).name + self.tmpdir = join(tempfile.gettempdir(), random_code) else: self.tmpdir = join(self.host_folder, '.' + random_code) makedirs(self.tmpdir, exist_ok=True) @@ -159,21 +455,21 @@ cmd = ' '.join([self.svmperf_learn, self.c_cmd, self.loss_cmd, traindat, self.model]) if self.verbose: - print('[Running]', cmd) - p = subprocess.run(cmd.split(), stdout=PIPE, stderr=STDOUT) + logging.getLogger(__name__).info(f'[Running] {cmd}') + p = subprocess.run(cmd.split(), stdout=PIPE, stderr=PIPE) if not exists(self.model): - print(p.stderr.decode('utf-8')) + logging.getLogger(__name__).error(p.stderr.decode('utf-8')) remove(traindat) if self.verbose: - print(p.stdout.decode('utf-8')) + logging.getLogger(__name__).info(p.stdout.decode('utf-8')) return self [docs] - def predict(self, X): + def predict(self, X): """ Predicts labels for the instances `X` @@ -188,7 +484,7 @@ [docs] - def decision_function(self, X, y=None): + def decision_function(self, X, y=None): """ Evaluate the decision function for the samples in `X`. @@ -211,11 +507,11 @@ cmd = ' '.join([self.svmperf_classify, testdat, self.model, predictions_path]) if self.verbose: - print('[Running]', cmd) + logging.getLogger(__name__).info(f'[Running] {cmd}') p = subprocess.run(cmd.split(), stdout=PIPE, stderr=STDOUT) if self.verbose: - print(p.stdout.decode('utf-8')) + logging.getLogger(__name__).info(p.stdout.decode('utf-8')) scores = np.loadtxt(predictions_path) remove(testdat) @@ -224,38 +520,82 @@ return scores - def __del__(self): + def __del__(self): if hasattr(self, 'tmpdir'): shutil.rmtree(self.tmpdir, ignore_errors=True) - + + + + + + + + + + + + + - + + + + - Built with Sphinx using a - theme - provided by Read the Docs. - + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/build/html/_modules/quapy/data/base.html b/docs/build/html/_modules/quapy/data/base.html index 27f34de..87203ec 100644 --- a/docs/build/html/_modules/quapy/data/base.html +++ b/docs/build/html/_modules/quapy/data/base.html @@ -1,96 +1,392 @@ - - - - - - quapy.data.base — QuaPy: A Python-based open-source framework for quantification 0.2.1 documentation - - + + + + + + + + quapy.data.base — QuaPy: A Python-based open-source framework for quantification 0.2.1 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Skip to main content - - - - - - - - - + + + + Back to top + + + + + + + + + + Ctrl+K + + - - - - - - - - - - QuaPy: A Python-based open-source framework for quantification - - - - - - - + + - - -Quickstart - - -Manuals - - -API - - - - + + + + + + + + + + + - - - QuaPy: A Python-based open-source framework for quantification - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Home + + + + + + + Manuals + + + + + + + API + + - - - - - - Module code - quapy.data.base - - - - - - - - Source code for quapy.data.base -import itertools -from functools import cached_property -from typing import Iterable + + + + + + + + + -import numpy as np -from scipy.sparse import issparse -from scipy.sparse import vstack -from sklearn.model_selection import train_test_split, RepeatedStratifiedKFold -from numpy.random import RandomState -from quapy.functional import strprev -from quapy.util import temp_seed -import quapy.functional as F + + + Search + Ctrl+K + + + + + + + + + + + + + + System Settings + Light + Dark + + + + + + + + + + + + + + GitHub + + + + + + + + + + + + + Search + Ctrl+K + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Home + + + + + + + Manuals + + + + + + + API + + + + + + + + + + + + + + + + + + + + + + + System Settings + Light + Dark + + + + + + + + + + + + + + GitHub + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Module code + + quapy.data.base + + + + + + + + + + + + + + + + + Source code for quapy.data.base +import itertools +from functools import cached_property +from typing import Iterable + +import numpy as np +from scipy.sparse import issparse +from scipy.sparse import vstack +from sklearn.model_selection import train_test_split, RepeatedStratifiedKFold +from numpy.random import RandomState +from quapy.functional import strprev +from quapy.util import temp_seed +import quapy.functional as F [docs] -class LabelledCollection: +class LabelledCollection: """ A LabelledCollection is a set of objects each with a label attached to each of them. This class implements several sampling routines and other utilities. @@ -102,7 +398,7 @@ (i.e., a prevalence of 0) """ - def __init__(self, instances, labels, classes=None): + def __init__(self, instances, labels, classes=None): if issparse(instances): self.instances = instances elif isinstance(instances, list) and len(instances) > 0 and isinstance(instances[0], str): @@ -121,7 +417,7 @@ self._index = None @property - def index(self): + def index(self): if not hasattr(self, '_index') or self._index is None: self._index = {class_: np.arange(len(self))[self.labels == class_] for class_ in self.classes_} return self._index @@ -129,7 +425,7 @@ [docs] @classmethod - def load(cls, path: str, loader_func: callable, classes=None, **loader_kwargs): + def load(cls, path: str, loader_func: callable, classes=None, **loader_kwargs): """ Loads a labelled set of data and convert it into a :class:`LabelledCollection` instance. The function in charge of reading the instances must be specified. This function can be a custom one, or any of the reading functions @@ -146,7 +442,7 @@ return LabelledCollection(*loader_func(path, **loader_kwargs), classes) - def __len__(self): + def __len__(self): """ Returns the length of this collection (number of labelled instances) @@ -156,7 +452,7 @@ [docs] - def prevalence(self): + def prevalence(self): """ Returns the prevalence, or relative frequency, of the classes in the codeframe. @@ -168,7 +464,7 @@ [docs] - def counts(self): + def counts(self): """ Returns the number of instances for each of the classes in the codeframe. @@ -179,7 +475,7 @@ @property - def n_classes(self): + def n_classes(self): """ The number of classes @@ -188,7 +484,7 @@ return len(self.classes_) @property - def n_instances(self): + def n_instances(self): """ The number of instances @@ -197,7 +493,7 @@ return len(self.labels) @property - def binary(self): + def binary(self): """ Returns True if the number of classes is 2 @@ -207,7 +503,7 @@ [docs] - def sampling_index(self, size, *prevs, shuffle=True, random_state=None): + def sampling_index(self, size, *prevs, shuffle=True, random_state=None): """ Returns an index to be used to extract a random sample of desired size and desired prevalence values. If the prevalence values are not specified, then returns the index of a uniform sampling. @@ -270,7 +566,7 @@ [docs] - def uniform_sampling_index(self, size, random_state=None): + def uniform_sampling_index(self, size, random_state=None): """ Returns an index to be used to extract a uniform sample of desired size. The sampling is drawn with replacement. @@ -288,7 +584,7 @@ [docs] - def sampling(self, size, *prevs, shuffle=True, random_state=None): + def sampling(self, size, *prevs, shuffle=True, random_state=None): """ Return a random sample (an instance of :class:`LabelledCollection`) of desired size and desired prevalence values. For each class, the sampling is drawn with replacement. @@ -308,7 +604,7 @@ [docs] - def uniform_sampling(self, size, random_state=None): + def uniform_sampling(self, size, random_state=None): """ Returns a uniform sample (an instance of :class:`LabelledCollection`) of desired size. The sampling is drawn with replacement. @@ -323,7 +619,7 @@ [docs] - def sampling_from_index(self, index): + def sampling_from_index(self, index): """ Returns an instance of :class:`LabelledCollection` whose elements are sampled from this collection using the index. @@ -338,7 +634,7 @@ [docs] - def split_stratified(self, train_prop=0.6, random_state=None): + def split_stratified(self, train_prop=0.6, random_state=None): """ Returns two instances of :class:`LabelledCollection` split with stratification from this collection, at desired proportion. @@ -360,7 +656,7 @@ [docs] - def split_random(self, train_prop=0.6, random_state=None): + def split_random(self, train_prop=0.6, random_state=None): """ Returns two instances of :class:`LabelledCollection` split randomly from this collection, at desired proportion. @@ -387,7 +683,7 @@ return training, test - def __add__(self, other): + def __add__(self, other): """ Returns a new :class:`LabelledCollection` as the union of this collection with another collection. Both labelled collections must have the same classes. @@ -403,7 +699,7 @@ [docs] @classmethod - def join(cls, *args: Iterable['LabelledCollection']): + def join(cls, *args: Iterable['LabelledCollection']): """ Returns a new :class:`LabelledCollection` as the union of the collections given in input. @@ -439,12 +735,14 @@ else: raise NotImplementedError('unsupported operation for collection types') labels = np.concatenate([lc.labels for lc in args]) - classes = np.unique(labels).sort() + # union of each collection's own classes_, so a class declared but absent from + # this particular join (e.g. an empty fold) is preserved at zero prevalence + classes = np.unique(np.concatenate([lc.classes_ for lc in args])) return LabelledCollection(instances, labels, classes=classes) @property - def classes(self): + def classes(self): """ Gets an array-like with the classes used in this collection @@ -453,7 +751,7 @@ return self.classes_ @property - def Xy(self): + def Xy(self): """ Gets the instances and labels. This is useful when working with `sklearn` estimators, e.g.: @@ -464,7 +762,7 @@ return self.instances, self.labels @property - def Xp(self): + def Xp(self): """ Gets the instances and the true prevalence. This is useful when implementing evaluation protocols from a :class:`LabelledCollection` object. @@ -474,7 +772,7 @@ return self.instances, self.prevalence() @property - def X(self): + def X(self): """ An alias to self.instances @@ -483,7 +781,7 @@ return self.instances @property - def y(self): + def y(self): """ An alias to self.labels @@ -492,7 +790,7 @@ return self.labels @property - def p(self): + def p(self): """ An alias to self.prevalence() @@ -503,7 +801,7 @@ [docs] - def stats(self, show=True): + def stats(self, show=True): """ Returns (and eventually prints) a dictionary with some stats of this collection. E.g.,: @@ -538,7 +836,7 @@ [docs] - def kFCV(self, nfolds=5, nrepeats=1, random_state=None): + def kFCV(self, nfolds=5, nrepeats=1, random_state=None): """ Generator of stratified folds to be used in k-fold cross validation. @@ -554,7 +852,7 @@ yield train, test - def __repr__(self): + def __repr__(self): repr=f'<{self.n_instances} instances (dtype={type(self.instances[0])}), ' repr+=f'n_classes={self.n_classes} {self.classes_}, prevalence={F.strprev(self.prevalence())}>' return repr @@ -563,7 +861,7 @@ [docs] -class Dataset: +class Dataset: """ Abstraction of training and test :class:`LabelledCollection` objects. @@ -573,7 +871,7 @@ :param name: a string representing the name of the dataset """ - def __init__(self, training: LabelledCollection, test: LabelledCollection, vocabulary: dict = None, name=''): + def __init__(self, training: LabelledCollection, test: LabelledCollection, vocabulary: dict = None, name=''): assert set(training.classes_) == set(test.classes_), 'incompatible labels in training and test collections' self.training = training self.test = test @@ -583,7 +881,7 @@ [docs] @classmethod - def SplitStratified(cls, collection: LabelledCollection, train_size=0.6): + def SplitStratified(cls, collection: LabelledCollection, train_size=0.6): """ Generates a :class:`Dataset` from a stratified split of a :class:`LabelledCollection` instance. See :meth:`LabelledCollection.split_stratified` @@ -596,7 +894,7 @@ @property - def classes_(self): + def classes_(self): """ The classes according to which the training collection is labelled @@ -605,7 +903,7 @@ return self.training.classes_ @property - def n_classes(self): + def n_classes(self): """ The number of classes according to which the training collection is labelled @@ -614,7 +912,7 @@ return self.training.n_classes @property - def binary(self): + def binary(self): """ Returns True if the training collection is labelled according to two classes @@ -625,7 +923,7 @@ [docs] @classmethod - def load(cls, train_path, test_path, loader_func: callable, classes=None, **loader_kwargs): + def load(cls, train_path, test_path, loader_func: callable, classes=None, **loader_kwargs): """ Loads a training and a test labelled set of data and convert it into a :class:`Dataset` instance. The function in charge of reading the instances must be specified. This function can be a custom one, or any of @@ -647,7 +945,7 @@ @property - def vocabulary_size(self): + def vocabulary_size(self): """ If the dataset is textual, and the vocabulary was indicated, returns the size of the vocabulary @@ -656,7 +954,7 @@ return len(self.vocabulary) @property - def train_test(self): + def train_test(self): """ Alias to `self.training` and `self.test` @@ -667,7 +965,7 @@ [docs] - def stats(self, show=True): + def stats(self, show=True): """ Returns (and eventually prints) a dictionary with some stats of this dataset. E.g.,: @@ -694,7 +992,7 @@ [docs] @classmethod - def kFCV(cls, data: LabelledCollection, nfolds=5, nrepeats=1, random_state=0): + def kFCV(cls, data: LabelledCollection, nfolds=5, nrepeats=1, random_state=0): """ Generator of stratified folds to be used in k-fold cross validation. This function is only a wrapper around :meth:`LabelledCollection.kFCV` that returns :class:`Dataset` instances made of training and test folds. @@ -711,7 +1009,7 @@ [docs] - def reduce(self, n_train=100, n_test=100, random_state=None): + def reduce(self, n_train=100, n_test=100, random_state=None): """ Reduce the number of instances in place for quick experiments. Preserves the prevalence of each set. @@ -732,36 +1030,80 @@ return self - def __repr__(self): + def __repr__(self): return f'training={self.training}; test={self.test}' - + + + + + + + + + + + + + - + + + + - Built with Sphinx using a - theme - provided by Read the Docs. - + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/build/html/_modules/quapy/data/datasets.html b/docs/build/html/_modules/quapy/data/datasets.html index 20d9588..cc6a18b 100644 --- a/docs/build/html/_modules/quapy/data/datasets.html +++ b/docs/build/html/_modules/quapy/data/datasets.html @@ -29,7 +29,7 @@ - + @@ -370,27 +370,28 @@ Source code for quapy.data.datasets -import os -from contextlib import contextmanager -import zipfile -from os.path import join -import pandas as pd -from quapy.data.base import Dataset, LabelledCollection -from quapy.data.preprocessing import text2tfidf, reduce_columns -from quapy.data.preprocessing import standardize as standardizer -from quapy.data.reader import * -from quapy.util import download_file_if_not_exists, download_file, get_quapy_home, pickled_resource -from sklearn.preprocessing import StandardScaler +import logging +import os +from contextlib import contextmanager +import zipfile +from os.path import join +import pandas as pd +from quapy.data.base import Dataset, LabelledCollection +from quapy.data.preprocessing import text2tfidf, reduce_columns +from quapy.data.preprocessing import standardize as standardizer +from quapy.data.reader import * +from quapy.util import download_file_if_not_exists, download_file, get_quapy_home, pickled_resource +from sklearn.preprocessing import StandardScaler -def _fetch_ucirepo(*args, **kwargs): +def _fetch_ucirepo(*args, **kwargs): try: - from ucimlrepo import fetch_ucirepo + from ucimlrepo import fetch_ucirepo except ImportError as exc: raise ImportError( "UCI dataset fetching requires the optional 'ucimlrepo' package. " "Install it to use fetch_UCIBinaryDataset or fetch_UCIMulticlassDataset." - ) from exc + ) from exc return fetch_ucirepo(*args, **kwargs) @@ -497,7 +498,7 @@ [docs] -def fetch_reviews(dataset_name, tfidf=False, min_df=None, data_home=None, pickle=False) -> Dataset: +def fetch_reviews(dataset_name, tfidf=False, min_df=None, data_home=None, pickle=False) -> Dataset: """ Loads a Reviews dataset as a Dataset instance, as used in `Esuli, A., Moreo, A., and Sebastiani, F. "A recurrent neural network for sentiment quantification." @@ -547,7 +548,7 @@ [docs] -def fetch_twitter(dataset_name, for_model_selection=False, min_df=None, data_home=None, pickle=False) -> Dataset: +def fetch_twitter(dataset_name, for_model_selection=False, min_df=None, data_home=None, pickle=False) -> Dataset: """ Loads a Twitter dataset as a :class:`quapy.data.base.Dataset` instance, as used in: `Gao, W., Sebastiani, F.: From classification to quantification in tweet sentiment analysis. @@ -589,8 +590,9 @@ if dataset_name in {'semeval13', 'semeval14', 'semeval15'}: trainset_name = 'semeval' testset_name = 'semeval' if for_model_selection else dataset_name - print(f"the training and development sets for datasets 'semeval13', 'semeval14', 'semeval15' are common " - f"(called 'semeval'); returning trainin-set='{trainset_name}' and test-set={testset_name}") + logging.getLogger(__name__).info( + f"the training and development sets for datasets 'semeval13', 'semeval14', 'semeval15' are common " + f"(called 'semeval'); returning trainin-set='{trainset_name}' and test-set={testset_name}") else: if dataset_name == 'semeval' and for_model_selection==False: raise ValueError('dataset "semeval" can only be used for model selection. ' @@ -624,7 +626,7 @@ [docs] -def fetch_UCIBinaryDataset(dataset_name, data_home=None, test_split=0.3, standardize=True, verbose=False) -> Dataset: +def fetch_UCIBinaryDataset(dataset_name, data_home=None, test_split=0.3, standardize=True, verbose=False) -> Dataset: """ Loads a UCI dataset as an instance of :class:`quapy.data.base.Dataset`, as used in `Pérez-Gállego, P., Quevedo, J. R., & del Coz, J. J. (2017). @@ -658,7 +660,7 @@ [docs] -def fetch_UCIBinaryLabelledCollection(dataset_name, data_home=None, standardize=True, verbose=False) -> LabelledCollection: +def fetch_UCIBinaryLabelledCollection(dataset_name, data_home=None, standardize=True, verbose=False) -> LabelledCollection: """ Loads a UCI collection as an instance of :class:`quapy.data.base.LabelledCollection`, as used in `Pérez-Gállego, P., Quevedo, J. R., & del Coz, J. J. (2017). @@ -851,7 +853,7 @@ file = join(data_home, "uci_datasets", dataset_group + ".pkl") @contextmanager - def download_tmp_file(url_group: str, filename: str): + def download_tmp_file(url_group: str, filename: str): """ Download a data file for a group of datasets temporarely. When used as a context, the file is removed once the context exits. @@ -869,7 +871,7 @@ finally: os.remove(data_path) - def download(id: int | None, group: str) -> dict: + def download(id: int | None, group: str) -> dict: """ Download the data to be pickled for a dataset group. Use the `fetch_ucirepo` api when possible. @@ -935,7 +937,7 @@ return data - def binarize_data(name, data: dict) -> LabelledCollection: + def binarize_data(name, data: dict) -> LabelledCollection: """ Filter and transform data to extract a binary dataset. @@ -980,7 +982,7 @@ [docs] -def fetch_UCIMulticlassDataset( +def fetch_UCIMulticlassDataset( dataset_name, data_home=None, min_test_split=0.3, @@ -1043,7 +1045,7 @@ [docs] -def fetch_UCIMulticlassLabelledCollection(dataset_name, data_home=None, min_class_support=100, standardize=True, verbose=False) -> LabelledCollection: +def fetch_UCIMulticlassLabelledCollection(dataset_name, data_home=None, min_class_support=100, standardize=True, verbose=False) -> LabelledCollection: """ Loads a UCI multiclass collection as an instance of :class:`quapy.data.base.LabelledCollection`. @@ -1146,7 +1148,7 @@ file = join(data_home, 'uci_multiclass', dataset_name+'.pkl') - def dummify_categorical_features(df_features, dataset_id): + def dummify_categorical_features(df_features, dataset_id): categorical_features = { 158: ["S1", "C1", "S2", "C2", "S3", "C3", "S4", "C4", "S5", "C5"], # poker_hand } @@ -1160,7 +1162,7 @@ return X - def download(id, name): + def download(id, name): df = _fetch_ucirepo(id=id) X_df = dummify_categorical_features(df.data.features, id) @@ -1173,7 +1175,7 @@ y = np.searchsorted(classes, y) return LabelledCollection(X, y) - def filter_classes(data: LabelledCollection, min_class_support): + def filter_classes(data: LabelledCollection, min_class_support): if min_class_support is None or min_class_support == 0.: return data if isinstance(min_class_support, float): @@ -1207,18 +1209,18 @@ -def _df_replace(df, col, repl={'yes': 1, 'no':0}, astype=float): +def _df_replace(df, col, repl={'yes': 1, 'no':0}, astype=float): df[col] = df[col].apply(lambda x:repl[x]).astype(astype, copy=False) -def _array_replace(arr, repl={"yes": 1, "no": 0}): +def _array_replace(arr, repl={"yes": 1, "no": 0}): for k, v in repl.items(): arr[arr == k] = v [docs] -def fetch_lequa2022(task, data_home=None): +def fetch_lequa2022(task, data_home=None): """ Loads the official datasets provided for the `LeQua 2022 <https://lequa2022.github.io/index>`_ competition. In brief, there are 4 tasks (T1A, T1B, T2A, T2B) having to do with text quantification @@ -1244,7 +1246,7 @@ that return a series of samples stored in a directory which are labelled by prevalence. """ - from quapy.data._lequa import load_raw_documents, load_vector_documents_2022, SamplesFromDir + from quapy.data._lequa import load_raw_documents, load_vector_documents_2022, SamplesFromDir assert task in LEQUA2022_TASKS, \ f'Unknown task {task}. Valid ones are {LEQUA2022_TASKS}' @@ -1258,7 +1260,7 @@ lequa_dir = join(data_home, 'lequa2022') os.makedirs(lequa_dir, exist_ok=True) - def download_unzip_and_remove(unzipped_path, url): + def download_unzip_and_remove(unzipped_path, url): tmp_path = join(lequa_dir, task + '_tmp.zip') download_file_if_not_exists(url, tmp_path) with zipfile.ZipFile(tmp_path) as file: @@ -1294,7 +1296,7 @@ [docs] -def fetch_lequa2024(task, data_home=None, merge_T3=False): +def fetch_lequa2024(task, data_home=None, merge_T3=False): """ Loads the official datasets provided for the `LeQua 2024 <https://lequa2024.github.io/index>`_ competition. LeQua 2024 defines four tasks (T1, T2, T3, T4) related to the problem of quantification; @@ -1325,7 +1327,7 @@ that return a series of samples stored in a directory which are labelled by prevalence. """ - from quapy.data._lequa import load_vector_documents_2024, SamplesFromDir, LabelledCollectionsFromDir + from quapy.data._lequa import load_vector_documents_2024, SamplesFromDir, LabelledCollectionsFromDir assert task in LEQUA2024_TASKS, \ f'Unknown task {task}. Valid ones are {LEQUA2024_TASKS}' @@ -1344,7 +1346,7 @@ lequa_dir = join(data_home, 'lequa2024') os.makedirs(lequa_dir, exist_ok=True) - def download_unzip_and_remove(unzipped_path, url): + def download_unzip_and_remove(unzipped_path, url): tmp_path = join(lequa_dir, task + '_tmp.zip') download_file_if_not_exists(url, tmp_path) with zipfile.ZipFile(tmp_path) as file: @@ -1384,7 +1386,7 @@ [docs] -def fetch_IFCB(single_sample_train=True, for_model_selection=False, data_home=None): +def fetch_IFCB(single_sample_train=True, for_model_selection=False, data_home=None): """ Loads the IFCB dataset for quantification from `Zenodo <https://zenodo.org/records/10036244>`_ (for more information on this dataset, please follow the zenodo link). @@ -1409,7 +1411,7 @@ i.e., a sampling protocol that returns a series of samples labelled by prevalence. """ - from quapy.data._ifcb import IFCBTrainSamplesFromDir, IFCBTestSamples, get_sample_list, generate_modelselection_split + from quapy.data._ifcb import IFCBTrainSamplesFromDir, IFCBTestSamples, get_sample_list, generate_modelselection_split if data_home is None: data_home = get_quapy_home() @@ -1421,7 +1423,7 @@ ifcb_dir = join(data_home, 'ifcb') os.makedirs(ifcb_dir, exist_ok=True) - def download_unzip_and_remove(unzipped_path, url): + def download_unzip_and_remove(unzipped_path, url): tmp_path = join(ifcb_dir, 'ifcb_tmp.zip') download_file_if_not_exists(url, tmp_path) with zipfile.ZipFile(tmp_path) as file: @@ -1466,7 +1468,7 @@ -def _fetch_image_embedding_splits(dataset_name, embedding, data_home=None) -> tuple[LabelledCollection,LabelledCollection,LabelledCollection]: +def _fetch_image_embedding_splits(dataset_name, embedding, data_home=None) -> tuple[LabelledCollection,LabelledCollection,LabelledCollection]: """ Loads a pre-generated embedding set (train, val, or test) of an image dataset from `Zenodo <https://zenodo.org/records/21131944>`_. @@ -1496,7 +1498,7 @@ trained_network = dataset_network[dataset_name] - def download_embedding_npz(dataset_name, trained_network, embedding): + def download_embedding_npz(dataset_name, trained_network, embedding): target_file = f'{dataset_name}_{trained_network}_{embedding}.npz' URL = f'https://zenodo.org/records/21131944/files/{target_file}' os.makedirs(join(data_home, 'image'), exist_ok=True) @@ -1512,14 +1514,12 @@ val = LabelledCollection(embedding_dict['val'], labels_dict['val'], classes=train.classes) test = LabelledCollection(embedding_dict['test'], labels_dict['test'], classes=train.classes) - print(f'{len(train)} | {len(val)} | {len(test)} | {train.X.shape[1]} | {train.n_classes} | {train.n_classes}') - return train, val, test [docs] -def fetch_image_embeddings(dataset_name, embedding, heldout_only=True, data_home=None) -> Dataset: +def fetch_image_embeddings(dataset_name, embedding, heldout_only=True, data_home=None) -> Dataset: """ Loads an image dataset with pre-generated embeddings. Available datasets include: @@ -1556,14 +1556,6 @@ -if __name__ == '__main__': - #train, val, test = _fetch_image_embedding_splits(dataset_name='mnist', embedding='logits') - #print(train) - #print(val) - #print(test) - - dataset = fetch_image_embeddings(dataset_name='svhn', embedding='features', heldout_only=True) - print(dataset) diff --git a/docs/build/html/_modules/quapy/data/reader.html b/docs/build/html/_modules/quapy/data/reader.html index 827d348..617442d 100644 --- a/docs/build/html/_modules/quapy/data/reader.html +++ b/docs/build/html/_modules/quapy/data/reader.html @@ -1,87 +1,385 @@ - - - - - - quapy.data.reader — QuaPy: A Python-based open-source framework for quantification 0.2.1 documentation - - + + + + + + + + quapy.data.reader — QuaPy: A Python-based open-source framework for quantification 0.2.1 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Skip to main content - - - - - - - - - + + + + Back to top + + + + + + + + + + Ctrl+K + + - - - - - - - - - - QuaPy: A Python-based open-source framework for quantification - - - - - - - + + - - -Quickstart - - -Manuals - - -API - - - - + + + + + + + + + + + - - - QuaPy: A Python-based open-source framework for quantification - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Home + + + + + + + Manuals + + + + + + + API + + - - - - - - Module code - quapy.data.reader - - - + + + + + + + + + + + + + Search + Ctrl+K + + + + + + + + + + + + + + System Settings + Light + Dark + + + + + + + + + + + + + + GitHub + + + + + + + + + + + + + Search + Ctrl+K + + + + + - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Home + + + + + + + Manuals + + + + + + + API + + + + + + + + + + + + + + + + + + + + + + + System Settings + Light + Dark + + + + + + + + + + + + + + GitHub + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Module code + + quapy.data.reader + + + + + + + + + + + + + + + + Source code for quapy.data.reader -import numpy as np -from scipy.sparse import dok_matrix -from tqdm import tqdm +import logging + +import numpy as np +from scipy.sparse import dok_matrix +from tqdm import tqdm [docs] -def from_text(path, encoding='utf-8', verbose=1, class2int=True): +def from_text(path, encoding='utf-8', verbose=1, class2int=True): """ Reads a labelled colletion of documents. File fomart <0 or 1>\t<document>\n @@ -108,14 +406,14 @@ all_sentences.append(sentence) all_labels.append(label) except ValueError: - print(f'format error in {line}') + logging.getLogger(__name__).warning(f'format error in {line}') return all_sentences, all_labels [docs] -def from_sparse(path): +def from_sparse(path): """ Reads a labelled collection of real-valued instances expressed in sparse format File format <-1 or 0 or 1>[\s col(int):val(float)]\n @@ -124,7 +422,7 @@ :return: a `csr_matrix` containing the instances (rows), and a ndarray containing the labels """ - def split_col_val(col_val): + def split_col_val(col_val): col, val = col_val.split(':') col, val = int(col) - 1, float(val) return col, val @@ -152,7 +450,7 @@ [docs] -def from_csv(path, encoding='utf-8'): +def from_csv(path, encoding='utf-8'): """ Reads a csv file in which columns are separated by ','. File format <label>,<feat1>,<feat2>,...,<featn>\n @@ -175,7 +473,7 @@ [docs] -def reindex_labels(y): +def reindex_labels(y): """ Re-indexes a list of labels as a list of indexes, and returns the classnames corresponding to the indexes. E.g.: @@ -198,7 +496,7 @@ [docs] -def binarize(y, pos_class): +def binarize(y, pos_class): """ Binarizes a categorical array-like collection of labels towards the positive class `pos_class`. E.g.,: @@ -218,31 +516,75 @@ - + + + + + + + + + + + + + - + + + + - Built with Sphinx using a - theme - provided by Read the Docs. - + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/build/html/_modules/quapy/error.html b/docs/build/html/_modules/quapy/error.html index 73a9a53..9b93ed4 100644 --- a/docs/build/html/_modules/quapy/error.html +++ b/docs/build/html/_modules/quapy/error.html @@ -1,89 +1,385 @@ - - - - - - quapy.error — QuaPy: A Python-based open-source framework for quantification 0.2.1 documentation - - + + + + + + + + quapy.error — QuaPy: A Python-based open-source framework for quantification 0.2.1 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Skip to main content - - - - - - - - - + + + + Back to top + + + + + + + + + + Ctrl+K + + - - - - - - - - - - QuaPy: A Python-based open-source framework for quantification - - - - - - - + + - - -Quickstart - - -Manuals - - -API - - - - + + + + + + + + + + + - - - QuaPy: A Python-based open-source framework for quantification - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Home + + + + + + + Manuals + + + + + + + API + + - - - - - - Module code - quapy.error - - - + + + + + + + + + + + + + Search + Ctrl+K + + + + + + + + + + + + + + System Settings + Light + Dark + + + + + + + + + + + + + + GitHub + + + + + + + + + + + + + Search + Ctrl+K + + + + + - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Home + + + + + + + Manuals + + + + + + + API + + + + + + + + + + + + + + + + + + + + + + + System Settings + Light + Dark + + + + + + + + + + + + + + GitHub + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Module code + + quapy.error + + + + + + + + + + + + + + + + Source code for quapy.error """Implementation of error measures used for quantification""" -import numpy as np -from sklearn.metrics import f1_score -import quapy as qp +import numpy as np +from sklearn.metrics import f1_score +import quapy as qp [docs] -def from_name(err_name): +def from_name(err_name): """Gets an error function from its name. E.g., `from_name("mae")` will return function :meth:`quapy.error.mae` @@ -98,7 +394,7 @@ [docs] -def f1e(y_true, y_pred): +def f1e(y_true, y_pred): """F1 error: simply computes the error in terms of macro :math:`F_1`, i.e., :math:`1-F_1^M`, where :math:`F_1` is the harmonic mean of precision and recall, defined as :math:`\\frac{2tp}{2tp+fp+fn}`, with `tp`, `fp`, and `fn` standing @@ -116,7 +412,7 @@ [docs] -def acce(y_true, y_pred): +def acce(y_true, y_pred): """Computes the error in terms of 1-accuracy. The accuracy is computed as :math:`\\frac{tp+tn}{tp+fp+fn+tn}`, with `tp`, `fp`, `fn`, and `tn` standing for true positives, false positives, false negatives, and true negatives, @@ -132,7 +428,7 @@ [docs] -def mae(prevs_true, prevs_hat): +def mae(prevs_true, prevs_hat): """Computes the mean absolute error (see :meth:`quapy.error.ae`) across the sample pairs. :param prevs_true: array-like of shape `(n_samples, n_classes,)` with the true prevalence values @@ -146,7 +442,7 @@ [docs] -def ae(prevs_true, prevs_hat): +def ae(prevs_true, prevs_hat): """Computes the absolute error between the two prevalence vectors. Absolute error between two prevalence vectors :math:`p` and :math:`\\hat{p}` is computed as :math:`AE(p,\\hat{p})=\\frac{1}{|\\mathcal{Y}|}\\sum_{y\\in \\mathcal{Y}}|\\hat{p}(y)-p(y)|`, @@ -165,7 +461,7 @@ [docs] -def nae(prevs_true, prevs_hat): +def nae(prevs_true, prevs_hat): """Computes the normalized absolute error between the two prevalence vectors. Normalized absolute error between two prevalence vectors :math:`p` and :math:`\\hat{p}` is computed as :math:`NAE(p,\\hat{p})=\\frac{AE(p,\\hat{p})}{z_{AE}}`, @@ -185,7 +481,7 @@ [docs] -def mnae(prevs_true, prevs_hat): +def mnae(prevs_true, prevs_hat): """Computes the mean normalized absolute error (see :meth:`quapy.error.nae`) across the sample pairs. :param prevs_true: array-like of shape `(n_samples, n_classes,)` with the true prevalence values @@ -199,7 +495,7 @@ [docs] -def mse(prevs_true, prevs_hat): +def mse(prevs_true, prevs_hat): """Computes the mean squared error (see :meth:`quapy.error.se`) across the sample pairs. :param prevs_true: array-like of shape `(n_samples, n_classes,)` with the @@ -214,7 +510,7 @@ [docs] -def se(prevs_true, prevs_hat): +def se(prevs_true, prevs_hat): """Computes the squared error between the two prevalence vectors. Squared error between two prevalence vectors :math:`p` and :math:`\\hat{p}` is computed as :math:`SE(p,\\hat{p})=\\frac{1}{|\\mathcal{Y}|}\\sum_{y\\in \\mathcal{Y}}(\\hat{p}(y)-p(y))^2`, @@ -233,7 +529,7 @@ [docs] -def sre(prevs_true, prevs_hat, prevs_train, eps=0.): +def sre(prevs_true, prevs_hat, prevs_train, eps=0.): """ Computes the squared ratio error between two prevalence vectors. The squared ratio error between prevalence vectors :math:`p` and @@ -269,7 +565,7 @@ [docs] -def msre(prevs_true, prevs_hat, prevs_train, eps=0.): +def msre(prevs_true, prevs_hat, prevs_train, eps=0.): """ Computes the mean squared ratio error (see :meth:`quapy.error.sre`) across the sample pairs. @@ -285,7 +581,7 @@ [docs] -def aitchisondist(prevs_true, prevs_hat): +def aitchisondist(prevs_true, prevs_hat): """ Computes the Aitchison distance between two prevalence vectors. The Aitchison distance between prevalence vectors :math:`p` and @@ -298,7 +594,7 @@ :param prevs_hat: array-like with the predicted prevalence values :return: Aitchison distance """ - from quapy.functional import CLRtransformation + from quapy.functional import CLRtransformation clr = CLRtransformation() return np.linalg.norm(clr(prevs_true) - clr(prevs_hat), axis=-1) @@ -307,7 +603,7 @@ [docs] -def maitchisondist(prevs_true, prevs_hat): +def maitchisondist(prevs_true, prevs_hat): """ Computes the mean Aitchison distance (see :meth:`quapy.error.aitchisondist`) across the sample pairs, i.e., @@ -324,7 +620,7 @@ [docs] -def mkld(prevs_true, prevs_hat, eps=None): +def mkld(prevs_true, prevs_hat, eps=None): """Computes the mean Kullback-Leibler divergence (see :meth:`quapy.error.kld`) across the sample pairs. The distributions are smoothed using the `eps` factor (see :meth:`quapy.error.smooth`). @@ -345,7 +641,7 @@ [docs] -def kld(prevs_true, prevs_hat, eps=None): +def kld(prevs_true, prevs_hat, eps=None): """Computes the Kullback-Leibler divergence between the two prevalence distributions. Kullback-Leibler divergence between two prevalence distributions :math:`p` and :math:`\\hat{p}` is computed as @@ -371,7 +667,7 @@ [docs] -def mnkld(prevs_true, prevs_hat, eps=None): +def mnkld(prevs_true, prevs_hat, eps=None): """Computes the mean Normalized Kullback-Leibler divergence (see :meth:`quapy.error.nkld`) across the sample pairs. The distributions are smoothed using the `eps` factor (see :meth:`quapy.error.smooth`). @@ -391,7 +687,7 @@ [docs] -def nkld(prevs_true, prevs_hat, eps=None): +def nkld(prevs_true, prevs_hat, eps=None): """Computes the Normalized Kullback-Leibler divergence between the two prevalence distributions. Normalized Kullback-Leibler divergence between two prevalence distributions :math:`p` and :math:`\\hat{p}` is computed as @@ -415,7 +711,7 @@ [docs] -def mrae(prevs_true, prevs_hat, eps=None): +def mrae(prevs_true, prevs_hat, eps=None): """Computes the mean relative absolute error (see :meth:`quapy.error.rae`) across the sample pairs. The distributions are smoothed using the `eps` factor (see :meth:`quapy.error.smooth`). @@ -436,7 +732,7 @@ [docs] -def rae(prevs_true, prevs_hat, eps=None): +def rae(prevs_true, prevs_hat, eps=None): """Computes the absolute relative error between the two prevalence vectors. Relative absolute error between two prevalence vectors :math:`p` and :math:`\\hat{p}` is computed as @@ -462,7 +758,7 @@ [docs] -def nrae(prevs_true, prevs_hat, eps=None): +def nrae(prevs_true, prevs_hat, eps=None): """Computes the normalized absolute relative error between the two prevalence vectors. Relative absolute error between two prevalence vectors :math:`p` and :math:`\\hat{p}` is computed as @@ -490,7 +786,7 @@ [docs] -def mnrae(prevs_true, prevs_hat, eps=None): +def mnrae(prevs_true, prevs_hat, eps=None): """Computes the mean normalized relative absolute error (see :meth:`quapy.error.nrae`) across the sample pairs. The distributions are smoothed using the `eps` factor (see :meth:`quapy.error.smooth`). @@ -511,7 +807,7 @@ [docs] -def nmd(prevs_true, prevs_hat): +def nmd(prevs_true, prevs_hat): """ Computes the Normalized Match Distance; which is the Normalized Distance multiplied by the factor `1/(n-1)` to guarantee the measure ranges between 0 (best prediction) and 1 (worst prediction). @@ -529,7 +825,7 @@ [docs] -def bias_binary(prevs_true, prevs_hat): +def bias_binary(prevs_true, prevs_hat): """ Computes the (positive) bias in a binary problem. The bias is simply the difference between the predicted positive value and the true positive value, so that a positive such value indicates the @@ -549,7 +845,7 @@ [docs] -def mean_bias_binary(prevs_true, prevs_hat): +def mean_bias_binary(prevs_true, prevs_hat): """ Computes the mean of the (positive) bias in a binary problem. :param prevs_true: array-like of shape `(n_classes,)` with the true prevalence values @@ -562,7 +858,7 @@ [docs] -def md(prevs_true, prevs_hat, ERROR_TOL=1E-3): +def md(prevs_true, prevs_hat, ERROR_TOL=1E-3): """ Computes the Match Distance, under the assumption that the cost in mistaking class i with class i+1 is 1 in all cases. @@ -582,7 +878,7 @@ [docs] -def smooth(prevs, eps): +def smooth(prevs, eps): """ Smooths a prevalence distribution with :math:`\\epsilon` (`eps`) as: :math:`\\underline{p}(y)=\\frac{\\epsilon+p(y)}{\\epsilon|\\mathcal{Y}|+ \\displaystyle\\sum_{y\\in \\mathcal{Y}}p(y)}` @@ -597,7 +893,7 @@ -def __check_eps(eps=None): +def __check_eps(eps=None): if eps is None: sample_size = qp.environ['SAMPLE_SIZE'] if sample_size is None: @@ -634,31 +930,75 @@ match_distance = md - + + + + + + + + + + + + + - + + + + - Built with Sphinx using a - theme - provided by Read the Docs. - + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/build/html/_modules/quapy/functional.html b/docs/build/html/_modules/quapy/functional.html index b7743b8..c275ca4 100644 --- a/docs/build/html/_modules/quapy/functional.html +++ b/docs/build/html/_modules/quapy/functional.html @@ -29,8 +29,9 @@ - - + + + @@ -41,6 +42,7 @@ + @@ -114,8 +116,14 @@ + + + + + + + - QuaPy @@ -131,7 +139,7 @@ - Quickstart + Home @@ -183,6 +191,21 @@ + + + + + + + + + + + GitHub + + + @@ -229,7 +252,7 @@ - Quickstart + Home @@ -272,6 +295,21 @@ + + + + + + + + + + + GitHub + + + @@ -332,17 +370,17 @@ Source code for quapy.functional -import warnings -from abc import ABC, abstractmethod -from collections import defaultdict -from functools import lru_cache -from typing import Literal, Union, Callable -from numpy.typing import ArrayLike +import warnings +from abc import ABC, abstractmethod +from collections import defaultdict +from functools import lru_cache +from typing import Literal, Union, Callable +from numpy.typing import ArrayLike -import scipy -import numpy as np +import scipy +import numpy as np -import quapy as qp +import quapy as qp # ------------------------------------------------------------------------------------------ @@ -351,7 +389,7 @@ [docs] -def classes_from_labels(labels): +def classes_from_labels(labels): """ Obtains a np.ndarray with the (sorted) classes :param labels: array-like with the instances' labels @@ -365,7 +403,7 @@ [docs] -def num_classes_from_labels(labels): +def num_classes_from_labels(labels): """ Obtains the number of classes from an array-like of instance's labels :param labels: array-like with the instances' labels @@ -380,7 +418,7 @@ [docs] -def counts_from_labels(labels: ArrayLike, classes: ArrayLike) -> np.ndarray: +def counts_from_labels(labels: ArrayLike, classes: ArrayLike) -> np.ndarray: """ Computes the raw count values from a vector of labels. @@ -401,7 +439,7 @@ [docs] -def prevalence_from_labels(labels: ArrayLike, classes: ArrayLike): +def prevalence_from_labels(labels: ArrayLike, classes: ArrayLike): """ Computes the prevalence values from a vector of labels. @@ -419,7 +457,7 @@ [docs] -def prevalence_from_probabilities(posteriors: ArrayLike, binarize: bool = False): +def prevalence_from_probabilities(posteriors: ArrayLike, binarize: bool = False): """ Returns a vector of prevalence values from a matrix of posterior probabilities. @@ -443,7 +481,7 @@ [docs] -def num_prevalence_combinations(n_prevpoints:int, n_classes:int, n_repeats:int=1) -> int: +def num_prevalence_combinations(n_prevpoints:int, n_classes:int, n_repeats:int=1) -> int: """ Computes the number of valid prevalence combinations in the n_classes-dimensional simplex if `n_prevpoints` equally distant prevalence values are generated and `n_repeats` repetitions are requested. @@ -472,7 +510,7 @@ [docs] -def get_nprevpoints_approximation(combinations_budget:int, n_classes:int, n_repeats:int=1) -> int: +def get_nprevpoints_approximation(combinations_budget:int, n_classes:int, n_repeats:int=1) -> int: """ Searches for the largest number of (equidistant) prevalence points to define for each of the `n_classes` classes so that the number of valid prevalence values generated as combinations of prevalence points (points in a @@ -500,7 +538,7 @@ [docs] -def as_binary_prevalence(positive_prevalence: Union[float, ArrayLike], clip_if_necessary: bool=False) -> np.ndarray: +def as_binary_prevalence(positive_prevalence: Union[float, ArrayLike], clip_if_necessary: bool=False) -> np.ndarray: """ Helper that, given a float representing the prevalence for the positive class, returns a np.ndarray of two values representing a binary distribution. @@ -522,7 +560,7 @@ [docs] -def strprev(prevalences: ArrayLike, prec: int=3) -> str: +def strprev(prevalences: ArrayLike, prec: int=3) -> str: """ Returns a string representation for a prevalence vector. E.g., @@ -539,7 +577,7 @@ [docs] -def check_prevalence_vector(prevalences: ArrayLike, raise_exception: bool=False, tolerance: float=1e-08, aggr=True): +def check_prevalence_vector(prevalences: ArrayLike, raise_exception: bool=False, tolerance: float=1e-08, aggr=True): """ Checks that `prevalences` is a valid prevalence vector, i.e., it contains values in [0,1] and the values sum up to 1. In other words, verifies that the `prevalences` vectors lies in the @@ -581,7 +619,7 @@ [docs] -def uniform_prevalence(n_classes): +def uniform_prevalence(n_classes): """ Returns a vector representing the uniform distribution for `n_classes` @@ -597,7 +635,7 @@ [docs] -def normalize_prevalence(prevalences: ArrayLike, method='l1'): +def normalize_prevalence(prevalences: ArrayLike, method='l1'): """ Normalizes a vector or matrix of prevalence values. The normalization consists of applying a L1 normalization in cases in which the prevalence values are not all-zeros, and to convert the prevalence values into `1/n_classes` in @@ -640,7 +678,7 @@ [docs] -def l1_norm(prevalences: ArrayLike) -> np.ndarray: +def l1_norm(prevalences: ArrayLike) -> np.ndarray: """ Applies L1 normalization to the `unnormalized_arr` so that it becomes a valid prevalence vector. Zero vectors are mapped onto the uniform distribution. Raises an exception if @@ -666,7 +704,7 @@ [docs] -def clip(prevalences: ArrayLike) -> np.ndarray: +def clip(prevalences: ArrayLike) -> np.ndarray: """ Clips the values in [0,1] and then applies the L1 normalization. @@ -681,7 +719,7 @@ [docs] -def projection_simplex_sort(unnormalized_arr: ArrayLike) -> np.ndarray: +def projection_simplex_sort(unnormalized_arr: ArrayLike) -> np.ndarray: """Projects a point onto the probability simplex. The code is adapted from Mathieu Blondel's BSD-licensed @@ -709,7 +747,7 @@ [docs] -def softmax(prevalences: ArrayLike) -> np.ndarray: +def softmax(prevalences: ArrayLike) -> np.ndarray: """ Applies the softmax function to all vectors even if the original vectors were valid distributions. If you want to leave valid vectors untouched, use condsoftmax instead. @@ -724,7 +762,7 @@ [docs] -def condsoftmax(prevalences: ArrayLike) -> np.ndarray: +def condsoftmax(prevalences: ArrayLike) -> np.ndarray: """ Applies the softmax function only to vectors that do not represent valid distributions. @@ -749,7 +787,7 @@ [docs] -def HellingerDistance(P: np.ndarray, Q: np.ndarray) -> float: +def HellingerDistance(P: np.ndarray, Q: np.ndarray) -> float: """ Computes the Hellingher Distance (HD) between (discretized) distributions `P` and `Q`. The HD for two discrete distributions of `k` bins is defined as: @@ -767,7 +805,7 @@ [docs] -def TopsoeDistance(P: np.ndarray, Q: np.ndarray, epsilon: float=1e-20): +def TopsoeDistance(P: np.ndarray, Q: np.ndarray, epsilon: float=1e-20): """ Topsoe distance between two (discretized) distributions `P` and `Q`. The Topsoe distance for two discrete distributions of `k` bins is defined as: @@ -786,7 +824,7 @@ [docs] -def get_divergence(divergence: Union[str, Callable]): +def get_divergence(divergence: Union[str, Callable]): """ Guarantees that the divergence received as argument is a function. That is, if this argument is already a callable, then it is returned, if it is instead a string, then tries to instantiate the corresponding @@ -815,7 +853,7 @@ [docs] -def argmin_prevalence(loss: Callable, +def argmin_prevalence(loss: Callable, n_classes: int, method: Literal["optim_minimize", "linear_search", "ternary_search"]='optim_minimize'): """ @@ -842,7 +880,7 @@ [docs] -def optim_minimize(loss: Callable, n_classes: int, return_loss=False): +def optim_minimize(loss: Callable, n_classes: int, return_loss=False): """ Searches for the optimal prevalence values, i.e., an `n_classes`-dimensional vector of the (`n_classes`-1)-simplex that yields the smallest lost. This optimization is carried out by means of a constrained search using scipy's @@ -854,7 +892,7 @@ :return: (ndarray) the best prevalence vector found or a tuple which also contains the value of the loss if return_loss=True """ - from scipy import optimize + from scipy import optimize # the initial point is set as the uniform distribution uniform_distribution = uniform_prevalence(n_classes=n_classes) @@ -873,7 +911,7 @@ [docs] -def linear_search(loss: Callable, n_classes: int): +def linear_search(loss: Callable, n_classes: int): """ Performs a linear search for the best prevalence value in binary problems. The search is carried out by exploring the range [0,1] stepping by 0.01. This search is inefficient, and is added only for completeness (some of the @@ -897,7 +935,7 @@ [docs] -def ternary_search(loss: Callable, n_classes: int): +def ternary_search(loss: Callable, n_classes: int): """ Performs a ternary search for the best prevalence value in binary problems. This search assumes the loss is unimodal over the interval [0,1]. @@ -933,7 +971,7 @@ [docs] -def prevalence_linspace(grid_points:int=21, repeats:int=1, smooth_limits_epsilon:float=0.01) -> np.ndarray: +def prevalence_linspace(grid_points:int=21, repeats:int=1, smooth_limits_epsilon:float=0.01) -> np.ndarray: """ Produces an array of uniformly separated values of prevalence. By default, produces an array of 21 prevalence values, with @@ -958,7 +996,7 @@ [docs] -def uniform_prevalence_sampling(n_classes: int, size: int=1) -> np.ndarray: +def uniform_prevalence_sampling(n_classes: int, size: int=1) -> np.ndarray: """ Implements the `Kraemer algorithm <http://www.cs.cmu.edu/~nasmith/papers/smith+tromble.tr04.pdf>`_ for sampling uniformly at random from the unit simplex. This implementation is adapted from this @@ -994,7 +1032,7 @@ [docs] -def solve_adjustment_binary(prevalence_estim: ArrayLike, tpr: float, fpr: float, clip: bool=True): +def solve_adjustment_binary(prevalence_estim: ArrayLike, tpr: float, fpr: float, clip: bool=True): """ Implements the adjustment of ACC and PACC for the binary case. The adjustment for a prevalence estimate of the positive class `p` comes down to computing: @@ -1021,7 +1059,7 @@ [docs] -def solve_adjustment( +def solve_adjustment( class_conditional_rates: np.ndarray, unadjusted_counts: np.ndarray, method: Literal["inversion", "invariant-ratio"], @@ -1067,14 +1105,18 @@ if method == "inversion": pass # We leave A and B unchanged elif method == "invariant-ratio": - # Change the last equation to replace it with the normalization condition + # Change the last equation to replace it with the normalization condition; + # copy first so this does not mutate the caller's arrays (np.asarray above + # returns the same object, not a copy, when the input is already float64) + A = A.copy() + B = B.copy() A[-1, :] = 1.0 B[-1] = 1.0 else: raise ValueError(f"unknown {method=}") if solver == "minimize": - def loss(prev): + def loss(prev): return np.linalg.norm(A @ prev - B) return optim_minimize(loss, n_classes=A.shape[0]) elif solver in ["exact-raise", "exact-cc"]: @@ -1102,7 +1144,7 @@ [docs] -class CompositionalTransformation(ABC): +class CompositionalTransformation(ABC): """ Abstract class of transformations for compositional data. """ @@ -1110,13 +1152,13 @@ EPSILON = 1e-12 @abstractmethod - def __call__(self, X): + def __call__(self, X): ... [docs] @abstractmethod - def inverse(self, Z): + def inverse(self, Z): ... @@ -1124,12 +1166,12 @@ [docs] -class CLRtransformation(CompositionalTransformation): +class CLRtransformation(CompositionalTransformation): """ Centered log-ratio (CLR) transformation. """ - def __call__(self, X): + def __call__(self, X): X = np.asarray(X) X = qp.error.smooth(X, self.EPSILON) geometric_mean = np.exp(np.mean(np.log(X), axis=-1, keepdims=True)) @@ -1137,7 +1179,7 @@ [docs] - def inverse(self, Z): + def inverse(self, Z): return scipy.special.softmax(Z, axis=-1) @@ -1145,12 +1187,12 @@ [docs] -class ILRtransformation(CompositionalTransformation): +class ILRtransformation(CompositionalTransformation): """ Isometric log-ratio (ILR) transformation. """ - def __call__(self, X): + def __call__(self, X): X = np.asarray(X) X = qp.error.smooth(X, self.EPSILON) basis = self.get_V(X.shape[-1]) @@ -1158,7 +1200,7 @@ [docs] - def inverse(self, Z): + def inverse(self, Z): Z = np.asarray(Z) basis = self.get_V(Z.shape[-1] + 1) logp = Z @ basis @@ -1169,7 +1211,7 @@ [docs] @lru_cache(maxsize=None) - def get_V(self, k): + def get_V(self, k): helmert = np.zeros((k, k)) for i in range(1, k): helmert[i, :i] = 1 @@ -1182,7 +1224,7 @@ [docs] -def normalized_entropy(p): +def normalized_entropy(p): """ Computes the normalized Shannon entropy of a prevalence vector. @@ -1198,7 +1240,7 @@ [docs] -def antagonistic_prevalence(p, strength=1): +def antagonistic_prevalence(p, strength=1): """ Reflects a prevalence vector in ILR space and maps it back to the simplex. @@ -1215,7 +1257,7 @@ [docs] -def in_simplex(x, atol=1e-8): +def in_simplex(x, atol=1e-8): """ Checks whether points lie in the probability simplex. diff --git a/docs/build/html/_modules/quapy/method/_kdey.html b/docs/build/html/_modules/quapy/method/_kdey.html index 6664ea9..3367861 100644 --- a/docs/build/html/_modules/quapy/method/_kdey.html +++ b/docs/build/html/_modules/quapy/method/_kdey.html @@ -29,8 +29,9 @@ - - + + + @@ -41,6 +42,7 @@ + @@ -114,8 +116,14 @@ + + + + + + + - QuaPy @@ -131,7 +139,7 @@ - Quickstart + Home @@ -183,6 +191,21 @@ + + + + + + + + + + + GitHub + + + @@ -229,7 +252,7 @@ - Quickstart + Home @@ -272,6 +295,21 @@ + + + + + + + + + + + GitHub + + + @@ -332,22 +370,22 @@ Source code for quapy.method._kdey -import numpy as np -from numbers import Real -from sklearn.base import BaseEstimator -from sklearn.neighbors import KernelDensity +import numpy as np +from numbers import Real +from sklearn.base import BaseEstimator +from sklearn.neighbors import KernelDensity -import quapy as qp -from quapy.method._helper import _labels_to_indices -from quapy.method.aggregative import AggregativeSoftQuantifier -import quapy.functional as F -from scipy.special import logsumexp -from sklearn.metrics.pairwise import rbf_kernel +import quapy as qp +from quapy.method._helper import _labels_to_indices +from quapy.method.aggregative import AggregativeSoftQuantifier +import quapy.functional as F +from scipy.special import logsumexp +from sklearn.metrics.pairwise import rbf_kernel [docs] -class KDEBase: +class KDEBase: """ Common ancestor for KDE-based methods. Implements some common routines. """ @@ -356,7 +394,7 @@ KERNELS = ['gaussian', 'aitchison', 'ilr'] @classmethod - def _check_bandwidth(cls, bandwidth, kernel): + def _check_bandwidth(cls, bandwidth, kernel): """ Checks that the bandwidth parameter is correct @@ -370,13 +408,13 @@ return bandwidth @classmethod - def _check_kernel(cls, kernel): + def _check_kernel(cls, kernel): assert kernel in KDEBase.KERNELS, f'unknown {kernel=}' return kernel [docs] - def get_kde_function(self, X, bandwidth, kernel): + def get_kde_function(self, X, bandwidth, kernel): """ Wraps the KDE function from scikit-learn. @@ -392,7 +430,7 @@ [docs] - def pdf(self, kde, X, kernel, log_densities=False): + def pdf(self, kde, X, kernel, log_densities=False): """ Wraps the density evalution of scikit-learn's KDE. Scikit-learn returns log-scores (s), so this function returns :math:`e^{s}` @@ -411,7 +449,7 @@ [docs] - def get_mixture_components(self, X, y, classes, bandwidth, kernel): + def get_mixture_components(self, X, y, classes, bandwidth, kernel): """ Returns an array containing the mixture components, i.e., the KDE functions for each class. @@ -433,7 +471,7 @@ [docs] - def transform_posteriors(self, X, kernel): + def transform_posteriors(self, X, kernel): if kernel in {'aitchison', 'ilr'}: X = self.shrink_posteriors(X) if kernel == 'aitchison': @@ -445,7 +483,7 @@ [docs] - def shrink_posteriors(self, X): + def shrink_posteriors(self, X): shrinkage = getattr(self, 'shrinkage', 0.0) if shrinkage <= 0: return X @@ -457,7 +495,7 @@ [docs] - def effective_bandwidth(self, bandwidth, kernel): + def effective_bandwidth(self, bandwidth, kernel): shrinkage = getattr(self, 'shrinkage', 0.0) if shrinkage > 0 and kernel in {'aitchison', 'ilr'} and isinstance(bandwidth, Real): return (1.0 - shrinkage) * float(bandwidth) @@ -466,7 +504,7 @@ [docs] - def clr_transform(self, X): + def clr_transform(self, X): if not hasattr(self, 'clr'): self.clr = F.CLRtransformation() return self.clr(X) @@ -474,7 +512,7 @@ [docs] - def ilr_transform(self, X): + def ilr_transform(self, X): if not hasattr(self, 'ilr'): self.ilr = F.ILRtransformation() return self.ilr(X) @@ -484,7 +522,7 @@ [docs] -class KDEyML(AggregativeSoftQuantifier, KDEBase): +class KDEyML(AggregativeSoftQuantifier, KDEBase): """ Kernel Density Estimation model for quantification (KDEy) relying on the Kullback-Leibler divergence (KLD) as the divergence measure to be minimized. This method was first proposed in the paper @@ -526,7 +564,7 @@ :param random_state: a seed to be set before fitting any base quantifier (default None) """ - def __init__(self, classifier: BaseEstimator=None, fit_classifier=True, val_split=5, bandwidth=0.1, + def __init__(self, classifier: BaseEstimator=None, fit_classifier=True, val_split=5, bandwidth=0.1, kernel='gaussian', shrinkage=0.0, random_state=None): super().__init__(classifier, fit_classifier, val_split) self.bandwidth = KDEBase._check_bandwidth(bandwidth, kernel) @@ -539,7 +577,7 @@ [docs] - def aggregation_fit(self, classif_predictions, labels): + def aggregation_fit(self, classif_predictions, labels): self.mix_densities = self.get_mixture_components( classif_predictions, labels, @@ -552,7 +590,7 @@ [docs] - def aggregate(self, posteriors: np.ndarray): + def aggregate(self, posteriors: np.ndarray): """ Searches for the mixture model parameter (the sought prevalence values) that maximizes the likelihood of the data (i.e., that minimizes the negative log-likelihood) @@ -569,14 +607,14 @@ for kde_i in self.mix_densities ] - def neg_loglikelihood(prev): + def neg_loglikelihood(prev): prev = qp.error.smooth(prev, eps=epsilon) test_loglikelihood = logsumexp(np.log(prev)[:, None] + test_log_densities, axis=0) return -np.sum(test_loglikelihood) else: test_densities = [self.pdf(kde_i, posteriors, self.kernel) for kde_i in self.mix_densities] - def neg_loglikelihood(prev): + def neg_loglikelihood(prev): test_mixture_likelihood = prev @ test_densities test_loglikelihood = np.log(test_mixture_likelihood + epsilon) return -np.sum(test_loglikelihood) @@ -588,7 +626,7 @@ [docs] -class KDEyHD(AggregativeSoftQuantifier, KDEBase): +class KDEyHD(AggregativeSoftQuantifier, KDEBase): """ Kernel Density Estimation model for quantification (KDEy) relying on the squared Hellinger Disntace (HD) as the divergence measure to be minimized. This method was first proposed in the paper @@ -633,7 +671,7 @@ :param montecarlo_trials: number of Monte Carlo trials (default 10000) """ - def __init__(self, classifier: BaseEstimator=None, fit_classifier=True, val_split=5, divergence: str='HD', + def __init__(self, classifier: BaseEstimator=None, fit_classifier=True, val_split=5, divergence: str='HD', bandwidth=0.1, random_state=None, montecarlo_trials=10000): super().__init__(classifier, fit_classifier, val_split) @@ -644,7 +682,7 @@ [docs] - def aggregation_fit(self, classif_predictions, labels): + def aggregation_fit(self, classif_predictions, labels): self.mix_densities = self.get_mixture_components( classif_predictions, labels, self.classes_, self.bandwidth, 'gaussian' ) @@ -663,7 +701,7 @@ [docs] - def aggregate(self, posteriors: np.ndarray): + def aggregate(self, posteriors: np.ndarray): # we retain all n*N examples (sampled from a mixture with uniform parameter), and then # apply importance sampling (IS). In this version we compute D(p_alpha||q) with IS n_classes = len(self.mix_densities) @@ -671,7 +709,7 @@ test_kde = self.get_kde_function(posteriors, self.bandwidth, 'gaussian') test_densities = self.pdf(test_kde, self.reference_samples, 'gaussian') - def f_squared_hellinger(u): + def f_squared_hellinger(u): return (np.sqrt(u)-1)**2 # todo: this will fail when self.divergence is a callable, and is not the right place to do it anyway @@ -687,7 +725,7 @@ p_class = self.reference_classwise_densities + epsilon fracs = p_class/qs - def divergence(prev): + def divergence(prev): # ps / qs = (prev @ p_class) / qs = prev @ (p_class / qs) = prev @ fracs ps_div_qs = prev @ fracs return np.mean( f(ps_div_qs) * iw ) @@ -699,7 +737,7 @@ [docs] -class KDEyCS(AggregativeSoftQuantifier): +class KDEyCS(AggregativeSoftQuantifier): """ Kernel Density Estimation model for quantification (KDEy) relying on the Cauchy-Schwarz divergence (CS) as the divergence measure to be minimized. This method was first proposed in the paper @@ -736,13 +774,13 @@ :param bandwidth: float, the bandwidth of the Kernel """ - def __init__(self, classifier: BaseEstimator=None, fit_classifier=True, val_split=5, bandwidth=0.1): + def __init__(self, classifier: BaseEstimator=None, fit_classifier=True, val_split=5, bandwidth=0.1): super().__init__(classifier, fit_classifier, val_split) self.bandwidth = KDEBase._check_bandwidth(bandwidth, kernel='gaussian') [docs] - def gram_matrix_mix_sum(self, X, Y=None): + def gram_matrix_mix_sum(self, X, Y=None): # this adapts the output of the rbf_kernel function (pairwise evaluations of Gaussian kernels k(x,y)) # to contain pairwise evaluations of N(x|mu,Sigma1+Sigma2) with mu=y and Sigma1 and Sigma2 are # two "scalar matrices" (h^2)*I each, so Sigma1+Sigma2 has scalar 2(h^2) (h is the bandwidth) @@ -757,7 +795,7 @@ [docs] - def aggregation_fit(self, classif_predictions, labels): + def aggregation_fit(self, classif_predictions, labels): P, y = classif_predictions, labels n = len(self.classes_) @@ -789,7 +827,7 @@ [docs] - def aggregate(self, posteriors: np.ndarray): + def aggregate(self, posteriors: np.ndarray): Ptr = self.Ptr Pte = posteriors y = self.ytr @@ -809,7 +847,7 @@ for i in range(n): tr_te_sums[i] = self.gram_matrix_mix_sum(Ptr[y==i], Pte) - def divergence(alpha): + def divergence(alpha): # called \overline{r} in the paper alpha_ratio = alpha * self.counts_inv diff --git a/docs/build/html/_modules/quapy/method/_neural.html b/docs/build/html/_modules/quapy/method/_neural.html index 3e6c7c3..3c8215e 100644 --- a/docs/build/html/_modules/quapy/method/_neural.html +++ b/docs/build/html/_modules/quapy/method/_neural.html @@ -29,7 +29,7 @@ - + @@ -370,23 +370,24 @@ Source code for quapy.method._neural -import os -from pathlib import Path -import random +import logging +import os +from pathlib import Path +import random -import torch -from torch.nn import MSELoss -from torch.nn.functional import relu +import torch +from torch.nn import MSELoss +from torch.nn.functional import relu -from quapy.protocol import UPP -from quapy.method.aggregative import * -from quapy.util import EarlyStop -from tqdm import tqdm +from quapy.protocol import UPP +from quapy.method.aggregative import * +from quapy.util import EarlyStop +from tqdm import tqdm [docs] -class QuaNetTrainer(BaseQuantifier): +class QuaNetTrainer(BaseQuantifier): """ Implementation of `QuaNet <https://dl.acm.org/doi/abs/10.1145/3269206.3269287>`_, a neural network for quantification. This implementation uses `PyTorch <https://pytorch.org/>`_ and can take advantage of GPU @@ -438,7 +439,7 @@ :param device: string, indicate "cpu" or "cuda" """ - def __init__(self, + def __init__(self, classifier, fit_classifier=True, sample_size=None, @@ -491,7 +492,7 @@ [docs] - def fit(self, X, y): + def fit(self, X, y): """ Trains QuaNet. @@ -549,7 +550,7 @@ order_by=0 if data.binary else None, **self.quanet_params ).to(self.device) - print(self.quanet) + logging.getLogger(__name__).debug(self.quanet) self.optim = torch.optim.Adam(self.quanet.parameters(), lr=self.lr) early_stop = EarlyStop(self.patience, lower_is_better=True) @@ -564,15 +565,16 @@ if early_stop.IMPROVED: torch.save(self.quanet.state_dict(), checkpoint) elif early_stop.STOP: - print(f'training ended by patience exhausted; loading best model parameters in {checkpoint} ' - f'for epoch {early_stop.best_epoch}') + logging.getLogger(__name__).info( + f'training ended by patience exhausted; loading best model parameters in {checkpoint} ' + f'for epoch {early_stop.best_epoch}') self.quanet.load_state_dict(torch.load(checkpoint)) break return self - def _get_aggregative_estims(self, posteriors): + def _get_aggregative_estims(self, posteriors): label_predictions = np.argmax(posteriors, axis=-1) prevs_estim = [] for quantifier in self.quantifiers.values(): @@ -585,7 +587,7 @@ [docs] - def predict(self, X): + def predict(self, X): posteriors = self.classifier.predict_proba(X) embeddings = self.classifier.transform(X) quant_estims = self._get_aggregative_estims(posteriors) @@ -598,7 +600,7 @@ return prevalence - def _epoch(self, data: LabelledCollection, posteriors, iterations, epoch, early_stop, train): + def _epoch(self, data: LabelledCollection, posteriors, iterations, epoch, early_stop, train): mse_loss = MSELoss() self.quanet.train(mode=train) @@ -650,7 +652,7 @@ [docs] - def get_params(self, deep=True): + def get_params(self, deep=True): classifier_params = self.classifier.get_params() classifier_params = {'classifier__'+k:v for k,v in classifier_params.items()} return {**classifier_params, **self.quanet_params} @@ -658,7 +660,7 @@ [docs] - def set_params(self, **parameters): + def set_params(self, **parameters): learner_params = {} for key, val in parameters.items(): if key in self.quanet_params: @@ -670,7 +672,7 @@ self.classifier.set_params(**learner_params) - def __check_params_colision(self, quanet_params, learner_params): + def __check_params_colision(self, quanet_params, learner_params): quanet_keys = set(quanet_params.keys()) learner_keys = set(learner_params.keys()) intersection = quanet_keys.intersection(learner_keys) @@ -680,7 +682,7 @@ [docs] - def clean_checkpoint(self): + def clean_checkpoint(self): """ Removes the checkpoint """ @@ -689,23 +691,23 @@ [docs] - def clean_checkpoint_dir(self): + def clean_checkpoint_dir(self): """ Removes anything contained in the checkpoint directory """ - import shutil + import shutil shutil.rmtree(self.checkpointdir, ignore_errors=True) @property - def classes_(self): + def classes_(self): return self._classes_ [docs] -def mae_loss(output, target): +def mae_loss(output, target): """ Torch-like wrapper for the Mean Absolute Error @@ -719,7 +721,7 @@ [docs] -class QuaNetModule(torch.nn.Module): +class QuaNetModule(torch.nn.Module): """ Implements the `QuaNet <https://dl.acm.org/doi/abs/10.1145/3269206.3269287>`_ forward pass. See :class:`QuaNetTrainer` for training QuaNet. @@ -736,7 +738,7 @@ :param order_by: integer, class for which the document embeddings are to be sorted """ - def __init__(self, + def __init__(self, doc_embedding_size, n_classes, stats_size, @@ -771,10 +773,10 @@ self.output = torch.nn.Linear(prev_size, n_classes) @property - def device(self): + def device(self): return torch.device('cuda') if next(self.parameters()).is_cuda else torch.device('cpu') - def _init_hidden(self): + def _init_hidden(self): directions = 2 if self.bidirectional else 1 var_hidden = torch.zeros(self.nlayers * directions, 1, self.hidden_size) var_cell = torch.zeros(self.nlayers * directions, 1, self.hidden_size) @@ -784,7 +786,7 @@ [docs] - def forward(self, doc_embeddings, doc_posteriors, statistics): + def forward(self, doc_embeddings, doc_posteriors, statistics): device = self.device doc_embeddings = torch.as_tensor(doc_embeddings, dtype=torch.float, device=device) doc_posteriors = torch.as_tensor(doc_posteriors, dtype=torch.float, device=device) diff --git a/docs/build/html/_modules/quapy/method/_threshold_optim.html b/docs/build/html/_modules/quapy/method/_threshold_optim.html index 6bb8003..304e087 100644 --- a/docs/build/html/_modules/quapy/method/_threshold_optim.html +++ b/docs/build/html/_modules/quapy/method/_threshold_optim.html @@ -1,92 +1,388 @@ - - - - - - quapy.method._threshold_optim — QuaPy: A Python-based open-source framework for quantification 0.2.1 documentation - - + + + + + + + + quapy.method._threshold_optim — QuaPy: A Python-based open-source framework for quantification 0.2.1 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Skip to main content - - - - - - - - - + + + + Back to top + + + + + + + + + + Ctrl+K + + - - - - - - - - - - QuaPy: A Python-based open-source framework for quantification - - - - - - - + + - - -Quickstart - - -Manuals - - -API - - - - + + + + + + + + + + + - - - QuaPy: A Python-based open-source framework for quantification - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Home + + + + + + + Manuals + + + + + + + API + + - - - - - - Module code - quapy.method._threshold_optim - - - - - - - - Source code for quapy.method._threshold_optim -from abc import abstractmethod + + + + + + + + + -import numpy as np -from sklearn.base import BaseEstimator -import quapy as qp -import quapy.functional as F -from quapy.data import LabelledCollection -from quapy.method.aggregative import BinaryAggregativeQuantifier + + + Search + Ctrl+K + + + + + + + + + + + + + + System Settings + Light + Dark + + + + + + + + + + + + + + GitHub + + + + + + + + + + + + + Search + Ctrl+K + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Home + + + + + + + Manuals + + + + + + + API + + + + + + + + + + + + + + + + + + + + + + + System Settings + Light + Dark + + + + + + + + + + + + + + GitHub + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Module code + + quapy.method._threshold_optim + + + + + + + + + + + + + + + + + Source code for quapy.method._threshold_optim +from abc import abstractmethod + +import numpy as np +from sklearn.base import BaseEstimator +import quapy as qp +import quapy.functional as F +from quapy.data import LabelledCollection +from quapy.method.aggregative import BinaryAggregativeQuantifier [docs] -class ThresholdOptimization(BinaryAggregativeQuantifier): +class ThresholdOptimization(BinaryAggregativeQuantifier): """ Abstract class of Threshold Optimization variants for :class:`ACC` as proposed by `Forman 2006 <https://dl.acm.org/doi/abs/10.1145/1150402.1150423>`_ and @@ -111,14 +407,14 @@ :param n_jobs: number of parallel workers """ - def __init__(self, classifier: BaseEstimator=None, fit_classifier=True, val_split=None, n_jobs=None): + def __init__(self, classifier: BaseEstimator=None, fit_classifier=True, val_split=None, n_jobs=None): super().__init__(classifier, fit_classifier, val_split) self.n_jobs = qp._get_njobs(n_jobs) [docs] @abstractmethod - def condition(self, tpr, fpr) -> float: + def condition(self, tpr, fpr) -> float: """ Implements the criterion according to which the threshold should be selected. This function should return the (float) score to be minimized. @@ -132,7 +428,7 @@ [docs] - def discard(self, tpr, fpr) -> bool: + def discard(self, tpr, fpr) -> bool: """ Indicates whether a combination of tpr and fpr should be discarded @@ -144,7 +440,7 @@ - def _eval_candidate_thresholds(self, decision_scores, y): + def _eval_candidate_thresholds(self, decision_scores, y): """ Seeks for the best `tpr` and `fpr` according to the score obtained at different decision thresholds. The scoring function is implemented in function `_condition`. @@ -181,7 +477,7 @@ [docs] - def aggregate_with_threshold(self, classif_predictions, tprs, fprs, thresholds): + def aggregate_with_threshold(self, classif_predictions, tprs, fprs, thresholds): # This function performs the adjusted count for given tpr, fpr, and threshold. # Note that, due to broadcasting, tprs, fprs, and thresholds could be arrays of length > 1 prevs_estims = np.mean(classif_predictions[:, None] >= thresholds, axis=0) @@ -190,26 +486,26 @@ return prevs_estims.squeeze() - def _compute_table(self, y, y_): + def _compute_table(self, y, y_): TP = np.logical_and(y == y_, y == self.pos_label).sum() FP = np.logical_and(y != y_, y == self.neg_label).sum() FN = np.logical_and(y != y_, y == self.pos_label).sum() TN = np.logical_and(y == y_, y == self.neg_label).sum() return TP, FP, FN, TN - def _compute_tpr(self, TP, FP): - if TP + FP == 0: + def _compute_tpr(self, TP, FN): + if TP + FN == 0: return 1 - return TP / (TP + FP) + return TP / (TP + FN) - def _compute_fpr(self, FP, TN): + def _compute_fpr(self, FP, TN): if FP + TN == 0: return 0 return FP / (FP + TN) [docs] - def aggregation_fit(self, classif_predictions, labels): + def aggregation_fit(self, classif_predictions, labels): decision_scores, y = classif_predictions, labels # the standard behavior is to keep the best threshold only self.tpr, self.fpr, self.threshold = self._eval_candidate_thresholds(decision_scores, y)[0] @@ -218,7 +514,7 @@ [docs] - def aggregate(self, classif_predictions: np.ndarray): + def aggregate(self, classif_predictions: np.ndarray): # the standard behavior is to compute the adjusted count using the best threshold found return self.aggregate_with_threshold(classif_predictions, self.tpr, self.fpr, self.threshold) @@ -227,7 +523,7 @@ [docs] -class T50(ThresholdOptimization): +class T50(ThresholdOptimization): """ Threshold Optimization variant for :class:`ACC` as proposed by `Forman 2006 <https://dl.acm.org/doi/abs/10.1145/1150402.1150423>`_ and @@ -249,12 +545,12 @@ """ - def __init__(self, classifier: BaseEstimator=None, fit_classifier=True, val_split=5): + def __init__(self, classifier: BaseEstimator=None, fit_classifier=True, val_split=5): super().__init__(classifier, fit_classifier, val_split) [docs] - def condition(self, tpr, fpr) -> float: + def condition(self, tpr, fpr) -> float: return abs(tpr - 0.5) @@ -262,7 +558,7 @@ [docs] -class MAX(ThresholdOptimization): +class MAX(ThresholdOptimization): """ Threshold Optimization variant for :class:`ACC` as proposed by `Forman 2006 <https://dl.acm.org/doi/abs/10.1145/1150402.1150423>`_ and @@ -282,12 +578,12 @@ """ - def __init__(self, classifier: BaseEstimator=None, fit_classifier=True, val_split=5): + def __init__(self, classifier: BaseEstimator=None, fit_classifier=True, val_split=5): super().__init__(classifier, fit_classifier, val_split) [docs] - def condition(self, tpr, fpr) -> float: + def condition(self, tpr, fpr) -> float: # MAX strives to maximize (tpr - fpr), which is equivalent to minimize (fpr - tpr) return (fpr - tpr) @@ -296,7 +592,7 @@ [docs] -class X(ThresholdOptimization): +class X(ThresholdOptimization): """ Threshold Optimization variant for :class:`ACC` as proposed by `Forman 2006 <https://dl.acm.org/doi/abs/10.1145/1150402.1150423>`_ and @@ -316,12 +612,12 @@ """ - def __init__(self, classifier: BaseEstimator=None, fit_classifier=True, val_split=5): + def __init__(self, classifier: BaseEstimator=None, fit_classifier=True, val_split=5): super().__init__(classifier, fit_classifier, val_split) [docs] - def condition(self, tpr, fpr) -> float: + def condition(self, tpr, fpr) -> float: return abs(1 - (tpr + fpr)) @@ -329,7 +625,7 @@ [docs] -class MS(ThresholdOptimization): +class MS(ThresholdOptimization): """ Median Sweep. Threshold Optimization variant for :class:`ACC` as proposed by `Forman 2006 <https://dl.acm.org/doi/abs/10.1145/1150402.1150423>`_ and @@ -348,18 +644,18 @@ for `k`); or as a tuple (X,y) defining the specific set of data to use for validation. """ - def __init__(self, classifier: BaseEstimator=None, fit_classifier=True, val_split=5): + def __init__(self, classifier: BaseEstimator=None, fit_classifier=True, val_split=5): super().__init__(classifier, fit_classifier, val_split) [docs] - def condition(self, tpr, fpr) -> float: + def condition(self, tpr, fpr) -> float: return 1 [docs] - def aggregation_fit(self, classif_predictions, labels): + def aggregation_fit(self, classif_predictions, labels): decision_scores, y = classif_predictions, labels # keeps all candidates tprs_fprs_thresholds = self._eval_candidate_thresholds(decision_scores, y) @@ -371,7 +667,7 @@ [docs] - def aggregate(self, classif_predictions: np.ndarray): + def aggregate(self, classif_predictions: np.ndarray): prevalences = self.aggregate_with_threshold(classif_predictions, self.tprs, self.fprs, self.thresholds) if prevalences.ndim==2: prevalences = np.median(prevalences, axis=0) @@ -382,7 +678,7 @@ [docs] -class MS2(MS): +class MS2(MS): """ Median Sweep 2. Threshold Optimization variant for :class:`ACC` as proposed by `Forman 2006 <https://dl.acm.org/doi/abs/10.1145/1150402.1150423>`_ and @@ -402,42 +698,86 @@ for `k`); or as a tuple (X,y) defining the specific set of data to use for validation. """ - def __init__(self, classifier: BaseEstimator=None, fit_classifier=True, val_split=5): + def __init__(self, classifier: BaseEstimator=None, fit_classifier=True, val_split=5): super().__init__(classifier, fit_classifier, val_split) [docs] - def discard(self, tpr, fpr) -> bool: + def discard(self, tpr, fpr) -> bool: return (tpr-fpr) <= 0.25 - + + + + + + + + + + + + + - + + + + - Built with Sphinx using a - theme - provided by Read the Docs. - + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/build/html/_modules/quapy/method/aggregative.html b/docs/build/html/_modules/quapy/method/aggregative.html index a6b98d4..0680ac3 100644 --- a/docs/build/html/_modules/quapy/method/aggregative.html +++ b/docs/build/html/_modules/quapy/method/aggregative.html @@ -29,8 +29,9 @@ - - + + + @@ -41,6 +42,7 @@ + @@ -114,8 +116,14 @@ + + + + + + + - QuaPy @@ -131,7 +139,7 @@ - Quickstart + Home @@ -183,6 +191,21 @@ + + + + + + + + + + + GitHub + + + @@ -229,7 +252,7 @@ - Quickstart + Home @@ -272,6 +295,21 @@ + + + + + + + + + + + GitHub + + + @@ -332,25 +370,25 @@ Source code for quapy.method.aggregative -from abc import ABC, abstractmethod -from argparse import ArgumentError -from copy import deepcopy -from typing import Callable, Literal, Union -import numpy as np -from sklearn.base import BaseEstimator -from sklearn.calibration import CalibratedClassifierCV -from sklearn.exceptions import NotFittedError -from sklearn.metrics import confusion_matrix -from sklearn.model_selection import cross_val_predict, train_test_split -from sklearn.utils.validation import check_is_fitted +import warnings +from abc import ABC, abstractmethod +from copy import deepcopy +from typing import Callable, Literal, Union +import numpy as np +from sklearn.base import BaseEstimator +from sklearn.calibration import CalibratedClassifierCV +from sklearn.exceptions import NotFittedError +from sklearn.metrics import confusion_matrix +from sklearn.model_selection import cross_val_predict, train_test_split +from sklearn.utils.validation import check_is_fitted -import quapy as qp -import quapy.functional as F -from quapy.functional import get_divergence -from quapy.classification.svmperf import SVMperf -from quapy.data import LabelledCollection -from quapy.method.base import BaseQuantifier, BinaryQuantifier, OneVsAllGeneric -from quapy.method._helper import ( +import quapy as qp +import quapy.functional as F +from quapy.functional import get_divergence +from quapy.classification.svmperf import SVMperf +from quapy.data import LabelledCollection +from quapy.method.base import BaseQuantifier, BinaryQuantifier, OneVsAllGeneric +from quapy.method._helper import ( _get_abstention_calibrators, _get_cvxpy, _rlls_check_mode, @@ -368,7 +406,7 @@ [docs] -class AggregativeQuantifier(BaseQuantifier, ABC): +class AggregativeQuantifier(BaseQuantifier, ABC): """ Abstract class for quantification methods that base their estimations on the aggregation of classification results. Aggregative quantifiers implement a pipeline that consists of generating classification predictions @@ -396,7 +434,7 @@ the training data be wasted. """ - def __init__(self, + def __init__(self, classifier: Union[None,BaseEstimator], fit_classifier:bool=True, val_split:Union[int,float,tuple,None]=5): @@ -418,9 +456,9 @@ (f'when {val_split=} is indicated as an integer, it represents the number of folds in a kFCV ' f'and must thus be >1') if val_split==5 and not fit_classifier: - print(f'Warning: {val_split=} will be ignored when the classifier is already trained ' - f'({fit_classifier=}). Parameter {self.val_split=} will be set to None. Set {val_split=} ' - f'to None to avoid this warning.') + warnings.warn(f'{val_split=} will be ignored when the classifier is already trained ' + f'({fit_classifier=}). Parameter {self.val_split=} will be set to None. Set {val_split=} ' + f'to None to avoid this warning.') self.val_split=None if val_split!=5: assert fit_classifier, (f'Parameter {val_split=} has been modified, but {fit_classifier=} ' @@ -457,7 +495,7 @@ assert fitted, (f'{fit_classifier=} requires the classifier to be already trained, ' f'but this does not seem to be') - def _check_init_parameters(self): + def _check_init_parameters(self): """ Implements any check to be performed in the parameters of the init method before undertaking the training of the quantifier. This is made as to allow for a quick execution stop when the @@ -467,7 +505,7 @@ """ pass - def _check_non_empty_classes(self, y): + def _check_non_empty_classes(self, y): """ Asserts all classes have positive instances. @@ -484,7 +522,7 @@ [docs] - def fit(self, X, y): + def fit(self, X, y): """ Trains the aggregative quantifier. This comes down to training a classifier (if requested) and an aggregation function. @@ -501,7 +539,7 @@ [docs] - def classifier_fit_predict(self, X, y): + def classifier_fit_predict(self, X, y): """ Trains the classifier if requested (`fit_classifier=True`) and generate the necessary predictions to train the aggregation function. @@ -551,7 +589,7 @@ [docs] @abstractmethod - def aggregation_fit(self, classif_predictions, labels): + def aggregation_fit(self, classif_predictions, labels): """ Trains the aggregation function. @@ -563,7 +601,7 @@ @property - def classifier(self): + def classifier(self): """ Gives access to the classifier @@ -572,7 +610,7 @@ return self.classifier_ @classifier.setter - def classifier(self, classifier): + def classifier(self, classifier): """ Setter for the classifier @@ -582,7 +620,7 @@ [docs] - def classify(self, X): + def classify(self, X): """ Provides the label predictions for the given instances. The predictions should respect the format expected by :meth:`aggregate`, e.g., posterior probabilities for probabilistic quantifiers, or crisp predictions for @@ -594,7 +632,7 @@ return getattr(self.classifier, self._classifier_method())(X) - def _classifier_method(self): + def _classifier_method(self): """ Name of the method that must be used for issuing label predictions. The default one is "decision_function". @@ -602,7 +640,7 @@ """ return 'decision_function' - def _check_classifier(self, adapt_if_necessary=False): + def _check_classifier(self, adapt_if_necessary=False): """ Guarantees that the underlying classifier implements the method required for issuing predictions, i.e., the method indicated by the :meth:`_classifier_method` @@ -614,7 +652,7 @@ [docs] - def predict(self, X): + def predict(self, X): """ Generate class prevalence estimates for the sample's instances by aggregating the label predictions generated by the classifier. @@ -629,7 +667,7 @@ [docs] @abstractmethod - def aggregate(self, classif_predictions: np.ndarray): + def aggregate(self, classif_predictions: np.ndarray): """ Implements the aggregation of the classifier predictions. @@ -640,7 +678,7 @@ @property - def classes_(self): + def classes_(self): """ Class labels, in the same order in which class prevalence values are to be computed. This default implementation actually returns the class labels of the learner. @@ -653,14 +691,14 @@ [docs] -class AggregativeCrispQuantifier(AggregativeQuantifier, ABC): +class AggregativeCrispQuantifier(AggregativeQuantifier, ABC): """ Abstract class for quantification methods that base their estimations on the aggregation of crisp decisions as returned by a hard classifier. Aggregative crisp quantifiers thus extend Aggregative Quantifiers by implementing specifications about crisp predictions. """ - def _classifier_method(self): + def _classifier_method(self): """ Name of the method that must be used for issuing label predictions. For crisp quantifiers, the method is 'predict', that returns an array of shape `(n_instances,)` of label predictions. @@ -673,7 +711,7 @@ [docs] -class AggregativeSoftQuantifier(AggregativeQuantifier, ABC): +class AggregativeSoftQuantifier(AggregativeQuantifier, ABC): """ Abstract class for quantification methods that base their estimations on the aggregation of posterior probabilities as returned by a probabilistic classifier. @@ -681,7 +719,7 @@ about soft predictions. """ - def _classifier_method(self): + def _classifier_method(self): """ Name of the method that must be used for issuing label predictions. For probabilistic quantifiers, the method is 'predict_proba', that returns an array of shape `(n_instances, n_dimensions,)` with posterior @@ -691,7 +729,7 @@ """ return 'predict_proba' - def _check_classifier(self, adapt_if_necessary=False): + def _check_classifier(self, adapt_if_necessary=False): """ Guarantees that the underlying classifier implements the method indicated by the :meth:`_classifier_method`. In case it does not, the classifier is calibrated (by means of the Platt's calibration method implemented by @@ -703,8 +741,8 @@ """ if not hasattr(self.classifier, self._classifier_method()): if adapt_if_necessary: - print(f'warning: The learner {self.classifier.__class__.__name__} does not seem to be ' - f'probabilistic. The learner will be calibrated (using CalibratedClassifierCV).') + warnings.warn(f'The learner {self.classifier.__class__.__name__} does not seem to be ' + f'probabilistic. The learner will be calibrated (using CalibratedClassifierCV).') self.classifier = CalibratedClassifierCV(self.classifier, cv=5) else: raise AssertionError(f'error: The learner {self.classifier.__class__.__name__} does not ' @@ -715,19 +753,19 @@ [docs] -class BinaryAggregativeQuantifier(AggregativeQuantifier, BinaryQuantifier): +class BinaryAggregativeQuantifier(AggregativeQuantifier, BinaryQuantifier): @property - def pos_label(self): + def pos_label(self): return self.classifier.classes_[1] @property - def neg_label(self): + def neg_label(self): return self.classifier.classes_[0] [docs] - def fit(self, X, y): + def fit(self, X, y): self._check_binary(y, self.__class__.__name__) return super().fit(X, y) @@ -738,19 +776,19 @@ # ------------------------------------ [docs] -class CC(AggregativeCrispQuantifier): +class CC(AggregativeCrispQuantifier): """ The most basic Quantification method. One that simply classifies all instances and counts how many have been attributed to each of the classes in order to compute class prevalence estimates. :param classifier: a sklearn's Estimator that generates a classifier """ - def __init__(self, classifier: BaseEstimator = None, fit_classifier: bool = True): + def __init__(self, classifier: BaseEstimator = None, fit_classifier: bool = True): super().__init__(classifier, fit_classifier, val_split=None) [docs] - def aggregation_fit(self, classif_predictions, labels): + def aggregation_fit(self, classif_predictions, labels): """ Nothing to do here! @@ -762,7 +800,7 @@ [docs] - def aggregate(self, classif_predictions: np.ndarray): + def aggregate(self, classif_predictions: np.ndarray): """ Computes class prevalence estimates by counting the prevalence of each of the predicted labels. @@ -776,7 +814,7 @@ [docs] -class PCC(AggregativeSoftQuantifier): +class PCC(AggregativeSoftQuantifier): """ `Probabilistic Classify & Count <https://ieeexplore.ieee.org/abstract/document/5694031>`_, the probabilistic variant of CC that relies on the posterior probabilities returned by a probabilistic classifier. @@ -784,12 +822,12 @@ :param classifier: a sklearn's Estimator that generates a classifier """ - def __init__(self, classifier: BaseEstimator = None, fit_classifier: bool = True, val_split=None): + def __init__(self, classifier: BaseEstimator = None, fit_classifier: bool = True, val_split=None): super().__init__(classifier, fit_classifier, val_split=val_split) [docs] - def aggregation_fit(self, classif_predictions, labels): + def aggregation_fit(self, classif_predictions, labels): """ Nothing to do here! @@ -801,7 +839,7 @@ [docs] - def aggregate(self, classif_posteriors): + def aggregate(self, classif_posteriors): return F.prevalence_from_probabilities(classif_posteriors, binarize=False) @@ -809,7 +847,7 @@ [docs] -class ACC(AggregativeCrispQuantifier): +class ACC(AggregativeCrispQuantifier): """ `Adjusted Classify & Count <https://link.springer.com/article/10.1007/s10618-008-0097-y>`_, the "adjusted" variant of :class:`CC`, that corrects the predictions of CC @@ -857,7 +895,7 @@ :param n_jobs: number of parallel workers """ - def __init__( + def __init__( self, classifier: BaseEstimator = None, fit_classifier = True, @@ -880,7 +918,7 @@ [docs] @classmethod - def newInvariantRatioEstimation(cls, classifier: BaseEstimator, fit_classifier=True, val_split=5, n_jobs=None): + def newInvariantRatioEstimation(cls, classifier: BaseEstimator, fit_classifier=True, val_split=5, n_jobs=None): """ Constructs a quantifier that implements the Invariant Ratio Estimator of `Vaz et al. 2018 <https://jmlr.org/papers/v20/18-456.html>`_. This amounts @@ -905,7 +943,7 @@ return ACC(classifier, fit_classifier=fit_classifier, val_split=val_split, method='invariant-ratio', norm='mapsimplex', n_jobs=n_jobs) - def _check_init_parameters(self): + def _check_init_parameters(self): if self.solver not in ACC.SOLVERS: raise ValueError(f"unknown solver; valid ones are {ACC.SOLVERS}") if self.method not in ACC.METHODS: @@ -915,7 +953,7 @@ [docs] - def aggregation_fit(self, classif_predictions, labels): + def aggregation_fit(self, classif_predictions, labels): """ Estimates the misclassification rates. :param classif_predictions: array-like with the predicted labels @@ -930,7 +968,7 @@ [docs] @classmethod - def getPteCondEstim(cls, classes, y, y_): + def getPteCondEstim(cls, classes, y, y_): """ Estimate the matrix with entry (i,j) being the estimate of P(hat_yi|yj), that is, the probability that a document that belongs to yj ends up being classified as belonging to yi @@ -953,7 +991,7 @@ [docs] - def aggregate(self, classif_predictions): + def aggregate(self, classif_predictions): prevs_estim = self.cc.aggregate(classif_predictions) estimate = F.solve_adjustment( class_conditional_rates=self.Pte_cond_estim_, @@ -968,7 +1006,7 @@ [docs] -class PACC(AggregativeSoftQuantifier): +class PACC(AggregativeSoftQuantifier): """ `Probabilistic Adjusted Classify & Count <https://ieeexplore.ieee.org/abstract/document/5694031>`_, the probabilistic variant of ACC that relies on the posterior probabilities returned by a probabilistic classifier. @@ -1015,7 +1053,7 @@ :param n_jobs: number of parallel workers """ - def __init__( + def __init__( self, classifier: BaseEstimator = None, fit_classifier=True, @@ -1031,7 +1069,7 @@ self.method = method self.norm = norm - def _check_init_parameters(self): + def _check_init_parameters(self): if self.solver not in ACC.SOLVERS: raise ValueError(f"unknown solver; valid ones are {ACC.SOLVERS}") if self.method not in ACC.METHODS: @@ -1041,7 +1079,7 @@ [docs] - def aggregation_fit(self, classif_predictions, labels): + def aggregation_fit(self, classif_predictions, labels): """ Estimates the misclassification rates @@ -1056,7 +1094,7 @@ [docs] - def aggregate(self, classif_posteriors): + def aggregate(self, classif_posteriors): prevs_estim = self.pcc.aggregate(classif_posteriors) estimate = F.solve_adjustment( @@ -1071,7 +1109,7 @@ [docs] @classmethod - def getPteCondEstim(cls, classes, y, y_): + def getPteCondEstim(cls, classes, y, y_): # estimate the matrix with entry (i,j) being the estimate of P(hat_yi|yj), that is, the probability that a # document that belongs to yj ends up being classified as belonging to yi n_classes = len(classes) @@ -1088,7 +1126,7 @@ [docs] -class RLLS(AggregativeSoftQuantifier): +class RLLS(AggregativeSoftQuantifier): """ `Regularized Learning for Domain Adaptation under Label Shifts <https://arxiv.org/abs/1903.09734>`_, used here as an aggregative @@ -1128,7 +1166,7 @@ :func:`quapy.functional.normalize_prevalence` """ - def __init__( + def __init__( self, classifier: BaseEstimator = None, fit_classifier=True, @@ -1147,7 +1185,7 @@ self.norm = norm self.last_w_ = None - def _check_init_parameters(self): + def _check_init_parameters(self): _get_cvxpy() _rlls_check_mode(self.mode) if not isinstance(self.alpha, (int, float)) or self.alpha < 0: @@ -1164,7 +1202,7 @@ [docs] - def aggregation_fit(self, classif_predictions, labels): + def aggregation_fit(self, classif_predictions, labels): if classif_predictions is None or labels is None: raise ValueError('RLLS requires source posterior predictions and source labels') @@ -1181,7 +1219,7 @@ [docs] - def aggregate(self, classif_posteriors): + def aggregate(self, classif_posteriors): qz = _rlls_predicted_marginal(classif_posteriors, mode=self.mode) w = _rlls_compute_weights( self.C_zy_, @@ -1199,7 +1237,7 @@ [docs] -class EMQ(AggregativeSoftQuantifier): +class EMQ(AggregativeSoftQuantifier): """ `Expectation Maximization for Quantification <https://ieeexplore.ieee.org/abstract/document/6789744>`_ (EMQ), aka `Saerens-Latinne-Decaestecker` (SLD) algorithm. @@ -1249,7 +1287,7 @@ ON_CALIB_ERROR_VALUES = ['raise', 'backup'] CALIB_OPTIONS = [None, 'nbvs', 'bcts', 'ts', 'vs'] - def __init__(self, classifier: BaseEstimator = None, fit_classifier=True, val_split=None, exact_train_prev=True, + def __init__(self, classifier: BaseEstimator = None, fit_classifier=True, val_split=None, exact_train_prev=True, calib=None, on_calib_error='raise', n_jobs=None): assert calib in EMQ.CALIB_OPTIONS, \ @@ -1261,12 +1299,12 @@ self.exact_train_prev = exact_train_prev self.calib = calib self.on_calib_error = on_calib_error - self.n_jobs = n_jobs + self.n_jobs = qp._get_njobs(n_jobs) [docs] @classmethod - def EMQ_BCTS(cls, classifier: BaseEstimator, fit_classifier=True, val_split=5, on_calib_error="raise", n_jobs=None): + def EMQ_BCTS(cls, classifier: BaseEstimator, fit_classifier=True, val_split=5, on_calib_error="raise", n_jobs=None): """ Constructs an instance of EMQ using the best configuration found in the `Alexandari et al. paper <http://proceedings.mlr.press/v119/alexandari20a.html>`_, i.e., one that relies on Bias-Corrected Temperature @@ -1298,23 +1336,23 @@ calib='bcts', on_calib_error=on_calib_error, n_jobs=n_jobs) - def _check_init_parameters(self): + def _check_init_parameters(self): if self.val_split is not None: if self.exact_train_prev and self.calib is None: - raise RuntimeWarning(f'The parameter {self.val_split=} was specified for EMQ, while the parameters ' - f'{self.exact_train_prev=} and {self.calib=}. This has no effect and causes an ' - f'unnecessary overload.') + warnings.warn(f'The parameter {self.val_split=} was specified for EMQ, while the parameters ' + f'{self.exact_train_prev=} and {self.calib=}. This has no effect and causes an ' + f'unnecessary overload.', RuntimeWarning) else: if self.calib is not None: - print(f'[warning] The parameter {self.calib=} requires the val_split be different from None. ' - f'This parameter will be set to 5. To avoid this warning, set this value to a float value ' - f'indicating the proportion of training data to be used as validation, or to an integer ' - f'indicating the number of folds for kFCV.') + warnings.warn(f'The parameter {self.calib=} requires the val_split be different from None. ' + f'This parameter will be set to 5. To avoid this warning, set this value to a float value ' + f'indicating the proportion of training data to be used as validation, or to an integer ' + f'indicating the number of folds for kFCV.') self.val_split = 5 [docs] - def classify(self, X): + def classify(self, X): """ Provides the posterior probabilities for the given instances. The calibration function, if required, has no effect in this step, and is only involved in the aggregate method. @@ -1327,13 +1365,13 @@ [docs] - def classifier_fit_predict(self, X, y): + def classifier_fit_predict(self, X, y): classif_predictions = super().classifier_fit_predict(X, y) self.train_prevalence = F.prevalence_from_labels(y, classes=self.classes_) return classif_predictions - def _fit_calibration(self, calibrator, P, y): + def _fit_calibration(self, calibrator, P, y): n_classes = len(self.classes_) if not np.issubdtype(y.dtype, np.number): @@ -1347,7 +1385,7 @@ elif self.on_calib_error == 'backup': self.calibration_function = lambda P: P - def _calibrate_if_requested(self, uncalib_posteriors): + def _calibrate_if_requested(self, uncalib_posteriors): if hasattr(self, 'calibration_function') and self.calibration_function is not None: try: calib_posteriors = self.calibration_function(uncalib_posteriors) @@ -1364,7 +1402,7 @@ [docs] - def aggregation_fit(self, classif_predictions, labels): + def aggregation_fit(self, classif_predictions, labels): """ Trains the aggregation function of EMQ. This comes down to recalibrating the posterior probabilities ir requested. @@ -1379,14 +1417,14 @@ requires_predictions = (self.calib is not None) or (not self.exact_train_prev) if P is None and requires_predictions: # classifier predictions were not generated because val_split=None - raise ArgumentError(self.val_split, self.__class__.__name__ + - ": Classifier predictions for the aggregative fit were not generated because " - "val_split=None. This usually happens when you enable calibrations or heuristics " - "during model selection but left val_split set to its default value (None). " - "Please provide one of the following values for val_split: (i) an integer >1 " - "(e.g. val_split=5) for k-fold cross-validation; (ii) a float in (0,1) (e.g. " - "val_split=0.3) for a proportion split; or (iii) a tuple (X, y) with explicit " - "validation data") + raise ValueError(self.__class__.__name__ + + ": Classifier predictions for the aggregative fit were not generated because " + "val_split=None. This usually happens when you enable calibrations or heuristics " + "during model selection but left val_split set to its default value (None). " + "Please provide one of the following values for val_split: (i) an integer >1 " + "(e.g. val_split=5) for k-fold cross-validation; (ii) a float in (0,1) (e.g. " + "val_split=0.3) for a proportion split; or (iii) a tuple (X, y) with explicit " + "validation data") if self.calib is not None: calibrator = _get_abstention_calibrators().get(self.calib, None) @@ -1403,7 +1441,7 @@ [docs] - def aggregate(self, classif_posteriors, epsilon=EPSILON): + def aggregate(self, classif_posteriors, epsilon=EPSILON): classif_posteriors = self._calibrate_if_requested(classif_posteriors) priors, posteriors = self.EM(self.train_prevalence, classif_posteriors, epsilon) return priors @@ -1411,7 +1449,7 @@ [docs] - def predict_proba(self, instances, epsilon=EPSILON): + def predict_proba(self, instances, epsilon=EPSILON): """ Returns the posterior probabilities updated by the EM algorithm. @@ -1428,7 +1466,7 @@ [docs] @classmethod - def EM(cls, tr_prev, posterior_probabilities, epsilon=EPSILON): + def EM(cls, tr_prev, posterior_probabilities, epsilon=EPSILON): """ Computes the `Expectation Maximization` routine. @@ -1466,7 +1504,7 @@ s += 1 if not converged: - print('[warning] the method has reached the maximum number of iterations; it might have not converged') + warnings.warn('the method has reached the maximum number of iterations; it might have not converged') return qs, ps @@ -1475,7 +1513,7 @@ [docs] -class HDy(AggregativeSoftQuantifier, BinaryAggregativeQuantifier): +class HDy(AggregativeSoftQuantifier, BinaryAggregativeQuantifier): """ `Hellinger Distance y <https://www.sciencedirect.com/science/article/pii/S0020025512004069>`_ (HDy). HDy is a probabilistic method for training binary quantifiers, that models quantification as the problem of @@ -1502,12 +1540,12 @@ for `k`); or as a tuple (X,y) defining the specific set of data to use for validation. """ - def __init__(self, classifier: BaseEstimator = None, fit_classifier=True, val_split=5): + def __init__(self, classifier: BaseEstimator = None, fit_classifier=True, val_split=5): super().__init__(classifier, fit_classifier, val_split) [docs] - def aggregation_fit(self, classif_predictions, labels): + def aggregation_fit(self, classif_predictions, labels): """ Trains the aggregation function of HDy. @@ -1522,7 +1560,7 @@ # pre-compute the histogram for positive and negative examples self.bins = np.linspace(10, 110, 11, dtype=int) # [10, 20, 30, ..., 100, 110] - def hist(P, bins): + def hist(P, bins): h = np.histogram(P, bins=bins, range=(0, 1), density=True)[0] return h / h.sum() @@ -1532,7 +1570,7 @@ [docs] - def aggregate(self, classif_posteriors): + def aggregate(self, classif_posteriors): # "In this work, the number of bins b used in HDx and HDy was chosen from 10 to 110 in steps of 10, # and the final estimated a priori probability was taken as the median of these 11 estimates." # (González-Castro, et al., 2013). @@ -1549,7 +1587,7 @@ # the authors proposed to search for the prevalence yielding the best matching as a linear search # at small steps (modern implementations resort to an optimization procedure, # see class DistributionMatching) - def loss(prev): + def loss(prev): class1_prev = prev[1] Px_train = class1_prev * Pxy1_density + (1 - class1_prev) * Pxy0_density return F.HellingerDistance(Px_train, Px_test) @@ -1564,7 +1602,7 @@ [docs] -class DyS(AggregativeSoftQuantifier, BinaryAggregativeQuantifier): +class DyS(AggregativeSoftQuantifier, BinaryAggregativeQuantifier): """ `DyS framework <https://ojs.aaai.org/index.php/AAAI/article/view/4376>`_ (DyS). DyS is a generalization of HDy method, using a Ternary Search in order to find the prevalence that @@ -1593,15 +1631,15 @@ :param n_jobs: number of parallel workers. """ - def __init__(self, classifier: BaseEstimator = None, fit_classifier=True, val_split=5, n_bins=8, + def __init__(self, classifier: BaseEstimator = None, fit_classifier=True, val_split=5, n_bins=8, divergence: Union[str, Callable] = 'HD', tol=1e-05, n_jobs=None): super().__init__(classifier, fit_classifier, val_split) self.tol = tol self.divergence = divergence self.n_bins = n_bins - self.n_jobs = n_jobs + self.n_jobs = qp._get_njobs(n_jobs) - def _ternary_search(self, f, left, right, tol): + def _ternary_search(self, f, left, right, tol): """ Find maximum of unimodal function f() within [left, right] """ @@ -1619,7 +1657,7 @@ [docs] - def aggregation_fit(self, classif_predictions, labels): + def aggregation_fit(self, classif_predictions, labels): """ Trains the aggregation function of DyS. @@ -1637,13 +1675,13 @@ [docs] - def aggregate(self, classif_posteriors): + def aggregate(self, classif_posteriors): Px = classif_posteriors[:, self.pos_label] # takes only the P(y=+1|x) Px_test = np.histogram(Px, bins=self.n_bins, range=(0, 1), density=True)[0] divergence = get_divergence(self.divergence) - def distribution_distance(prev): + def distribution_distance(prev): Px_train = prev * self.Pxy1_density + (1 - prev) * self.Pxy0_density return divergence(Px_train, Px_test) @@ -1655,7 +1693,7 @@ [docs] -class SMM(AggregativeSoftQuantifier, BinaryAggregativeQuantifier): +class SMM(AggregativeSoftQuantifier, BinaryAggregativeQuantifier): """ `SMM method <https://ieeexplore.ieee.org/document/9260028>`_ (SMM). SMM is a simplification of matching distribution methods where the representation of the examples @@ -1674,12 +1712,12 @@ for `k`); or as a tuple (X,y) defining the specific set of data to use for validation. """ - def __init__(self, classifier: BaseEstimator = None, fit_classifier=True, val_split=5): + def __init__(self, classifier: BaseEstimator = None, fit_classifier=True, val_split=5): super().__init__(classifier, fit_classifier, val_split) [docs] - def aggregation_fit(self, classif_predictions, labels): + def aggregation_fit(self, classif_predictions, labels): """ Trains the aggregation function of SMM. @@ -1697,7 +1735,7 @@ [docs] - def aggregate(self, classif_posteriors): + def aggregate(self, classif_posteriors): Px = classif_posteriors[:, self.pos_label] # takes only the P(y=+1|x) Px_mean = np.mean(Px) @@ -1709,7 +1747,7 @@ [docs] -class DMy(AggregativeSoftQuantifier): +class DMy(AggregativeSoftQuantifier): """ Generic Distribution Matching quantifier for binary or multiclass quantification based on the space of posterior probabilities. This implementation takes the number of bins, the divergence, and the possibility to work on CDF @@ -1742,19 +1780,19 @@ :param n_jobs: number of parallel workers (default None) """ - def __init__(self, classifier: BaseEstimator = None, fit_classifier=True, val_split=5, nbins=8, + def __init__(self, classifier: BaseEstimator = None, fit_classifier=True, val_split=5, nbins=8, divergence: Union[str, Callable] = 'HD', cdf=False, search='optim_minimize', n_jobs=None): super().__init__(classifier, fit_classifier, val_split) self.nbins = nbins self.divergence = divergence self.cdf = cdf self.search = search - self.n_jobs = n_jobs + self.n_jobs = qp._get_njobs(n_jobs) [docs] @classmethod - def HDy(cls, classifier: BaseEstimator = None, fit_classifier=True, val_split=5, n_jobs=None): + def HDy(cls, classifier: BaseEstimator = None, fit_classifier=True, val_split=5, n_jobs=None): """ Historical HDy preset expressed as a configuration of :class:`DMy`. @@ -1783,7 +1821,7 @@ return AggregativeMedianEstimator(base_quantifier=base, param_grid=param_grid, n_jobs=n_jobs) - def _get_distributions(self, posteriors): + def _get_distributions(self, posteriors): histograms = [] post_dims = posteriors.shape[1] if post_dims == 2: @@ -1801,7 +1839,7 @@ [docs] - def aggregation_fit(self, classif_predictions, labels): + def aggregation_fit(self, classif_predictions, labels): """ Trains the aggregation function of a distribution matching method. This comes down to generating the validation distributions out of the training data. @@ -1829,7 +1867,7 @@ [docs] - def aggregate(self, posteriors: np.ndarray): + def aggregate(self, posteriors: np.ndarray): """ Searches for the mixture model parameter (the sought prevalence values) that yields a validation distribution (the mixture) that best matches the test distribution, in terms of the divergence measure of choice. @@ -1844,7 +1882,7 @@ divergence = get_divergence(self.divergence) n_classes, n_channels, nbins = self.validation_distribution.shape - def loss(prev): + def loss(prev): prev = np.expand_dims(prev, axis=0) mixture_distribution = (prev @ self.validation_distribution.reshape(n_classes, -1)).reshape(n_channels, -1) divs = [divergence(test_distribution[ch], mixture_distribution[ch]) for ch in range(n_channels)] @@ -1857,7 +1895,7 @@ [docs] -def newELM(svmperf_base=None, loss='01', C=1): +def newELM(svmperf_base=None, loss='01', C=1): """ Explicit Loss Minimization (ELM) quantifiers. Quantifiers based on ELM represent a family of methods based on structured output learning; @@ -1887,7 +1925,7 @@ [docs] -def newSVMQ(svmperf_base=None, C=1): +def newSVMQ(svmperf_base=None, C=1): """ SVM(Q) is an Explicit Loss Minimization (ELM) quantifier set to optimize for the `Q` loss combining a classification-oriented loss and a quantification-oriented loss, as proposed by @@ -1914,7 +1952,9 @@ -def newSVMKLD(svmperf_base=None, C=1): + +[docs] +def newSVMKLD(svmperf_base=None, C=1): """ SVM(KLD) is an Explicit Loss Minimization (ELM) quantifier set to optimize for the Kullback-Leibler Divergence as proposed by `Esuli et al. 2015 <https://dl.acm.org/doi/abs/10.1145/2700406>`_. @@ -1936,14 +1976,15 @@ :return: returns an instance of CC set to work with SVMperf (with loss and C set properly) as the underlying classifier """ - return newELM(svmperf_base, loss='kld', C=C) + return newELM(svmperf_base, loss='kld', C=C) - -[docs] -def newSVMKLD(svmperf_base=None, C=1): + + +[docs] +def newSVMNKLD(svmperf_base=None, C=1): """ - SVM(KLD) is an Explicit Loss Minimization (ELM) quantifier set to optimize for the Kullback-Leibler Divergence + SVM(NKLD) is an Explicit Loss Minimization (ELM) quantifier set to optimize for the Kullback-Leibler Divergence normalized via the logistic function, as proposed by `Esuli et al. 2015 <https://dl.acm.org/doi/abs/10.1145/2700406>`_. Equivalent to: @@ -1970,7 +2011,7 @@ [docs] -def newSVMAE(svmperf_base=None, C=1): +def newSVMAE(svmperf_base=None, C=1): """ SVM(KLD) is an Explicit Loss Minimization (ELM) quantifier set to optimize for the Absolute Error as first used by `Moreo and Sebastiani, 2021 <https://arxiv.org/abs/2011.02552>`_. @@ -1998,7 +2039,7 @@ [docs] -def newSVMRAE(svmperf_base=None, C=1): +def newSVMRAE(svmperf_base=None, C=1): """ SVM(KLD) is an Explicit Loss Minimization (ELM) quantifier set to optimize for the Relative Absolute Error as first used by `Moreo and Sebastiani, 2021 <https://arxiv.org/abs/2011.02552>`_. @@ -2026,7 +2067,7 @@ [docs] -class OneVsAllAggregative(OneVsAllGeneric, AggregativeQuantifier): +class OneVsAllAggregative(OneVsAllGeneric, AggregativeQuantifier): """ Allows any binary quantifier to perform quantification on single-label datasets. The method maintains one binary quantifier for each class, and then l1-normalizes the outputs so that the @@ -2042,7 +2083,7 @@ is removed and no longer available at predict time. """ - def __init__(self, binary_quantifier=None, n_jobs=None, parallel_backend='multiprocessing'): + def __init__(self, binary_quantifier=None, n_jobs=None, parallel_backend='multiprocessing'): if binary_quantifier is None: binary_quantifier = PACC() assert isinstance(binary_quantifier, BaseQuantifier), \ @@ -2055,7 +2096,7 @@ [docs] - def classify(self, X): + def classify(self, X): """ If the base quantifier is not probabilistic, returns a matrix of shape `(n,m,)` with `n` the number of instances and `m` the number of classes. The entry `(i,j)` is a binary value indicating whether instance @@ -2079,34 +2120,34 @@ [docs] - def aggregate(self, classif_predictions): + def aggregate(self, classif_predictions): prevalences = self._parallel(self._delayed_binary_aggregate, classif_predictions) return F.normalize_prevalence(prevalences) [docs] - def aggregation_fit(self, classif_predictions, labels): - self._parallel(self._delayed_binary_aggregate_fit(c, classif_predictions, labels)) + def aggregation_fit(self, classif_predictions, labels): + self._parallel(self._delayed_binary_aggregate_fit, classif_predictions, labels) return self - def _delayed_binary_classification(self, c, X): + def _delayed_binary_classification(self, c, X): return self.dict_binary_quantifiers[c].classify(X) - def _delayed_binary_aggregate(self, c, classif_predictions): + def _delayed_binary_aggregate(self, c, classif_predictions): # the estimation for the positive class prevalence return self.dict_binary_quantifiers[c].aggregate(classif_predictions[:, c])[1] - def _delayed_binary_aggregate_fit(self, c, classif_predictions, labels): + def _delayed_binary_aggregate_fit(self, c, classif_predictions, labels): # trains the aggregation function of the cth quantifier - return self.dict_binary_quantifiers[c].aggregate_fit(classif_predictions[:, c], labels) + return self.dict_binary_quantifiers[c].aggregation_fit(classif_predictions[:, c], labels == c) [docs] -class AggregativeMedianEstimator(BinaryQuantifier): +class AggregativeMedianEstimator(BinaryQuantifier): """ This method is a meta-quantifier that returns, as the estimated class prevalence values, the median of the estimation returned by differently (hyper)parameterized base quantifiers. @@ -2119,7 +2160,7 @@ :param n_jobs: number of parallel workers """ - def __init__(self, base_quantifier: AggregativeQuantifier, param_grid: dict, random_state=None, n_jobs=None): + def __init__(self, base_quantifier: AggregativeQuantifier, param_grid: dict, random_state=None, n_jobs=None): self.base_quantifier = base_quantifier self.param_grid = param_grid self.random_state = random_state @@ -2127,17 +2168,17 @@ [docs] - def get_params(self, deep=True): + def get_params(self, deep=True): return self.base_quantifier.get_params(deep) [docs] - def set_params(self, **params): + def set_params(self, **params): self.base_quantifier.set_params(**params) - def _delayed_fit(self, args): + def _delayed_fit(self, args): with qp.util.temp_seed(self.random_state): params, X, y = args model = deepcopy(self.base_quantifier) @@ -2145,7 +2186,7 @@ model.fit(X, y) return model - def _delayed_fit_classifier(self, args): + def _delayed_fit_classifier(self, args): with qp.util.temp_seed(self.random_state): cls_params, X, y = args model = deepcopy(self.base_quantifier) @@ -2153,7 +2194,7 @@ predictions, labels = model.classifier_fit_predict(X, y) return (model, predictions, labels) - def _delayed_fit_aggregation(self, args): + def _delayed_fit_aggregation(self, args): with qp.util.temp_seed(self.random_state): ((model, predictions, y), q_params) = args model = deepcopy(model) @@ -2163,8 +2204,8 @@ [docs] - def fit(self, X, y): - import itertools + def fit(self, X, y): + import itertools self._check_binary(y, self.__class__.__name__) @@ -2177,8 +2218,7 @@ ((params, X, y) for params in cls_configs), seed=qp.environ.get('_R_SEED', None), n_jobs=self.n_jobs, - asarray=False, - backend='threading' + asarray=False ) else: model = self.base_quantifier @@ -2190,8 +2230,7 @@ self._delayed_fit_aggregation, itertools.product(models_preds, q_configs), seed=qp.environ.get('_R_SEED', None), - n_jobs=self.n_jobs, - backend='threading' + n_jobs=self.n_jobs ) else: configs = qp.model_selection.expand_grid(self.param_grid) @@ -2199,25 +2238,23 @@ self._delayed_fit, ((params, X, y) for params in configs), seed=qp.environ.get('_R_SEED', None), - n_jobs=self.n_jobs, - backend='threading' + n_jobs=self.n_jobs ) return self - def _delayed_predict(self, args): + def _delayed_predict(self, args): model, instances = args return model.predict(instances) [docs] - def predict(self, instances): + def predict(self, instances): prev_preds = qp.util.parallel( self._delayed_predict, ((model, instances) for model in self.models), seed=qp.environ.get('_R_SEED', None), - n_jobs=self.n_jobs, - backend='threading' + n_jobs=self.n_jobs ) return np.median(prev_preds, axis=0) @@ -2228,7 +2265,7 @@ # imports # --------------------------------------------------------------- -from . import _threshold_optim +from . import _threshold_optim T50 = _threshold_optim.T50 MAX = _threshold_optim.MAX @@ -2236,7 +2273,7 @@ MS = _threshold_optim.MS MS2 = _threshold_optim.MS2 -from . import _kdey +from . import _kdey KDEyML = _kdey.KDEyML KDEyHD = _kdey.KDEyHD diff --git a/docs/build/html/_modules/quapy/method/base.html b/docs/build/html/_modules/quapy/method/base.html index e649c5b..5519615 100644 --- a/docs/build/html/_modules/quapy/method/base.html +++ b/docs/build/html/_modules/quapy/method/base.html @@ -1,95 +1,392 @@ - - - - - - quapy.method.base — QuaPy: A Python-based open-source framework for quantification 0.2.1 documentation - - + + + + + + + + quapy.method.base — QuaPy: A Python-based open-source framework for quantification 0.2.1 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Skip to main content - - - - - - - - - + + + + Back to top + + + + + + + + + + Ctrl+K + + - - - - - - - - - - QuaPy: A Python-based open-source framework for quantification - - - - - - - + + - - -
+ Source code for quapy.classification.methods -import numpy as np -from sklearn.base import BaseEstimator -from sklearn.decomposition import TruncatedSVD -from sklearn.linear_model import LogisticRegression +import numpy as np +from sklearn.base import BaseEstimator +from sklearn.decomposition import TruncatedSVD +from sklearn.linear_model import LogisticRegression [docs] -class LowRankLogisticRegression(BaseEstimator): +class LowRankLogisticRegression(BaseEstimator): """ An example of a classification method (i.e., an object that implements `fit`, `predict`, and `predict_proba`) that also generates embedded inputs (i.e., that implements `transform`), as those required for @@ -96,13 +392,13 @@ `Logistic Regression <https://scikit-learn.org/stable/modules/generated/sklearn.linear_model.LogisticRegression.html>`__ classifier """ - def __init__(self, n_components=100, **kwargs): + def __init__(self, n_components=100, **kwargs): self.n_components = n_components self.classifier = LogisticRegression(**kwargs) [docs] - def get_params(self): + def get_params(self): """ Get hyper-parameters for this estimator. @@ -115,7 +411,7 @@ [docs] - def set_params(self, **params): + def set_params(self, **params): """ Set the parameters of this estimator. @@ -132,7 +428,7 @@ [docs] - def fit(self, X, y): + def fit(self, X, y): """ Fit the model according to the given training data. The fit consists of fitting `TruncatedSVD` and then `LogisticRegression` on the low-rank representation. @@ -153,7 +449,7 @@ [docs] - def predict(self, X): + def predict(self, X): """ Predicts labels for the instances `X` embedded into the low-rank space. @@ -167,7 +463,7 @@ [docs] - def predict_proba(self, X): + def predict_proba(self, X): """ Predicts posterior probabilities for the instances `X` embedded into the low-rank space. @@ -180,7 +476,7 @@ [docs] - def transform(self, X): + def transform(self, X): """ Returns the low-rank approximation of `X` with `n_components` dimensions, or `X` unaltered if `n_components` >= `X.shape[1]`. @@ -197,7 +493,7 @@ [docs] -class MockClassifierFromPosteriors(BaseEstimator): +class MockClassifierFromPosteriors(BaseEstimator): """ Mock classifier that bypasses classifier training when the input instances are already posterior probabilities produced by a pretrained probabilistic @@ -208,50 +504,94 @@ [docs] - def fit(self, X, y): + def fit(self, X, y): self.classes_ = np.sort(np.unique(y)) return self [docs] - def predict(self, X): + def predict(self, X): return np.argmax(X, axis=1) [docs] - def predict_proba(self, X): + def predict_proba(self, X): return X - + + + + + + + + + + + + + - + + + + - Built with Sphinx using a - theme - provided by Read the Docs. - + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/build/html/_modules/quapy/classification/neural.html b/docs/build/html/_modules/quapy/classification/neural.html index e5788fa..871d391 100644 --- a/docs/build/html/_modules/quapy/classification/neural.html +++ b/docs/build/html/_modules/quapy/classification/neural.html @@ -29,7 +29,7 @@ - + @@ -370,26 +370,27 @@ Source code for quapy.classification.neural -import os -from abc import ABCMeta, abstractmethod -from pathlib import Path +import logging +import os +from abc import ABCMeta, abstractmethod +from pathlib import Path -import numpy as np -import torch -import torch.nn as nn -import torch.nn.functional as F -from sklearn.metrics import accuracy_score, f1_score -from torch.nn.utils.rnn import pad_sequence -from tqdm import tqdm +import numpy as np +import torch +import torch.nn as nn +import torch.nn.functional as F +from sklearn.metrics import accuracy_score, f1_score +from torch.nn.utils.rnn import pad_sequence +from tqdm import tqdm -import quapy as qp -from quapy.data import LabelledCollection -from quapy.util import EarlyStop +import quapy as qp +from quapy.data import LabelledCollection +from quapy.util import EarlyStop [docs] -class NeuralClassifierTrainer: +class NeuralClassifierTrainer: """ Trains a neural network for text classification. @@ -407,7 +408,7 @@ according to the evaluation in the held-out validation split (default '../checkpoint/classifier_net.dat') """ - def __init__(self, + def __init__(self, net: 'TextClassifierNet', lr=1e-3, weight_decay=0, @@ -416,7 +417,7 @@ batch_size=64, batch_size_test=512, padding_length=300, - device='cuda', + device='cpu', checkpointpath='../checkpoint/classifier_net.dat'): super().__init__() @@ -437,12 +438,12 @@ self.learner_hyperparams = self.net.get_params() self.checkpointpath = checkpointpath - print(f'[NeuralNetwork running on {device}]') + logging.getLogger(__name__).info(f'NeuralNetwork running on {device}') os.makedirs(Path(checkpointpath).parent, exist_ok=True) [docs] - def reset_net_params(self, vocab_size, n_classes): + def reset_net_params(self, vocab_size, n_classes): """Reinitialize the network parameters :param vocab_size: the size of the vocabulary @@ -455,7 +456,7 @@ [docs] - def get_params(self): + def get_params(self): """Get hyper-parameters for this estimator :return: a dictionary with parameter names mapped to their values @@ -465,7 +466,7 @@ [docs] - def set_params(self, **params): + def set_params(self, **params): """Set the parameters of this trainer and the learner it is training. In this current version, parameter names for the trainer and learner should be disjoint. @@ -491,14 +492,14 @@ @property - def device(self): + def device(self): """ Gets the device in which the network is allocated :return: device """ return next(self.net.parameters()).device - def _train_epoch(self, data, status, pbar, epoch): + def _train_epoch(self, data, status, pbar, epoch): self.net.train() criterion = torch.nn.CrossEntropyLoss() losses, predictions, true_labels = [], [], [] @@ -518,7 +519,7 @@ status["f1"] = f1_score(true_labels, predictions, average='macro') self.__update_progress_bar(pbar, epoch) - def _test_epoch(self, data, status, pbar, epoch): + def _test_epoch(self, data, status, pbar, epoch): self.net.eval() criterion = torch.nn.CrossEntropyLoss() losses, predictions, true_labels = [], [], [] @@ -536,7 +537,7 @@ status["f1"] = f1_score(true_labels, predictions, average='macro') self.__update_progress_bar(pbar, epoch) - def __update_progress_bar(self, pbar, epoch): + def __update_progress_bar(self, pbar, epoch): pbar.set_description(f'[{self.net.__class__.__name__}] training epoch={epoch} ' f'tr-loss={self.status["tr"]["loss"]:.5f} ' f'tr-acc={100 * self.status["tr"]["acc"]:.2f}% ' @@ -548,7 +549,7 @@ [docs] - def fit(self, instances, labels, val_split=0.3): + def fit(self, instances, labels, val_split=0.3): """ Fits the model according to the given training data. @@ -583,21 +584,22 @@ if self.early_stop.IMPROVED: torch.save(self.net.state_dict(), checkpoint) elif self.early_stop.STOP: - print(f'training ended by patience exhasted; loading best model parameters in {checkpoint} ' - f'for epoch {self.early_stop.best_epoch}') + logging.getLogger(__name__).info( + f'training ended by patience exhausted; loading best model parameters in {checkpoint} ' + f'for epoch {self.early_stop.best_epoch}') self.net.load_state_dict(torch.load(checkpoint)) break - print('performing one training pass over the validation set...') + logging.getLogger(__name__).info('performing one training pass over the validation set...') self._train_epoch(valid_generator, self.status['tr'], pbar, epoch=0) - print('[done]') + logging.getLogger(__name__).info('done') return self [docs] - def predict(self, instances): + def predict(self, instances): """ Predicts labels for the instances @@ -610,7 +612,7 @@ [docs] - def predict_proba(self, instances): + def predict_proba(self, instances): """ Predicts posterior probabilities for the instances @@ -629,7 +631,7 @@ [docs] - def transform(self, instances): + def transform(self, instances): """ Returns the embeddings of the instances @@ -651,7 +653,7 @@ [docs] -class TorchDataset(torch.utils.data.Dataset): +class TorchDataset(torch.utils.data.Dataset): """ Transforms labelled instances into a Torch's :class:`torch.utils.data.DataLoader` object @@ -659,19 +661,19 @@ :param labels: array-like of shape `(n_samples, n_classes)` with the class labels """ - def __init__(self, instances, labels=None): + def __init__(self, instances, labels=None): self.instances = instances self.labels = labels - def __len__(self): + def __len__(self): return len(self.instances) - def __getitem__(self, index): + def __getitem__(self, index): return {'doc': self.instances[index], 'label': self.labels[index] if self.labels is not None else None} [docs] - def asDataloader(self, batch_size, shuffle, pad_length, device): + def asDataloader(self, batch_size, shuffle, pad_length, device): """ Converts the labelled collection into a Torch DataLoader with dynamic padding for the batch @@ -684,7 +686,7 @@ :param device: whether to allocate tensors in cpu or in cuda :return: a :class:`torch.utils.data.DataLoader` object """ - def collate(batch): + def collate(batch): data = [torch.LongTensor(item['doc'][:pad_length]) for item in batch] data = pad_sequence(data, batch_first=True, padding_value=qp.environ['PAD_INDEX']).to(device) targets = [item['label'] for item in batch] @@ -702,7 +704,7 @@ [docs] -class TextClassifierNet(torch.nn.Module, metaclass=ABCMeta): +class TextClassifierNet(torch.nn.Module, metaclass=ABCMeta): """ Abstract Text classifier (`torch.nn.Module`) """ @@ -710,7 +712,7 @@ [docs] @abstractmethod - def document_embedding(self, x): + def document_embedding(self, x): """Embeds documents (i.e., performs the forward pass up to the next-to-last layer). @@ -725,7 +727,7 @@ [docs] - def forward(self, x): + def forward(self, x): """Performs the forward pass. :param x: a batch of instances, typically generated by a torch's `DataLoader` @@ -739,7 +741,7 @@ [docs] - def dimensions(self): + def dimensions(self): """Gets the number of dimensions of the embedding space :return: integer @@ -749,7 +751,7 @@ [docs] - def predict_proba(self, x): + def predict_proba(self, x): """ Predicts posterior probabilities for the instances in `x` @@ -764,7 +766,7 @@ [docs] - def xavier_uniform(self): + def xavier_uniform(self): """ Performs Xavier initialization of the network parameters """ @@ -776,7 +778,7 @@ [docs] @abstractmethod - def get_params(self): + def get_params(self): """ Get hyper-parameters for this estimator @@ -786,7 +788,7 @@ @property - def vocabulary_size(self): + def vocabulary_size(self): """ Return the size of the vocabulary @@ -798,7 +800,7 @@ [docs] -class LSTMnet(TextClassifierNet): +class LSTMnet(TextClassifierNet): """ An implementation of :class:`quapy.classification.neural.TextClassifierNet` based on Long Short Term Memory networks. @@ -812,7 +814,7 @@ :param drop_p: drop probability for dropout (default 0.5) """ - def __init__(self, vocabulary_size, n_classes, embedding_size=100, hidden_size=256, repr_size=100, lstm_class_nlayers=1, + def __init__(self, vocabulary_size, n_classes, embedding_size=100, hidden_size=256, repr_size=100, lstm_class_nlayers=1, drop_p=0.5): super().__init__() @@ -834,7 +836,7 @@ self.doc_embedder = torch.nn.Linear(hidden_size, self.dim) self.output = torch.nn.Linear(self.dim, n_classes) - def __init_hidden(self, set_size): + def __init_hidden(self, set_size): opt = self.hyperparams var_hidden = torch.zeros(opt['lstm_class_nlayers'], set_size, opt['hidden_size']) var_cell = torch.zeros(opt['lstm_class_nlayers'], set_size, opt['hidden_size']) @@ -844,7 +846,7 @@ [docs] - def document_embedding(self, x): + def document_embedding(self, x): """Embeds documents (i.e., performs the forward pass up to the next-to-last layer). @@ -863,7 +865,7 @@ [docs] - def get_params(self): + def get_params(self): """ Get hyper-parameters for this estimator @@ -873,7 +875,7 @@ @property - def vocabulary_size(self): + def vocabulary_size(self): """ Return the size of the vocabulary @@ -885,7 +887,7 @@ [docs] -class CNNnet(TextClassifierNet): +class CNNnet(TextClassifierNet): """ An implementation of :class:`quapy.classification.neural.TextClassifierNet` based on Convolutional Neural Networks. @@ -902,7 +904,7 @@ :param drop_p: drop probability for dropout (default 0.5) """ - def __init__(self, vocabulary_size, n_classes, embedding_size=100, hidden_size=256, repr_size=100, + def __init__(self, vocabulary_size, n_classes, embedding_size=100, hidden_size=256, repr_size=100, kernel_heights=[3, 5, 7], stride=1, padding=0, drop_p=0.5): super(CNNnet, self).__init__() @@ -927,7 +929,7 @@ self.doc_embedder = torch.nn.Linear(len(kernel_heights) * hidden_size, self.dim) self.output = nn.Linear(self.dim, n_classes) - def __conv_block(self, input, conv_layer): + def __conv_block(self, input, conv_layer): conv_out = conv_layer(input) # conv_out.size() = (batch_size, out_channels, dim, 1) activation = F.relu(conv_out.squeeze(3)) # activation.size() = (batch_size, out_channels, dim1) max_out = F.max_pool1d(activation, activation.size()[2]).squeeze(2) # maxpool_out.size() = (batch_size, out_channels) @@ -935,7 +937,7 @@ [docs] - def document_embedding(self, input): + def document_embedding(self, input): """Embeds documents (i.e., performs the forward pass up to the next-to-last layer). @@ -960,7 +962,7 @@ [docs] - def get_params(self): + def get_params(self): """ Get hyper-parameters for this estimator @@ -970,7 +972,7 @@ @property - def vocabulary_size(self): + def vocabulary_size(self): """ Return the size of the vocabulary diff --git a/docs/build/html/_modules/quapy/classification/svmperf.html b/docs/build/html/_modules/quapy/classification/svmperf.html index 9b60c1a..e855546 100644 --- a/docs/build/html/_modules/quapy/classification/svmperf.html +++ b/docs/build/html/_modules/quapy/classification/svmperf.html @@ -1,94 +1,391 @@ - - - - - - quapy.classification.svmperf — QuaPy: A Python-based open-source framework for quantification 0.2.1 documentation - - + + + + + + + + quapy.classification.svmperf — QuaPy: A Python-based open-source framework for quantification 0.2.1 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Skip to main content - - - - - - - - - + + + + Back to top + + + + + + + + + + Ctrl+K + + - - - - - - - - - - QuaPy: A Python-based open-source framework for quantification - - - - - - - + + - - -Quickstart - - -Manuals - - -API - - - - + + + + + + + + + + + - - - QuaPy: A Python-based open-source framework for quantification - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Home + + + + + + + Manuals + + + + + + + API + + - - - - - - Module code - quapy.classification.svmperf - - - + + + + + + + + + + + + + Search + Ctrl+K + + + + + + + + + + + + + + System Settings + Light + Dark + + + + + + + + + + + + + + GitHub + + + + + + + + + + + + + Search + Ctrl+K + + + + + - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Home + + + + + + + Manuals + + + + + + + API + + + + + + + + + + + + + + + + + + + + + + + System Settings + Light + Dark + + + + + + + + + + + + + + GitHub + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Module code + + quapy.classification.svmperf + + + + + + + + + + + + + + + + Source code for quapy.classification.svmperf -import random -import shutil -import subprocess -import tempfile -from os import remove, makedirs -from os.path import join, exists -from subprocess import PIPE, STDOUT -import numpy as np -from sklearn.base import BaseEstimator, ClassifierMixin -from sklearn.datasets import dump_svmlight_file +import logging +import random +import shutil +import subprocess +import tempfile +from os import remove, makedirs +from os.path import join, exists +from subprocess import PIPE, STDOUT +import numpy as np +from sklearn.base import BaseEstimator, ClassifierMixin +from sklearn.datasets import dump_svmlight_file [docs] -class SVMperf(BaseEstimator, ClassifierMixin): +class SVMperf(BaseEstimator, ClassifierMixin): """A wrapper for the `SVM-perf package <https://www.cs.cornell.edu/people/tj/svm_light/svm_perf.html>`__ by Thorsten Joachims. When using losses for quantification, the source code has to be patched. See the `installation documentation <https://hlt-isti.github.io/QuaPy/build/html/Installation.html#svm-perf-with-quantification-oriented-losses>`__ @@ -110,7 +407,7 @@ # losses with their respective codes in svm_perf implementation valid_losses = {'01':0, 'f1':1, 'kld':12, 'nkld':13, 'q':22, 'qacc':23, 'qf1':24, 'qgm':25, 'mae':26, 'mrae':27} - def __init__(self, svmperf_base, C=0.01, verbose=False, loss='01', host_folder=None): + def __init__(self, svmperf_base, C=0.01, verbose=False, loss='01', host_folder=None): assert exists(svmperf_base), \ (f'path {svmperf_base} does not seem to point to a valid path;' f'did you install svm-perf? ' @@ -123,7 +420,7 @@ [docs] - def fit(self, X, y): + def fit(self, X, y): """ Trains the SVM for the multivariate performance loss @@ -146,8 +443,7 @@ # this would allow to run parallel instances of predict random_code = 'svmperfprocess'+'-'.join(str(local_random.randint(0, 1000000)) for _ in range(5)) if self.host_folder is None: - # tmp dir are removed after the fit terminates in multiprocessing... - self.tmpdir = tempfile.TemporaryDirectory(suffix=random_code).name + self.tmpdir = join(tempfile.gettempdir(), random_code) else: self.tmpdir = join(self.host_folder, '.' + random_code) makedirs(self.tmpdir, exist_ok=True) @@ -159,21 +455,21 @@ cmd = ' '.join([self.svmperf_learn, self.c_cmd, self.loss_cmd, traindat, self.model]) if self.verbose: - print('[Running]', cmd) - p = subprocess.run(cmd.split(), stdout=PIPE, stderr=STDOUT) + logging.getLogger(__name__).info(f'[Running] {cmd}') + p = subprocess.run(cmd.split(), stdout=PIPE, stderr=PIPE) if not exists(self.model): - print(p.stderr.decode('utf-8')) + logging.getLogger(__name__).error(p.stderr.decode('utf-8')) remove(traindat) if self.verbose: - print(p.stdout.decode('utf-8')) + logging.getLogger(__name__).info(p.stdout.decode('utf-8')) return self [docs] - def predict(self, X): + def predict(self, X): """ Predicts labels for the instances `X` @@ -188,7 +484,7 @@ [docs] - def decision_function(self, X, y=None): + def decision_function(self, X, y=None): """ Evaluate the decision function for the samples in `X`. @@ -211,11 +507,11 @@ cmd = ' '.join([self.svmperf_classify, testdat, self.model, predictions_path]) if self.verbose: - print('[Running]', cmd) + logging.getLogger(__name__).info(f'[Running] {cmd}') p = subprocess.run(cmd.split(), stdout=PIPE, stderr=STDOUT) if self.verbose: - print(p.stdout.decode('utf-8')) + logging.getLogger(__name__).info(p.stdout.decode('utf-8')) scores = np.loadtxt(predictions_path) remove(testdat) @@ -224,38 +520,82 @@ return scores - def __del__(self): + def __del__(self): if hasattr(self, 'tmpdir'): shutil.rmtree(self.tmpdir, ignore_errors=True) - + + + + + + + + + + + + + - + + + + - Built with Sphinx using a - theme - provided by Read the Docs. - + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/build/html/_modules/quapy/data/base.html b/docs/build/html/_modules/quapy/data/base.html index 27f34de..87203ec 100644 --- a/docs/build/html/_modules/quapy/data/base.html +++ b/docs/build/html/_modules/quapy/data/base.html @@ -1,96 +1,392 @@ - - - - - - quapy.data.base — QuaPy: A Python-based open-source framework for quantification 0.2.1 documentation - - + + + + + + + + quapy.data.base — QuaPy: A Python-based open-source framework for quantification 0.2.1 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Skip to main content - - - - - - - - - + + + + Back to top + + + + + + + + + + Ctrl+K + + - - - - - - - - - - QuaPy: A Python-based open-source framework for quantification - - - - - - - + + - - -Quickstart - - -Manuals - - -API - - - - + + + + + + + + + + + - - - QuaPy: A Python-based open-source framework for quantification - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Home + + + + + + + Manuals + + + + + + + API + + - - - - - - Module code - quapy.data.base - - - - - - - - Source code for quapy.data.base -import itertools -from functools import cached_property -from typing import Iterable + + + + + + + + + -import numpy as np -from scipy.sparse import issparse -from scipy.sparse import vstack -from sklearn.model_selection import train_test_split, RepeatedStratifiedKFold -from numpy.random import RandomState -from quapy.functional import strprev -from quapy.util import temp_seed -import quapy.functional as F + + + Search + Ctrl+K + + + + + + + + + + + + + + System Settings + Light + Dark + + + + + + + + + + + + + + GitHub + + + + + + + + + + + + + Search + Ctrl+K + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Home + + + + + + + Manuals + + + + + + + API + + + + + + + + + + + + + + + + + + + + + + + System Settings + Light + Dark + + + + + + + + + + + + + + GitHub + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Module code + + quapy.data.base + + + + + + + + + + + + + + + + + Source code for quapy.data.base +import itertools +from functools import cached_property +from typing import Iterable + +import numpy as np +from scipy.sparse import issparse +from scipy.sparse import vstack +from sklearn.model_selection import train_test_split, RepeatedStratifiedKFold +from numpy.random import RandomState +from quapy.functional import strprev +from quapy.util import temp_seed +import quapy.functional as F [docs] -class LabelledCollection: +class LabelledCollection: """ A LabelledCollection is a set of objects each with a label attached to each of them. This class implements several sampling routines and other utilities. @@ -102,7 +398,7 @@ (i.e., a prevalence of 0) """ - def __init__(self, instances, labels, classes=None): + def __init__(self, instances, labels, classes=None): if issparse(instances): self.instances = instances elif isinstance(instances, list) and len(instances) > 0 and isinstance(instances[0], str): @@ -121,7 +417,7 @@ self._index = None @property - def index(self): + def index(self): if not hasattr(self, '_index') or self._index is None: self._index = {class_: np.arange(len(self))[self.labels == class_] for class_ in self.classes_} return self._index @@ -129,7 +425,7 @@ [docs] @classmethod - def load(cls, path: str, loader_func: callable, classes=None, **loader_kwargs): + def load(cls, path: str, loader_func: callable, classes=None, **loader_kwargs): """ Loads a labelled set of data and convert it into a :class:`LabelledCollection` instance. The function in charge of reading the instances must be specified. This function can be a custom one, or any of the reading functions @@ -146,7 +442,7 @@ return LabelledCollection(*loader_func(path, **loader_kwargs), classes) - def __len__(self): + def __len__(self): """ Returns the length of this collection (number of labelled instances) @@ -156,7 +452,7 @@ [docs] - def prevalence(self): + def prevalence(self): """ Returns the prevalence, or relative frequency, of the classes in the codeframe. @@ -168,7 +464,7 @@ [docs] - def counts(self): + def counts(self): """ Returns the number of instances for each of the classes in the codeframe. @@ -179,7 +475,7 @@ @property - def n_classes(self): + def n_classes(self): """ The number of classes @@ -188,7 +484,7 @@ return len(self.classes_) @property - def n_instances(self): + def n_instances(self): """ The number of instances @@ -197,7 +493,7 @@ return len(self.labels) @property - def binary(self): + def binary(self): """ Returns True if the number of classes is 2 @@ -207,7 +503,7 @@ [docs] - def sampling_index(self, size, *prevs, shuffle=True, random_state=None): + def sampling_index(self, size, *prevs, shuffle=True, random_state=None): """ Returns an index to be used to extract a random sample of desired size and desired prevalence values. If the prevalence values are not specified, then returns the index of a uniform sampling. @@ -270,7 +566,7 @@ [docs] - def uniform_sampling_index(self, size, random_state=None): + def uniform_sampling_index(self, size, random_state=None): """ Returns an index to be used to extract a uniform sample of desired size. The sampling is drawn with replacement. @@ -288,7 +584,7 @@ [docs] - def sampling(self, size, *prevs, shuffle=True, random_state=None): + def sampling(self, size, *prevs, shuffle=True, random_state=None): """ Return a random sample (an instance of :class:`LabelledCollection`) of desired size and desired prevalence values. For each class, the sampling is drawn with replacement. @@ -308,7 +604,7 @@ [docs] - def uniform_sampling(self, size, random_state=None): + def uniform_sampling(self, size, random_state=None): """ Returns a uniform sample (an instance of :class:`LabelledCollection`) of desired size. The sampling is drawn with replacement. @@ -323,7 +619,7 @@ [docs] - def sampling_from_index(self, index): + def sampling_from_index(self, index): """ Returns an instance of :class:`LabelledCollection` whose elements are sampled from this collection using the index. @@ -338,7 +634,7 @@ [docs] - def split_stratified(self, train_prop=0.6, random_state=None): + def split_stratified(self, train_prop=0.6, random_state=None): """ Returns two instances of :class:`LabelledCollection` split with stratification from this collection, at desired proportion. @@ -360,7 +656,7 @@ [docs] - def split_random(self, train_prop=0.6, random_state=None): + def split_random(self, train_prop=0.6, random_state=None): """ Returns two instances of :class:`LabelledCollection` split randomly from this collection, at desired proportion. @@ -387,7 +683,7 @@ return training, test - def __add__(self, other): + def __add__(self, other): """ Returns a new :class:`LabelledCollection` as the union of this collection with another collection. Both labelled collections must have the same classes. @@ -403,7 +699,7 @@ [docs] @classmethod - def join(cls, *args: Iterable['LabelledCollection']): + def join(cls, *args: Iterable['LabelledCollection']): """ Returns a new :class:`LabelledCollection` as the union of the collections given in input. @@ -439,12 +735,14 @@ else: raise NotImplementedError('unsupported operation for collection types') labels = np.concatenate([lc.labels for lc in args]) - classes = np.unique(labels).sort() + # union of each collection's own classes_, so a class declared but absent from + # this particular join (e.g. an empty fold) is preserved at zero prevalence + classes = np.unique(np.concatenate([lc.classes_ for lc in args])) return LabelledCollection(instances, labels, classes=classes) @property - def classes(self): + def classes(self): """ Gets an array-like with the classes used in this collection @@ -453,7 +751,7 @@ return self.classes_ @property - def Xy(self): + def Xy(self): """ Gets the instances and labels. This is useful when working with `sklearn` estimators, e.g.: @@ -464,7 +762,7 @@ return self.instances, self.labels @property - def Xp(self): + def Xp(self): """ Gets the instances and the true prevalence. This is useful when implementing evaluation protocols from a :class:`LabelledCollection` object. @@ -474,7 +772,7 @@ return self.instances, self.prevalence() @property - def X(self): + def X(self): """ An alias to self.instances @@ -483,7 +781,7 @@ return self.instances @property - def y(self): + def y(self): """ An alias to self.labels @@ -492,7 +790,7 @@ return self.labels @property - def p(self): + def p(self): """ An alias to self.prevalence() @@ -503,7 +801,7 @@ [docs] - def stats(self, show=True): + def stats(self, show=True): """ Returns (and eventually prints) a dictionary with some stats of this collection. E.g.,: @@ -538,7 +836,7 @@ [docs] - def kFCV(self, nfolds=5, nrepeats=1, random_state=None): + def kFCV(self, nfolds=5, nrepeats=1, random_state=None): """ Generator of stratified folds to be used in k-fold cross validation. @@ -554,7 +852,7 @@ yield train, test - def __repr__(self): + def __repr__(self): repr=f'<{self.n_instances} instances (dtype={type(self.instances[0])}), ' repr+=f'n_classes={self.n_classes} {self.classes_}, prevalence={F.strprev(self.prevalence())}>' return repr @@ -563,7 +861,7 @@ [docs] -class Dataset: +class Dataset: """ Abstraction of training and test :class:`LabelledCollection` objects. @@ -573,7 +871,7 @@ :param name: a string representing the name of the dataset """ - def __init__(self, training: LabelledCollection, test: LabelledCollection, vocabulary: dict = None, name=''): + def __init__(self, training: LabelledCollection, test: LabelledCollection, vocabulary: dict = None, name=''): assert set(training.classes_) == set(test.classes_), 'incompatible labels in training and test collections' self.training = training self.test = test @@ -583,7 +881,7 @@ [docs] @classmethod - def SplitStratified(cls, collection: LabelledCollection, train_size=0.6): + def SplitStratified(cls, collection: LabelledCollection, train_size=0.6): """ Generates a :class:`Dataset` from a stratified split of a :class:`LabelledCollection` instance. See :meth:`LabelledCollection.split_stratified` @@ -596,7 +894,7 @@ @property - def classes_(self): + def classes_(self): """ The classes according to which the training collection is labelled @@ -605,7 +903,7 @@ return self.training.classes_ @property - def n_classes(self): + def n_classes(self): """ The number of classes according to which the training collection is labelled @@ -614,7 +912,7 @@ return self.training.n_classes @property - def binary(self): + def binary(self): """ Returns True if the training collection is labelled according to two classes @@ -625,7 +923,7 @@ [docs] @classmethod - def load(cls, train_path, test_path, loader_func: callable, classes=None, **loader_kwargs): + def load(cls, train_path, test_path, loader_func: callable, classes=None, **loader_kwargs): """ Loads a training and a test labelled set of data and convert it into a :class:`Dataset` instance. The function in charge of reading the instances must be specified. This function can be a custom one, or any of @@ -647,7 +945,7 @@ @property - def vocabulary_size(self): + def vocabulary_size(self): """ If the dataset is textual, and the vocabulary was indicated, returns the size of the vocabulary @@ -656,7 +954,7 @@ return len(self.vocabulary) @property - def train_test(self): + def train_test(self): """ Alias to `self.training` and `self.test` @@ -667,7 +965,7 @@ [docs] - def stats(self, show=True): + def stats(self, show=True): """ Returns (and eventually prints) a dictionary with some stats of this dataset. E.g.,: @@ -694,7 +992,7 @@ [docs] @classmethod - def kFCV(cls, data: LabelledCollection, nfolds=5, nrepeats=1, random_state=0): + def kFCV(cls, data: LabelledCollection, nfolds=5, nrepeats=1, random_state=0): """ Generator of stratified folds to be used in k-fold cross validation. This function is only a wrapper around :meth:`LabelledCollection.kFCV` that returns :class:`Dataset` instances made of training and test folds. @@ -711,7 +1009,7 @@ [docs] - def reduce(self, n_train=100, n_test=100, random_state=None): + def reduce(self, n_train=100, n_test=100, random_state=None): """ Reduce the number of instances in place for quick experiments. Preserves the prevalence of each set. @@ -732,36 +1030,80 @@ return self - def __repr__(self): + def __repr__(self): return f'training={self.training}; test={self.test}' - + + + + + + + + + + + + + - + + + + - Built with Sphinx using a - theme - provided by Read the Docs. - + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/build/html/_modules/quapy/data/datasets.html b/docs/build/html/_modules/quapy/data/datasets.html index 20d9588..cc6a18b 100644 --- a/docs/build/html/_modules/quapy/data/datasets.html +++ b/docs/build/html/_modules/quapy/data/datasets.html @@ -29,7 +29,7 @@ - + @@ -370,27 +370,28 @@ Source code for quapy.data.datasets -import os -from contextlib import contextmanager -import zipfile -from os.path import join -import pandas as pd -from quapy.data.base import Dataset, LabelledCollection -from quapy.data.preprocessing import text2tfidf, reduce_columns -from quapy.data.preprocessing import standardize as standardizer -from quapy.data.reader import * -from quapy.util import download_file_if_not_exists, download_file, get_quapy_home, pickled_resource -from sklearn.preprocessing import StandardScaler +import logging +import os +from contextlib import contextmanager +import zipfile +from os.path import join +import pandas as pd +from quapy.data.base import Dataset, LabelledCollection +from quapy.data.preprocessing import text2tfidf, reduce_columns +from quapy.data.preprocessing import standardize as standardizer +from quapy.data.reader import * +from quapy.util import download_file_if_not_exists, download_file, get_quapy_home, pickled_resource +from sklearn.preprocessing import StandardScaler -def _fetch_ucirepo(*args, **kwargs): +def _fetch_ucirepo(*args, **kwargs): try: - from ucimlrepo import fetch_ucirepo + from ucimlrepo import fetch_ucirepo except ImportError as exc: raise ImportError( "UCI dataset fetching requires the optional 'ucimlrepo' package. " "Install it to use fetch_UCIBinaryDataset or fetch_UCIMulticlassDataset." - ) from exc + ) from exc return fetch_ucirepo(*args, **kwargs) @@ -497,7 +498,7 @@ [docs] -def fetch_reviews(dataset_name, tfidf=False, min_df=None, data_home=None, pickle=False) -> Dataset: +def fetch_reviews(dataset_name, tfidf=False, min_df=None, data_home=None, pickle=False) -> Dataset: """ Loads a Reviews dataset as a Dataset instance, as used in `Esuli, A., Moreo, A., and Sebastiani, F. "A recurrent neural network for sentiment quantification." @@ -547,7 +548,7 @@ [docs] -def fetch_twitter(dataset_name, for_model_selection=False, min_df=None, data_home=None, pickle=False) -> Dataset: +def fetch_twitter(dataset_name, for_model_selection=False, min_df=None, data_home=None, pickle=False) -> Dataset: """ Loads a Twitter dataset as a :class:`quapy.data.base.Dataset` instance, as used in: `Gao, W., Sebastiani, F.: From classification to quantification in tweet sentiment analysis. @@ -589,8 +590,9 @@ if dataset_name in {'semeval13', 'semeval14', 'semeval15'}: trainset_name = 'semeval' testset_name = 'semeval' if for_model_selection else dataset_name - print(f"the training and development sets for datasets 'semeval13', 'semeval14', 'semeval15' are common " - f"(called 'semeval'); returning trainin-set='{trainset_name}' and test-set={testset_name}") + logging.getLogger(__name__).info( + f"the training and development sets for datasets 'semeval13', 'semeval14', 'semeval15' are common " + f"(called 'semeval'); returning trainin-set='{trainset_name}' and test-set={testset_name}") else: if dataset_name == 'semeval' and for_model_selection==False: raise ValueError('dataset "semeval" can only be used for model selection. ' @@ -624,7 +626,7 @@ [docs] -def fetch_UCIBinaryDataset(dataset_name, data_home=None, test_split=0.3, standardize=True, verbose=False) -> Dataset: +def fetch_UCIBinaryDataset(dataset_name, data_home=None, test_split=0.3, standardize=True, verbose=False) -> Dataset: """ Loads a UCI dataset as an instance of :class:`quapy.data.base.Dataset`, as used in `Pérez-Gállego, P., Quevedo, J. R., & del Coz, J. J. (2017). @@ -658,7 +660,7 @@ [docs] -def fetch_UCIBinaryLabelledCollection(dataset_name, data_home=None, standardize=True, verbose=False) -> LabelledCollection: +def fetch_UCIBinaryLabelledCollection(dataset_name, data_home=None, standardize=True, verbose=False) -> LabelledCollection: """ Loads a UCI collection as an instance of :class:`quapy.data.base.LabelledCollection`, as used in `Pérez-Gállego, P., Quevedo, J. R., & del Coz, J. J. (2017). @@ -851,7 +853,7 @@ file = join(data_home, "uci_datasets", dataset_group + ".pkl") @contextmanager - def download_tmp_file(url_group: str, filename: str): + def download_tmp_file(url_group: str, filename: str): """ Download a data file for a group of datasets temporarely. When used as a context, the file is removed once the context exits. @@ -869,7 +871,7 @@ finally: os.remove(data_path) - def download(id: int | None, group: str) -> dict: + def download(id: int | None, group: str) -> dict: """ Download the data to be pickled for a dataset group. Use the `fetch_ucirepo` api when possible. @@ -935,7 +937,7 @@ return data - def binarize_data(name, data: dict) -> LabelledCollection: + def binarize_data(name, data: dict) -> LabelledCollection: """ Filter and transform data to extract a binary dataset. @@ -980,7 +982,7 @@ [docs] -def fetch_UCIMulticlassDataset( +def fetch_UCIMulticlassDataset( dataset_name, data_home=None, min_test_split=0.3, @@ -1043,7 +1045,7 @@ [docs] -def fetch_UCIMulticlassLabelledCollection(dataset_name, data_home=None, min_class_support=100, standardize=True, verbose=False) -> LabelledCollection: +def fetch_UCIMulticlassLabelledCollection(dataset_name, data_home=None, min_class_support=100, standardize=True, verbose=False) -> LabelledCollection: """ Loads a UCI multiclass collection as an instance of :class:`quapy.data.base.LabelledCollection`. @@ -1146,7 +1148,7 @@ file = join(data_home, 'uci_multiclass', dataset_name+'.pkl') - def dummify_categorical_features(df_features, dataset_id): + def dummify_categorical_features(df_features, dataset_id): categorical_features = { 158: ["S1", "C1", "S2", "C2", "S3", "C3", "S4", "C4", "S5", "C5"], # poker_hand } @@ -1160,7 +1162,7 @@ return X - def download(id, name): + def download(id, name): df = _fetch_ucirepo(id=id) X_df = dummify_categorical_features(df.data.features, id) @@ -1173,7 +1175,7 @@ y = np.searchsorted(classes, y) return LabelledCollection(X, y) - def filter_classes(data: LabelledCollection, min_class_support): + def filter_classes(data: LabelledCollection, min_class_support): if min_class_support is None or min_class_support == 0.: return data if isinstance(min_class_support, float): @@ -1207,18 +1209,18 @@ -def _df_replace(df, col, repl={'yes': 1, 'no':0}, astype=float): +def _df_replace(df, col, repl={'yes': 1, 'no':0}, astype=float): df[col] = df[col].apply(lambda x:repl[x]).astype(astype, copy=False) -def _array_replace(arr, repl={"yes": 1, "no": 0}): +def _array_replace(arr, repl={"yes": 1, "no": 0}): for k, v in repl.items(): arr[arr == k] = v [docs] -def fetch_lequa2022(task, data_home=None): +def fetch_lequa2022(task, data_home=None): """ Loads the official datasets provided for the `LeQua 2022 <https://lequa2022.github.io/index>`_ competition. In brief, there are 4 tasks (T1A, T1B, T2A, T2B) having to do with text quantification @@ -1244,7 +1246,7 @@ that return a series of samples stored in a directory which are labelled by prevalence. """ - from quapy.data._lequa import load_raw_documents, load_vector_documents_2022, SamplesFromDir + from quapy.data._lequa import load_raw_documents, load_vector_documents_2022, SamplesFromDir assert task in LEQUA2022_TASKS, \ f'Unknown task {task}. Valid ones are {LEQUA2022_TASKS}' @@ -1258,7 +1260,7 @@ lequa_dir = join(data_home, 'lequa2022') os.makedirs(lequa_dir, exist_ok=True) - def download_unzip_and_remove(unzipped_path, url): + def download_unzip_and_remove(unzipped_path, url): tmp_path = join(lequa_dir, task + '_tmp.zip') download_file_if_not_exists(url, tmp_path) with zipfile.ZipFile(tmp_path) as file: @@ -1294,7 +1296,7 @@ [docs] -def fetch_lequa2024(task, data_home=None, merge_T3=False): +def fetch_lequa2024(task, data_home=None, merge_T3=False): """ Loads the official datasets provided for the `LeQua 2024 <https://lequa2024.github.io/index>`_ competition. LeQua 2024 defines four tasks (T1, T2, T3, T4) related to the problem of quantification; @@ -1325,7 +1327,7 @@ that return a series of samples stored in a directory which are labelled by prevalence. """ - from quapy.data._lequa import load_vector_documents_2024, SamplesFromDir, LabelledCollectionsFromDir + from quapy.data._lequa import load_vector_documents_2024, SamplesFromDir, LabelledCollectionsFromDir assert task in LEQUA2024_TASKS, \ f'Unknown task {task}. Valid ones are {LEQUA2024_TASKS}' @@ -1344,7 +1346,7 @@ lequa_dir = join(data_home, 'lequa2024') os.makedirs(lequa_dir, exist_ok=True) - def download_unzip_and_remove(unzipped_path, url): + def download_unzip_and_remove(unzipped_path, url): tmp_path = join(lequa_dir, task + '_tmp.zip') download_file_if_not_exists(url, tmp_path) with zipfile.ZipFile(tmp_path) as file: @@ -1384,7 +1386,7 @@ [docs] -def fetch_IFCB(single_sample_train=True, for_model_selection=False, data_home=None): +def fetch_IFCB(single_sample_train=True, for_model_selection=False, data_home=None): """ Loads the IFCB dataset for quantification from `Zenodo <https://zenodo.org/records/10036244>`_ (for more information on this dataset, please follow the zenodo link). @@ -1409,7 +1411,7 @@ i.e., a sampling protocol that returns a series of samples labelled by prevalence. """ - from quapy.data._ifcb import IFCBTrainSamplesFromDir, IFCBTestSamples, get_sample_list, generate_modelselection_split + from quapy.data._ifcb import IFCBTrainSamplesFromDir, IFCBTestSamples, get_sample_list, generate_modelselection_split if data_home is None: data_home = get_quapy_home() @@ -1421,7 +1423,7 @@ ifcb_dir = join(data_home, 'ifcb') os.makedirs(ifcb_dir, exist_ok=True) - def download_unzip_and_remove(unzipped_path, url): + def download_unzip_and_remove(unzipped_path, url): tmp_path = join(ifcb_dir, 'ifcb_tmp.zip') download_file_if_not_exists(url, tmp_path) with zipfile.ZipFile(tmp_path) as file: @@ -1466,7 +1468,7 @@ -def _fetch_image_embedding_splits(dataset_name, embedding, data_home=None) -> tuple[LabelledCollection,LabelledCollection,LabelledCollection]: +def _fetch_image_embedding_splits(dataset_name, embedding, data_home=None) -> tuple[LabelledCollection,LabelledCollection,LabelledCollection]: """ Loads a pre-generated embedding set (train, val, or test) of an image dataset from `Zenodo <https://zenodo.org/records/21131944>`_. @@ -1496,7 +1498,7 @@ trained_network = dataset_network[dataset_name] - def download_embedding_npz(dataset_name, trained_network, embedding): + def download_embedding_npz(dataset_name, trained_network, embedding): target_file = f'{dataset_name}_{trained_network}_{embedding}.npz' URL = f'https://zenodo.org/records/21131944/files/{target_file}' os.makedirs(join(data_home, 'image'), exist_ok=True) @@ -1512,14 +1514,12 @@ val = LabelledCollection(embedding_dict['val'], labels_dict['val'], classes=train.classes) test = LabelledCollection(embedding_dict['test'], labels_dict['test'], classes=train.classes) - print(f'{len(train)} | {len(val)} | {len(test)} | {train.X.shape[1]} | {train.n_classes} | {train.n_classes}') - return train, val, test [docs] -def fetch_image_embeddings(dataset_name, embedding, heldout_only=True, data_home=None) -> Dataset: +def fetch_image_embeddings(dataset_name, embedding, heldout_only=True, data_home=None) -> Dataset: """ Loads an image dataset with pre-generated embeddings. Available datasets include: @@ -1556,14 +1556,6 @@ -if __name__ == '__main__': - #train, val, test = _fetch_image_embedding_splits(dataset_name='mnist', embedding='logits') - #print(train) - #print(val) - #print(test) - - dataset = fetch_image_embeddings(dataset_name='svhn', embedding='features', heldout_only=True) - print(dataset) diff --git a/docs/build/html/_modules/quapy/data/reader.html b/docs/build/html/_modules/quapy/data/reader.html index 827d348..617442d 100644 --- a/docs/build/html/_modules/quapy/data/reader.html +++ b/docs/build/html/_modules/quapy/data/reader.html @@ -1,87 +1,385 @@ - - - - - - quapy.data.reader — QuaPy: A Python-based open-source framework for quantification 0.2.1 documentation - - + + + + + + + + quapy.data.reader — QuaPy: A Python-based open-source framework for quantification 0.2.1 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Skip to main content - - - - - - - - - + + + + Back to top + + + + + + + + + + Ctrl+K + + - - - - - - - - - - QuaPy: A Python-based open-source framework for quantification - - - - - - - + + - - -Quickstart - - -Manuals - - -API - - - - + + + + + + + + + + + - - - QuaPy: A Python-based open-source framework for quantification - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Home + + + + + + + Manuals + + + + + + + API + + - - - - - - Module code - quapy.data.reader - - - + + + + + + + + + + + + + Search + Ctrl+K + + + + + + + + + + + + + + System Settings + Light + Dark + + + + + + + + + + + + + + GitHub + + + + + + + + + + + + + Search + Ctrl+K + + + + + - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Home + + + + + + + Manuals + + + + + + + API + + + + + + + + + + + + + + + + + + + + + + + System Settings + Light + Dark + + + + + + + + + + + + + + GitHub + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Module code + + quapy.data.reader + + + + + + + + + + + + + + + + Source code for quapy.data.reader -import numpy as np -from scipy.sparse import dok_matrix -from tqdm import tqdm +import logging + +import numpy as np +from scipy.sparse import dok_matrix +from tqdm import tqdm [docs] -def from_text(path, encoding='utf-8', verbose=1, class2int=True): +def from_text(path, encoding='utf-8', verbose=1, class2int=True): """ Reads a labelled colletion of documents. File fomart <0 or 1>\t<document>\n @@ -108,14 +406,14 @@ all_sentences.append(sentence) all_labels.append(label) except ValueError: - print(f'format error in {line}') + logging.getLogger(__name__).warning(f'format error in {line}') return all_sentences, all_labels [docs] -def from_sparse(path): +def from_sparse(path): """ Reads a labelled collection of real-valued instances expressed in sparse format File format <-1 or 0 or 1>[\s col(int):val(float)]\n @@ -124,7 +422,7 @@ :return: a `csr_matrix` containing the instances (rows), and a ndarray containing the labels """ - def split_col_val(col_val): + def split_col_val(col_val): col, val = col_val.split(':') col, val = int(col) - 1, float(val) return col, val @@ -152,7 +450,7 @@ [docs] -def from_csv(path, encoding='utf-8'): +def from_csv(path, encoding='utf-8'): """ Reads a csv file in which columns are separated by ','. File format <label>,<feat1>,<feat2>,...,<featn>\n @@ -175,7 +473,7 @@ [docs] -def reindex_labels(y): +def reindex_labels(y): """ Re-indexes a list of labels as a list of indexes, and returns the classnames corresponding to the indexes. E.g.: @@ -198,7 +496,7 @@ [docs] -def binarize(y, pos_class): +def binarize(y, pos_class): """ Binarizes a categorical array-like collection of labels towards the positive class `pos_class`. E.g.,: @@ -218,31 +516,75 @@ - + + + + + + + + + + + + + - + + + + - Built with Sphinx using a - theme - provided by Read the Docs. - + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/build/html/_modules/quapy/error.html b/docs/build/html/_modules/quapy/error.html index 73a9a53..9b93ed4 100644 --- a/docs/build/html/_modules/quapy/error.html +++ b/docs/build/html/_modules/quapy/error.html @@ -1,89 +1,385 @@ - - - - - - quapy.error — QuaPy: A Python-based open-source framework for quantification 0.2.1 documentation - - + + + + + + + + quapy.error — QuaPy: A Python-based open-source framework for quantification 0.2.1 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Skip to main content - - - - - - - - - + + + + Back to top + + + + + + + + + + Ctrl+K + + - - - - - - - - - - QuaPy: A Python-based open-source framework for quantification - - - - - - - + + - - -Quickstart - - -Manuals - - -API - - - - + + + + + + + + + + + - - - QuaPy: A Python-based open-source framework for quantification - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Home + + + + + + + Manuals + + + + + + + API + + - - - - - - Module code - quapy.error - - - + + + + + + + + + + + + + Search + Ctrl+K + + + + + + + + + + + + + + System Settings + Light + Dark + + + + + + + + + + + + + + GitHub + + + + + + + + + + + + + Search + Ctrl+K + + + + + - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Home + + + + + + + Manuals + + + + + + + API + + + + + + + + + + + + + + + + + + + + + + + System Settings + Light + Dark + + + + + + + + + + + + + + GitHub + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Module code + + quapy.error + + + + + + + + + + + + + + + + Source code for quapy.error """Implementation of error measures used for quantification""" -import numpy as np -from sklearn.metrics import f1_score -import quapy as qp +import numpy as np +from sklearn.metrics import f1_score +import quapy as qp [docs] -def from_name(err_name): +def from_name(err_name): """Gets an error function from its name. E.g., `from_name("mae")` will return function :meth:`quapy.error.mae` @@ -98,7 +394,7 @@ [docs] -def f1e(y_true, y_pred): +def f1e(y_true, y_pred): """F1 error: simply computes the error in terms of macro :math:`F_1`, i.e., :math:`1-F_1^M`, where :math:`F_1` is the harmonic mean of precision and recall, defined as :math:`\\frac{2tp}{2tp+fp+fn}`, with `tp`, `fp`, and `fn` standing @@ -116,7 +412,7 @@ [docs] -def acce(y_true, y_pred): +def acce(y_true, y_pred): """Computes the error in terms of 1-accuracy. The accuracy is computed as :math:`\\frac{tp+tn}{tp+fp+fn+tn}`, with `tp`, `fp`, `fn`, and `tn` standing for true positives, false positives, false negatives, and true negatives, @@ -132,7 +428,7 @@ [docs] -def mae(prevs_true, prevs_hat): +def mae(prevs_true, prevs_hat): """Computes the mean absolute error (see :meth:`quapy.error.ae`) across the sample pairs. :param prevs_true: array-like of shape `(n_samples, n_classes,)` with the true prevalence values @@ -146,7 +442,7 @@ [docs] -def ae(prevs_true, prevs_hat): +def ae(prevs_true, prevs_hat): """Computes the absolute error between the two prevalence vectors. Absolute error between two prevalence vectors :math:`p` and :math:`\\hat{p}` is computed as :math:`AE(p,\\hat{p})=\\frac{1}{|\\mathcal{Y}|}\\sum_{y\\in \\mathcal{Y}}|\\hat{p}(y)-p(y)|`, @@ -165,7 +461,7 @@ [docs] -def nae(prevs_true, prevs_hat): +def nae(prevs_true, prevs_hat): """Computes the normalized absolute error between the two prevalence vectors. Normalized absolute error between two prevalence vectors :math:`p` and :math:`\\hat{p}` is computed as :math:`NAE(p,\\hat{p})=\\frac{AE(p,\\hat{p})}{z_{AE}}`, @@ -185,7 +481,7 @@ [docs] -def mnae(prevs_true, prevs_hat): +def mnae(prevs_true, prevs_hat): """Computes the mean normalized absolute error (see :meth:`quapy.error.nae`) across the sample pairs. :param prevs_true: array-like of shape `(n_samples, n_classes,)` with the true prevalence values @@ -199,7 +495,7 @@ [docs] -def mse(prevs_true, prevs_hat): +def mse(prevs_true, prevs_hat): """Computes the mean squared error (see :meth:`quapy.error.se`) across the sample pairs. :param prevs_true: array-like of shape `(n_samples, n_classes,)` with the @@ -214,7 +510,7 @@ [docs] -def se(prevs_true, prevs_hat): +def se(prevs_true, prevs_hat): """Computes the squared error between the two prevalence vectors. Squared error between two prevalence vectors :math:`p` and :math:`\\hat{p}` is computed as :math:`SE(p,\\hat{p})=\\frac{1}{|\\mathcal{Y}|}\\sum_{y\\in \\mathcal{Y}}(\\hat{p}(y)-p(y))^2`, @@ -233,7 +529,7 @@ [docs] -def sre(prevs_true, prevs_hat, prevs_train, eps=0.): +def sre(prevs_true, prevs_hat, prevs_train, eps=0.): """ Computes the squared ratio error between two prevalence vectors. The squared ratio error between prevalence vectors :math:`p` and @@ -269,7 +565,7 @@ [docs] -def msre(prevs_true, prevs_hat, prevs_train, eps=0.): +def msre(prevs_true, prevs_hat, prevs_train, eps=0.): """ Computes the mean squared ratio error (see :meth:`quapy.error.sre`) across the sample pairs. @@ -285,7 +581,7 @@ [docs] -def aitchisondist(prevs_true, prevs_hat): +def aitchisondist(prevs_true, prevs_hat): """ Computes the Aitchison distance between two prevalence vectors. The Aitchison distance between prevalence vectors :math:`p` and @@ -298,7 +594,7 @@ :param prevs_hat: array-like with the predicted prevalence values :return: Aitchison distance """ - from quapy.functional import CLRtransformation + from quapy.functional import CLRtransformation clr = CLRtransformation() return np.linalg.norm(clr(prevs_true) - clr(prevs_hat), axis=-1) @@ -307,7 +603,7 @@ [docs] -def maitchisondist(prevs_true, prevs_hat): +def maitchisondist(prevs_true, prevs_hat): """ Computes the mean Aitchison distance (see :meth:`quapy.error.aitchisondist`) across the sample pairs, i.e., @@ -324,7 +620,7 @@ [docs] -def mkld(prevs_true, prevs_hat, eps=None): +def mkld(prevs_true, prevs_hat, eps=None): """Computes the mean Kullback-Leibler divergence (see :meth:`quapy.error.kld`) across the sample pairs. The distributions are smoothed using the `eps` factor (see :meth:`quapy.error.smooth`). @@ -345,7 +641,7 @@ [docs] -def kld(prevs_true, prevs_hat, eps=None): +def kld(prevs_true, prevs_hat, eps=None): """Computes the Kullback-Leibler divergence between the two prevalence distributions. Kullback-Leibler divergence between two prevalence distributions :math:`p` and :math:`\\hat{p}` is computed as @@ -371,7 +667,7 @@ [docs] -def mnkld(prevs_true, prevs_hat, eps=None): +def mnkld(prevs_true, prevs_hat, eps=None): """Computes the mean Normalized Kullback-Leibler divergence (see :meth:`quapy.error.nkld`) across the sample pairs. The distributions are smoothed using the `eps` factor (see :meth:`quapy.error.smooth`). @@ -391,7 +687,7 @@ [docs] -def nkld(prevs_true, prevs_hat, eps=None): +def nkld(prevs_true, prevs_hat, eps=None): """Computes the Normalized Kullback-Leibler divergence between the two prevalence distributions. Normalized Kullback-Leibler divergence between two prevalence distributions :math:`p` and :math:`\\hat{p}` is computed as @@ -415,7 +711,7 @@ [docs] -def mrae(prevs_true, prevs_hat, eps=None): +def mrae(prevs_true, prevs_hat, eps=None): """Computes the mean relative absolute error (see :meth:`quapy.error.rae`) across the sample pairs. The distributions are smoothed using the `eps` factor (see :meth:`quapy.error.smooth`). @@ -436,7 +732,7 @@ [docs] -def rae(prevs_true, prevs_hat, eps=None): +def rae(prevs_true, prevs_hat, eps=None): """Computes the absolute relative error between the two prevalence vectors. Relative absolute error between two prevalence vectors :math:`p` and :math:`\\hat{p}` is computed as @@ -462,7 +758,7 @@ [docs] -def nrae(prevs_true, prevs_hat, eps=None): +def nrae(prevs_true, prevs_hat, eps=None): """Computes the normalized absolute relative error between the two prevalence vectors. Relative absolute error between two prevalence vectors :math:`p` and :math:`\\hat{p}` is computed as @@ -490,7 +786,7 @@ [docs] -def mnrae(prevs_true, prevs_hat, eps=None): +def mnrae(prevs_true, prevs_hat, eps=None): """Computes the mean normalized relative absolute error (see :meth:`quapy.error.nrae`) across the sample pairs. The distributions are smoothed using the `eps` factor (see :meth:`quapy.error.smooth`). @@ -511,7 +807,7 @@ [docs] -def nmd(prevs_true, prevs_hat): +def nmd(prevs_true, prevs_hat): """ Computes the Normalized Match Distance; which is the Normalized Distance multiplied by the factor `1/(n-1)` to guarantee the measure ranges between 0 (best prediction) and 1 (worst prediction). @@ -529,7 +825,7 @@ [docs] -def bias_binary(prevs_true, prevs_hat): +def bias_binary(prevs_true, prevs_hat): """ Computes the (positive) bias in a binary problem. The bias is simply the difference between the predicted positive value and the true positive value, so that a positive such value indicates the @@ -549,7 +845,7 @@ [docs] -def mean_bias_binary(prevs_true, prevs_hat): +def mean_bias_binary(prevs_true, prevs_hat): """ Computes the mean of the (positive) bias in a binary problem. :param prevs_true: array-like of shape `(n_classes,)` with the true prevalence values @@ -562,7 +858,7 @@ [docs] -def md(prevs_true, prevs_hat, ERROR_TOL=1E-3): +def md(prevs_true, prevs_hat, ERROR_TOL=1E-3): """ Computes the Match Distance, under the assumption that the cost in mistaking class i with class i+1 is 1 in all cases. @@ -582,7 +878,7 @@ [docs] -def smooth(prevs, eps): +def smooth(prevs, eps): """ Smooths a prevalence distribution with :math:`\\epsilon` (`eps`) as: :math:`\\underline{p}(y)=\\frac{\\epsilon+p(y)}{\\epsilon|\\mathcal{Y}|+ \\displaystyle\\sum_{y\\in \\mathcal{Y}}p(y)}` @@ -597,7 +893,7 @@ -def __check_eps(eps=None): +def __check_eps(eps=None): if eps is None: sample_size = qp.environ['SAMPLE_SIZE'] if sample_size is None: @@ -634,31 +930,75 @@ match_distance = md - + + + + + + + + + + + + + - + + + + - Built with Sphinx using a - theme - provided by Read the Docs. - + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/build/html/_modules/quapy/functional.html b/docs/build/html/_modules/quapy/functional.html index b7743b8..c275ca4 100644 --- a/docs/build/html/_modules/quapy/functional.html +++ b/docs/build/html/_modules/quapy/functional.html @@ -29,8 +29,9 @@ - - + + + @@ -41,6 +42,7 @@ + @@ -114,8 +116,14 @@ + + + + + + + - QuaPy @@ -131,7 +139,7 @@ - Quickstart + Home @@ -183,6 +191,21 @@ + + + + + + + + + + + GitHub + + + @@ -229,7 +252,7 @@ - Quickstart + Home @@ -272,6 +295,21 @@ + + + + + + + + + + + GitHub + + + @@ -332,17 +370,17 @@ Source code for quapy.functional -import warnings -from abc import ABC, abstractmethod -from collections import defaultdict -from functools import lru_cache -from typing import Literal, Union, Callable -from numpy.typing import ArrayLike +import warnings +from abc import ABC, abstractmethod +from collections import defaultdict +from functools import lru_cache +from typing import Literal, Union, Callable +from numpy.typing import ArrayLike -import scipy -import numpy as np +import scipy +import numpy as np -import quapy as qp +import quapy as qp # ------------------------------------------------------------------------------------------ @@ -351,7 +389,7 @@ [docs] -def classes_from_labels(labels): +def classes_from_labels(labels): """ Obtains a np.ndarray with the (sorted) classes :param labels: array-like with the instances' labels @@ -365,7 +403,7 @@ [docs] -def num_classes_from_labels(labels): +def num_classes_from_labels(labels): """ Obtains the number of classes from an array-like of instance's labels :param labels: array-like with the instances' labels @@ -380,7 +418,7 @@ [docs] -def counts_from_labels(labels: ArrayLike, classes: ArrayLike) -> np.ndarray: +def counts_from_labels(labels: ArrayLike, classes: ArrayLike) -> np.ndarray: """ Computes the raw count values from a vector of labels. @@ -401,7 +439,7 @@ [docs] -def prevalence_from_labels(labels: ArrayLike, classes: ArrayLike): +def prevalence_from_labels(labels: ArrayLike, classes: ArrayLike): """ Computes the prevalence values from a vector of labels. @@ -419,7 +457,7 @@ [docs] -def prevalence_from_probabilities(posteriors: ArrayLike, binarize: bool = False): +def prevalence_from_probabilities(posteriors: ArrayLike, binarize: bool = False): """ Returns a vector of prevalence values from a matrix of posterior probabilities. @@ -443,7 +481,7 @@ [docs] -def num_prevalence_combinations(n_prevpoints:int, n_classes:int, n_repeats:int=1) -> int: +def num_prevalence_combinations(n_prevpoints:int, n_classes:int, n_repeats:int=1) -> int: """ Computes the number of valid prevalence combinations in the n_classes-dimensional simplex if `n_prevpoints` equally distant prevalence values are generated and `n_repeats` repetitions are requested. @@ -472,7 +510,7 @@ [docs] -def get_nprevpoints_approximation(combinations_budget:int, n_classes:int, n_repeats:int=1) -> int: +def get_nprevpoints_approximation(combinations_budget:int, n_classes:int, n_repeats:int=1) -> int: """ Searches for the largest number of (equidistant) prevalence points to define for each of the `n_classes` classes so that the number of valid prevalence values generated as combinations of prevalence points (points in a @@ -500,7 +538,7 @@ [docs] -def as_binary_prevalence(positive_prevalence: Union[float, ArrayLike], clip_if_necessary: bool=False) -> np.ndarray: +def as_binary_prevalence(positive_prevalence: Union[float, ArrayLike], clip_if_necessary: bool=False) -> np.ndarray: """ Helper that, given a float representing the prevalence for the positive class, returns a np.ndarray of two values representing a binary distribution. @@ -522,7 +560,7 @@ [docs] -def strprev(prevalences: ArrayLike, prec: int=3) -> str: +def strprev(prevalences: ArrayLike, prec: int=3) -> str: """ Returns a string representation for a prevalence vector. E.g., @@ -539,7 +577,7 @@ [docs] -def check_prevalence_vector(prevalences: ArrayLike, raise_exception: bool=False, tolerance: float=1e-08, aggr=True): +def check_prevalence_vector(prevalences: ArrayLike, raise_exception: bool=False, tolerance: float=1e-08, aggr=True): """ Checks that `prevalences` is a valid prevalence vector, i.e., it contains values in [0,1] and the values sum up to 1. In other words, verifies that the `prevalences` vectors lies in the @@ -581,7 +619,7 @@ [docs] -def uniform_prevalence(n_classes): +def uniform_prevalence(n_classes): """ Returns a vector representing the uniform distribution for `n_classes` @@ -597,7 +635,7 @@ [docs] -def normalize_prevalence(prevalences: ArrayLike, method='l1'): +def normalize_prevalence(prevalences: ArrayLike, method='l1'): """ Normalizes a vector or matrix of prevalence values. The normalization consists of applying a L1 normalization in cases in which the prevalence values are not all-zeros, and to convert the prevalence values into `1/n_classes` in @@ -640,7 +678,7 @@ [docs] -def l1_norm(prevalences: ArrayLike) -> np.ndarray: +def l1_norm(prevalences: ArrayLike) -> np.ndarray: """ Applies L1 normalization to the `unnormalized_arr` so that it becomes a valid prevalence vector. Zero vectors are mapped onto the uniform distribution. Raises an exception if @@ -666,7 +704,7 @@ [docs] -def clip(prevalences: ArrayLike) -> np.ndarray: +def clip(prevalences: ArrayLike) -> np.ndarray: """ Clips the values in [0,1] and then applies the L1 normalization. @@ -681,7 +719,7 @@ [docs] -def projection_simplex_sort(unnormalized_arr: ArrayLike) -> np.ndarray: +def projection_simplex_sort(unnormalized_arr: ArrayLike) -> np.ndarray: """Projects a point onto the probability simplex. The code is adapted from Mathieu Blondel's BSD-licensed @@ -709,7 +747,7 @@ [docs] -def softmax(prevalences: ArrayLike) -> np.ndarray: +def softmax(prevalences: ArrayLike) -> np.ndarray: """ Applies the softmax function to all vectors even if the original vectors were valid distributions. If you want to leave valid vectors untouched, use condsoftmax instead. @@ -724,7 +762,7 @@ [docs] -def condsoftmax(prevalences: ArrayLike) -> np.ndarray: +def condsoftmax(prevalences: ArrayLike) -> np.ndarray: """ Applies the softmax function only to vectors that do not represent valid distributions. @@ -749,7 +787,7 @@ [docs] -def HellingerDistance(P: np.ndarray, Q: np.ndarray) -> float: +def HellingerDistance(P: np.ndarray, Q: np.ndarray) -> float: """ Computes the Hellingher Distance (HD) between (discretized) distributions `P` and `Q`. The HD for two discrete distributions of `k` bins is defined as: @@ -767,7 +805,7 @@ [docs] -def TopsoeDistance(P: np.ndarray, Q: np.ndarray, epsilon: float=1e-20): +def TopsoeDistance(P: np.ndarray, Q: np.ndarray, epsilon: float=1e-20): """ Topsoe distance between two (discretized) distributions `P` and `Q`. The Topsoe distance for two discrete distributions of `k` bins is defined as: @@ -786,7 +824,7 @@ [docs] -def get_divergence(divergence: Union[str, Callable]): +def get_divergence(divergence: Union[str, Callable]): """ Guarantees that the divergence received as argument is a function. That is, if this argument is already a callable, then it is returned, if it is instead a string, then tries to instantiate the corresponding @@ -815,7 +853,7 @@ [docs] -def argmin_prevalence(loss: Callable, +def argmin_prevalence(loss: Callable, n_classes: int, method: Literal["optim_minimize", "linear_search", "ternary_search"]='optim_minimize'): """ @@ -842,7 +880,7 @@ [docs] -def optim_minimize(loss: Callable, n_classes: int, return_loss=False): +def optim_minimize(loss: Callable, n_classes: int, return_loss=False): """ Searches for the optimal prevalence values, i.e., an `n_classes`-dimensional vector of the (`n_classes`-1)-simplex that yields the smallest lost. This optimization is carried out by means of a constrained search using scipy's @@ -854,7 +892,7 @@ :return: (ndarray) the best prevalence vector found or a tuple which also contains the value of the loss if return_loss=True """ - from scipy import optimize + from scipy import optimize # the initial point is set as the uniform distribution uniform_distribution = uniform_prevalence(n_classes=n_classes) @@ -873,7 +911,7 @@ [docs] -def linear_search(loss: Callable, n_classes: int): +def linear_search(loss: Callable, n_classes: int): """ Performs a linear search for the best prevalence value in binary problems. The search is carried out by exploring the range [0,1] stepping by 0.01. This search is inefficient, and is added only for completeness (some of the @@ -897,7 +935,7 @@ [docs] -def ternary_search(loss: Callable, n_classes: int): +def ternary_search(loss: Callable, n_classes: int): """ Performs a ternary search for the best prevalence value in binary problems. This search assumes the loss is unimodal over the interval [0,1]. @@ -933,7 +971,7 @@ [docs] -def prevalence_linspace(grid_points:int=21, repeats:int=1, smooth_limits_epsilon:float=0.01) -> np.ndarray: +def prevalence_linspace(grid_points:int=21, repeats:int=1, smooth_limits_epsilon:float=0.01) -> np.ndarray: """ Produces an array of uniformly separated values of prevalence. By default, produces an array of 21 prevalence values, with @@ -958,7 +996,7 @@ [docs] -def uniform_prevalence_sampling(n_classes: int, size: int=1) -> np.ndarray: +def uniform_prevalence_sampling(n_classes: int, size: int=1) -> np.ndarray: """ Implements the `Kraemer algorithm <http://www.cs.cmu.edu/~nasmith/papers/smith+tromble.tr04.pdf>`_ for sampling uniformly at random from the unit simplex. This implementation is adapted from this @@ -994,7 +1032,7 @@ [docs] -def solve_adjustment_binary(prevalence_estim: ArrayLike, tpr: float, fpr: float, clip: bool=True): +def solve_adjustment_binary(prevalence_estim: ArrayLike, tpr: float, fpr: float, clip: bool=True): """ Implements the adjustment of ACC and PACC for the binary case. The adjustment for a prevalence estimate of the positive class `p` comes down to computing: @@ -1021,7 +1059,7 @@ [docs] -def solve_adjustment( +def solve_adjustment( class_conditional_rates: np.ndarray, unadjusted_counts: np.ndarray, method: Literal["inversion", "invariant-ratio"], @@ -1067,14 +1105,18 @@ if method == "inversion": pass # We leave A and B unchanged elif method == "invariant-ratio": - # Change the last equation to replace it with the normalization condition + # Change the last equation to replace it with the normalization condition; + # copy first so this does not mutate the caller's arrays (np.asarray above + # returns the same object, not a copy, when the input is already float64) + A = A.copy() + B = B.copy() A[-1, :] = 1.0 B[-1] = 1.0 else: raise ValueError(f"unknown {method=}") if solver == "minimize": - def loss(prev): + def loss(prev): return np.linalg.norm(A @ prev - B) return optim_minimize(loss, n_classes=A.shape[0]) elif solver in ["exact-raise", "exact-cc"]: @@ -1102,7 +1144,7 @@ [docs] -class CompositionalTransformation(ABC): +class CompositionalTransformation(ABC): """ Abstract class of transformations for compositional data. """ @@ -1110,13 +1152,13 @@ EPSILON = 1e-12 @abstractmethod - def __call__(self, X): + def __call__(self, X): ... [docs] @abstractmethod - def inverse(self, Z): + def inverse(self, Z): ... @@ -1124,12 +1166,12 @@ [docs] -class CLRtransformation(CompositionalTransformation): +class CLRtransformation(CompositionalTransformation): """ Centered log-ratio (CLR) transformation. """ - def __call__(self, X): + def __call__(self, X): X = np.asarray(X) X = qp.error.smooth(X, self.EPSILON) geometric_mean = np.exp(np.mean(np.log(X), axis=-1, keepdims=True)) @@ -1137,7 +1179,7 @@ [docs] - def inverse(self, Z): + def inverse(self, Z): return scipy.special.softmax(Z, axis=-1) @@ -1145,12 +1187,12 @@ [docs] -class ILRtransformation(CompositionalTransformation): +class ILRtransformation(CompositionalTransformation): """ Isometric log-ratio (ILR) transformation. """ - def __call__(self, X): + def __call__(self, X): X = np.asarray(X) X = qp.error.smooth(X, self.EPSILON) basis = self.get_V(X.shape[-1]) @@ -1158,7 +1200,7 @@ [docs] - def inverse(self, Z): + def inverse(self, Z): Z = np.asarray(Z) basis = self.get_V(Z.shape[-1] + 1) logp = Z @ basis @@ -1169,7 +1211,7 @@ [docs] @lru_cache(maxsize=None) - def get_V(self, k): + def get_V(self, k): helmert = np.zeros((k, k)) for i in range(1, k): helmert[i, :i] = 1 @@ -1182,7 +1224,7 @@ [docs] -def normalized_entropy(p): +def normalized_entropy(p): """ Computes the normalized Shannon entropy of a prevalence vector. @@ -1198,7 +1240,7 @@ [docs] -def antagonistic_prevalence(p, strength=1): +def antagonistic_prevalence(p, strength=1): """ Reflects a prevalence vector in ILR space and maps it back to the simplex. @@ -1215,7 +1257,7 @@ [docs] -def in_simplex(x, atol=1e-8): +def in_simplex(x, atol=1e-8): """ Checks whether points lie in the probability simplex. diff --git a/docs/build/html/_modules/quapy/method/_kdey.html b/docs/build/html/_modules/quapy/method/_kdey.html index 6664ea9..3367861 100644 --- a/docs/build/html/_modules/quapy/method/_kdey.html +++ b/docs/build/html/_modules/quapy/method/_kdey.html @@ -29,8 +29,9 @@ - - + + + @@ -41,6 +42,7 @@ + @@ -114,8 +116,14 @@ + + + + + + + - QuaPy @@ -131,7 +139,7 @@ - Quickstart + Home @@ -183,6 +191,21 @@ + + + + + + + + + + + GitHub + + + @@ -229,7 +252,7 @@ - Quickstart + Home @@ -272,6 +295,21 @@ + + + + + + + + + + + GitHub + + + @@ -332,22 +370,22 @@ Source code for quapy.method._kdey -import numpy as np -from numbers import Real -from sklearn.base import BaseEstimator -from sklearn.neighbors import KernelDensity +import numpy as np +from numbers import Real +from sklearn.base import BaseEstimator +from sklearn.neighbors import KernelDensity -import quapy as qp -from quapy.method._helper import _labels_to_indices -from quapy.method.aggregative import AggregativeSoftQuantifier -import quapy.functional as F -from scipy.special import logsumexp -from sklearn.metrics.pairwise import rbf_kernel +import quapy as qp +from quapy.method._helper import _labels_to_indices +from quapy.method.aggregative import AggregativeSoftQuantifier +import quapy.functional as F +from scipy.special import logsumexp +from sklearn.metrics.pairwise import rbf_kernel [docs] -class KDEBase: +class KDEBase: """ Common ancestor for KDE-based methods. Implements some common routines. """ @@ -356,7 +394,7 @@ KERNELS = ['gaussian', 'aitchison', 'ilr'] @classmethod - def _check_bandwidth(cls, bandwidth, kernel): + def _check_bandwidth(cls, bandwidth, kernel): """ Checks that the bandwidth parameter is correct @@ -370,13 +408,13 @@ return bandwidth @classmethod - def _check_kernel(cls, kernel): + def _check_kernel(cls, kernel): assert kernel in KDEBase.KERNELS, f'unknown {kernel=}' return kernel [docs] - def get_kde_function(self, X, bandwidth, kernel): + def get_kde_function(self, X, bandwidth, kernel): """ Wraps the KDE function from scikit-learn. @@ -392,7 +430,7 @@ [docs] - def pdf(self, kde, X, kernel, log_densities=False): + def pdf(self, kde, X, kernel, log_densities=False): """ Wraps the density evalution of scikit-learn's KDE. Scikit-learn returns log-scores (s), so this function returns :math:`e^{s}` @@ -411,7 +449,7 @@ [docs] - def get_mixture_components(self, X, y, classes, bandwidth, kernel): + def get_mixture_components(self, X, y, classes, bandwidth, kernel): """ Returns an array containing the mixture components, i.e., the KDE functions for each class. @@ -433,7 +471,7 @@ [docs] - def transform_posteriors(self, X, kernel): + def transform_posteriors(self, X, kernel): if kernel in {'aitchison', 'ilr'}: X = self.shrink_posteriors(X) if kernel == 'aitchison': @@ -445,7 +483,7 @@ [docs] - def shrink_posteriors(self, X): + def shrink_posteriors(self, X): shrinkage = getattr(self, 'shrinkage', 0.0) if shrinkage <= 0: return X @@ -457,7 +495,7 @@ [docs] - def effective_bandwidth(self, bandwidth, kernel): + def effective_bandwidth(self, bandwidth, kernel): shrinkage = getattr(self, 'shrinkage', 0.0) if shrinkage > 0 and kernel in {'aitchison', 'ilr'} and isinstance(bandwidth, Real): return (1.0 - shrinkage) * float(bandwidth) @@ -466,7 +504,7 @@ [docs] - def clr_transform(self, X): + def clr_transform(self, X): if not hasattr(self, 'clr'): self.clr = F.CLRtransformation() return self.clr(X) @@ -474,7 +512,7 @@ [docs] - def ilr_transform(self, X): + def ilr_transform(self, X): if not hasattr(self, 'ilr'): self.ilr = F.ILRtransformation() return self.ilr(X) @@ -484,7 +522,7 @@ [docs] -class KDEyML(AggregativeSoftQuantifier, KDEBase): +class KDEyML(AggregativeSoftQuantifier, KDEBase): """ Kernel Density Estimation model for quantification (KDEy) relying on the Kullback-Leibler divergence (KLD) as the divergence measure to be minimized. This method was first proposed in the paper @@ -526,7 +564,7 @@ :param random_state: a seed to be set before fitting any base quantifier (default None) """ - def __init__(self, classifier: BaseEstimator=None, fit_classifier=True, val_split=5, bandwidth=0.1, + def __init__(self, classifier: BaseEstimator=None, fit_classifier=True, val_split=5, bandwidth=0.1, kernel='gaussian', shrinkage=0.0, random_state=None): super().__init__(classifier, fit_classifier, val_split) self.bandwidth = KDEBase._check_bandwidth(bandwidth, kernel) @@ -539,7 +577,7 @@ [docs] - def aggregation_fit(self, classif_predictions, labels): + def aggregation_fit(self, classif_predictions, labels): self.mix_densities = self.get_mixture_components( classif_predictions, labels, @@ -552,7 +590,7 @@ [docs] - def aggregate(self, posteriors: np.ndarray): + def aggregate(self, posteriors: np.ndarray): """ Searches for the mixture model parameter (the sought prevalence values) that maximizes the likelihood of the data (i.e., that minimizes the negative log-likelihood) @@ -569,14 +607,14 @@ for kde_i in self.mix_densities ] - def neg_loglikelihood(prev): + def neg_loglikelihood(prev): prev = qp.error.smooth(prev, eps=epsilon) test_loglikelihood = logsumexp(np.log(prev)[:, None] + test_log_densities, axis=0) return -np.sum(test_loglikelihood) else: test_densities = [self.pdf(kde_i, posteriors, self.kernel) for kde_i in self.mix_densities] - def neg_loglikelihood(prev): + def neg_loglikelihood(prev): test_mixture_likelihood = prev @ test_densities test_loglikelihood = np.log(test_mixture_likelihood + epsilon) return -np.sum(test_loglikelihood) @@ -588,7 +626,7 @@ [docs] -class KDEyHD(AggregativeSoftQuantifier, KDEBase): +class KDEyHD(AggregativeSoftQuantifier, KDEBase): """ Kernel Density Estimation model for quantification (KDEy) relying on the squared Hellinger Disntace (HD) as the divergence measure to be minimized. This method was first proposed in the paper @@ -633,7 +671,7 @@ :param montecarlo_trials: number of Monte Carlo trials (default 10000) """ - def __init__(self, classifier: BaseEstimator=None, fit_classifier=True, val_split=5, divergence: str='HD', + def __init__(self, classifier: BaseEstimator=None, fit_classifier=True, val_split=5, divergence: str='HD', bandwidth=0.1, random_state=None, montecarlo_trials=10000): super().__init__(classifier, fit_classifier, val_split) @@ -644,7 +682,7 @@ [docs] - def aggregation_fit(self, classif_predictions, labels): + def aggregation_fit(self, classif_predictions, labels): self.mix_densities = self.get_mixture_components( classif_predictions, labels, self.classes_, self.bandwidth, 'gaussian' ) @@ -663,7 +701,7 @@ [docs] - def aggregate(self, posteriors: np.ndarray): + def aggregate(self, posteriors: np.ndarray): # we retain all n*N examples (sampled from a mixture with uniform parameter), and then # apply importance sampling (IS). In this version we compute D(p_alpha||q) with IS n_classes = len(self.mix_densities) @@ -671,7 +709,7 @@ test_kde = self.get_kde_function(posteriors, self.bandwidth, 'gaussian') test_densities = self.pdf(test_kde, self.reference_samples, 'gaussian') - def f_squared_hellinger(u): + def f_squared_hellinger(u): return (np.sqrt(u)-1)**2 # todo: this will fail when self.divergence is a callable, and is not the right place to do it anyway @@ -687,7 +725,7 @@ p_class = self.reference_classwise_densities + epsilon fracs = p_class/qs - def divergence(prev): + def divergence(prev): # ps / qs = (prev @ p_class) / qs = prev @ (p_class / qs) = prev @ fracs ps_div_qs = prev @ fracs return np.mean( f(ps_div_qs) * iw ) @@ -699,7 +737,7 @@ [docs] -class KDEyCS(AggregativeSoftQuantifier): +class KDEyCS(AggregativeSoftQuantifier): """ Kernel Density Estimation model for quantification (KDEy) relying on the Cauchy-Schwarz divergence (CS) as the divergence measure to be minimized. This method was first proposed in the paper @@ -736,13 +774,13 @@ :param bandwidth: float, the bandwidth of the Kernel """ - def __init__(self, classifier: BaseEstimator=None, fit_classifier=True, val_split=5, bandwidth=0.1): + def __init__(self, classifier: BaseEstimator=None, fit_classifier=True, val_split=5, bandwidth=0.1): super().__init__(classifier, fit_classifier, val_split) self.bandwidth = KDEBase._check_bandwidth(bandwidth, kernel='gaussian') [docs] - def gram_matrix_mix_sum(self, X, Y=None): + def gram_matrix_mix_sum(self, X, Y=None): # this adapts the output of the rbf_kernel function (pairwise evaluations of Gaussian kernels k(x,y)) # to contain pairwise evaluations of N(x|mu,Sigma1+Sigma2) with mu=y and Sigma1 and Sigma2 are # two "scalar matrices" (h^2)*I each, so Sigma1+Sigma2 has scalar 2(h^2) (h is the bandwidth) @@ -757,7 +795,7 @@ [docs] - def aggregation_fit(self, classif_predictions, labels): + def aggregation_fit(self, classif_predictions, labels): P, y = classif_predictions, labels n = len(self.classes_) @@ -789,7 +827,7 @@ [docs] - def aggregate(self, posteriors: np.ndarray): + def aggregate(self, posteriors: np.ndarray): Ptr = self.Ptr Pte = posteriors y = self.ytr @@ -809,7 +847,7 @@ for i in range(n): tr_te_sums[i] = self.gram_matrix_mix_sum(Ptr[y==i], Pte) - def divergence(alpha): + def divergence(alpha): # called \overline{r} in the paper alpha_ratio = alpha * self.counts_inv diff --git a/docs/build/html/_modules/quapy/method/_neural.html b/docs/build/html/_modules/quapy/method/_neural.html index 3e6c7c3..3c8215e 100644 --- a/docs/build/html/_modules/quapy/method/_neural.html +++ b/docs/build/html/_modules/quapy/method/_neural.html @@ -29,7 +29,7 @@ - + @@ -370,23 +370,24 @@ Source code for quapy.method._neural -import os -from pathlib import Path -import random +import logging +import os +from pathlib import Path +import random -import torch -from torch.nn import MSELoss -from torch.nn.functional import relu +import torch +from torch.nn import MSELoss +from torch.nn.functional import relu -from quapy.protocol import UPP -from quapy.method.aggregative import * -from quapy.util import EarlyStop -from tqdm import tqdm +from quapy.protocol import UPP +from quapy.method.aggregative import * +from quapy.util import EarlyStop +from tqdm import tqdm [docs] -class QuaNetTrainer(BaseQuantifier): +class QuaNetTrainer(BaseQuantifier): """ Implementation of `QuaNet <https://dl.acm.org/doi/abs/10.1145/3269206.3269287>`_, a neural network for quantification. This implementation uses `PyTorch <https://pytorch.org/>`_ and can take advantage of GPU @@ -438,7 +439,7 @@ :param device: string, indicate "cpu" or "cuda" """ - def __init__(self, + def __init__(self, classifier, fit_classifier=True, sample_size=None, @@ -491,7 +492,7 @@ [docs] - def fit(self, X, y): + def fit(self, X, y): """ Trains QuaNet. @@ -549,7 +550,7 @@ order_by=0 if data.binary else None, **self.quanet_params ).to(self.device) - print(self.quanet) + logging.getLogger(__name__).debug(self.quanet) self.optim = torch.optim.Adam(self.quanet.parameters(), lr=self.lr) early_stop = EarlyStop(self.patience, lower_is_better=True) @@ -564,15 +565,16 @@ if early_stop.IMPROVED: torch.save(self.quanet.state_dict(), checkpoint) elif early_stop.STOP: - print(f'training ended by patience exhausted; loading best model parameters in {checkpoint} ' - f'for epoch {early_stop.best_epoch}') + logging.getLogger(__name__).info( + f'training ended by patience exhausted; loading best model parameters in {checkpoint} ' + f'for epoch {early_stop.best_epoch}') self.quanet.load_state_dict(torch.load(checkpoint)) break return self - def _get_aggregative_estims(self, posteriors): + def _get_aggregative_estims(self, posteriors): label_predictions = np.argmax(posteriors, axis=-1) prevs_estim = [] for quantifier in self.quantifiers.values(): @@ -585,7 +587,7 @@ [docs] - def predict(self, X): + def predict(self, X): posteriors = self.classifier.predict_proba(X) embeddings = self.classifier.transform(X) quant_estims = self._get_aggregative_estims(posteriors) @@ -598,7 +600,7 @@ return prevalence - def _epoch(self, data: LabelledCollection, posteriors, iterations, epoch, early_stop, train): + def _epoch(self, data: LabelledCollection, posteriors, iterations, epoch, early_stop, train): mse_loss = MSELoss() self.quanet.train(mode=train) @@ -650,7 +652,7 @@ [docs] - def get_params(self, deep=True): + def get_params(self, deep=True): classifier_params = self.classifier.get_params() classifier_params = {'classifier__'+k:v for k,v in classifier_params.items()} return {**classifier_params, **self.quanet_params} @@ -658,7 +660,7 @@ [docs] - def set_params(self, **parameters): + def set_params(self, **parameters): learner_params = {} for key, val in parameters.items(): if key in self.quanet_params: @@ -670,7 +672,7 @@ self.classifier.set_params(**learner_params) - def __check_params_colision(self, quanet_params, learner_params): + def __check_params_colision(self, quanet_params, learner_params): quanet_keys = set(quanet_params.keys()) learner_keys = set(learner_params.keys()) intersection = quanet_keys.intersection(learner_keys) @@ -680,7 +682,7 @@ [docs] - def clean_checkpoint(self): + def clean_checkpoint(self): """ Removes the checkpoint """ @@ -689,23 +691,23 @@ [docs] - def clean_checkpoint_dir(self): + def clean_checkpoint_dir(self): """ Removes anything contained in the checkpoint directory """ - import shutil + import shutil shutil.rmtree(self.checkpointdir, ignore_errors=True) @property - def classes_(self): + def classes_(self): return self._classes_ [docs] -def mae_loss(output, target): +def mae_loss(output, target): """ Torch-like wrapper for the Mean Absolute Error @@ -719,7 +721,7 @@ [docs] -class QuaNetModule(torch.nn.Module): +class QuaNetModule(torch.nn.Module): """ Implements the `QuaNet <https://dl.acm.org/doi/abs/10.1145/3269206.3269287>`_ forward pass. See :class:`QuaNetTrainer` for training QuaNet. @@ -736,7 +738,7 @@ :param order_by: integer, class for which the document embeddings are to be sorted """ - def __init__(self, + def __init__(self, doc_embedding_size, n_classes, stats_size, @@ -771,10 +773,10 @@ self.output = torch.nn.Linear(prev_size, n_classes) @property - def device(self): + def device(self): return torch.device('cuda') if next(self.parameters()).is_cuda else torch.device('cpu') - def _init_hidden(self): + def _init_hidden(self): directions = 2 if self.bidirectional else 1 var_hidden = torch.zeros(self.nlayers * directions, 1, self.hidden_size) var_cell = torch.zeros(self.nlayers * directions, 1, self.hidden_size) @@ -784,7 +786,7 @@ [docs] - def forward(self, doc_embeddings, doc_posteriors, statistics): + def forward(self, doc_embeddings, doc_posteriors, statistics): device = self.device doc_embeddings = torch.as_tensor(doc_embeddings, dtype=torch.float, device=device) doc_posteriors = torch.as_tensor(doc_posteriors, dtype=torch.float, device=device) diff --git a/docs/build/html/_modules/quapy/method/_threshold_optim.html b/docs/build/html/_modules/quapy/method/_threshold_optim.html index 6bb8003..304e087 100644 --- a/docs/build/html/_modules/quapy/method/_threshold_optim.html +++ b/docs/build/html/_modules/quapy/method/_threshold_optim.html @@ -1,92 +1,388 @@ - - - - - - quapy.method._threshold_optim — QuaPy: A Python-based open-source framework for quantification 0.2.1 documentation - - + + + + + + + + quapy.method._threshold_optim — QuaPy: A Python-based open-source framework for quantification 0.2.1 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Skip to main content - - - - - - - - - + + + + Back to top + + + + + + + + + + Ctrl+K + + - - - - - - - - - - QuaPy: A Python-based open-source framework for quantification - - - - - - - + + - - -Quickstart - - -Manuals - - -API - - - - + + + + + + + + + + + - - - QuaPy: A Python-based open-source framework for quantification - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Home + + + + + + + Manuals + + + + + + + API + + - - - - - - Module code - quapy.method._threshold_optim - - - - - - - - Source code for quapy.method._threshold_optim -from abc import abstractmethod + + + + + + + + + -import numpy as np -from sklearn.base import BaseEstimator -import quapy as qp -import quapy.functional as F -from quapy.data import LabelledCollection -from quapy.method.aggregative import BinaryAggregativeQuantifier + + + Search + Ctrl+K + + + + + + + + + + + + + + System Settings + Light + Dark + + + + + + + + + + + + + + GitHub + + + + + + + + + + + + + Search + Ctrl+K + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Home + + + + + + + Manuals + + + + + + + API + + + + + + + + + + + + + + + + + + + + + + + System Settings + Light + Dark + + + + + + + + + + + + + + GitHub + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Module code + + quapy.method._threshold_optim + + + + + + + + + + + + + + + + + Source code for quapy.method._threshold_optim +from abc import abstractmethod + +import numpy as np +from sklearn.base import BaseEstimator +import quapy as qp +import quapy.functional as F +from quapy.data import LabelledCollection +from quapy.method.aggregative import BinaryAggregativeQuantifier [docs] -class ThresholdOptimization(BinaryAggregativeQuantifier): +class ThresholdOptimization(BinaryAggregativeQuantifier): """ Abstract class of Threshold Optimization variants for :class:`ACC` as proposed by `Forman 2006 <https://dl.acm.org/doi/abs/10.1145/1150402.1150423>`_ and @@ -111,14 +407,14 @@ :param n_jobs: number of parallel workers """ - def __init__(self, classifier: BaseEstimator=None, fit_classifier=True, val_split=None, n_jobs=None): + def __init__(self, classifier: BaseEstimator=None, fit_classifier=True, val_split=None, n_jobs=None): super().__init__(classifier, fit_classifier, val_split) self.n_jobs = qp._get_njobs(n_jobs) [docs] @abstractmethod - def condition(self, tpr, fpr) -> float: + def condition(self, tpr, fpr) -> float: """ Implements the criterion according to which the threshold should be selected. This function should return the (float) score to be minimized. @@ -132,7 +428,7 @@ [docs] - def discard(self, tpr, fpr) -> bool: + def discard(self, tpr, fpr) -> bool: """ Indicates whether a combination of tpr and fpr should be discarded @@ -144,7 +440,7 @@ - def _eval_candidate_thresholds(self, decision_scores, y): + def _eval_candidate_thresholds(self, decision_scores, y): """ Seeks for the best `tpr` and `fpr` according to the score obtained at different decision thresholds. The scoring function is implemented in function `_condition`. @@ -181,7 +477,7 @@ [docs] - def aggregate_with_threshold(self, classif_predictions, tprs, fprs, thresholds): + def aggregate_with_threshold(self, classif_predictions, tprs, fprs, thresholds): # This function performs the adjusted count for given tpr, fpr, and threshold. # Note that, due to broadcasting, tprs, fprs, and thresholds could be arrays of length > 1 prevs_estims = np.mean(classif_predictions[:, None] >= thresholds, axis=0) @@ -190,26 +486,26 @@ return prevs_estims.squeeze() - def _compute_table(self, y, y_): + def _compute_table(self, y, y_): TP = np.logical_and(y == y_, y == self.pos_label).sum() FP = np.logical_and(y != y_, y == self.neg_label).sum() FN = np.logical_and(y != y_, y == self.pos_label).sum() TN = np.logical_and(y == y_, y == self.neg_label).sum() return TP, FP, FN, TN - def _compute_tpr(self, TP, FP): - if TP + FP == 0: + def _compute_tpr(self, TP, FN): + if TP + FN == 0: return 1 - return TP / (TP + FP) + return TP / (TP + FN) - def _compute_fpr(self, FP, TN): + def _compute_fpr(self, FP, TN): if FP + TN == 0: return 0 return FP / (FP + TN) [docs] - def aggregation_fit(self, classif_predictions, labels): + def aggregation_fit(self, classif_predictions, labels): decision_scores, y = classif_predictions, labels # the standard behavior is to keep the best threshold only self.tpr, self.fpr, self.threshold = self._eval_candidate_thresholds(decision_scores, y)[0] @@ -218,7 +514,7 @@ [docs] - def aggregate(self, classif_predictions: np.ndarray): + def aggregate(self, classif_predictions: np.ndarray): # the standard behavior is to compute the adjusted count using the best threshold found return self.aggregate_with_threshold(classif_predictions, self.tpr, self.fpr, self.threshold) @@ -227,7 +523,7 @@ [docs] -class T50(ThresholdOptimization): +class T50(ThresholdOptimization): """ Threshold Optimization variant for :class:`ACC` as proposed by `Forman 2006 <https://dl.acm.org/doi/abs/10.1145/1150402.1150423>`_ and @@ -249,12 +545,12 @@ """ - def __init__(self, classifier: BaseEstimator=None, fit_classifier=True, val_split=5): + def __init__(self, classifier: BaseEstimator=None, fit_classifier=True, val_split=5): super().__init__(classifier, fit_classifier, val_split) [docs] - def condition(self, tpr, fpr) -> float: + def condition(self, tpr, fpr) -> float: return abs(tpr - 0.5) @@ -262,7 +558,7 @@ [docs] -class MAX(ThresholdOptimization): +class MAX(ThresholdOptimization): """ Threshold Optimization variant for :class:`ACC` as proposed by `Forman 2006 <https://dl.acm.org/doi/abs/10.1145/1150402.1150423>`_ and @@ -282,12 +578,12 @@ """ - def __init__(self, classifier: BaseEstimator=None, fit_classifier=True, val_split=5): + def __init__(self, classifier: BaseEstimator=None, fit_classifier=True, val_split=5): super().__init__(classifier, fit_classifier, val_split) [docs] - def condition(self, tpr, fpr) -> float: + def condition(self, tpr, fpr) -> float: # MAX strives to maximize (tpr - fpr), which is equivalent to minimize (fpr - tpr) return (fpr - tpr) @@ -296,7 +592,7 @@ [docs] -class X(ThresholdOptimization): +class X(ThresholdOptimization): """ Threshold Optimization variant for :class:`ACC` as proposed by `Forman 2006 <https://dl.acm.org/doi/abs/10.1145/1150402.1150423>`_ and @@ -316,12 +612,12 @@ """ - def __init__(self, classifier: BaseEstimator=None, fit_classifier=True, val_split=5): + def __init__(self, classifier: BaseEstimator=None, fit_classifier=True, val_split=5): super().__init__(classifier, fit_classifier, val_split) [docs] - def condition(self, tpr, fpr) -> float: + def condition(self, tpr, fpr) -> float: return abs(1 - (tpr + fpr)) @@ -329,7 +625,7 @@ [docs] -class MS(ThresholdOptimization): +class MS(ThresholdOptimization): """ Median Sweep. Threshold Optimization variant for :class:`ACC` as proposed by `Forman 2006 <https://dl.acm.org/doi/abs/10.1145/1150402.1150423>`_ and @@ -348,18 +644,18 @@ for `k`); or as a tuple (X,y) defining the specific set of data to use for validation. """ - def __init__(self, classifier: BaseEstimator=None, fit_classifier=True, val_split=5): + def __init__(self, classifier: BaseEstimator=None, fit_classifier=True, val_split=5): super().__init__(classifier, fit_classifier, val_split) [docs] - def condition(self, tpr, fpr) -> float: + def condition(self, tpr, fpr) -> float: return 1 [docs] - def aggregation_fit(self, classif_predictions, labels): + def aggregation_fit(self, classif_predictions, labels): decision_scores, y = classif_predictions, labels # keeps all candidates tprs_fprs_thresholds = self._eval_candidate_thresholds(decision_scores, y) @@ -371,7 +667,7 @@ [docs] - def aggregate(self, classif_predictions: np.ndarray): + def aggregate(self, classif_predictions: np.ndarray): prevalences = self.aggregate_with_threshold(classif_predictions, self.tprs, self.fprs, self.thresholds) if prevalences.ndim==2: prevalences = np.median(prevalences, axis=0) @@ -382,7 +678,7 @@ [docs] -class MS2(MS): +class MS2(MS): """ Median Sweep 2. Threshold Optimization variant for :class:`ACC` as proposed by `Forman 2006 <https://dl.acm.org/doi/abs/10.1145/1150402.1150423>`_ and @@ -402,42 +698,86 @@ for `k`); or as a tuple (X,y) defining the specific set of data to use for validation. """ - def __init__(self, classifier: BaseEstimator=None, fit_classifier=True, val_split=5): + def __init__(self, classifier: BaseEstimator=None, fit_classifier=True, val_split=5): super().__init__(classifier, fit_classifier, val_split) [docs] - def discard(self, tpr, fpr) -> bool: + def discard(self, tpr, fpr) -> bool: return (tpr-fpr) <= 0.25 - + + + + + + + + + + + + + - + + + + - Built with Sphinx using a - theme - provided by Read the Docs. - + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/build/html/_modules/quapy/method/aggregative.html b/docs/build/html/_modules/quapy/method/aggregative.html index a6b98d4..0680ac3 100644 --- a/docs/build/html/_modules/quapy/method/aggregative.html +++ b/docs/build/html/_modules/quapy/method/aggregative.html @@ -29,8 +29,9 @@ - - + + + @@ -41,6 +42,7 @@ + @@ -114,8 +116,14 @@ + + + + + + + - QuaPy @@ -131,7 +139,7 @@ - Quickstart + Home @@ -183,6 +191,21 @@ + + + + + + + + + + + GitHub + + + @@ -229,7 +252,7 @@ - Quickstart + Home @@ -272,6 +295,21 @@ + + + + + + + + + + + GitHub + + + @@ -332,25 +370,25 @@ Source code for quapy.method.aggregative -from abc import ABC, abstractmethod -from argparse import ArgumentError -from copy import deepcopy -from typing import Callable, Literal, Union -import numpy as np -from sklearn.base import BaseEstimator -from sklearn.calibration import CalibratedClassifierCV -from sklearn.exceptions import NotFittedError -from sklearn.metrics import confusion_matrix -from sklearn.model_selection import cross_val_predict, train_test_split -from sklearn.utils.validation import check_is_fitted +import warnings +from abc import ABC, abstractmethod +from copy import deepcopy +from typing import Callable, Literal, Union +import numpy as np +from sklearn.base import BaseEstimator +from sklearn.calibration import CalibratedClassifierCV +from sklearn.exceptions import NotFittedError +from sklearn.metrics import confusion_matrix +from sklearn.model_selection import cross_val_predict, train_test_split +from sklearn.utils.validation import check_is_fitted -import quapy as qp -import quapy.functional as F -from quapy.functional import get_divergence -from quapy.classification.svmperf import SVMperf -from quapy.data import LabelledCollection -from quapy.method.base import BaseQuantifier, BinaryQuantifier, OneVsAllGeneric -from quapy.method._helper import ( +import quapy as qp +import quapy.functional as F +from quapy.functional import get_divergence +from quapy.classification.svmperf import SVMperf +from quapy.data import LabelledCollection +from quapy.method.base import BaseQuantifier, BinaryQuantifier, OneVsAllGeneric +from quapy.method._helper import ( _get_abstention_calibrators, _get_cvxpy, _rlls_check_mode, @@ -368,7 +406,7 @@ [docs] -class AggregativeQuantifier(BaseQuantifier, ABC): +class AggregativeQuantifier(BaseQuantifier, ABC): """ Abstract class for quantification methods that base their estimations on the aggregation of classification results. Aggregative quantifiers implement a pipeline that consists of generating classification predictions @@ -396,7 +434,7 @@ the training data be wasted. """ - def __init__(self, + def __init__(self, classifier: Union[None,BaseEstimator], fit_classifier:bool=True, val_split:Union[int,float,tuple,None]=5): @@ -418,9 +456,9 @@ (f'when {val_split=} is indicated as an integer, it represents the number of folds in a kFCV ' f'and must thus be >1') if val_split==5 and not fit_classifier: - print(f'Warning: {val_split=} will be ignored when the classifier is already trained ' - f'({fit_classifier=}). Parameter {self.val_split=} will be set to None. Set {val_split=} ' - f'to None to avoid this warning.') + warnings.warn(f'{val_split=} will be ignored when the classifier is already trained ' + f'({fit_classifier=}). Parameter {self.val_split=} will be set to None. Set {val_split=} ' + f'to None to avoid this warning.') self.val_split=None if val_split!=5: assert fit_classifier, (f'Parameter {val_split=} has been modified, but {fit_classifier=} ' @@ -457,7 +495,7 @@ assert fitted, (f'{fit_classifier=} requires the classifier to be already trained, ' f'but this does not seem to be') - def _check_init_parameters(self): + def _check_init_parameters(self): """ Implements any check to be performed in the parameters of the init method before undertaking the training of the quantifier. This is made as to allow for a quick execution stop when the @@ -467,7 +505,7 @@ """ pass - def _check_non_empty_classes(self, y): + def _check_non_empty_classes(self, y): """ Asserts all classes have positive instances. @@ -484,7 +522,7 @@ [docs] - def fit(self, X, y): + def fit(self, X, y): """ Trains the aggregative quantifier. This comes down to training a classifier (if requested) and an aggregation function. @@ -501,7 +539,7 @@ [docs] - def classifier_fit_predict(self, X, y): + def classifier_fit_predict(self, X, y): """ Trains the classifier if requested (`fit_classifier=True`) and generate the necessary predictions to train the aggregation function. @@ -551,7 +589,7 @@ [docs] @abstractmethod - def aggregation_fit(self, classif_predictions, labels): + def aggregation_fit(self, classif_predictions, labels): """ Trains the aggregation function. @@ -563,7 +601,7 @@ @property - def classifier(self): + def classifier(self): """ Gives access to the classifier @@ -572,7 +610,7 @@ return self.classifier_ @classifier.setter - def classifier(self, classifier): + def classifier(self, classifier): """ Setter for the classifier @@ -582,7 +620,7 @@ [docs] - def classify(self, X): + def classify(self, X): """ Provides the label predictions for the given instances. The predictions should respect the format expected by :meth:`aggregate`, e.g., posterior probabilities for probabilistic quantifiers, or crisp predictions for @@ -594,7 +632,7 @@ return getattr(self.classifier, self._classifier_method())(X) - def _classifier_method(self): + def _classifier_method(self): """ Name of the method that must be used for issuing label predictions. The default one is "decision_function". @@ -602,7 +640,7 @@ """ return 'decision_function' - def _check_classifier(self, adapt_if_necessary=False): + def _check_classifier(self, adapt_if_necessary=False): """ Guarantees that the underlying classifier implements the method required for issuing predictions, i.e., the method indicated by the :meth:`_classifier_method` @@ -614,7 +652,7 @@ [docs] - def predict(self, X): + def predict(self, X): """ Generate class prevalence estimates for the sample's instances by aggregating the label predictions generated by the classifier. @@ -629,7 +667,7 @@ [docs] @abstractmethod - def aggregate(self, classif_predictions: np.ndarray): + def aggregate(self, classif_predictions: np.ndarray): """ Implements the aggregation of the classifier predictions. @@ -640,7 +678,7 @@ @property - def classes_(self): + def classes_(self): """ Class labels, in the same order in which class prevalence values are to be computed. This default implementation actually returns the class labels of the learner. @@ -653,14 +691,14 @@ [docs] -class AggregativeCrispQuantifier(AggregativeQuantifier, ABC): +class AggregativeCrispQuantifier(AggregativeQuantifier, ABC): """ Abstract class for quantification methods that base their estimations on the aggregation of crisp decisions as returned by a hard classifier. Aggregative crisp quantifiers thus extend Aggregative Quantifiers by implementing specifications about crisp predictions. """ - def _classifier_method(self): + def _classifier_method(self): """ Name of the method that must be used for issuing label predictions. For crisp quantifiers, the method is 'predict', that returns an array of shape `(n_instances,)` of label predictions. @@ -673,7 +711,7 @@ [docs] -class AggregativeSoftQuantifier(AggregativeQuantifier, ABC): +class AggregativeSoftQuantifier(AggregativeQuantifier, ABC): """ Abstract class for quantification methods that base their estimations on the aggregation of posterior probabilities as returned by a probabilistic classifier. @@ -681,7 +719,7 @@ about soft predictions. """ - def _classifier_method(self): + def _classifier_method(self): """ Name of the method that must be used for issuing label predictions. For probabilistic quantifiers, the method is 'predict_proba', that returns an array of shape `(n_instances, n_dimensions,)` with posterior @@ -691,7 +729,7 @@ """ return 'predict_proba' - def _check_classifier(self, adapt_if_necessary=False): + def _check_classifier(self, adapt_if_necessary=False): """ Guarantees that the underlying classifier implements the method indicated by the :meth:`_classifier_method`. In case it does not, the classifier is calibrated (by means of the Platt's calibration method implemented by @@ -703,8 +741,8 @@ """ if not hasattr(self.classifier, self._classifier_method()): if adapt_if_necessary: - print(f'warning: The learner {self.classifier.__class__.__name__} does not seem to be ' - f'probabilistic. The learner will be calibrated (using CalibratedClassifierCV).') + warnings.warn(f'The learner {self.classifier.__class__.__name__} does not seem to be ' + f'probabilistic. The learner will be calibrated (using CalibratedClassifierCV).') self.classifier = CalibratedClassifierCV(self.classifier, cv=5) else: raise AssertionError(f'error: The learner {self.classifier.__class__.__name__} does not ' @@ -715,19 +753,19 @@ [docs] -class BinaryAggregativeQuantifier(AggregativeQuantifier, BinaryQuantifier): +class BinaryAggregativeQuantifier(AggregativeQuantifier, BinaryQuantifier): @property - def pos_label(self): + def pos_label(self): return self.classifier.classes_[1] @property - def neg_label(self): + def neg_label(self): return self.classifier.classes_[0] [docs] - def fit(self, X, y): + def fit(self, X, y): self._check_binary(y, self.__class__.__name__) return super().fit(X, y) @@ -738,19 +776,19 @@ # ------------------------------------ [docs] -class CC(AggregativeCrispQuantifier): +class CC(AggregativeCrispQuantifier): """ The most basic Quantification method. One that simply classifies all instances and counts how many have been attributed to each of the classes in order to compute class prevalence estimates. :param classifier: a sklearn's Estimator that generates a classifier """ - def __init__(self, classifier: BaseEstimator = None, fit_classifier: bool = True): + def __init__(self, classifier: BaseEstimator = None, fit_classifier: bool = True): super().__init__(classifier, fit_classifier, val_split=None) [docs] - def aggregation_fit(self, classif_predictions, labels): + def aggregation_fit(self, classif_predictions, labels): """ Nothing to do here! @@ -762,7 +800,7 @@ [docs] - def aggregate(self, classif_predictions: np.ndarray): + def aggregate(self, classif_predictions: np.ndarray): """ Computes class prevalence estimates by counting the prevalence of each of the predicted labels. @@ -776,7 +814,7 @@ [docs] -class PCC(AggregativeSoftQuantifier): +class PCC(AggregativeSoftQuantifier): """ `Probabilistic Classify & Count <https://ieeexplore.ieee.org/abstract/document/5694031>`_, the probabilistic variant of CC that relies on the posterior probabilities returned by a probabilistic classifier. @@ -784,12 +822,12 @@ :param classifier: a sklearn's Estimator that generates a classifier """ - def __init__(self, classifier: BaseEstimator = None, fit_classifier: bool = True, val_split=None): + def __init__(self, classifier: BaseEstimator = None, fit_classifier: bool = True, val_split=None): super().__init__(classifier, fit_classifier, val_split=val_split) [docs] - def aggregation_fit(self, classif_predictions, labels): + def aggregation_fit(self, classif_predictions, labels): """ Nothing to do here! @@ -801,7 +839,7 @@ [docs] - def aggregate(self, classif_posteriors): + def aggregate(self, classif_posteriors): return F.prevalence_from_probabilities(classif_posteriors, binarize=False) @@ -809,7 +847,7 @@ [docs] -class ACC(AggregativeCrispQuantifier): +class ACC(AggregativeCrispQuantifier): """ `Adjusted Classify & Count <https://link.springer.com/article/10.1007/s10618-008-0097-y>`_, the "adjusted" variant of :class:`CC`, that corrects the predictions of CC @@ -857,7 +895,7 @@ :param n_jobs: number of parallel workers """ - def __init__( + def __init__( self, classifier: BaseEstimator = None, fit_classifier = True, @@ -880,7 +918,7 @@ [docs] @classmethod - def newInvariantRatioEstimation(cls, classifier: BaseEstimator, fit_classifier=True, val_split=5, n_jobs=None): + def newInvariantRatioEstimation(cls, classifier: BaseEstimator, fit_classifier=True, val_split=5, n_jobs=None): """ Constructs a quantifier that implements the Invariant Ratio Estimator of `Vaz et al. 2018 <https://jmlr.org/papers/v20/18-456.html>`_. This amounts @@ -905,7 +943,7 @@ return ACC(classifier, fit_classifier=fit_classifier, val_split=val_split, method='invariant-ratio', norm='mapsimplex', n_jobs=n_jobs) - def _check_init_parameters(self): + def _check_init_parameters(self): if self.solver not in ACC.SOLVERS: raise ValueError(f"unknown solver; valid ones are {ACC.SOLVERS}") if self.method not in ACC.METHODS: @@ -915,7 +953,7 @@ [docs] - def aggregation_fit(self, classif_predictions, labels): + def aggregation_fit(self, classif_predictions, labels): """ Estimates the misclassification rates. :param classif_predictions: array-like with the predicted labels @@ -930,7 +968,7 @@ [docs] @classmethod - def getPteCondEstim(cls, classes, y, y_): + def getPteCondEstim(cls, classes, y, y_): """ Estimate the matrix with entry (i,j) being the estimate of P(hat_yi|yj), that is, the probability that a document that belongs to yj ends up being classified as belonging to yi @@ -953,7 +991,7 @@ [docs] - def aggregate(self, classif_predictions): + def aggregate(self, classif_predictions): prevs_estim = self.cc.aggregate(classif_predictions) estimate = F.solve_adjustment( class_conditional_rates=self.Pte_cond_estim_, @@ -968,7 +1006,7 @@ [docs] -class PACC(AggregativeSoftQuantifier): +class PACC(AggregativeSoftQuantifier): """ `Probabilistic Adjusted Classify & Count <https://ieeexplore.ieee.org/abstract/document/5694031>`_, the probabilistic variant of ACC that relies on the posterior probabilities returned by a probabilistic classifier. @@ -1015,7 +1053,7 @@ :param n_jobs: number of parallel workers """ - def __init__( + def __init__( self, classifier: BaseEstimator = None, fit_classifier=True, @@ -1031,7 +1069,7 @@ self.method = method self.norm = norm - def _check_init_parameters(self): + def _check_init_parameters(self): if self.solver not in ACC.SOLVERS: raise ValueError(f"unknown solver; valid ones are {ACC.SOLVERS}") if self.method not in ACC.METHODS: @@ -1041,7 +1079,7 @@ [docs] - def aggregation_fit(self, classif_predictions, labels): + def aggregation_fit(self, classif_predictions, labels): """ Estimates the misclassification rates @@ -1056,7 +1094,7 @@ [docs] - def aggregate(self, classif_posteriors): + def aggregate(self, classif_posteriors): prevs_estim = self.pcc.aggregate(classif_posteriors) estimate = F.solve_adjustment( @@ -1071,7 +1109,7 @@ [docs] @classmethod - def getPteCondEstim(cls, classes, y, y_): + def getPteCondEstim(cls, classes, y, y_): # estimate the matrix with entry (i,j) being the estimate of P(hat_yi|yj), that is, the probability that a # document that belongs to yj ends up being classified as belonging to yi n_classes = len(classes) @@ -1088,7 +1126,7 @@ [docs] -class RLLS(AggregativeSoftQuantifier): +class RLLS(AggregativeSoftQuantifier): """ `Regularized Learning for Domain Adaptation under Label Shifts <https://arxiv.org/abs/1903.09734>`_, used here as an aggregative @@ -1128,7 +1166,7 @@ :func:`quapy.functional.normalize_prevalence` """ - def __init__( + def __init__( self, classifier: BaseEstimator = None, fit_classifier=True, @@ -1147,7 +1185,7 @@ self.norm = norm self.last_w_ = None - def _check_init_parameters(self): + def _check_init_parameters(self): _get_cvxpy() _rlls_check_mode(self.mode) if not isinstance(self.alpha, (int, float)) or self.alpha < 0: @@ -1164,7 +1202,7 @@ [docs] - def aggregation_fit(self, classif_predictions, labels): + def aggregation_fit(self, classif_predictions, labels): if classif_predictions is None or labels is None: raise ValueError('RLLS requires source posterior predictions and source labels') @@ -1181,7 +1219,7 @@ [docs] - def aggregate(self, classif_posteriors): + def aggregate(self, classif_posteriors): qz = _rlls_predicted_marginal(classif_posteriors, mode=self.mode) w = _rlls_compute_weights( self.C_zy_, @@ -1199,7 +1237,7 @@ [docs] -class EMQ(AggregativeSoftQuantifier): +class EMQ(AggregativeSoftQuantifier): """ `Expectation Maximization for Quantification <https://ieeexplore.ieee.org/abstract/document/6789744>`_ (EMQ), aka `Saerens-Latinne-Decaestecker` (SLD) algorithm. @@ -1249,7 +1287,7 @@ ON_CALIB_ERROR_VALUES = ['raise', 'backup'] CALIB_OPTIONS = [None, 'nbvs', 'bcts', 'ts', 'vs'] - def __init__(self, classifier: BaseEstimator = None, fit_classifier=True, val_split=None, exact_train_prev=True, + def __init__(self, classifier: BaseEstimator = None, fit_classifier=True, val_split=None, exact_train_prev=True, calib=None, on_calib_error='raise', n_jobs=None): assert calib in EMQ.CALIB_OPTIONS, \ @@ -1261,12 +1299,12 @@ self.exact_train_prev = exact_train_prev self.calib = calib self.on_calib_error = on_calib_error - self.n_jobs = n_jobs + self.n_jobs = qp._get_njobs(n_jobs) [docs] @classmethod - def EMQ_BCTS(cls, classifier: BaseEstimator, fit_classifier=True, val_split=5, on_calib_error="raise", n_jobs=None): + def EMQ_BCTS(cls, classifier: BaseEstimator, fit_classifier=True, val_split=5, on_calib_error="raise", n_jobs=None): """ Constructs an instance of EMQ using the best configuration found in the `Alexandari et al. paper <http://proceedings.mlr.press/v119/alexandari20a.html>`_, i.e., one that relies on Bias-Corrected Temperature @@ -1298,23 +1336,23 @@ calib='bcts', on_calib_error=on_calib_error, n_jobs=n_jobs) - def _check_init_parameters(self): + def _check_init_parameters(self): if self.val_split is not None: if self.exact_train_prev and self.calib is None: - raise RuntimeWarning(f'The parameter {self.val_split=} was specified for EMQ, while the parameters ' - f'{self.exact_train_prev=} and {self.calib=}. This has no effect and causes an ' - f'unnecessary overload.') + warnings.warn(f'The parameter {self.val_split=} was specified for EMQ, while the parameters ' + f'{self.exact_train_prev=} and {self.calib=}. This has no effect and causes an ' + f'unnecessary overload.', RuntimeWarning) else: if self.calib is not None: - print(f'[warning] The parameter {self.calib=} requires the val_split be different from None. ' - f'This parameter will be set to 5. To avoid this warning, set this value to a float value ' - f'indicating the proportion of training data to be used as validation, or to an integer ' - f'indicating the number of folds for kFCV.') + warnings.warn(f'The parameter {self.calib=} requires the val_split be different from None. ' + f'This parameter will be set to 5. To avoid this warning, set this value to a float value ' + f'indicating the proportion of training data to be used as validation, or to an integer ' + f'indicating the number of folds for kFCV.') self.val_split = 5 [docs] - def classify(self, X): + def classify(self, X): """ Provides the posterior probabilities for the given instances. The calibration function, if required, has no effect in this step, and is only involved in the aggregate method. @@ -1327,13 +1365,13 @@ [docs] - def classifier_fit_predict(self, X, y): + def classifier_fit_predict(self, X, y): classif_predictions = super().classifier_fit_predict(X, y) self.train_prevalence = F.prevalence_from_labels(y, classes=self.classes_) return classif_predictions - def _fit_calibration(self, calibrator, P, y): + def _fit_calibration(self, calibrator, P, y): n_classes = len(self.classes_) if not np.issubdtype(y.dtype, np.number): @@ -1347,7 +1385,7 @@ elif self.on_calib_error == 'backup': self.calibration_function = lambda P: P - def _calibrate_if_requested(self, uncalib_posteriors): + def _calibrate_if_requested(self, uncalib_posteriors): if hasattr(self, 'calibration_function') and self.calibration_function is not None: try: calib_posteriors = self.calibration_function(uncalib_posteriors) @@ -1364,7 +1402,7 @@ [docs] - def aggregation_fit(self, classif_predictions, labels): + def aggregation_fit(self, classif_predictions, labels): """ Trains the aggregation function of EMQ. This comes down to recalibrating the posterior probabilities ir requested. @@ -1379,14 +1417,14 @@ requires_predictions = (self.calib is not None) or (not self.exact_train_prev) if P is None and requires_predictions: # classifier predictions were not generated because val_split=None - raise ArgumentError(self.val_split, self.__class__.__name__ + - ": Classifier predictions for the aggregative fit were not generated because " - "val_split=None. This usually happens when you enable calibrations or heuristics " - "during model selection but left val_split set to its default value (None). " - "Please provide one of the following values for val_split: (i) an integer >1 " - "(e.g. val_split=5) for k-fold cross-validation; (ii) a float in (0,1) (e.g. " - "val_split=0.3) for a proportion split; or (iii) a tuple (X, y) with explicit " - "validation data") + raise ValueError(self.__class__.__name__ + + ": Classifier predictions for the aggregative fit were not generated because " + "val_split=None. This usually happens when you enable calibrations or heuristics " + "during model selection but left val_split set to its default value (None). " + "Please provide one of the following values for val_split: (i) an integer >1 " + "(e.g. val_split=5) for k-fold cross-validation; (ii) a float in (0,1) (e.g. " + "val_split=0.3) for a proportion split; or (iii) a tuple (X, y) with explicit " + "validation data") if self.calib is not None: calibrator = _get_abstention_calibrators().get(self.calib, None) @@ -1403,7 +1441,7 @@ [docs] - def aggregate(self, classif_posteriors, epsilon=EPSILON): + def aggregate(self, classif_posteriors, epsilon=EPSILON): classif_posteriors = self._calibrate_if_requested(classif_posteriors) priors, posteriors = self.EM(self.train_prevalence, classif_posteriors, epsilon) return priors @@ -1411,7 +1449,7 @@ [docs] - def predict_proba(self, instances, epsilon=EPSILON): + def predict_proba(self, instances, epsilon=EPSILON): """ Returns the posterior probabilities updated by the EM algorithm. @@ -1428,7 +1466,7 @@ [docs] @classmethod - def EM(cls, tr_prev, posterior_probabilities, epsilon=EPSILON): + def EM(cls, tr_prev, posterior_probabilities, epsilon=EPSILON): """ Computes the `Expectation Maximization` routine. @@ -1466,7 +1504,7 @@ s += 1 if not converged: - print('[warning] the method has reached the maximum number of iterations; it might have not converged') + warnings.warn('the method has reached the maximum number of iterations; it might have not converged') return qs, ps @@ -1475,7 +1513,7 @@ [docs] -class HDy(AggregativeSoftQuantifier, BinaryAggregativeQuantifier): +class HDy(AggregativeSoftQuantifier, BinaryAggregativeQuantifier): """ `Hellinger Distance y <https://www.sciencedirect.com/science/article/pii/S0020025512004069>`_ (HDy). HDy is a probabilistic method for training binary quantifiers, that models quantification as the problem of @@ -1502,12 +1540,12 @@ for `k`); or as a tuple (X,y) defining the specific set of data to use for validation. """ - def __init__(self, classifier: BaseEstimator = None, fit_classifier=True, val_split=5): + def __init__(self, classifier: BaseEstimator = None, fit_classifier=True, val_split=5): super().__init__(classifier, fit_classifier, val_split) [docs] - def aggregation_fit(self, classif_predictions, labels): + def aggregation_fit(self, classif_predictions, labels): """ Trains the aggregation function of HDy. @@ -1522,7 +1560,7 @@ # pre-compute the histogram for positive and negative examples self.bins = np.linspace(10, 110, 11, dtype=int) # [10, 20, 30, ..., 100, 110] - def hist(P, bins): + def hist(P, bins): h = np.histogram(P, bins=bins, range=(0, 1), density=True)[0] return h / h.sum() @@ -1532,7 +1570,7 @@ [docs] - def aggregate(self, classif_posteriors): + def aggregate(self, classif_posteriors): # "In this work, the number of bins b used in HDx and HDy was chosen from 10 to 110 in steps of 10, # and the final estimated a priori probability was taken as the median of these 11 estimates." # (González-Castro, et al., 2013). @@ -1549,7 +1587,7 @@ # the authors proposed to search for the prevalence yielding the best matching as a linear search # at small steps (modern implementations resort to an optimization procedure, # see class DistributionMatching) - def loss(prev): + def loss(prev): class1_prev = prev[1] Px_train = class1_prev * Pxy1_density + (1 - class1_prev) * Pxy0_density return F.HellingerDistance(Px_train, Px_test) @@ -1564,7 +1602,7 @@ [docs] -class DyS(AggregativeSoftQuantifier, BinaryAggregativeQuantifier): +class DyS(AggregativeSoftQuantifier, BinaryAggregativeQuantifier): """ `DyS framework <https://ojs.aaai.org/index.php/AAAI/article/view/4376>`_ (DyS). DyS is a generalization of HDy method, using a Ternary Search in order to find the prevalence that @@ -1593,15 +1631,15 @@ :param n_jobs: number of parallel workers. """ - def __init__(self, classifier: BaseEstimator = None, fit_classifier=True, val_split=5, n_bins=8, + def __init__(self, classifier: BaseEstimator = None, fit_classifier=True, val_split=5, n_bins=8, divergence: Union[str, Callable] = 'HD', tol=1e-05, n_jobs=None): super().__init__(classifier, fit_classifier, val_split) self.tol = tol self.divergence = divergence self.n_bins = n_bins - self.n_jobs = n_jobs + self.n_jobs = qp._get_njobs(n_jobs) - def _ternary_search(self, f, left, right, tol): + def _ternary_search(self, f, left, right, tol): """ Find maximum of unimodal function f() within [left, right] """ @@ -1619,7 +1657,7 @@ [docs] - def aggregation_fit(self, classif_predictions, labels): + def aggregation_fit(self, classif_predictions, labels): """ Trains the aggregation function of DyS. @@ -1637,13 +1675,13 @@ [docs] - def aggregate(self, classif_posteriors): + def aggregate(self, classif_posteriors): Px = classif_posteriors[:, self.pos_label] # takes only the P(y=+1|x) Px_test = np.histogram(Px, bins=self.n_bins, range=(0, 1), density=True)[0] divergence = get_divergence(self.divergence) - def distribution_distance(prev): + def distribution_distance(prev): Px_train = prev * self.Pxy1_density + (1 - prev) * self.Pxy0_density return divergence(Px_train, Px_test) @@ -1655,7 +1693,7 @@ [docs] -class SMM(AggregativeSoftQuantifier, BinaryAggregativeQuantifier): +class SMM(AggregativeSoftQuantifier, BinaryAggregativeQuantifier): """ `SMM method <https://ieeexplore.ieee.org/document/9260028>`_ (SMM). SMM is a simplification of matching distribution methods where the representation of the examples @@ -1674,12 +1712,12 @@ for `k`); or as a tuple (X,y) defining the specific set of data to use for validation. """ - def __init__(self, classifier: BaseEstimator = None, fit_classifier=True, val_split=5): + def __init__(self, classifier: BaseEstimator = None, fit_classifier=True, val_split=5): super().__init__(classifier, fit_classifier, val_split) [docs] - def aggregation_fit(self, classif_predictions, labels): + def aggregation_fit(self, classif_predictions, labels): """ Trains the aggregation function of SMM. @@ -1697,7 +1735,7 @@ [docs] - def aggregate(self, classif_posteriors): + def aggregate(self, classif_posteriors): Px = classif_posteriors[:, self.pos_label] # takes only the P(y=+1|x) Px_mean = np.mean(Px) @@ -1709,7 +1747,7 @@ [docs] -class DMy(AggregativeSoftQuantifier): +class DMy(AggregativeSoftQuantifier): """ Generic Distribution Matching quantifier for binary or multiclass quantification based on the space of posterior probabilities. This implementation takes the number of bins, the divergence, and the possibility to work on CDF @@ -1742,19 +1780,19 @@ :param n_jobs: number of parallel workers (default None) """ - def __init__(self, classifier: BaseEstimator = None, fit_classifier=True, val_split=5, nbins=8, + def __init__(self, classifier: BaseEstimator = None, fit_classifier=True, val_split=5, nbins=8, divergence: Union[str, Callable] = 'HD', cdf=False, search='optim_minimize', n_jobs=None): super().__init__(classifier, fit_classifier, val_split) self.nbins = nbins self.divergence = divergence self.cdf = cdf self.search = search - self.n_jobs = n_jobs + self.n_jobs = qp._get_njobs(n_jobs) [docs] @classmethod - def HDy(cls, classifier: BaseEstimator = None, fit_classifier=True, val_split=5, n_jobs=None): + def HDy(cls, classifier: BaseEstimator = None, fit_classifier=True, val_split=5, n_jobs=None): """ Historical HDy preset expressed as a configuration of :class:`DMy`. @@ -1783,7 +1821,7 @@ return AggregativeMedianEstimator(base_quantifier=base, param_grid=param_grid, n_jobs=n_jobs) - def _get_distributions(self, posteriors): + def _get_distributions(self, posteriors): histograms = [] post_dims = posteriors.shape[1] if post_dims == 2: @@ -1801,7 +1839,7 @@ [docs] - def aggregation_fit(self, classif_predictions, labels): + def aggregation_fit(self, classif_predictions, labels): """ Trains the aggregation function of a distribution matching method. This comes down to generating the validation distributions out of the training data. @@ -1829,7 +1867,7 @@ [docs] - def aggregate(self, posteriors: np.ndarray): + def aggregate(self, posteriors: np.ndarray): """ Searches for the mixture model parameter (the sought prevalence values) that yields a validation distribution (the mixture) that best matches the test distribution, in terms of the divergence measure of choice. @@ -1844,7 +1882,7 @@ divergence = get_divergence(self.divergence) n_classes, n_channels, nbins = self.validation_distribution.shape - def loss(prev): + def loss(prev): prev = np.expand_dims(prev, axis=0) mixture_distribution = (prev @ self.validation_distribution.reshape(n_classes, -1)).reshape(n_channels, -1) divs = [divergence(test_distribution[ch], mixture_distribution[ch]) for ch in range(n_channels)] @@ -1857,7 +1895,7 @@ [docs] -def newELM(svmperf_base=None, loss='01', C=1): +def newELM(svmperf_base=None, loss='01', C=1): """ Explicit Loss Minimization (ELM) quantifiers. Quantifiers based on ELM represent a family of methods based on structured output learning; @@ -1887,7 +1925,7 @@ [docs] -def newSVMQ(svmperf_base=None, C=1): +def newSVMQ(svmperf_base=None, C=1): """ SVM(Q) is an Explicit Loss Minimization (ELM) quantifier set to optimize for the `Q` loss combining a classification-oriented loss and a quantification-oriented loss, as proposed by @@ -1914,7 +1952,9 @@ -def newSVMKLD(svmperf_base=None, C=1): + +[docs] +def newSVMKLD(svmperf_base=None, C=1): """ SVM(KLD) is an Explicit Loss Minimization (ELM) quantifier set to optimize for the Kullback-Leibler Divergence as proposed by `Esuli et al. 2015 <https://dl.acm.org/doi/abs/10.1145/2700406>`_. @@ -1936,14 +1976,15 @@ :return: returns an instance of CC set to work with SVMperf (with loss and C set properly) as the underlying classifier """ - return newELM(svmperf_base, loss='kld', C=C) + return newELM(svmperf_base, loss='kld', C=C) - -[docs] -def newSVMKLD(svmperf_base=None, C=1): + + +[docs] +def newSVMNKLD(svmperf_base=None, C=1): """ - SVM(KLD) is an Explicit Loss Minimization (ELM) quantifier set to optimize for the Kullback-Leibler Divergence + SVM(NKLD) is an Explicit Loss Minimization (ELM) quantifier set to optimize for the Kullback-Leibler Divergence normalized via the logistic function, as proposed by `Esuli et al. 2015 <https://dl.acm.org/doi/abs/10.1145/2700406>`_. Equivalent to: @@ -1970,7 +2011,7 @@ [docs] -def newSVMAE(svmperf_base=None, C=1): +def newSVMAE(svmperf_base=None, C=1): """ SVM(KLD) is an Explicit Loss Minimization (ELM) quantifier set to optimize for the Absolute Error as first used by `Moreo and Sebastiani, 2021 <https://arxiv.org/abs/2011.02552>`_. @@ -1998,7 +2039,7 @@ [docs] -def newSVMRAE(svmperf_base=None, C=1): +def newSVMRAE(svmperf_base=None, C=1): """ SVM(KLD) is an Explicit Loss Minimization (ELM) quantifier set to optimize for the Relative Absolute Error as first used by `Moreo and Sebastiani, 2021 <https://arxiv.org/abs/2011.02552>`_. @@ -2026,7 +2067,7 @@ [docs] -class OneVsAllAggregative(OneVsAllGeneric, AggregativeQuantifier): +class OneVsAllAggregative(OneVsAllGeneric, AggregativeQuantifier): """ Allows any binary quantifier to perform quantification on single-label datasets. The method maintains one binary quantifier for each class, and then l1-normalizes the outputs so that the @@ -2042,7 +2083,7 @@ is removed and no longer available at predict time. """ - def __init__(self, binary_quantifier=None, n_jobs=None, parallel_backend='multiprocessing'): + def __init__(self, binary_quantifier=None, n_jobs=None, parallel_backend='multiprocessing'): if binary_quantifier is None: binary_quantifier = PACC() assert isinstance(binary_quantifier, BaseQuantifier), \ @@ -2055,7 +2096,7 @@ [docs] - def classify(self, X): + def classify(self, X): """ If the base quantifier is not probabilistic, returns a matrix of shape `(n,m,)` with `n` the number of instances and `m` the number of classes. The entry `(i,j)` is a binary value indicating whether instance @@ -2079,34 +2120,34 @@ [docs] - def aggregate(self, classif_predictions): + def aggregate(self, classif_predictions): prevalences = self._parallel(self._delayed_binary_aggregate, classif_predictions) return F.normalize_prevalence(prevalences) [docs] - def aggregation_fit(self, classif_predictions, labels): - self._parallel(self._delayed_binary_aggregate_fit(c, classif_predictions, labels)) + def aggregation_fit(self, classif_predictions, labels): + self._parallel(self._delayed_binary_aggregate_fit, classif_predictions, labels) return self - def _delayed_binary_classification(self, c, X): + def _delayed_binary_classification(self, c, X): return self.dict_binary_quantifiers[c].classify(X) - def _delayed_binary_aggregate(self, c, classif_predictions): + def _delayed_binary_aggregate(self, c, classif_predictions): # the estimation for the positive class prevalence return self.dict_binary_quantifiers[c].aggregate(classif_predictions[:, c])[1] - def _delayed_binary_aggregate_fit(self, c, classif_predictions, labels): + def _delayed_binary_aggregate_fit(self, c, classif_predictions, labels): # trains the aggregation function of the cth quantifier - return self.dict_binary_quantifiers[c].aggregate_fit(classif_predictions[:, c], labels) + return self.dict_binary_quantifiers[c].aggregation_fit(classif_predictions[:, c], labels == c) [docs] -class AggregativeMedianEstimator(BinaryQuantifier): +class AggregativeMedianEstimator(BinaryQuantifier): """ This method is a meta-quantifier that returns, as the estimated class prevalence values, the median of the estimation returned by differently (hyper)parameterized base quantifiers. @@ -2119,7 +2160,7 @@ :param n_jobs: number of parallel workers """ - def __init__(self, base_quantifier: AggregativeQuantifier, param_grid: dict, random_state=None, n_jobs=None): + def __init__(self, base_quantifier: AggregativeQuantifier, param_grid: dict, random_state=None, n_jobs=None): self.base_quantifier = base_quantifier self.param_grid = param_grid self.random_state = random_state @@ -2127,17 +2168,17 @@ [docs] - def get_params(self, deep=True): + def get_params(self, deep=True): return self.base_quantifier.get_params(deep) [docs] - def set_params(self, **params): + def set_params(self, **params): self.base_quantifier.set_params(**params) - def _delayed_fit(self, args): + def _delayed_fit(self, args): with qp.util.temp_seed(self.random_state): params, X, y = args model = deepcopy(self.base_quantifier) @@ -2145,7 +2186,7 @@ model.fit(X, y) return model - def _delayed_fit_classifier(self, args): + def _delayed_fit_classifier(self, args): with qp.util.temp_seed(self.random_state): cls_params, X, y = args model = deepcopy(self.base_quantifier) @@ -2153,7 +2194,7 @@ predictions, labels = model.classifier_fit_predict(X, y) return (model, predictions, labels) - def _delayed_fit_aggregation(self, args): + def _delayed_fit_aggregation(self, args): with qp.util.temp_seed(self.random_state): ((model, predictions, y), q_params) = args model = deepcopy(model) @@ -2163,8 +2204,8 @@ [docs] - def fit(self, X, y): - import itertools + def fit(self, X, y): + import itertools self._check_binary(y, self.__class__.__name__) @@ -2177,8 +2218,7 @@ ((params, X, y) for params in cls_configs), seed=qp.environ.get('_R_SEED', None), n_jobs=self.n_jobs, - asarray=False, - backend='threading' + asarray=False ) else: model = self.base_quantifier @@ -2190,8 +2230,7 @@ self._delayed_fit_aggregation, itertools.product(models_preds, q_configs), seed=qp.environ.get('_R_SEED', None), - n_jobs=self.n_jobs, - backend='threading' + n_jobs=self.n_jobs ) else: configs = qp.model_selection.expand_grid(self.param_grid) @@ -2199,25 +2238,23 @@ self._delayed_fit, ((params, X, y) for params in configs), seed=qp.environ.get('_R_SEED', None), - n_jobs=self.n_jobs, - backend='threading' + n_jobs=self.n_jobs ) return self - def _delayed_predict(self, args): + def _delayed_predict(self, args): model, instances = args return model.predict(instances) [docs] - def predict(self, instances): + def predict(self, instances): prev_preds = qp.util.parallel( self._delayed_predict, ((model, instances) for model in self.models), seed=qp.environ.get('_R_SEED', None), - n_jobs=self.n_jobs, - backend='threading' + n_jobs=self.n_jobs ) return np.median(prev_preds, axis=0) @@ -2228,7 +2265,7 @@ # imports # --------------------------------------------------------------- -from . import _threshold_optim +from . import _threshold_optim T50 = _threshold_optim.T50 MAX = _threshold_optim.MAX @@ -2236,7 +2273,7 @@ MS = _threshold_optim.MS MS2 = _threshold_optim.MS2 -from . import _kdey +from . import _kdey KDEyML = _kdey.KDEyML KDEyHD = _kdey.KDEyHD diff --git a/docs/build/html/_modules/quapy/method/base.html b/docs/build/html/_modules/quapy/method/base.html index e649c5b..5519615 100644 --- a/docs/build/html/_modules/quapy/method/base.html +++ b/docs/build/html/_modules/quapy/method/base.html @@ -1,95 +1,392 @@ - - - - - - quapy.method.base — QuaPy: A Python-based open-source framework for quantification 0.2.1 documentation - - + + + + + + + + quapy.method.base — QuaPy: A Python-based open-source framework for quantification 0.2.1 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Skip to main content - - - - - - - - - + + + + Back to top + + + + + + + + + + Ctrl+K + + - - - - - - - - - - QuaPy: A Python-based open-source framework for quantification - - - - - - - + + - - -
Source code for quapy.classification.neural -import os -from abc import ABCMeta, abstractmethod -from pathlib import Path +import logging +import os +from abc import ABCMeta, abstractmethod +from pathlib import Path -import numpy as np -import torch -import torch.nn as nn -import torch.nn.functional as F -from sklearn.metrics import accuracy_score, f1_score -from torch.nn.utils.rnn import pad_sequence -from tqdm import tqdm +import numpy as np +import torch +import torch.nn as nn +import torch.nn.functional as F +from sklearn.metrics import accuracy_score, f1_score +from torch.nn.utils.rnn import pad_sequence +from tqdm import tqdm -import quapy as qp -from quapy.data import LabelledCollection -from quapy.util import EarlyStop +import quapy as qp +from quapy.data import LabelledCollection +from quapy.util import EarlyStop [docs] -class NeuralClassifierTrainer: +class NeuralClassifierTrainer: """ Trains a neural network for text classification. @@ -407,7 +408,7 @@ according to the evaluation in the held-out validation split (default '../checkpoint/classifier_net.dat') """ - def __init__(self, + def __init__(self, net: 'TextClassifierNet', lr=1e-3, weight_decay=0, @@ -416,7 +417,7 @@ batch_size=64, batch_size_test=512, padding_length=300, - device='cuda', + device='cpu', checkpointpath='../checkpoint/classifier_net.dat'): super().__init__() @@ -437,12 +438,12 @@ self.learner_hyperparams = self.net.get_params() self.checkpointpath = checkpointpath - print(f'[NeuralNetwork running on {device}]') + logging.getLogger(__name__).info(f'NeuralNetwork running on {device}') os.makedirs(Path(checkpointpath).parent, exist_ok=True) [docs] - def reset_net_params(self, vocab_size, n_classes): + def reset_net_params(self, vocab_size, n_classes): """Reinitialize the network parameters :param vocab_size: the size of the vocabulary @@ -455,7 +456,7 @@ [docs] - def get_params(self): + def get_params(self): """Get hyper-parameters for this estimator :return: a dictionary with parameter names mapped to their values @@ -465,7 +466,7 @@ [docs] - def set_params(self, **params): + def set_params(self, **params): """Set the parameters of this trainer and the learner it is training. In this current version, parameter names for the trainer and learner should be disjoint. @@ -491,14 +492,14 @@ @property - def device(self): + def device(self): """ Gets the device in which the network is allocated :return: device """ return next(self.net.parameters()).device - def _train_epoch(self, data, status, pbar, epoch): + def _train_epoch(self, data, status, pbar, epoch): self.net.train() criterion = torch.nn.CrossEntropyLoss() losses, predictions, true_labels = [], [], [] @@ -518,7 +519,7 @@ status["f1"] = f1_score(true_labels, predictions, average='macro') self.__update_progress_bar(pbar, epoch) - def _test_epoch(self, data, status, pbar, epoch): + def _test_epoch(self, data, status, pbar, epoch): self.net.eval() criterion = torch.nn.CrossEntropyLoss() losses, predictions, true_labels = [], [], [] @@ -536,7 +537,7 @@ status["f1"] = f1_score(true_labels, predictions, average='macro') self.__update_progress_bar(pbar, epoch) - def __update_progress_bar(self, pbar, epoch): + def __update_progress_bar(self, pbar, epoch): pbar.set_description(f'[{self.net.__class__.__name__}] training epoch={epoch} ' f'tr-loss={self.status["tr"]["loss"]:.5f} ' f'tr-acc={100 * self.status["tr"]["acc"]:.2f}% ' @@ -548,7 +549,7 @@ [docs] - def fit(self, instances, labels, val_split=0.3): + def fit(self, instances, labels, val_split=0.3): """ Fits the model according to the given training data. @@ -583,21 +584,22 @@ if self.early_stop.IMPROVED: torch.save(self.net.state_dict(), checkpoint) elif self.early_stop.STOP: - print(f'training ended by patience exhasted; loading best model parameters in {checkpoint} ' - f'for epoch {self.early_stop.best_epoch}') + logging.getLogger(__name__).info( + f'training ended by patience exhausted; loading best model parameters in {checkpoint} ' + f'for epoch {self.early_stop.best_epoch}') self.net.load_state_dict(torch.load(checkpoint)) break - print('performing one training pass over the validation set...') + logging.getLogger(__name__).info('performing one training pass over the validation set...') self._train_epoch(valid_generator, self.status['tr'], pbar, epoch=0) - print('[done]') + logging.getLogger(__name__).info('done') return self [docs] - def predict(self, instances): + def predict(self, instances): """ Predicts labels for the instances @@ -610,7 +612,7 @@ [docs] - def predict_proba(self, instances): + def predict_proba(self, instances): """ Predicts posterior probabilities for the instances @@ -629,7 +631,7 @@ [docs] - def transform(self, instances): + def transform(self, instances): """ Returns the embeddings of the instances @@ -651,7 +653,7 @@ [docs] -class TorchDataset(torch.utils.data.Dataset): +class TorchDataset(torch.utils.data.Dataset): """ Transforms labelled instances into a Torch's :class:`torch.utils.data.DataLoader` object @@ -659,19 +661,19 @@ :param labels: array-like of shape `(n_samples, n_classes)` with the class labels """ - def __init__(self, instances, labels=None): + def __init__(self, instances, labels=None): self.instances = instances self.labels = labels - def __len__(self): + def __len__(self): return len(self.instances) - def __getitem__(self, index): + def __getitem__(self, index): return {'doc': self.instances[index], 'label': self.labels[index] if self.labels is not None else None} [docs] - def asDataloader(self, batch_size, shuffle, pad_length, device): + def asDataloader(self, batch_size, shuffle, pad_length, device): """ Converts the labelled collection into a Torch DataLoader with dynamic padding for the batch @@ -684,7 +686,7 @@ :param device: whether to allocate tensors in cpu or in cuda :return: a :class:`torch.utils.data.DataLoader` object """ - def collate(batch): + def collate(batch): data = [torch.LongTensor(item['doc'][:pad_length]) for item in batch] data = pad_sequence(data, batch_first=True, padding_value=qp.environ['PAD_INDEX']).to(device) targets = [item['label'] for item in batch] @@ -702,7 +704,7 @@ [docs] -class TextClassifierNet(torch.nn.Module, metaclass=ABCMeta): +class TextClassifierNet(torch.nn.Module, metaclass=ABCMeta): """ Abstract Text classifier (`torch.nn.Module`) """ @@ -710,7 +712,7 @@ [docs] @abstractmethod - def document_embedding(self, x): + def document_embedding(self, x): """Embeds documents (i.e., performs the forward pass up to the next-to-last layer). @@ -725,7 +727,7 @@ [docs] - def forward(self, x): + def forward(self, x): """Performs the forward pass. :param x: a batch of instances, typically generated by a torch's `DataLoader` @@ -739,7 +741,7 @@ [docs] - def dimensions(self): + def dimensions(self): """Gets the number of dimensions of the embedding space :return: integer @@ -749,7 +751,7 @@ [docs] - def predict_proba(self, x): + def predict_proba(self, x): """ Predicts posterior probabilities for the instances in `x` @@ -764,7 +766,7 @@ [docs] - def xavier_uniform(self): + def xavier_uniform(self): """ Performs Xavier initialization of the network parameters """ @@ -776,7 +778,7 @@ [docs] @abstractmethod - def get_params(self): + def get_params(self): """ Get hyper-parameters for this estimator @@ -786,7 +788,7 @@ @property - def vocabulary_size(self): + def vocabulary_size(self): """ Return the size of the vocabulary @@ -798,7 +800,7 @@ [docs] -class LSTMnet(TextClassifierNet): +class LSTMnet(TextClassifierNet): """ An implementation of :class:`quapy.classification.neural.TextClassifierNet` based on Long Short Term Memory networks. @@ -812,7 +814,7 @@ :param drop_p: drop probability for dropout (default 0.5) """ - def __init__(self, vocabulary_size, n_classes, embedding_size=100, hidden_size=256, repr_size=100, lstm_class_nlayers=1, + def __init__(self, vocabulary_size, n_classes, embedding_size=100, hidden_size=256, repr_size=100, lstm_class_nlayers=1, drop_p=0.5): super().__init__() @@ -834,7 +836,7 @@ self.doc_embedder = torch.nn.Linear(hidden_size, self.dim) self.output = torch.nn.Linear(self.dim, n_classes) - def __init_hidden(self, set_size): + def __init_hidden(self, set_size): opt = self.hyperparams var_hidden = torch.zeros(opt['lstm_class_nlayers'], set_size, opt['hidden_size']) var_cell = torch.zeros(opt['lstm_class_nlayers'], set_size, opt['hidden_size']) @@ -844,7 +846,7 @@ [docs] - def document_embedding(self, x): + def document_embedding(self, x): """Embeds documents (i.e., performs the forward pass up to the next-to-last layer). @@ -863,7 +865,7 @@ [docs] - def get_params(self): + def get_params(self): """ Get hyper-parameters for this estimator @@ -873,7 +875,7 @@ @property - def vocabulary_size(self): + def vocabulary_size(self): """ Return the size of the vocabulary @@ -885,7 +887,7 @@ [docs] -class CNNnet(TextClassifierNet): +class CNNnet(TextClassifierNet): """ An implementation of :class:`quapy.classification.neural.TextClassifierNet` based on Convolutional Neural Networks. @@ -902,7 +904,7 @@ :param drop_p: drop probability for dropout (default 0.5) """ - def __init__(self, vocabulary_size, n_classes, embedding_size=100, hidden_size=256, repr_size=100, + def __init__(self, vocabulary_size, n_classes, embedding_size=100, hidden_size=256, repr_size=100, kernel_heights=[3, 5, 7], stride=1, padding=0, drop_p=0.5): super(CNNnet, self).__init__() @@ -927,7 +929,7 @@ self.doc_embedder = torch.nn.Linear(len(kernel_heights) * hidden_size, self.dim) self.output = nn.Linear(self.dim, n_classes) - def __conv_block(self, input, conv_layer): + def __conv_block(self, input, conv_layer): conv_out = conv_layer(input) # conv_out.size() = (batch_size, out_channels, dim, 1) activation = F.relu(conv_out.squeeze(3)) # activation.size() = (batch_size, out_channels, dim1) max_out = F.max_pool1d(activation, activation.size()[2]).squeeze(2) # maxpool_out.size() = (batch_size, out_channels) @@ -935,7 +937,7 @@ [docs] - def document_embedding(self, input): + def document_embedding(self, input): """Embeds documents (i.e., performs the forward pass up to the next-to-last layer). @@ -960,7 +962,7 @@ [docs] - def get_params(self): + def get_params(self): """ Get hyper-parameters for this estimator @@ -970,7 +972,7 @@ @property - def vocabulary_size(self): + def vocabulary_size(self): """ Return the size of the vocabulary diff --git a/docs/build/html/_modules/quapy/classification/svmperf.html b/docs/build/html/_modules/quapy/classification/svmperf.html index 9b60c1a..e855546 100644 --- a/docs/build/html/_modules/quapy/classification/svmperf.html +++ b/docs/build/html/_modules/quapy/classification/svmperf.html @@ -1,94 +1,391 @@ - - - - - - quapy.classification.svmperf — QuaPy: A Python-based open-source framework for quantification 0.2.1 documentation - - + + + + + + + + quapy.classification.svmperf — QuaPy: A Python-based open-source framework for quantification 0.2.1 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Skip to main content - - - - - - - - - + + + + Back to top + + + + + + + + + + Ctrl+K + + - - - - - - - - - - QuaPy: A Python-based open-source framework for quantification - - - - - - - + + - - -Quickstart - - -Manuals - - -API - - - - + + + + + + + + + + + - - - QuaPy: A Python-based open-source framework for quantification - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Home + + + + + + + Manuals + + + + + + + API + + - - - - - - Module code - quapy.classification.svmperf - - - + + + + + + + + + + + + + Search + Ctrl+K + + + + + + + + + + + + + + System Settings + Light + Dark + + + + + + + + + + + + + + GitHub + + + + + + + + + + + + + Search + Ctrl+K + + + + + - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Home + + + + + + + Manuals + + + + + + + API + + + + + + + + + + + + + + + + + + + + + + + System Settings + Light + Dark + + + + + + + + + + + + + + GitHub + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Module code + + quapy.classification.svmperf + + + + + + + + + + + + + + + + Source code for quapy.classification.svmperf -import random -import shutil -import subprocess -import tempfile -from os import remove, makedirs -from os.path import join, exists -from subprocess import PIPE, STDOUT -import numpy as np -from sklearn.base import BaseEstimator, ClassifierMixin -from sklearn.datasets import dump_svmlight_file +import logging +import random +import shutil +import subprocess +import tempfile +from os import remove, makedirs +from os.path import join, exists +from subprocess import PIPE, STDOUT +import numpy as np +from sklearn.base import BaseEstimator, ClassifierMixin +from sklearn.datasets import dump_svmlight_file [docs] -class SVMperf(BaseEstimator, ClassifierMixin): +class SVMperf(BaseEstimator, ClassifierMixin): """A wrapper for the `SVM-perf package <https://www.cs.cornell.edu/people/tj/svm_light/svm_perf.html>`__ by Thorsten Joachims. When using losses for quantification, the source code has to be patched. See the `installation documentation <https://hlt-isti.github.io/QuaPy/build/html/Installation.html#svm-perf-with-quantification-oriented-losses>`__ @@ -110,7 +407,7 @@ # losses with their respective codes in svm_perf implementation valid_losses = {'01':0, 'f1':1, 'kld':12, 'nkld':13, 'q':22, 'qacc':23, 'qf1':24, 'qgm':25, 'mae':26, 'mrae':27} - def __init__(self, svmperf_base, C=0.01, verbose=False, loss='01', host_folder=None): + def __init__(self, svmperf_base, C=0.01, verbose=False, loss='01', host_folder=None): assert exists(svmperf_base), \ (f'path {svmperf_base} does not seem to point to a valid path;' f'did you install svm-perf? ' @@ -123,7 +420,7 @@ [docs] - def fit(self, X, y): + def fit(self, X, y): """ Trains the SVM for the multivariate performance loss @@ -146,8 +443,7 @@ # this would allow to run parallel instances of predict random_code = 'svmperfprocess'+'-'.join(str(local_random.randint(0, 1000000)) for _ in range(5)) if self.host_folder is None: - # tmp dir are removed after the fit terminates in multiprocessing... - self.tmpdir = tempfile.TemporaryDirectory(suffix=random_code).name + self.tmpdir = join(tempfile.gettempdir(), random_code) else: self.tmpdir = join(self.host_folder, '.' + random_code) makedirs(self.tmpdir, exist_ok=True) @@ -159,21 +455,21 @@ cmd = ' '.join([self.svmperf_learn, self.c_cmd, self.loss_cmd, traindat, self.model]) if self.verbose: - print('[Running]', cmd) - p = subprocess.run(cmd.split(), stdout=PIPE, stderr=STDOUT) + logging.getLogger(__name__).info(f'[Running] {cmd}') + p = subprocess.run(cmd.split(), stdout=PIPE, stderr=PIPE) if not exists(self.model): - print(p.stderr.decode('utf-8')) + logging.getLogger(__name__).error(p.stderr.decode('utf-8')) remove(traindat) if self.verbose: - print(p.stdout.decode('utf-8')) + logging.getLogger(__name__).info(p.stdout.decode('utf-8')) return self [docs] - def predict(self, X): + def predict(self, X): """ Predicts labels for the instances `X` @@ -188,7 +484,7 @@ [docs] - def decision_function(self, X, y=None): + def decision_function(self, X, y=None): """ Evaluate the decision function for the samples in `X`. @@ -211,11 +507,11 @@ cmd = ' '.join([self.svmperf_classify, testdat, self.model, predictions_path]) if self.verbose: - print('[Running]', cmd) + logging.getLogger(__name__).info(f'[Running] {cmd}') p = subprocess.run(cmd.split(), stdout=PIPE, stderr=STDOUT) if self.verbose: - print(p.stdout.decode('utf-8')) + logging.getLogger(__name__).info(p.stdout.decode('utf-8')) scores = np.loadtxt(predictions_path) remove(testdat) @@ -224,38 +520,82 @@ return scores - def __del__(self): + def __del__(self): if hasattr(self, 'tmpdir'): shutil.rmtree(self.tmpdir, ignore_errors=True) - + + + + + + + + + + + + + - + + + + - Built with Sphinx using a - theme - provided by Read the Docs. - + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/build/html/_modules/quapy/data/base.html b/docs/build/html/_modules/quapy/data/base.html index 27f34de..87203ec 100644 --- a/docs/build/html/_modules/quapy/data/base.html +++ b/docs/build/html/_modules/quapy/data/base.html @@ -1,96 +1,392 @@ - - - - - - quapy.data.base — QuaPy: A Python-based open-source framework for quantification 0.2.1 documentation - - + + + + + + + + quapy.data.base — QuaPy: A Python-based open-source framework for quantification 0.2.1 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Skip to main content - - - - - - - - - + + + + Back to top + + + + + + + + + + Ctrl+K + + - - - - - - - - - - QuaPy: A Python-based open-source framework for quantification - - - - - - - + + - - -Quickstart - - -Manuals - - -API - - - - + + + + + + + + + + + - - - QuaPy: A Python-based open-source framework for quantification - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Home + + + + + + + Manuals + + + + + + + API + + - - - - - - Module code - quapy.data.base - - - - - - - - Source code for quapy.data.base -import itertools -from functools import cached_property -from typing import Iterable + + + + + + + + + -import numpy as np -from scipy.sparse import issparse -from scipy.sparse import vstack -from sklearn.model_selection import train_test_split, RepeatedStratifiedKFold -from numpy.random import RandomState -from quapy.functional import strprev -from quapy.util import temp_seed -import quapy.functional as F + + + Search + Ctrl+K + + + + + + + + + + + + + + System Settings + Light + Dark + + + + + + + + + + + + + + GitHub + + + + + + + + + + + + + Search + Ctrl+K + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Home + + + + + + + Manuals + + + + + + + API + + + + + + + + + + + + + + + + + + + + + + + System Settings + Light + Dark + + + + + + + + + + + + + + GitHub + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Module code + + quapy.data.base + + + + + + + + + + + + + + + + + Source code for quapy.data.base +import itertools +from functools import cached_property +from typing import Iterable + +import numpy as np +from scipy.sparse import issparse +from scipy.sparse import vstack +from sklearn.model_selection import train_test_split, RepeatedStratifiedKFold +from numpy.random import RandomState +from quapy.functional import strprev +from quapy.util import temp_seed +import quapy.functional as F [docs] -class LabelledCollection: +class LabelledCollection: """ A LabelledCollection is a set of objects each with a label attached to each of them. This class implements several sampling routines and other utilities. @@ -102,7 +398,7 @@ (i.e., a prevalence of 0) """ - def __init__(self, instances, labels, classes=None): + def __init__(self, instances, labels, classes=None): if issparse(instances): self.instances = instances elif isinstance(instances, list) and len(instances) > 0 and isinstance(instances[0], str): @@ -121,7 +417,7 @@ self._index = None @property - def index(self): + def index(self): if not hasattr(self, '_index') or self._index is None: self._index = {class_: np.arange(len(self))[self.labels == class_] for class_ in self.classes_} return self._index @@ -129,7 +425,7 @@ [docs] @classmethod - def load(cls, path: str, loader_func: callable, classes=None, **loader_kwargs): + def load(cls, path: str, loader_func: callable, classes=None, **loader_kwargs): """ Loads a labelled set of data and convert it into a :class:`LabelledCollection` instance. The function in charge of reading the instances must be specified. This function can be a custom one, or any of the reading functions @@ -146,7 +442,7 @@ return LabelledCollection(*loader_func(path, **loader_kwargs), classes) - def __len__(self): + def __len__(self): """ Returns the length of this collection (number of labelled instances) @@ -156,7 +452,7 @@ [docs] - def prevalence(self): + def prevalence(self): """ Returns the prevalence, or relative frequency, of the classes in the codeframe. @@ -168,7 +464,7 @@ [docs] - def counts(self): + def counts(self): """ Returns the number of instances for each of the classes in the codeframe. @@ -179,7 +475,7 @@ @property - def n_classes(self): + def n_classes(self): """ The number of classes @@ -188,7 +484,7 @@ return len(self.classes_) @property - def n_instances(self): + def n_instances(self): """ The number of instances @@ -197,7 +493,7 @@ return len(self.labels) @property - def binary(self): + def binary(self): """ Returns True if the number of classes is 2 @@ -207,7 +503,7 @@ [docs] - def sampling_index(self, size, *prevs, shuffle=True, random_state=None): + def sampling_index(self, size, *prevs, shuffle=True, random_state=None): """ Returns an index to be used to extract a random sample of desired size and desired prevalence values. If the prevalence values are not specified, then returns the index of a uniform sampling. @@ -270,7 +566,7 @@ [docs] - def uniform_sampling_index(self, size, random_state=None): + def uniform_sampling_index(self, size, random_state=None): """ Returns an index to be used to extract a uniform sample of desired size. The sampling is drawn with replacement. @@ -288,7 +584,7 @@ [docs] - def sampling(self, size, *prevs, shuffle=True, random_state=None): + def sampling(self, size, *prevs, shuffle=True, random_state=None): """ Return a random sample (an instance of :class:`LabelledCollection`) of desired size and desired prevalence values. For each class, the sampling is drawn with replacement. @@ -308,7 +604,7 @@ [docs] - def uniform_sampling(self, size, random_state=None): + def uniform_sampling(self, size, random_state=None): """ Returns a uniform sample (an instance of :class:`LabelledCollection`) of desired size. The sampling is drawn with replacement. @@ -323,7 +619,7 @@ [docs] - def sampling_from_index(self, index): + def sampling_from_index(self, index): """ Returns an instance of :class:`LabelledCollection` whose elements are sampled from this collection using the index. @@ -338,7 +634,7 @@ [docs] - def split_stratified(self, train_prop=0.6, random_state=None): + def split_stratified(self, train_prop=0.6, random_state=None): """ Returns two instances of :class:`LabelledCollection` split with stratification from this collection, at desired proportion. @@ -360,7 +656,7 @@ [docs] - def split_random(self, train_prop=0.6, random_state=None): + def split_random(self, train_prop=0.6, random_state=None): """ Returns two instances of :class:`LabelledCollection` split randomly from this collection, at desired proportion. @@ -387,7 +683,7 @@ return training, test - def __add__(self, other): + def __add__(self, other): """ Returns a new :class:`LabelledCollection` as the union of this collection with another collection. Both labelled collections must have the same classes. @@ -403,7 +699,7 @@ [docs] @classmethod - def join(cls, *args: Iterable['LabelledCollection']): + def join(cls, *args: Iterable['LabelledCollection']): """ Returns a new :class:`LabelledCollection` as the union of the collections given in input. @@ -439,12 +735,14 @@ else: raise NotImplementedError('unsupported operation for collection types') labels = np.concatenate([lc.labels for lc in args]) - classes = np.unique(labels).sort() + # union of each collection's own classes_, so a class declared but absent from + # this particular join (e.g. an empty fold) is preserved at zero prevalence + classes = np.unique(np.concatenate([lc.classes_ for lc in args])) return LabelledCollection(instances, labels, classes=classes) @property - def classes(self): + def classes(self): """ Gets an array-like with the classes used in this collection @@ -453,7 +751,7 @@ return self.classes_ @property - def Xy(self): + def Xy(self): """ Gets the instances and labels. This is useful when working with `sklearn` estimators, e.g.: @@ -464,7 +762,7 @@ return self.instances, self.labels @property - def Xp(self): + def Xp(self): """ Gets the instances and the true prevalence. This is useful when implementing evaluation protocols from a :class:`LabelledCollection` object. @@ -474,7 +772,7 @@ return self.instances, self.prevalence() @property - def X(self): + def X(self): """ An alias to self.instances @@ -483,7 +781,7 @@ return self.instances @property - def y(self): + def y(self): """ An alias to self.labels @@ -492,7 +790,7 @@ return self.labels @property - def p(self): + def p(self): """ An alias to self.prevalence() @@ -503,7 +801,7 @@ [docs] - def stats(self, show=True): + def stats(self, show=True): """ Returns (and eventually prints) a dictionary with some stats of this collection. E.g.,: @@ -538,7 +836,7 @@ [docs] - def kFCV(self, nfolds=5, nrepeats=1, random_state=None): + def kFCV(self, nfolds=5, nrepeats=1, random_state=None): """ Generator of stratified folds to be used in k-fold cross validation. @@ -554,7 +852,7 @@ yield train, test - def __repr__(self): + def __repr__(self): repr=f'<{self.n_instances} instances (dtype={type(self.instances[0])}), ' repr+=f'n_classes={self.n_classes} {self.classes_}, prevalence={F.strprev(self.prevalence())}>' return repr @@ -563,7 +861,7 @@ [docs] -class Dataset: +class Dataset: """ Abstraction of training and test :class:`LabelledCollection` objects. @@ -573,7 +871,7 @@ :param name: a string representing the name of the dataset """ - def __init__(self, training: LabelledCollection, test: LabelledCollection, vocabulary: dict = None, name=''): + def __init__(self, training: LabelledCollection, test: LabelledCollection, vocabulary: dict = None, name=''): assert set(training.classes_) == set(test.classes_), 'incompatible labels in training and test collections' self.training = training self.test = test @@ -583,7 +881,7 @@ [docs] @classmethod - def SplitStratified(cls, collection: LabelledCollection, train_size=0.6): + def SplitStratified(cls, collection: LabelledCollection, train_size=0.6): """ Generates a :class:`Dataset` from a stratified split of a :class:`LabelledCollection` instance. See :meth:`LabelledCollection.split_stratified` @@ -596,7 +894,7 @@ @property - def classes_(self): + def classes_(self): """ The classes according to which the training collection is labelled @@ -605,7 +903,7 @@ return self.training.classes_ @property - def n_classes(self): + def n_classes(self): """ The number of classes according to which the training collection is labelled @@ -614,7 +912,7 @@ return self.training.n_classes @property - def binary(self): + def binary(self): """ Returns True if the training collection is labelled according to two classes @@ -625,7 +923,7 @@ [docs] @classmethod - def load(cls, train_path, test_path, loader_func: callable, classes=None, **loader_kwargs): + def load(cls, train_path, test_path, loader_func: callable, classes=None, **loader_kwargs): """ Loads a training and a test labelled set of data and convert it into a :class:`Dataset` instance. The function in charge of reading the instances must be specified. This function can be a custom one, or any of @@ -647,7 +945,7 @@ @property - def vocabulary_size(self): + def vocabulary_size(self): """ If the dataset is textual, and the vocabulary was indicated, returns the size of the vocabulary @@ -656,7 +954,7 @@ return len(self.vocabulary) @property - def train_test(self): + def train_test(self): """ Alias to `self.training` and `self.test` @@ -667,7 +965,7 @@ [docs] - def stats(self, show=True): + def stats(self, show=True): """ Returns (and eventually prints) a dictionary with some stats of this dataset. E.g.,: @@ -694,7 +992,7 @@ [docs] @classmethod - def kFCV(cls, data: LabelledCollection, nfolds=5, nrepeats=1, random_state=0): + def kFCV(cls, data: LabelledCollection, nfolds=5, nrepeats=1, random_state=0): """ Generator of stratified folds to be used in k-fold cross validation. This function is only a wrapper around :meth:`LabelledCollection.kFCV` that returns :class:`Dataset` instances made of training and test folds. @@ -711,7 +1009,7 @@ [docs] - def reduce(self, n_train=100, n_test=100, random_state=None): + def reduce(self, n_train=100, n_test=100, random_state=None): """ Reduce the number of instances in place for quick experiments. Preserves the prevalence of each set. @@ -732,36 +1030,80 @@ return self - def __repr__(self): + def __repr__(self): return f'training={self.training}; test={self.test}' - + + + + + + + + + + + + + - + + + + - Built with Sphinx using a - theme - provided by Read the Docs. - + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/build/html/_modules/quapy/data/datasets.html b/docs/build/html/_modules/quapy/data/datasets.html index 20d9588..cc6a18b 100644 --- a/docs/build/html/_modules/quapy/data/datasets.html +++ b/docs/build/html/_modules/quapy/data/datasets.html @@ -29,7 +29,7 @@ - + @@ -370,27 +370,28 @@ Source code for quapy.data.datasets -import os -from contextlib import contextmanager -import zipfile -from os.path import join -import pandas as pd -from quapy.data.base import Dataset, LabelledCollection -from quapy.data.preprocessing import text2tfidf, reduce_columns -from quapy.data.preprocessing import standardize as standardizer -from quapy.data.reader import * -from quapy.util import download_file_if_not_exists, download_file, get_quapy_home, pickled_resource -from sklearn.preprocessing import StandardScaler +import logging +import os +from contextlib import contextmanager +import zipfile +from os.path import join +import pandas as pd +from quapy.data.base import Dataset, LabelledCollection +from quapy.data.preprocessing import text2tfidf, reduce_columns +from quapy.data.preprocessing import standardize as standardizer +from quapy.data.reader import * +from quapy.util import download_file_if_not_exists, download_file, get_quapy_home, pickled_resource +from sklearn.preprocessing import StandardScaler -def _fetch_ucirepo(*args, **kwargs): +def _fetch_ucirepo(*args, **kwargs): try: - from ucimlrepo import fetch_ucirepo + from ucimlrepo import fetch_ucirepo except ImportError as exc: raise ImportError( "UCI dataset fetching requires the optional 'ucimlrepo' package. " "Install it to use fetch_UCIBinaryDataset or fetch_UCIMulticlassDataset." - ) from exc + ) from exc return fetch_ucirepo(*args, **kwargs) @@ -497,7 +498,7 @@ [docs] -def fetch_reviews(dataset_name, tfidf=False, min_df=None, data_home=None, pickle=False) -> Dataset: +def fetch_reviews(dataset_name, tfidf=False, min_df=None, data_home=None, pickle=False) -> Dataset: """ Loads a Reviews dataset as a Dataset instance, as used in `Esuli, A., Moreo, A., and Sebastiani, F. "A recurrent neural network for sentiment quantification." @@ -547,7 +548,7 @@ [docs] -def fetch_twitter(dataset_name, for_model_selection=False, min_df=None, data_home=None, pickle=False) -> Dataset: +def fetch_twitter(dataset_name, for_model_selection=False, min_df=None, data_home=None, pickle=False) -> Dataset: """ Loads a Twitter dataset as a :class:`quapy.data.base.Dataset` instance, as used in: `Gao, W., Sebastiani, F.: From classification to quantification in tweet sentiment analysis. @@ -589,8 +590,9 @@ if dataset_name in {'semeval13', 'semeval14', 'semeval15'}: trainset_name = 'semeval' testset_name = 'semeval' if for_model_selection else dataset_name - print(f"the training and development sets for datasets 'semeval13', 'semeval14', 'semeval15' are common " - f"(called 'semeval'); returning trainin-set='{trainset_name}' and test-set={testset_name}") + logging.getLogger(__name__).info( + f"the training and development sets for datasets 'semeval13', 'semeval14', 'semeval15' are common " + f"(called 'semeval'); returning trainin-set='{trainset_name}' and test-set={testset_name}") else: if dataset_name == 'semeval' and for_model_selection==False: raise ValueError('dataset "semeval" can only be used for model selection. ' @@ -624,7 +626,7 @@ [docs] -def fetch_UCIBinaryDataset(dataset_name, data_home=None, test_split=0.3, standardize=True, verbose=False) -> Dataset: +def fetch_UCIBinaryDataset(dataset_name, data_home=None, test_split=0.3, standardize=True, verbose=False) -> Dataset: """ Loads a UCI dataset as an instance of :class:`quapy.data.base.Dataset`, as used in `Pérez-Gállego, P., Quevedo, J. R., & del Coz, J. J. (2017). @@ -658,7 +660,7 @@ [docs] -def fetch_UCIBinaryLabelledCollection(dataset_name, data_home=None, standardize=True, verbose=False) -> LabelledCollection: +def fetch_UCIBinaryLabelledCollection(dataset_name, data_home=None, standardize=True, verbose=False) -> LabelledCollection: """ Loads a UCI collection as an instance of :class:`quapy.data.base.LabelledCollection`, as used in `Pérez-Gállego, P., Quevedo, J. R., & del Coz, J. J. (2017). @@ -851,7 +853,7 @@ file = join(data_home, "uci_datasets", dataset_group + ".pkl") @contextmanager - def download_tmp_file(url_group: str, filename: str): + def download_tmp_file(url_group: str, filename: str): """ Download a data file for a group of datasets temporarely. When used as a context, the file is removed once the context exits. @@ -869,7 +871,7 @@ finally: os.remove(data_path) - def download(id: int | None, group: str) -> dict: + def download(id: int | None, group: str) -> dict: """ Download the data to be pickled for a dataset group. Use the `fetch_ucirepo` api when possible. @@ -935,7 +937,7 @@ return data - def binarize_data(name, data: dict) -> LabelledCollection: + def binarize_data(name, data: dict) -> LabelledCollection: """ Filter and transform data to extract a binary dataset. @@ -980,7 +982,7 @@ [docs] -def fetch_UCIMulticlassDataset( +def fetch_UCIMulticlassDataset( dataset_name, data_home=None, min_test_split=0.3, @@ -1043,7 +1045,7 @@ [docs] -def fetch_UCIMulticlassLabelledCollection(dataset_name, data_home=None, min_class_support=100, standardize=True, verbose=False) -> LabelledCollection: +def fetch_UCIMulticlassLabelledCollection(dataset_name, data_home=None, min_class_support=100, standardize=True, verbose=False) -> LabelledCollection: """ Loads a UCI multiclass collection as an instance of :class:`quapy.data.base.LabelledCollection`. @@ -1146,7 +1148,7 @@ file = join(data_home, 'uci_multiclass', dataset_name+'.pkl') - def dummify_categorical_features(df_features, dataset_id): + def dummify_categorical_features(df_features, dataset_id): categorical_features = { 158: ["S1", "C1", "S2", "C2", "S3", "C3", "S4", "C4", "S5", "C5"], # poker_hand } @@ -1160,7 +1162,7 @@ return X - def download(id, name): + def download(id, name): df = _fetch_ucirepo(id=id) X_df = dummify_categorical_features(df.data.features, id) @@ -1173,7 +1175,7 @@ y = np.searchsorted(classes, y) return LabelledCollection(X, y) - def filter_classes(data: LabelledCollection, min_class_support): + def filter_classes(data: LabelledCollection, min_class_support): if min_class_support is None or min_class_support == 0.: return data if isinstance(min_class_support, float): @@ -1207,18 +1209,18 @@ -def _df_replace(df, col, repl={'yes': 1, 'no':0}, astype=float): +def _df_replace(df, col, repl={'yes': 1, 'no':0}, astype=float): df[col] = df[col].apply(lambda x:repl[x]).astype(astype, copy=False) -def _array_replace(arr, repl={"yes": 1, "no": 0}): +def _array_replace(arr, repl={"yes": 1, "no": 0}): for k, v in repl.items(): arr[arr == k] = v [docs] -def fetch_lequa2022(task, data_home=None): +def fetch_lequa2022(task, data_home=None): """ Loads the official datasets provided for the `LeQua 2022 <https://lequa2022.github.io/index>`_ competition. In brief, there are 4 tasks (T1A, T1B, T2A, T2B) having to do with text quantification @@ -1244,7 +1246,7 @@ that return a series of samples stored in a directory which are labelled by prevalence. """ - from quapy.data._lequa import load_raw_documents, load_vector_documents_2022, SamplesFromDir + from quapy.data._lequa import load_raw_documents, load_vector_documents_2022, SamplesFromDir assert task in LEQUA2022_TASKS, \ f'Unknown task {task}. Valid ones are {LEQUA2022_TASKS}' @@ -1258,7 +1260,7 @@ lequa_dir = join(data_home, 'lequa2022') os.makedirs(lequa_dir, exist_ok=True) - def download_unzip_and_remove(unzipped_path, url): + def download_unzip_and_remove(unzipped_path, url): tmp_path = join(lequa_dir, task + '_tmp.zip') download_file_if_not_exists(url, tmp_path) with zipfile.ZipFile(tmp_path) as file: @@ -1294,7 +1296,7 @@ [docs] -def fetch_lequa2024(task, data_home=None, merge_T3=False): +def fetch_lequa2024(task, data_home=None, merge_T3=False): """ Loads the official datasets provided for the `LeQua 2024 <https://lequa2024.github.io/index>`_ competition. LeQua 2024 defines four tasks (T1, T2, T3, T4) related to the problem of quantification; @@ -1325,7 +1327,7 @@ that return a series of samples stored in a directory which are labelled by prevalence. """ - from quapy.data._lequa import load_vector_documents_2024, SamplesFromDir, LabelledCollectionsFromDir + from quapy.data._lequa import load_vector_documents_2024, SamplesFromDir, LabelledCollectionsFromDir assert task in LEQUA2024_TASKS, \ f'Unknown task {task}. Valid ones are {LEQUA2024_TASKS}' @@ -1344,7 +1346,7 @@ lequa_dir = join(data_home, 'lequa2024') os.makedirs(lequa_dir, exist_ok=True) - def download_unzip_and_remove(unzipped_path, url): + def download_unzip_and_remove(unzipped_path, url): tmp_path = join(lequa_dir, task + '_tmp.zip') download_file_if_not_exists(url, tmp_path) with zipfile.ZipFile(tmp_path) as file: @@ -1384,7 +1386,7 @@ [docs] -def fetch_IFCB(single_sample_train=True, for_model_selection=False, data_home=None): +def fetch_IFCB(single_sample_train=True, for_model_selection=False, data_home=None): """ Loads the IFCB dataset for quantification from `Zenodo <https://zenodo.org/records/10036244>`_ (for more information on this dataset, please follow the zenodo link). @@ -1409,7 +1411,7 @@ i.e., a sampling protocol that returns a series of samples labelled by prevalence. """ - from quapy.data._ifcb import IFCBTrainSamplesFromDir, IFCBTestSamples, get_sample_list, generate_modelselection_split + from quapy.data._ifcb import IFCBTrainSamplesFromDir, IFCBTestSamples, get_sample_list, generate_modelselection_split if data_home is None: data_home = get_quapy_home() @@ -1421,7 +1423,7 @@ ifcb_dir = join(data_home, 'ifcb') os.makedirs(ifcb_dir, exist_ok=True) - def download_unzip_and_remove(unzipped_path, url): + def download_unzip_and_remove(unzipped_path, url): tmp_path = join(ifcb_dir, 'ifcb_tmp.zip') download_file_if_not_exists(url, tmp_path) with zipfile.ZipFile(tmp_path) as file: @@ -1466,7 +1468,7 @@ -def _fetch_image_embedding_splits(dataset_name, embedding, data_home=None) -> tuple[LabelledCollection,LabelledCollection,LabelledCollection]: +def _fetch_image_embedding_splits(dataset_name, embedding, data_home=None) -> tuple[LabelledCollection,LabelledCollection,LabelledCollection]: """ Loads a pre-generated embedding set (train, val, or test) of an image dataset from `Zenodo <https://zenodo.org/records/21131944>`_. @@ -1496,7 +1498,7 @@ trained_network = dataset_network[dataset_name] - def download_embedding_npz(dataset_name, trained_network, embedding): + def download_embedding_npz(dataset_name, trained_network, embedding): target_file = f'{dataset_name}_{trained_network}_{embedding}.npz' URL = f'https://zenodo.org/records/21131944/files/{target_file}' os.makedirs(join(data_home, 'image'), exist_ok=True) @@ -1512,14 +1514,12 @@ val = LabelledCollection(embedding_dict['val'], labels_dict['val'], classes=train.classes) test = LabelledCollection(embedding_dict['test'], labels_dict['test'], classes=train.classes) - print(f'{len(train)} | {len(val)} | {len(test)} | {train.X.shape[1]} | {train.n_classes} | {train.n_classes}') - return train, val, test [docs] -def fetch_image_embeddings(dataset_name, embedding, heldout_only=True, data_home=None) -> Dataset: +def fetch_image_embeddings(dataset_name, embedding, heldout_only=True, data_home=None) -> Dataset: """ Loads an image dataset with pre-generated embeddings. Available datasets include: @@ -1556,14 +1556,6 @@ -if __name__ == '__main__': - #train, val, test = _fetch_image_embedding_splits(dataset_name='mnist', embedding='logits') - #print(train) - #print(val) - #print(test) - - dataset = fetch_image_embeddings(dataset_name='svhn', embedding='features', heldout_only=True) - print(dataset) diff --git a/docs/build/html/_modules/quapy/data/reader.html b/docs/build/html/_modules/quapy/data/reader.html index 827d348..617442d 100644 --- a/docs/build/html/_modules/quapy/data/reader.html +++ b/docs/build/html/_modules/quapy/data/reader.html @@ -1,87 +1,385 @@ - - - - - - quapy.data.reader — QuaPy: A Python-based open-source framework for quantification 0.2.1 documentation - - + + + + + + + + quapy.data.reader — QuaPy: A Python-based open-source framework for quantification 0.2.1 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Skip to main content - - - - - - - - - + + + + Back to top + + + + + + + + + + Ctrl+K + + - - - - - - - - - - QuaPy: A Python-based open-source framework for quantification - - - - - - - + + - - -Quickstart - - -Manuals - - -API - - - - + + + + + + + + + + + - - - QuaPy: A Python-based open-source framework for quantification - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Home + + + + + + + Manuals + + + + + + + API + + - - - - - - Module code - quapy.data.reader - - - + + + + + + + + + + + + + Search + Ctrl+K + + + + + + + + + + + + + + System Settings + Light + Dark + + + + + + + + + + + + + + GitHub + + + + + + + + + + + + + Search + Ctrl+K + + + + + - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Home + + + + + + + Manuals + + + + + + + API + + + + + + + + + + + + + + + + + + + + + + + System Settings + Light + Dark + + + + + + + + + + + + + + GitHub + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Module code + + quapy.data.reader + + + + + + + + + + + + + + + + Source code for quapy.data.reader -import numpy as np -from scipy.sparse import dok_matrix -from tqdm import tqdm +import logging + +import numpy as np +from scipy.sparse import dok_matrix +from tqdm import tqdm [docs] -def from_text(path, encoding='utf-8', verbose=1, class2int=True): +def from_text(path, encoding='utf-8', verbose=1, class2int=True): """ Reads a labelled colletion of documents. File fomart <0 or 1>\t<document>\n @@ -108,14 +406,14 @@ all_sentences.append(sentence) all_labels.append(label) except ValueError: - print(f'format error in {line}') + logging.getLogger(__name__).warning(f'format error in {line}') return all_sentences, all_labels [docs] -def from_sparse(path): +def from_sparse(path): """ Reads a labelled collection of real-valued instances expressed in sparse format File format <-1 or 0 or 1>[\s col(int):val(float)]\n @@ -124,7 +422,7 @@ :return: a `csr_matrix` containing the instances (rows), and a ndarray containing the labels """ - def split_col_val(col_val): + def split_col_val(col_val): col, val = col_val.split(':') col, val = int(col) - 1, float(val) return col, val @@ -152,7 +450,7 @@ [docs] -def from_csv(path, encoding='utf-8'): +def from_csv(path, encoding='utf-8'): """ Reads a csv file in which columns are separated by ','. File format <label>,<feat1>,<feat2>,...,<featn>\n @@ -175,7 +473,7 @@ [docs] -def reindex_labels(y): +def reindex_labels(y): """ Re-indexes a list of labels as a list of indexes, and returns the classnames corresponding to the indexes. E.g.: @@ -198,7 +496,7 @@ [docs] -def binarize(y, pos_class): +def binarize(y, pos_class): """ Binarizes a categorical array-like collection of labels towards the positive class `pos_class`. E.g.,: @@ -218,31 +516,75 @@ - + + + + + + + + + + + + + - + + + + - Built with Sphinx using a - theme - provided by Read the Docs. - + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/build/html/_modules/quapy/error.html b/docs/build/html/_modules/quapy/error.html index 73a9a53..9b93ed4 100644 --- a/docs/build/html/_modules/quapy/error.html +++ b/docs/build/html/_modules/quapy/error.html @@ -1,89 +1,385 @@ - - - - - - quapy.error — QuaPy: A Python-based open-source framework for quantification 0.2.1 documentation - - + + + + + + + + quapy.error — QuaPy: A Python-based open-source framework for quantification 0.2.1 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Skip to main content - - - - - - - - - + + + + Back to top + + + + + + + + + + Ctrl+K + + - - - - - - - - - - QuaPy: A Python-based open-source framework for quantification - - - - - - - + + - - -Quickstart - - -Manuals - - -API - - - - + + + + + + + + + + + - - - QuaPy: A Python-based open-source framework for quantification - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Home + + + + + + + Manuals + + + + + + + API + + - - - - - - Module code - quapy.error - - - + + + + + + + + + + + + + Search + Ctrl+K + + + + + + + + + + + + + + System Settings + Light + Dark + + + + + + + + + + + + + + GitHub + + + + + + + + + + + + + Search + Ctrl+K + + + + + - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Home + + + + + + + Manuals + + + + + + + API + + + + + + + + + + + + + + + + + + + + + + + System Settings + Light + Dark + + + + + + + + + + + + + + GitHub + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Module code + + quapy.error + + + + + + + + + + + + + + + + Source code for quapy.error """Implementation of error measures used for quantification""" -import numpy as np -from sklearn.metrics import f1_score -import quapy as qp +import numpy as np +from sklearn.metrics import f1_score +import quapy as qp [docs] -def from_name(err_name): +def from_name(err_name): """Gets an error function from its name. E.g., `from_name("mae")` will return function :meth:`quapy.error.mae` @@ -98,7 +394,7 @@ [docs] -def f1e(y_true, y_pred): +def f1e(y_true, y_pred): """F1 error: simply computes the error in terms of macro :math:`F_1`, i.e., :math:`1-F_1^M`, where :math:`F_1` is the harmonic mean of precision and recall, defined as :math:`\\frac{2tp}{2tp+fp+fn}`, with `tp`, `fp`, and `fn` standing @@ -116,7 +412,7 @@ [docs] -def acce(y_true, y_pred): +def acce(y_true, y_pred): """Computes the error in terms of 1-accuracy. The accuracy is computed as :math:`\\frac{tp+tn}{tp+fp+fn+tn}`, with `tp`, `fp`, `fn`, and `tn` standing for true positives, false positives, false negatives, and true negatives, @@ -132,7 +428,7 @@ [docs] -def mae(prevs_true, prevs_hat): +def mae(prevs_true, prevs_hat): """Computes the mean absolute error (see :meth:`quapy.error.ae`) across the sample pairs. :param prevs_true: array-like of shape `(n_samples, n_classes,)` with the true prevalence values @@ -146,7 +442,7 @@ [docs] -def ae(prevs_true, prevs_hat): +def ae(prevs_true, prevs_hat): """Computes the absolute error between the two prevalence vectors. Absolute error between two prevalence vectors :math:`p` and :math:`\\hat{p}` is computed as :math:`AE(p,\\hat{p})=\\frac{1}{|\\mathcal{Y}|}\\sum_{y\\in \\mathcal{Y}}|\\hat{p}(y)-p(y)|`, @@ -165,7 +461,7 @@ [docs] -def nae(prevs_true, prevs_hat): +def nae(prevs_true, prevs_hat): """Computes the normalized absolute error between the two prevalence vectors. Normalized absolute error between two prevalence vectors :math:`p` and :math:`\\hat{p}` is computed as :math:`NAE(p,\\hat{p})=\\frac{AE(p,\\hat{p})}{z_{AE}}`, @@ -185,7 +481,7 @@ [docs] -def mnae(prevs_true, prevs_hat): +def mnae(prevs_true, prevs_hat): """Computes the mean normalized absolute error (see :meth:`quapy.error.nae`) across the sample pairs. :param prevs_true: array-like of shape `(n_samples, n_classes,)` with the true prevalence values @@ -199,7 +495,7 @@ [docs] -def mse(prevs_true, prevs_hat): +def mse(prevs_true, prevs_hat): """Computes the mean squared error (see :meth:`quapy.error.se`) across the sample pairs. :param prevs_true: array-like of shape `(n_samples, n_classes,)` with the @@ -214,7 +510,7 @@ [docs] -def se(prevs_true, prevs_hat): +def se(prevs_true, prevs_hat): """Computes the squared error between the two prevalence vectors. Squared error between two prevalence vectors :math:`p` and :math:`\\hat{p}` is computed as :math:`SE(p,\\hat{p})=\\frac{1}{|\\mathcal{Y}|}\\sum_{y\\in \\mathcal{Y}}(\\hat{p}(y)-p(y))^2`, @@ -233,7 +529,7 @@ [docs] -def sre(prevs_true, prevs_hat, prevs_train, eps=0.): +def sre(prevs_true, prevs_hat, prevs_train, eps=0.): """ Computes the squared ratio error between two prevalence vectors. The squared ratio error between prevalence vectors :math:`p` and @@ -269,7 +565,7 @@ [docs] -def msre(prevs_true, prevs_hat, prevs_train, eps=0.): +def msre(prevs_true, prevs_hat, prevs_train, eps=0.): """ Computes the mean squared ratio error (see :meth:`quapy.error.sre`) across the sample pairs. @@ -285,7 +581,7 @@ [docs] -def aitchisondist(prevs_true, prevs_hat): +def aitchisondist(prevs_true, prevs_hat): """ Computes the Aitchison distance between two prevalence vectors. The Aitchison distance between prevalence vectors :math:`p` and @@ -298,7 +594,7 @@ :param prevs_hat: array-like with the predicted prevalence values :return: Aitchison distance """ - from quapy.functional import CLRtransformation + from quapy.functional import CLRtransformation clr = CLRtransformation() return np.linalg.norm(clr(prevs_true) - clr(prevs_hat), axis=-1) @@ -307,7 +603,7 @@ [docs] -def maitchisondist(prevs_true, prevs_hat): +def maitchisondist(prevs_true, prevs_hat): """ Computes the mean Aitchison distance (see :meth:`quapy.error.aitchisondist`) across the sample pairs, i.e., @@ -324,7 +620,7 @@ [docs] -def mkld(prevs_true, prevs_hat, eps=None): +def mkld(prevs_true, prevs_hat, eps=None): """Computes the mean Kullback-Leibler divergence (see :meth:`quapy.error.kld`) across the sample pairs. The distributions are smoothed using the `eps` factor (see :meth:`quapy.error.smooth`). @@ -345,7 +641,7 @@ [docs] -def kld(prevs_true, prevs_hat, eps=None): +def kld(prevs_true, prevs_hat, eps=None): """Computes the Kullback-Leibler divergence between the two prevalence distributions. Kullback-Leibler divergence between two prevalence distributions :math:`p` and :math:`\\hat{p}` is computed as @@ -371,7 +667,7 @@ [docs] -def mnkld(prevs_true, prevs_hat, eps=None): +def mnkld(prevs_true, prevs_hat, eps=None): """Computes the mean Normalized Kullback-Leibler divergence (see :meth:`quapy.error.nkld`) across the sample pairs. The distributions are smoothed using the `eps` factor (see :meth:`quapy.error.smooth`). @@ -391,7 +687,7 @@ [docs] -def nkld(prevs_true, prevs_hat, eps=None): +def nkld(prevs_true, prevs_hat, eps=None): """Computes the Normalized Kullback-Leibler divergence between the two prevalence distributions. Normalized Kullback-Leibler divergence between two prevalence distributions :math:`p` and :math:`\\hat{p}` is computed as @@ -415,7 +711,7 @@ [docs] -def mrae(prevs_true, prevs_hat, eps=None): +def mrae(prevs_true, prevs_hat, eps=None): """Computes the mean relative absolute error (see :meth:`quapy.error.rae`) across the sample pairs. The distributions are smoothed using the `eps` factor (see :meth:`quapy.error.smooth`). @@ -436,7 +732,7 @@ [docs] -def rae(prevs_true, prevs_hat, eps=None): +def rae(prevs_true, prevs_hat, eps=None): """Computes the absolute relative error between the two prevalence vectors. Relative absolute error between two prevalence vectors :math:`p` and :math:`\\hat{p}` is computed as @@ -462,7 +758,7 @@ [docs] -def nrae(prevs_true, prevs_hat, eps=None): +def nrae(prevs_true, prevs_hat, eps=None): """Computes the normalized absolute relative error between the two prevalence vectors. Relative absolute error between two prevalence vectors :math:`p` and :math:`\\hat{p}` is computed as @@ -490,7 +786,7 @@ [docs] -def mnrae(prevs_true, prevs_hat, eps=None): +def mnrae(prevs_true, prevs_hat, eps=None): """Computes the mean normalized relative absolute error (see :meth:`quapy.error.nrae`) across the sample pairs. The distributions are smoothed using the `eps` factor (see :meth:`quapy.error.smooth`). @@ -511,7 +807,7 @@ [docs] -def nmd(prevs_true, prevs_hat): +def nmd(prevs_true, prevs_hat): """ Computes the Normalized Match Distance; which is the Normalized Distance multiplied by the factor `1/(n-1)` to guarantee the measure ranges between 0 (best prediction) and 1 (worst prediction). @@ -529,7 +825,7 @@ [docs] -def bias_binary(prevs_true, prevs_hat): +def bias_binary(prevs_true, prevs_hat): """ Computes the (positive) bias in a binary problem. The bias is simply the difference between the predicted positive value and the true positive value, so that a positive such value indicates the @@ -549,7 +845,7 @@ [docs] -def mean_bias_binary(prevs_true, prevs_hat): +def mean_bias_binary(prevs_true, prevs_hat): """ Computes the mean of the (positive) bias in a binary problem. :param prevs_true: array-like of shape `(n_classes,)` with the true prevalence values @@ -562,7 +858,7 @@ [docs] -def md(prevs_true, prevs_hat, ERROR_TOL=1E-3): +def md(prevs_true, prevs_hat, ERROR_TOL=1E-3): """ Computes the Match Distance, under the assumption that the cost in mistaking class i with class i+1 is 1 in all cases. @@ -582,7 +878,7 @@ [docs] -def smooth(prevs, eps): +def smooth(prevs, eps): """ Smooths a prevalence distribution with :math:`\\epsilon` (`eps`) as: :math:`\\underline{p}(y)=\\frac{\\epsilon+p(y)}{\\epsilon|\\mathcal{Y}|+ \\displaystyle\\sum_{y\\in \\mathcal{Y}}p(y)}` @@ -597,7 +893,7 @@ -def __check_eps(eps=None): +def __check_eps(eps=None): if eps is None: sample_size = qp.environ['SAMPLE_SIZE'] if sample_size is None: @@ -634,31 +930,75 @@ match_distance = md - + + + + + + + + + + + + + - + + + + - Built with Sphinx using a - theme - provided by Read the Docs. - + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/build/html/_modules/quapy/functional.html b/docs/build/html/_modules/quapy/functional.html index b7743b8..c275ca4 100644 --- a/docs/build/html/_modules/quapy/functional.html +++ b/docs/build/html/_modules/quapy/functional.html @@ -29,8 +29,9 @@ - - + + + @@ -41,6 +42,7 @@ + @@ -114,8 +116,14 @@ + + + + + + + - QuaPy @@ -131,7 +139,7 @@ - Quickstart + Home @@ -183,6 +191,21 @@ + + + + + + + + + + + GitHub + + + @@ -229,7 +252,7 @@ - Quickstart + Home @@ -272,6 +295,21 @@ + + + + + + + + + + + GitHub + + + @@ -332,17 +370,17 @@ Source code for quapy.functional -import warnings -from abc import ABC, abstractmethod -from collections import defaultdict -from functools import lru_cache -from typing import Literal, Union, Callable -from numpy.typing import ArrayLike +import warnings +from abc import ABC, abstractmethod +from collections import defaultdict +from functools import lru_cache +from typing import Literal, Union, Callable +from numpy.typing import ArrayLike -import scipy -import numpy as np +import scipy +import numpy as np -import quapy as qp +import quapy as qp # ------------------------------------------------------------------------------------------ @@ -351,7 +389,7 @@ [docs] -def classes_from_labels(labels): +def classes_from_labels(labels): """ Obtains a np.ndarray with the (sorted) classes :param labels: array-like with the instances' labels @@ -365,7 +403,7 @@ [docs] -def num_classes_from_labels(labels): +def num_classes_from_labels(labels): """ Obtains the number of classes from an array-like of instance's labels :param labels: array-like with the instances' labels @@ -380,7 +418,7 @@ [docs] -def counts_from_labels(labels: ArrayLike, classes: ArrayLike) -> np.ndarray: +def counts_from_labels(labels: ArrayLike, classes: ArrayLike) -> np.ndarray: """ Computes the raw count values from a vector of labels. @@ -401,7 +439,7 @@ [docs] -def prevalence_from_labels(labels: ArrayLike, classes: ArrayLike): +def prevalence_from_labels(labels: ArrayLike, classes: ArrayLike): """ Computes the prevalence values from a vector of labels. @@ -419,7 +457,7 @@ [docs] -def prevalence_from_probabilities(posteriors: ArrayLike, binarize: bool = False): +def prevalence_from_probabilities(posteriors: ArrayLike, binarize: bool = False): """ Returns a vector of prevalence values from a matrix of posterior probabilities. @@ -443,7 +481,7 @@ [docs] -def num_prevalence_combinations(n_prevpoints:int, n_classes:int, n_repeats:int=1) -> int: +def num_prevalence_combinations(n_prevpoints:int, n_classes:int, n_repeats:int=1) -> int: """ Computes the number of valid prevalence combinations in the n_classes-dimensional simplex if `n_prevpoints` equally distant prevalence values are generated and `n_repeats` repetitions are requested. @@ -472,7 +510,7 @@ [docs] -def get_nprevpoints_approximation(combinations_budget:int, n_classes:int, n_repeats:int=1) -> int: +def get_nprevpoints_approximation(combinations_budget:int, n_classes:int, n_repeats:int=1) -> int: """ Searches for the largest number of (equidistant) prevalence points to define for each of the `n_classes` classes so that the number of valid prevalence values generated as combinations of prevalence points (points in a @@ -500,7 +538,7 @@ [docs] -def as_binary_prevalence(positive_prevalence: Union[float, ArrayLike], clip_if_necessary: bool=False) -> np.ndarray: +def as_binary_prevalence(positive_prevalence: Union[float, ArrayLike], clip_if_necessary: bool=False) -> np.ndarray: """ Helper that, given a float representing the prevalence for the positive class, returns a np.ndarray of two values representing a binary distribution. @@ -522,7 +560,7 @@ [docs] -def strprev(prevalences: ArrayLike, prec: int=3) -> str: +def strprev(prevalences: ArrayLike, prec: int=3) -> str: """ Returns a string representation for a prevalence vector. E.g., @@ -539,7 +577,7 @@ [docs] -def check_prevalence_vector(prevalences: ArrayLike, raise_exception: bool=False, tolerance: float=1e-08, aggr=True): +def check_prevalence_vector(prevalences: ArrayLike, raise_exception: bool=False, tolerance: float=1e-08, aggr=True): """ Checks that `prevalences` is a valid prevalence vector, i.e., it contains values in [0,1] and the values sum up to 1. In other words, verifies that the `prevalences` vectors lies in the @@ -581,7 +619,7 @@ [docs] -def uniform_prevalence(n_classes): +def uniform_prevalence(n_classes): """ Returns a vector representing the uniform distribution for `n_classes` @@ -597,7 +635,7 @@ [docs] -def normalize_prevalence(prevalences: ArrayLike, method='l1'): +def normalize_prevalence(prevalences: ArrayLike, method='l1'): """ Normalizes a vector or matrix of prevalence values. The normalization consists of applying a L1 normalization in cases in which the prevalence values are not all-zeros, and to convert the prevalence values into `1/n_classes` in @@ -640,7 +678,7 @@ [docs] -def l1_norm(prevalences: ArrayLike) -> np.ndarray: +def l1_norm(prevalences: ArrayLike) -> np.ndarray: """ Applies L1 normalization to the `unnormalized_arr` so that it becomes a valid prevalence vector. Zero vectors are mapped onto the uniform distribution. Raises an exception if @@ -666,7 +704,7 @@ [docs] -def clip(prevalences: ArrayLike) -> np.ndarray: +def clip(prevalences: ArrayLike) -> np.ndarray: """ Clips the values in [0,1] and then applies the L1 normalization. @@ -681,7 +719,7 @@ [docs] -def projection_simplex_sort(unnormalized_arr: ArrayLike) -> np.ndarray: +def projection_simplex_sort(unnormalized_arr: ArrayLike) -> np.ndarray: """Projects a point onto the probability simplex. The code is adapted from Mathieu Blondel's BSD-licensed @@ -709,7 +747,7 @@ [docs] -def softmax(prevalences: ArrayLike) -> np.ndarray: +def softmax(prevalences: ArrayLike) -> np.ndarray: """ Applies the softmax function to all vectors even if the original vectors were valid distributions. If you want to leave valid vectors untouched, use condsoftmax instead. @@ -724,7 +762,7 @@ [docs] -def condsoftmax(prevalences: ArrayLike) -> np.ndarray: +def condsoftmax(prevalences: ArrayLike) -> np.ndarray: """ Applies the softmax function only to vectors that do not represent valid distributions. @@ -749,7 +787,7 @@ [docs] -def HellingerDistance(P: np.ndarray, Q: np.ndarray) -> float: +def HellingerDistance(P: np.ndarray, Q: np.ndarray) -> float: """ Computes the Hellingher Distance (HD) between (discretized) distributions `P` and `Q`. The HD for two discrete distributions of `k` bins is defined as: @@ -767,7 +805,7 @@ [docs] -def TopsoeDistance(P: np.ndarray, Q: np.ndarray, epsilon: float=1e-20): +def TopsoeDistance(P: np.ndarray, Q: np.ndarray, epsilon: float=1e-20): """ Topsoe distance between two (discretized) distributions `P` and `Q`. The Topsoe distance for two discrete distributions of `k` bins is defined as: @@ -786,7 +824,7 @@ [docs] -def get_divergence(divergence: Union[str, Callable]): +def get_divergence(divergence: Union[str, Callable]): """ Guarantees that the divergence received as argument is a function. That is, if this argument is already a callable, then it is returned, if it is instead a string, then tries to instantiate the corresponding @@ -815,7 +853,7 @@ [docs] -def argmin_prevalence(loss: Callable, +def argmin_prevalence(loss: Callable, n_classes: int, method: Literal["optim_minimize", "linear_search", "ternary_search"]='optim_minimize'): """ @@ -842,7 +880,7 @@ [docs] -def optim_minimize(loss: Callable, n_classes: int, return_loss=False): +def optim_minimize(loss: Callable, n_classes: int, return_loss=False): """ Searches for the optimal prevalence values, i.e., an `n_classes`-dimensional vector of the (`n_classes`-1)-simplex that yields the smallest lost. This optimization is carried out by means of a constrained search using scipy's @@ -854,7 +892,7 @@ :return: (ndarray) the best prevalence vector found or a tuple which also contains the value of the loss if return_loss=True """ - from scipy import optimize + from scipy import optimize # the initial point is set as the uniform distribution uniform_distribution = uniform_prevalence(n_classes=n_classes) @@ -873,7 +911,7 @@ [docs] -def linear_search(loss: Callable, n_classes: int): +def linear_search(loss: Callable, n_classes: int): """ Performs a linear search for the best prevalence value in binary problems. The search is carried out by exploring the range [0,1] stepping by 0.01. This search is inefficient, and is added only for completeness (some of the @@ -897,7 +935,7 @@ [docs] -def ternary_search(loss: Callable, n_classes: int): +def ternary_search(loss: Callable, n_classes: int): """ Performs a ternary search for the best prevalence value in binary problems. This search assumes the loss is unimodal over the interval [0,1]. @@ -933,7 +971,7 @@ [docs] -def prevalence_linspace(grid_points:int=21, repeats:int=1, smooth_limits_epsilon:float=0.01) -> np.ndarray: +def prevalence_linspace(grid_points:int=21, repeats:int=1, smooth_limits_epsilon:float=0.01) -> np.ndarray: """ Produces an array of uniformly separated values of prevalence. By default, produces an array of 21 prevalence values, with @@ -958,7 +996,7 @@ [docs] -def uniform_prevalence_sampling(n_classes: int, size: int=1) -> np.ndarray: +def uniform_prevalence_sampling(n_classes: int, size: int=1) -> np.ndarray: """ Implements the `Kraemer algorithm <http://www.cs.cmu.edu/~nasmith/papers/smith+tromble.tr04.pdf>`_ for sampling uniformly at random from the unit simplex. This implementation is adapted from this @@ -994,7 +1032,7 @@ [docs] -def solve_adjustment_binary(prevalence_estim: ArrayLike, tpr: float, fpr: float, clip: bool=True): +def solve_adjustment_binary(prevalence_estim: ArrayLike, tpr: float, fpr: float, clip: bool=True): """ Implements the adjustment of ACC and PACC for the binary case. The adjustment for a prevalence estimate of the positive class `p` comes down to computing: @@ -1021,7 +1059,7 @@ [docs] -def solve_adjustment( +def solve_adjustment( class_conditional_rates: np.ndarray, unadjusted_counts: np.ndarray, method: Literal["inversion", "invariant-ratio"], @@ -1067,14 +1105,18 @@ if method == "inversion": pass # We leave A and B unchanged elif method == "invariant-ratio": - # Change the last equation to replace it with the normalization condition + # Change the last equation to replace it with the normalization condition; + # copy first so this does not mutate the caller's arrays (np.asarray above + # returns the same object, not a copy, when the input is already float64) + A = A.copy() + B = B.copy() A[-1, :] = 1.0 B[-1] = 1.0 else: raise ValueError(f"unknown {method=}") if solver == "minimize": - def loss(prev): + def loss(prev): return np.linalg.norm(A @ prev - B) return optim_minimize(loss, n_classes=A.shape[0]) elif solver in ["exact-raise", "exact-cc"]: @@ -1102,7 +1144,7 @@ [docs] -class CompositionalTransformation(ABC): +class CompositionalTransformation(ABC): """ Abstract class of transformations for compositional data. """ @@ -1110,13 +1152,13 @@ EPSILON = 1e-12 @abstractmethod - def __call__(self, X): + def __call__(self, X): ... [docs] @abstractmethod - def inverse(self, Z): + def inverse(self, Z): ... @@ -1124,12 +1166,12 @@ [docs] -class CLRtransformation(CompositionalTransformation): +class CLRtransformation(CompositionalTransformation): """ Centered log-ratio (CLR) transformation. """ - def __call__(self, X): + def __call__(self, X): X = np.asarray(X) X = qp.error.smooth(X, self.EPSILON) geometric_mean = np.exp(np.mean(np.log(X), axis=-1, keepdims=True)) @@ -1137,7 +1179,7 @@ [docs] - def inverse(self, Z): + def inverse(self, Z): return scipy.special.softmax(Z, axis=-1) @@ -1145,12 +1187,12 @@ [docs] -class ILRtransformation(CompositionalTransformation): +class ILRtransformation(CompositionalTransformation): """ Isometric log-ratio (ILR) transformation. """ - def __call__(self, X): + def __call__(self, X): X = np.asarray(X) X = qp.error.smooth(X, self.EPSILON) basis = self.get_V(X.shape[-1]) @@ -1158,7 +1200,7 @@ [docs] - def inverse(self, Z): + def inverse(self, Z): Z = np.asarray(Z) basis = self.get_V(Z.shape[-1] + 1) logp = Z @ basis @@ -1169,7 +1211,7 @@ [docs] @lru_cache(maxsize=None) - def get_V(self, k): + def get_V(self, k): helmert = np.zeros((k, k)) for i in range(1, k): helmert[i, :i] = 1 @@ -1182,7 +1224,7 @@ [docs] -def normalized_entropy(p): +def normalized_entropy(p): """ Computes the normalized Shannon entropy of a prevalence vector. @@ -1198,7 +1240,7 @@ [docs] -def antagonistic_prevalence(p, strength=1): +def antagonistic_prevalence(p, strength=1): """ Reflects a prevalence vector in ILR space and maps it back to the simplex. @@ -1215,7 +1257,7 @@ [docs] -def in_simplex(x, atol=1e-8): +def in_simplex(x, atol=1e-8): """ Checks whether points lie in the probability simplex. diff --git a/docs/build/html/_modules/quapy/method/_kdey.html b/docs/build/html/_modules/quapy/method/_kdey.html index 6664ea9..3367861 100644 --- a/docs/build/html/_modules/quapy/method/_kdey.html +++ b/docs/build/html/_modules/quapy/method/_kdey.html @@ -29,8 +29,9 @@ - - + + + @@ -41,6 +42,7 @@ + @@ -114,8 +116,14 @@ + + + + + + + - QuaPy @@ -131,7 +139,7 @@ - Quickstart + Home @@ -183,6 +191,21 @@ + + + + + + + + + + + GitHub + + + @@ -229,7 +252,7 @@ - Quickstart + Home @@ -272,6 +295,21 @@ + + + + + + + + + + + GitHub + + + @@ -332,22 +370,22 @@ Source code for quapy.method._kdey -import numpy as np -from numbers import Real -from sklearn.base import BaseEstimator -from sklearn.neighbors import KernelDensity +import numpy as np +from numbers import Real +from sklearn.base import BaseEstimator +from sklearn.neighbors import KernelDensity -import quapy as qp -from quapy.method._helper import _labels_to_indices -from quapy.method.aggregative import AggregativeSoftQuantifier -import quapy.functional as F -from scipy.special import logsumexp -from sklearn.metrics.pairwise import rbf_kernel +import quapy as qp +from quapy.method._helper import _labels_to_indices +from quapy.method.aggregative import AggregativeSoftQuantifier +import quapy.functional as F +from scipy.special import logsumexp +from sklearn.metrics.pairwise import rbf_kernel [docs] -class KDEBase: +class KDEBase: """ Common ancestor for KDE-based methods. Implements some common routines. """ @@ -356,7 +394,7 @@ KERNELS = ['gaussian', 'aitchison', 'ilr'] @classmethod - def _check_bandwidth(cls, bandwidth, kernel): + def _check_bandwidth(cls, bandwidth, kernel): """ Checks that the bandwidth parameter is correct @@ -370,13 +408,13 @@ return bandwidth @classmethod - def _check_kernel(cls, kernel): + def _check_kernel(cls, kernel): assert kernel in KDEBase.KERNELS, f'unknown {kernel=}' return kernel [docs] - def get_kde_function(self, X, bandwidth, kernel): + def get_kde_function(self, X, bandwidth, kernel): """ Wraps the KDE function from scikit-learn. @@ -392,7 +430,7 @@ [docs] - def pdf(self, kde, X, kernel, log_densities=False): + def pdf(self, kde, X, kernel, log_densities=False): """ Wraps the density evalution of scikit-learn's KDE. Scikit-learn returns log-scores (s), so this function returns :math:`e^{s}` @@ -411,7 +449,7 @@ [docs] - def get_mixture_components(self, X, y, classes, bandwidth, kernel): + def get_mixture_components(self, X, y, classes, bandwidth, kernel): """ Returns an array containing the mixture components, i.e., the KDE functions for each class. @@ -433,7 +471,7 @@ [docs] - def transform_posteriors(self, X, kernel): + def transform_posteriors(self, X, kernel): if kernel in {'aitchison', 'ilr'}: X = self.shrink_posteriors(X) if kernel == 'aitchison': @@ -445,7 +483,7 @@ [docs] - def shrink_posteriors(self, X): + def shrink_posteriors(self, X): shrinkage = getattr(self, 'shrinkage', 0.0) if shrinkage <= 0: return X @@ -457,7 +495,7 @@ [docs] - def effective_bandwidth(self, bandwidth, kernel): + def effective_bandwidth(self, bandwidth, kernel): shrinkage = getattr(self, 'shrinkage', 0.0) if shrinkage > 0 and kernel in {'aitchison', 'ilr'} and isinstance(bandwidth, Real): return (1.0 - shrinkage) * float(bandwidth) @@ -466,7 +504,7 @@ [docs] - def clr_transform(self, X): + def clr_transform(self, X): if not hasattr(self, 'clr'): self.clr = F.CLRtransformation() return self.clr(X) @@ -474,7 +512,7 @@ [docs] - def ilr_transform(self, X): + def ilr_transform(self, X): if not hasattr(self, 'ilr'): self.ilr = F.ILRtransformation() return self.ilr(X) @@ -484,7 +522,7 @@ [docs] -class KDEyML(AggregativeSoftQuantifier, KDEBase): +class KDEyML(AggregativeSoftQuantifier, KDEBase): """ Kernel Density Estimation model for quantification (KDEy) relying on the Kullback-Leibler divergence (KLD) as the divergence measure to be minimized. This method was first proposed in the paper @@ -526,7 +564,7 @@ :param random_state: a seed to be set before fitting any base quantifier (default None) """ - def __init__(self, classifier: BaseEstimator=None, fit_classifier=True, val_split=5, bandwidth=0.1, + def __init__(self, classifier: BaseEstimator=None, fit_classifier=True, val_split=5, bandwidth=0.1, kernel='gaussian', shrinkage=0.0, random_state=None): super().__init__(classifier, fit_classifier, val_split) self.bandwidth = KDEBase._check_bandwidth(bandwidth, kernel) @@ -539,7 +577,7 @@ [docs] - def aggregation_fit(self, classif_predictions, labels): + def aggregation_fit(self, classif_predictions, labels): self.mix_densities = self.get_mixture_components( classif_predictions, labels, @@ -552,7 +590,7 @@ [docs] - def aggregate(self, posteriors: np.ndarray): + def aggregate(self, posteriors: np.ndarray): """ Searches for the mixture model parameter (the sought prevalence values) that maximizes the likelihood of the data (i.e., that minimizes the negative log-likelihood) @@ -569,14 +607,14 @@ for kde_i in self.mix_densities ] - def neg_loglikelihood(prev): + def neg_loglikelihood(prev): prev = qp.error.smooth(prev, eps=epsilon) test_loglikelihood = logsumexp(np.log(prev)[:, None] + test_log_densities, axis=0) return -np.sum(test_loglikelihood) else: test_densities = [self.pdf(kde_i, posteriors, self.kernel) for kde_i in self.mix_densities] - def neg_loglikelihood(prev): + def neg_loglikelihood(prev): test_mixture_likelihood = prev @ test_densities test_loglikelihood = np.log(test_mixture_likelihood + epsilon) return -np.sum(test_loglikelihood) @@ -588,7 +626,7 @@ [docs] -class KDEyHD(AggregativeSoftQuantifier, KDEBase): +class KDEyHD(AggregativeSoftQuantifier, KDEBase): """ Kernel Density Estimation model for quantification (KDEy) relying on the squared Hellinger Disntace (HD) as the divergence measure to be minimized. This method was first proposed in the paper @@ -633,7 +671,7 @@ :param montecarlo_trials: number of Monte Carlo trials (default 10000) """ - def __init__(self, classifier: BaseEstimator=None, fit_classifier=True, val_split=5, divergence: str='HD', + def __init__(self, classifier: BaseEstimator=None, fit_classifier=True, val_split=5, divergence: str='HD', bandwidth=0.1, random_state=None, montecarlo_trials=10000): super().__init__(classifier, fit_classifier, val_split) @@ -644,7 +682,7 @@ [docs] - def aggregation_fit(self, classif_predictions, labels): + def aggregation_fit(self, classif_predictions, labels): self.mix_densities = self.get_mixture_components( classif_predictions, labels, self.classes_, self.bandwidth, 'gaussian' ) @@ -663,7 +701,7 @@ [docs] - def aggregate(self, posteriors: np.ndarray): + def aggregate(self, posteriors: np.ndarray): # we retain all n*N examples (sampled from a mixture with uniform parameter), and then # apply importance sampling (IS). In this version we compute D(p_alpha||q) with IS n_classes = len(self.mix_densities) @@ -671,7 +709,7 @@ test_kde = self.get_kde_function(posteriors, self.bandwidth, 'gaussian') test_densities = self.pdf(test_kde, self.reference_samples, 'gaussian') - def f_squared_hellinger(u): + def f_squared_hellinger(u): return (np.sqrt(u)-1)**2 # todo: this will fail when self.divergence is a callable, and is not the right place to do it anyway @@ -687,7 +725,7 @@ p_class = self.reference_classwise_densities + epsilon fracs = p_class/qs - def divergence(prev): + def divergence(prev): # ps / qs = (prev @ p_class) / qs = prev @ (p_class / qs) = prev @ fracs ps_div_qs = prev @ fracs return np.mean( f(ps_div_qs) * iw ) @@ -699,7 +737,7 @@ [docs] -class KDEyCS(AggregativeSoftQuantifier): +class KDEyCS(AggregativeSoftQuantifier): """ Kernel Density Estimation model for quantification (KDEy) relying on the Cauchy-Schwarz divergence (CS) as the divergence measure to be minimized. This method was first proposed in the paper @@ -736,13 +774,13 @@ :param bandwidth: float, the bandwidth of the Kernel """ - def __init__(self, classifier: BaseEstimator=None, fit_classifier=True, val_split=5, bandwidth=0.1): + def __init__(self, classifier: BaseEstimator=None, fit_classifier=True, val_split=5, bandwidth=0.1): super().__init__(classifier, fit_classifier, val_split) self.bandwidth = KDEBase._check_bandwidth(bandwidth, kernel='gaussian') [docs] - def gram_matrix_mix_sum(self, X, Y=None): + def gram_matrix_mix_sum(self, X, Y=None): # this adapts the output of the rbf_kernel function (pairwise evaluations of Gaussian kernels k(x,y)) # to contain pairwise evaluations of N(x|mu,Sigma1+Sigma2) with mu=y and Sigma1 and Sigma2 are # two "scalar matrices" (h^2)*I each, so Sigma1+Sigma2 has scalar 2(h^2) (h is the bandwidth) @@ -757,7 +795,7 @@ [docs] - def aggregation_fit(self, classif_predictions, labels): + def aggregation_fit(self, classif_predictions, labels): P, y = classif_predictions, labels n = len(self.classes_) @@ -789,7 +827,7 @@ [docs] - def aggregate(self, posteriors: np.ndarray): + def aggregate(self, posteriors: np.ndarray): Ptr = self.Ptr Pte = posteriors y = self.ytr @@ -809,7 +847,7 @@ for i in range(n): tr_te_sums[i] = self.gram_matrix_mix_sum(Ptr[y==i], Pte) - def divergence(alpha): + def divergence(alpha): # called \overline{r} in the paper alpha_ratio = alpha * self.counts_inv diff --git a/docs/build/html/_modules/quapy/method/_neural.html b/docs/build/html/_modules/quapy/method/_neural.html index 3e6c7c3..3c8215e 100644 --- a/docs/build/html/_modules/quapy/method/_neural.html +++ b/docs/build/html/_modules/quapy/method/_neural.html @@ -29,7 +29,7 @@ - + @@ -370,23 +370,24 @@ Source code for quapy.method._neural -import os -from pathlib import Path -import random +import logging +import os +from pathlib import Path +import random -import torch -from torch.nn import MSELoss -from torch.nn.functional import relu +import torch +from torch.nn import MSELoss +from torch.nn.functional import relu -from quapy.protocol import UPP -from quapy.method.aggregative import * -from quapy.util import EarlyStop -from tqdm import tqdm +from quapy.protocol import UPP +from quapy.method.aggregative import * +from quapy.util import EarlyStop +from tqdm import tqdm [docs] -class QuaNetTrainer(BaseQuantifier): +class QuaNetTrainer(BaseQuantifier): """ Implementation of `QuaNet <https://dl.acm.org/doi/abs/10.1145/3269206.3269287>`_, a neural network for quantification. This implementation uses `PyTorch <https://pytorch.org/>`_ and can take advantage of GPU @@ -438,7 +439,7 @@ :param device: string, indicate "cpu" or "cuda" """ - def __init__(self, + def __init__(self, classifier, fit_classifier=True, sample_size=None, @@ -491,7 +492,7 @@ [docs] - def fit(self, X, y): + def fit(self, X, y): """ Trains QuaNet. @@ -549,7 +550,7 @@ order_by=0 if data.binary else None, **self.quanet_params ).to(self.device) - print(self.quanet) + logging.getLogger(__name__).debug(self.quanet) self.optim = torch.optim.Adam(self.quanet.parameters(), lr=self.lr) early_stop = EarlyStop(self.patience, lower_is_better=True) @@ -564,15 +565,16 @@ if early_stop.IMPROVED: torch.save(self.quanet.state_dict(), checkpoint) elif early_stop.STOP: - print(f'training ended by patience exhausted; loading best model parameters in {checkpoint} ' - f'for epoch {early_stop.best_epoch}') + logging.getLogger(__name__).info( + f'training ended by patience exhausted; loading best model parameters in {checkpoint} ' + f'for epoch {early_stop.best_epoch}') self.quanet.load_state_dict(torch.load(checkpoint)) break return self - def _get_aggregative_estims(self, posteriors): + def _get_aggregative_estims(self, posteriors): label_predictions = np.argmax(posteriors, axis=-1) prevs_estim = [] for quantifier in self.quantifiers.values(): @@ -585,7 +587,7 @@ [docs] - def predict(self, X): + def predict(self, X): posteriors = self.classifier.predict_proba(X) embeddings = self.classifier.transform(X) quant_estims = self._get_aggregative_estims(posteriors) @@ -598,7 +600,7 @@ return prevalence - def _epoch(self, data: LabelledCollection, posteriors, iterations, epoch, early_stop, train): + def _epoch(self, data: LabelledCollection, posteriors, iterations, epoch, early_stop, train): mse_loss = MSELoss() self.quanet.train(mode=train) @@ -650,7 +652,7 @@ [docs] - def get_params(self, deep=True): + def get_params(self, deep=True): classifier_params = self.classifier.get_params() classifier_params = {'classifier__'+k:v for k,v in classifier_params.items()} return {**classifier_params, **self.quanet_params} @@ -658,7 +660,7 @@ [docs] - def set_params(self, **parameters): + def set_params(self, **parameters): learner_params = {} for key, val in parameters.items(): if key in self.quanet_params: @@ -670,7 +672,7 @@ self.classifier.set_params(**learner_params) - def __check_params_colision(self, quanet_params, learner_params): + def __check_params_colision(self, quanet_params, learner_params): quanet_keys = set(quanet_params.keys()) learner_keys = set(learner_params.keys()) intersection = quanet_keys.intersection(learner_keys) @@ -680,7 +682,7 @@ [docs] - def clean_checkpoint(self): + def clean_checkpoint(self): """ Removes the checkpoint """ @@ -689,23 +691,23 @@ [docs] - def clean_checkpoint_dir(self): + def clean_checkpoint_dir(self): """ Removes anything contained in the checkpoint directory """ - import shutil + import shutil shutil.rmtree(self.checkpointdir, ignore_errors=True) @property - def classes_(self): + def classes_(self): return self._classes_ [docs] -def mae_loss(output, target): +def mae_loss(output, target): """ Torch-like wrapper for the Mean Absolute Error @@ -719,7 +721,7 @@ [docs] -class QuaNetModule(torch.nn.Module): +class QuaNetModule(torch.nn.Module): """ Implements the `QuaNet <https://dl.acm.org/doi/abs/10.1145/3269206.3269287>`_ forward pass. See :class:`QuaNetTrainer` for training QuaNet. @@ -736,7 +738,7 @@ :param order_by: integer, class for which the document embeddings are to be sorted """ - def __init__(self, + def __init__(self, doc_embedding_size, n_classes, stats_size, @@ -771,10 +773,10 @@ self.output = torch.nn.Linear(prev_size, n_classes) @property - def device(self): + def device(self): return torch.device('cuda') if next(self.parameters()).is_cuda else torch.device('cpu') - def _init_hidden(self): + def _init_hidden(self): directions = 2 if self.bidirectional else 1 var_hidden = torch.zeros(self.nlayers * directions, 1, self.hidden_size) var_cell = torch.zeros(self.nlayers * directions, 1, self.hidden_size) @@ -784,7 +786,7 @@ [docs] - def forward(self, doc_embeddings, doc_posteriors, statistics): + def forward(self, doc_embeddings, doc_posteriors, statistics): device = self.device doc_embeddings = torch.as_tensor(doc_embeddings, dtype=torch.float, device=device) doc_posteriors = torch.as_tensor(doc_posteriors, dtype=torch.float, device=device) diff --git a/docs/build/html/_modules/quapy/method/_threshold_optim.html b/docs/build/html/_modules/quapy/method/_threshold_optim.html index 6bb8003..304e087 100644 --- a/docs/build/html/_modules/quapy/method/_threshold_optim.html +++ b/docs/build/html/_modules/quapy/method/_threshold_optim.html @@ -1,92 +1,388 @@ - - - - - - quapy.method._threshold_optim — QuaPy: A Python-based open-source framework for quantification 0.2.1 documentation - - + + + + + + + + quapy.method._threshold_optim — QuaPy: A Python-based open-source framework for quantification 0.2.1 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Skip to main content - - - - - - - - - + + + + Back to top + + + + + + + + + + Ctrl+K + + - - - - - - - - - - QuaPy: A Python-based open-source framework for quantification - - - - - - - + + - - -Quickstart - - -Manuals - - -API - - - - + + + + + + + + + + + - - - QuaPy: A Python-based open-source framework for quantification - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Home + + + + + + + Manuals + + + + + + + API + + - - - - - - Module code - quapy.method._threshold_optim - - - - - - - - Source code for quapy.method._threshold_optim -from abc import abstractmethod + + + + + + + + + -import numpy as np -from sklearn.base import BaseEstimator -import quapy as qp -import quapy.functional as F -from quapy.data import LabelledCollection -from quapy.method.aggregative import BinaryAggregativeQuantifier + + + Search + Ctrl+K + + + + + + + + + + + + + + System Settings + Light + Dark + + + + + + + + + + + + + + GitHub + + + + + + + + + + + + + Search + Ctrl+K + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Home + + + + + + + Manuals + + + + + + + API + + + + + + + + + + + + + + + + + + + + + + + System Settings + Light + Dark + + + + + + + + + + + + + + GitHub + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Module code + + quapy.method._threshold_optim + + + + + + + + + + + + + + + + + Source code for quapy.method._threshold_optim +from abc import abstractmethod + +import numpy as np +from sklearn.base import BaseEstimator +import quapy as qp +import quapy.functional as F +from quapy.data import LabelledCollection +from quapy.method.aggregative import BinaryAggregativeQuantifier [docs] -class ThresholdOptimization(BinaryAggregativeQuantifier): +class ThresholdOptimization(BinaryAggregativeQuantifier): """ Abstract class of Threshold Optimization variants for :class:`ACC` as proposed by `Forman 2006 <https://dl.acm.org/doi/abs/10.1145/1150402.1150423>`_ and @@ -111,14 +407,14 @@ :param n_jobs: number of parallel workers """ - def __init__(self, classifier: BaseEstimator=None, fit_classifier=True, val_split=None, n_jobs=None): + def __init__(self, classifier: BaseEstimator=None, fit_classifier=True, val_split=None, n_jobs=None): super().__init__(classifier, fit_classifier, val_split) self.n_jobs = qp._get_njobs(n_jobs) [docs] @abstractmethod - def condition(self, tpr, fpr) -> float: + def condition(self, tpr, fpr) -> float: """ Implements the criterion according to which the threshold should be selected. This function should return the (float) score to be minimized. @@ -132,7 +428,7 @@ [docs] - def discard(self, tpr, fpr) -> bool: + def discard(self, tpr, fpr) -> bool: """ Indicates whether a combination of tpr and fpr should be discarded @@ -144,7 +440,7 @@ - def _eval_candidate_thresholds(self, decision_scores, y): + def _eval_candidate_thresholds(self, decision_scores, y): """ Seeks for the best `tpr` and `fpr` according to the score obtained at different decision thresholds. The scoring function is implemented in function `_condition`. @@ -181,7 +477,7 @@ [docs] - def aggregate_with_threshold(self, classif_predictions, tprs, fprs, thresholds): + def aggregate_with_threshold(self, classif_predictions, tprs, fprs, thresholds): # This function performs the adjusted count for given tpr, fpr, and threshold. # Note that, due to broadcasting, tprs, fprs, and thresholds could be arrays of length > 1 prevs_estims = np.mean(classif_predictions[:, None] >= thresholds, axis=0) @@ -190,26 +486,26 @@ return prevs_estims.squeeze() - def _compute_table(self, y, y_): + def _compute_table(self, y, y_): TP = np.logical_and(y == y_, y == self.pos_label).sum() FP = np.logical_and(y != y_, y == self.neg_label).sum() FN = np.logical_and(y != y_, y == self.pos_label).sum() TN = np.logical_and(y == y_, y == self.neg_label).sum() return TP, FP, FN, TN - def _compute_tpr(self, TP, FP): - if TP + FP == 0: + def _compute_tpr(self, TP, FN): + if TP + FN == 0: return 1 - return TP / (TP + FP) + return TP / (TP + FN) - def _compute_fpr(self, FP, TN): + def _compute_fpr(self, FP, TN): if FP + TN == 0: return 0 return FP / (FP + TN) [docs] - def aggregation_fit(self, classif_predictions, labels): + def aggregation_fit(self, classif_predictions, labels): decision_scores, y = classif_predictions, labels # the standard behavior is to keep the best threshold only self.tpr, self.fpr, self.threshold = self._eval_candidate_thresholds(decision_scores, y)[0] @@ -218,7 +514,7 @@ [docs] - def aggregate(self, classif_predictions: np.ndarray): + def aggregate(self, classif_predictions: np.ndarray): # the standard behavior is to compute the adjusted count using the best threshold found return self.aggregate_with_threshold(classif_predictions, self.tpr, self.fpr, self.threshold) @@ -227,7 +523,7 @@ [docs] -class T50(ThresholdOptimization): +class T50(ThresholdOptimization): """ Threshold Optimization variant for :class:`ACC` as proposed by `Forman 2006 <https://dl.acm.org/doi/abs/10.1145/1150402.1150423>`_ and @@ -249,12 +545,12 @@ """ - def __init__(self, classifier: BaseEstimator=None, fit_classifier=True, val_split=5): + def __init__(self, classifier: BaseEstimator=None, fit_classifier=True, val_split=5): super().__init__(classifier, fit_classifier, val_split) [docs] - def condition(self, tpr, fpr) -> float: + def condition(self, tpr, fpr) -> float: return abs(tpr - 0.5) @@ -262,7 +558,7 @@ [docs] -class MAX(ThresholdOptimization): +class MAX(ThresholdOptimization): """ Threshold Optimization variant for :class:`ACC` as proposed by `Forman 2006 <https://dl.acm.org/doi/abs/10.1145/1150402.1150423>`_ and @@ -282,12 +578,12 @@ """ - def __init__(self, classifier: BaseEstimator=None, fit_classifier=True, val_split=5): + def __init__(self, classifier: BaseEstimator=None, fit_classifier=True, val_split=5): super().__init__(classifier, fit_classifier, val_split) [docs] - def condition(self, tpr, fpr) -> float: + def condition(self, tpr, fpr) -> float: # MAX strives to maximize (tpr - fpr), which is equivalent to minimize (fpr - tpr) return (fpr - tpr) @@ -296,7 +592,7 @@ [docs] -class X(ThresholdOptimization): +class X(ThresholdOptimization): """ Threshold Optimization variant for :class:`ACC` as proposed by `Forman 2006 <https://dl.acm.org/doi/abs/10.1145/1150402.1150423>`_ and @@ -316,12 +612,12 @@ """ - def __init__(self, classifier: BaseEstimator=None, fit_classifier=True, val_split=5): + def __init__(self, classifier: BaseEstimator=None, fit_classifier=True, val_split=5): super().__init__(classifier, fit_classifier, val_split) [docs] - def condition(self, tpr, fpr) -> float: + def condition(self, tpr, fpr) -> float: return abs(1 - (tpr + fpr)) @@ -329,7 +625,7 @@ [docs] -class MS(ThresholdOptimization): +class MS(ThresholdOptimization): """ Median Sweep. Threshold Optimization variant for :class:`ACC` as proposed by `Forman 2006 <https://dl.acm.org/doi/abs/10.1145/1150402.1150423>`_ and @@ -348,18 +644,18 @@ for `k`); or as a tuple (X,y) defining the specific set of data to use for validation. """ - def __init__(self, classifier: BaseEstimator=None, fit_classifier=True, val_split=5): + def __init__(self, classifier: BaseEstimator=None, fit_classifier=True, val_split=5): super().__init__(classifier, fit_classifier, val_split) [docs] - def condition(self, tpr, fpr) -> float: + def condition(self, tpr, fpr) -> float: return 1 [docs] - def aggregation_fit(self, classif_predictions, labels): + def aggregation_fit(self, classif_predictions, labels): decision_scores, y = classif_predictions, labels # keeps all candidates tprs_fprs_thresholds = self._eval_candidate_thresholds(decision_scores, y) @@ -371,7 +667,7 @@ [docs] - def aggregate(self, classif_predictions: np.ndarray): + def aggregate(self, classif_predictions: np.ndarray): prevalences = self.aggregate_with_threshold(classif_predictions, self.tprs, self.fprs, self.thresholds) if prevalences.ndim==2: prevalences = np.median(prevalences, axis=0) @@ -382,7 +678,7 @@ [docs] -class MS2(MS): +class MS2(MS): """ Median Sweep 2. Threshold Optimization variant for :class:`ACC` as proposed by `Forman 2006 <https://dl.acm.org/doi/abs/10.1145/1150402.1150423>`_ and @@ -402,42 +698,86 @@ for `k`); or as a tuple (X,y) defining the specific set of data to use for validation. """ - def __init__(self, classifier: BaseEstimator=None, fit_classifier=True, val_split=5): + def __init__(self, classifier: BaseEstimator=None, fit_classifier=True, val_split=5): super().__init__(classifier, fit_classifier, val_split) [docs] - def discard(self, tpr, fpr) -> bool: + def discard(self, tpr, fpr) -> bool: return (tpr-fpr) <= 0.25 - + + + + + + + + + + + + + - + + + + - Built with Sphinx using a - theme - provided by Read the Docs. - + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/build/html/_modules/quapy/method/aggregative.html b/docs/build/html/_modules/quapy/method/aggregative.html index a6b98d4..0680ac3 100644 --- a/docs/build/html/_modules/quapy/method/aggregative.html +++ b/docs/build/html/_modules/quapy/method/aggregative.html @@ -29,8 +29,9 @@ - - + + + @@ -41,6 +42,7 @@ + @@ -114,8 +116,14 @@ + + + + + + + - QuaPy @@ -131,7 +139,7 @@ - Quickstart + Home @@ -183,6 +191,21 @@ + + + + + + + + + + + GitHub + + + @@ -229,7 +252,7 @@ - Quickstart + Home @@ -272,6 +295,21 @@ + + + + + + + + + + + GitHub + + + @@ -332,25 +370,25 @@ Source code for quapy.method.aggregative -from abc import ABC, abstractmethod -from argparse import ArgumentError -from copy import deepcopy -from typing import Callable, Literal, Union -import numpy as np -from sklearn.base import BaseEstimator -from sklearn.calibration import CalibratedClassifierCV -from sklearn.exceptions import NotFittedError -from sklearn.metrics import confusion_matrix -from sklearn.model_selection import cross_val_predict, train_test_split -from sklearn.utils.validation import check_is_fitted +import warnings +from abc import ABC, abstractmethod +from copy import deepcopy +from typing import Callable, Literal, Union +import numpy as np +from sklearn.base import BaseEstimator +from sklearn.calibration import CalibratedClassifierCV +from sklearn.exceptions import NotFittedError +from sklearn.metrics import confusion_matrix +from sklearn.model_selection import cross_val_predict, train_test_split +from sklearn.utils.validation import check_is_fitted -import quapy as qp -import quapy.functional as F -from quapy.functional import get_divergence -from quapy.classification.svmperf import SVMperf -from quapy.data import LabelledCollection -from quapy.method.base import BaseQuantifier, BinaryQuantifier, OneVsAllGeneric -from quapy.method._helper import ( +import quapy as qp +import quapy.functional as F +from quapy.functional import get_divergence +from quapy.classification.svmperf import SVMperf +from quapy.data import LabelledCollection +from quapy.method.base import BaseQuantifier, BinaryQuantifier, OneVsAllGeneric +from quapy.method._helper import ( _get_abstention_calibrators, _get_cvxpy, _rlls_check_mode, @@ -368,7 +406,7 @@ [docs] -class AggregativeQuantifier(BaseQuantifier, ABC): +class AggregativeQuantifier(BaseQuantifier, ABC): """ Abstract class for quantification methods that base their estimations on the aggregation of classification results. Aggregative quantifiers implement a pipeline that consists of generating classification predictions @@ -396,7 +434,7 @@ the training data be wasted. """ - def __init__(self, + def __init__(self, classifier: Union[None,BaseEstimator], fit_classifier:bool=True, val_split:Union[int,float,tuple,None]=5): @@ -418,9 +456,9 @@ (f'when {val_split=} is indicated as an integer, it represents the number of folds in a kFCV ' f'and must thus be >1') if val_split==5 and not fit_classifier: - print(f'Warning: {val_split=} will be ignored when the classifier is already trained ' - f'({fit_classifier=}). Parameter {self.val_split=} will be set to None. Set {val_split=} ' - f'to None to avoid this warning.') + warnings.warn(f'{val_split=} will be ignored when the classifier is already trained ' + f'({fit_classifier=}). Parameter {self.val_split=} will be set to None. Set {val_split=} ' + f'to None to avoid this warning.') self.val_split=None if val_split!=5: assert fit_classifier, (f'Parameter {val_split=} has been modified, but {fit_classifier=} ' @@ -457,7 +495,7 @@ assert fitted, (f'{fit_classifier=} requires the classifier to be already trained, ' f'but this does not seem to be') - def _check_init_parameters(self): + def _check_init_parameters(self): """ Implements any check to be performed in the parameters of the init method before undertaking the training of the quantifier. This is made as to allow for a quick execution stop when the @@ -467,7 +505,7 @@ """ pass - def _check_non_empty_classes(self, y): + def _check_non_empty_classes(self, y): """ Asserts all classes have positive instances. @@ -484,7 +522,7 @@ [docs] - def fit(self, X, y): + def fit(self, X, y): """ Trains the aggregative quantifier. This comes down to training a classifier (if requested) and an aggregation function. @@ -501,7 +539,7 @@ [docs] - def classifier_fit_predict(self, X, y): + def classifier_fit_predict(self, X, y): """ Trains the classifier if requested (`fit_classifier=True`) and generate the necessary predictions to train the aggregation function. @@ -551,7 +589,7 @@ [docs] @abstractmethod - def aggregation_fit(self, classif_predictions, labels): + def aggregation_fit(self, classif_predictions, labels): """ Trains the aggregation function. @@ -563,7 +601,7 @@ @property - def classifier(self): + def classifier(self): """ Gives access to the classifier @@ -572,7 +610,7 @@ return self.classifier_ @classifier.setter - def classifier(self, classifier): + def classifier(self, classifier): """ Setter for the classifier @@ -582,7 +620,7 @@ [docs] - def classify(self, X): + def classify(self, X): """ Provides the label predictions for the given instances. The predictions should respect the format expected by :meth:`aggregate`, e.g., posterior probabilities for probabilistic quantifiers, or crisp predictions for @@ -594,7 +632,7 @@ return getattr(self.classifier, self._classifier_method())(X) - def _classifier_method(self): + def _classifier_method(self): """ Name of the method that must be used for issuing label predictions. The default one is "decision_function". @@ -602,7 +640,7 @@ """ return 'decision_function' - def _check_classifier(self, adapt_if_necessary=False): + def _check_classifier(self, adapt_if_necessary=False): """ Guarantees that the underlying classifier implements the method required for issuing predictions, i.e., the method indicated by the :meth:`_classifier_method` @@ -614,7 +652,7 @@ [docs] - def predict(self, X): + def predict(self, X): """ Generate class prevalence estimates for the sample's instances by aggregating the label predictions generated by the classifier. @@ -629,7 +667,7 @@ [docs] @abstractmethod - def aggregate(self, classif_predictions: np.ndarray): + def aggregate(self, classif_predictions: np.ndarray): """ Implements the aggregation of the classifier predictions. @@ -640,7 +678,7 @@ @property - def classes_(self): + def classes_(self): """ Class labels, in the same order in which class prevalence values are to be computed. This default implementation actually returns the class labels of the learner. @@ -653,14 +691,14 @@ [docs] -class AggregativeCrispQuantifier(AggregativeQuantifier, ABC): +class AggregativeCrispQuantifier(AggregativeQuantifier, ABC): """ Abstract class for quantification methods that base their estimations on the aggregation of crisp decisions as returned by a hard classifier. Aggregative crisp quantifiers thus extend Aggregative Quantifiers by implementing specifications about crisp predictions. """ - def _classifier_method(self): + def _classifier_method(self): """ Name of the method that must be used for issuing label predictions. For crisp quantifiers, the method is 'predict', that returns an array of shape `(n_instances,)` of label predictions. @@ -673,7 +711,7 @@ [docs] -class AggregativeSoftQuantifier(AggregativeQuantifier, ABC): +class AggregativeSoftQuantifier(AggregativeQuantifier, ABC): """ Abstract class for quantification methods that base their estimations on the aggregation of posterior probabilities as returned by a probabilistic classifier. @@ -681,7 +719,7 @@ about soft predictions. """ - def _classifier_method(self): + def _classifier_method(self): """ Name of the method that must be used for issuing label predictions. For probabilistic quantifiers, the method is 'predict_proba', that returns an array of shape `(n_instances, n_dimensions,)` with posterior @@ -691,7 +729,7 @@ """ return 'predict_proba' - def _check_classifier(self, adapt_if_necessary=False): + def _check_classifier(self, adapt_if_necessary=False): """ Guarantees that the underlying classifier implements the method indicated by the :meth:`_classifier_method`. In case it does not, the classifier is calibrated (by means of the Platt's calibration method implemented by @@ -703,8 +741,8 @@ """ if not hasattr(self.classifier, self._classifier_method()): if adapt_if_necessary: - print(f'warning: The learner {self.classifier.__class__.__name__} does not seem to be ' - f'probabilistic. The learner will be calibrated (using CalibratedClassifierCV).') + warnings.warn(f'The learner {self.classifier.__class__.__name__} does not seem to be ' + f'probabilistic. The learner will be calibrated (using CalibratedClassifierCV).') self.classifier = CalibratedClassifierCV(self.classifier, cv=5) else: raise AssertionError(f'error: The learner {self.classifier.__class__.__name__} does not ' @@ -715,19 +753,19 @@ [docs] -class BinaryAggregativeQuantifier(AggregativeQuantifier, BinaryQuantifier): +class BinaryAggregativeQuantifier(AggregativeQuantifier, BinaryQuantifier): @property - def pos_label(self): + def pos_label(self): return self.classifier.classes_[1] @property - def neg_label(self): + def neg_label(self): return self.classifier.classes_[0] [docs] - def fit(self, X, y): + def fit(self, X, y): self._check_binary(y, self.__class__.__name__) return super().fit(X, y) @@ -738,19 +776,19 @@ # ------------------------------------ [docs] -class CC(AggregativeCrispQuantifier): +class CC(AggregativeCrispQuantifier): """ The most basic Quantification method. One that simply classifies all instances and counts how many have been attributed to each of the classes in order to compute class prevalence estimates. :param classifier: a sklearn's Estimator that generates a classifier """ - def __init__(self, classifier: BaseEstimator = None, fit_classifier: bool = True): + def __init__(self, classifier: BaseEstimator = None, fit_classifier: bool = True): super().__init__(classifier, fit_classifier, val_split=None) [docs] - def aggregation_fit(self, classif_predictions, labels): + def aggregation_fit(self, classif_predictions, labels): """ Nothing to do here! @@ -762,7 +800,7 @@ [docs] - def aggregate(self, classif_predictions: np.ndarray): + def aggregate(self, classif_predictions: np.ndarray): """ Computes class prevalence estimates by counting the prevalence of each of the predicted labels. @@ -776,7 +814,7 @@ [docs] -class PCC(AggregativeSoftQuantifier): +class PCC(AggregativeSoftQuantifier): """ `Probabilistic Classify & Count <https://ieeexplore.ieee.org/abstract/document/5694031>`_, the probabilistic variant of CC that relies on the posterior probabilities returned by a probabilistic classifier. @@ -784,12 +822,12 @@ :param classifier: a sklearn's Estimator that generates a classifier """ - def __init__(self, classifier: BaseEstimator = None, fit_classifier: bool = True, val_split=None): + def __init__(self, classifier: BaseEstimator = None, fit_classifier: bool = True, val_split=None): super().__init__(classifier, fit_classifier, val_split=val_split) [docs] - def aggregation_fit(self, classif_predictions, labels): + def aggregation_fit(self, classif_predictions, labels): """ Nothing to do here! @@ -801,7 +839,7 @@ [docs] - def aggregate(self, classif_posteriors): + def aggregate(self, classif_posteriors): return F.prevalence_from_probabilities(classif_posteriors, binarize=False) @@ -809,7 +847,7 @@ [docs] -class ACC(AggregativeCrispQuantifier): +class ACC(AggregativeCrispQuantifier): """ `Adjusted Classify & Count <https://link.springer.com/article/10.1007/s10618-008-0097-y>`_, the "adjusted" variant of :class:`CC`, that corrects the predictions of CC @@ -857,7 +895,7 @@ :param n_jobs: number of parallel workers """ - def __init__( + def __init__( self, classifier: BaseEstimator = None, fit_classifier = True, @@ -880,7 +918,7 @@ [docs] @classmethod - def newInvariantRatioEstimation(cls, classifier: BaseEstimator, fit_classifier=True, val_split=5, n_jobs=None): + def newInvariantRatioEstimation(cls, classifier: BaseEstimator, fit_classifier=True, val_split=5, n_jobs=None): """ Constructs a quantifier that implements the Invariant Ratio Estimator of `Vaz et al. 2018 <https://jmlr.org/papers/v20/18-456.html>`_. This amounts @@ -905,7 +943,7 @@ return ACC(classifier, fit_classifier=fit_classifier, val_split=val_split, method='invariant-ratio', norm='mapsimplex', n_jobs=n_jobs) - def _check_init_parameters(self): + def _check_init_parameters(self): if self.solver not in ACC.SOLVERS: raise ValueError(f"unknown solver; valid ones are {ACC.SOLVERS}") if self.method not in ACC.METHODS: @@ -915,7 +953,7 @@ [docs] - def aggregation_fit(self, classif_predictions, labels): + def aggregation_fit(self, classif_predictions, labels): """ Estimates the misclassification rates. :param classif_predictions: array-like with the predicted labels @@ -930,7 +968,7 @@ [docs] @classmethod - def getPteCondEstim(cls, classes, y, y_): + def getPteCondEstim(cls, classes, y, y_): """ Estimate the matrix with entry (i,j) being the estimate of P(hat_yi|yj), that is, the probability that a document that belongs to yj ends up being classified as belonging to yi @@ -953,7 +991,7 @@ [docs] - def aggregate(self, classif_predictions): + def aggregate(self, classif_predictions): prevs_estim = self.cc.aggregate(classif_predictions) estimate = F.solve_adjustment( class_conditional_rates=self.Pte_cond_estim_, @@ -968,7 +1006,7 @@ [docs] -class PACC(AggregativeSoftQuantifier): +class PACC(AggregativeSoftQuantifier): """ `Probabilistic Adjusted Classify & Count <https://ieeexplore.ieee.org/abstract/document/5694031>`_, the probabilistic variant of ACC that relies on the posterior probabilities returned by a probabilistic classifier. @@ -1015,7 +1053,7 @@ :param n_jobs: number of parallel workers """ - def __init__( + def __init__( self, classifier: BaseEstimator = None, fit_classifier=True, @@ -1031,7 +1069,7 @@ self.method = method self.norm = norm - def _check_init_parameters(self): + def _check_init_parameters(self): if self.solver not in ACC.SOLVERS: raise ValueError(f"unknown solver; valid ones are {ACC.SOLVERS}") if self.method not in ACC.METHODS: @@ -1041,7 +1079,7 @@ [docs] - def aggregation_fit(self, classif_predictions, labels): + def aggregation_fit(self, classif_predictions, labels): """ Estimates the misclassification rates @@ -1056,7 +1094,7 @@ [docs] - def aggregate(self, classif_posteriors): + def aggregate(self, classif_posteriors): prevs_estim = self.pcc.aggregate(classif_posteriors) estimate = F.solve_adjustment( @@ -1071,7 +1109,7 @@ [docs] @classmethod - def getPteCondEstim(cls, classes, y, y_): + def getPteCondEstim(cls, classes, y, y_): # estimate the matrix with entry (i,j) being the estimate of P(hat_yi|yj), that is, the probability that a # document that belongs to yj ends up being classified as belonging to yi n_classes = len(classes) @@ -1088,7 +1126,7 @@ [docs] -class RLLS(AggregativeSoftQuantifier): +class RLLS(AggregativeSoftQuantifier): """ `Regularized Learning for Domain Adaptation under Label Shifts <https://arxiv.org/abs/1903.09734>`_, used here as an aggregative @@ -1128,7 +1166,7 @@ :func:`quapy.functional.normalize_prevalence` """ - def __init__( + def __init__( self, classifier: BaseEstimator = None, fit_classifier=True, @@ -1147,7 +1185,7 @@ self.norm = norm self.last_w_ = None - def _check_init_parameters(self): + def _check_init_parameters(self): _get_cvxpy() _rlls_check_mode(self.mode) if not isinstance(self.alpha, (int, float)) or self.alpha < 0: @@ -1164,7 +1202,7 @@ [docs] - def aggregation_fit(self, classif_predictions, labels): + def aggregation_fit(self, classif_predictions, labels): if classif_predictions is None or labels is None: raise ValueError('RLLS requires source posterior predictions and source labels') @@ -1181,7 +1219,7 @@ [docs] - def aggregate(self, classif_posteriors): + def aggregate(self, classif_posteriors): qz = _rlls_predicted_marginal(classif_posteriors, mode=self.mode) w = _rlls_compute_weights( self.C_zy_, @@ -1199,7 +1237,7 @@ [docs] -class EMQ(AggregativeSoftQuantifier): +class EMQ(AggregativeSoftQuantifier): """ `Expectation Maximization for Quantification <https://ieeexplore.ieee.org/abstract/document/6789744>`_ (EMQ), aka `Saerens-Latinne-Decaestecker` (SLD) algorithm. @@ -1249,7 +1287,7 @@ ON_CALIB_ERROR_VALUES = ['raise', 'backup'] CALIB_OPTIONS = [None, 'nbvs', 'bcts', 'ts', 'vs'] - def __init__(self, classifier: BaseEstimator = None, fit_classifier=True, val_split=None, exact_train_prev=True, + def __init__(self, classifier: BaseEstimator = None, fit_classifier=True, val_split=None, exact_train_prev=True, calib=None, on_calib_error='raise', n_jobs=None): assert calib in EMQ.CALIB_OPTIONS, \ @@ -1261,12 +1299,12 @@ self.exact_train_prev = exact_train_prev self.calib = calib self.on_calib_error = on_calib_error - self.n_jobs = n_jobs + self.n_jobs = qp._get_njobs(n_jobs) [docs] @classmethod - def EMQ_BCTS(cls, classifier: BaseEstimator, fit_classifier=True, val_split=5, on_calib_error="raise", n_jobs=None): + def EMQ_BCTS(cls, classifier: BaseEstimator, fit_classifier=True, val_split=5, on_calib_error="raise", n_jobs=None): """ Constructs an instance of EMQ using the best configuration found in the `Alexandari et al. paper <http://proceedings.mlr.press/v119/alexandari20a.html>`_, i.e., one that relies on Bias-Corrected Temperature @@ -1298,23 +1336,23 @@ calib='bcts', on_calib_error=on_calib_error, n_jobs=n_jobs) - def _check_init_parameters(self): + def _check_init_parameters(self): if self.val_split is not None: if self.exact_train_prev and self.calib is None: - raise RuntimeWarning(f'The parameter {self.val_split=} was specified for EMQ, while the parameters ' - f'{self.exact_train_prev=} and {self.calib=}. This has no effect and causes an ' - f'unnecessary overload.') + warnings.warn(f'The parameter {self.val_split=} was specified for EMQ, while the parameters ' + f'{self.exact_train_prev=} and {self.calib=}. This has no effect and causes an ' + f'unnecessary overload.', RuntimeWarning) else: if self.calib is not None: - print(f'[warning] The parameter {self.calib=} requires the val_split be different from None. ' - f'This parameter will be set to 5. To avoid this warning, set this value to a float value ' - f'indicating the proportion of training data to be used as validation, or to an integer ' - f'indicating the number of folds for kFCV.') + warnings.warn(f'The parameter {self.calib=} requires the val_split be different from None. ' + f'This parameter will be set to 5. To avoid this warning, set this value to a float value ' + f'indicating the proportion of training data to be used as validation, or to an integer ' + f'indicating the number of folds for kFCV.') self.val_split = 5 [docs] - def classify(self, X): + def classify(self, X): """ Provides the posterior probabilities for the given instances. The calibration function, if required, has no effect in this step, and is only involved in the aggregate method. @@ -1327,13 +1365,13 @@ [docs] - def classifier_fit_predict(self, X, y): + def classifier_fit_predict(self, X, y): classif_predictions = super().classifier_fit_predict(X, y) self.train_prevalence = F.prevalence_from_labels(y, classes=self.classes_) return classif_predictions - def _fit_calibration(self, calibrator, P, y): + def _fit_calibration(self, calibrator, P, y): n_classes = len(self.classes_) if not np.issubdtype(y.dtype, np.number): @@ -1347,7 +1385,7 @@ elif self.on_calib_error == 'backup': self.calibration_function = lambda P: P - def _calibrate_if_requested(self, uncalib_posteriors): + def _calibrate_if_requested(self, uncalib_posteriors): if hasattr(self, 'calibration_function') and self.calibration_function is not None: try: calib_posteriors = self.calibration_function(uncalib_posteriors) @@ -1364,7 +1402,7 @@ [docs] - def aggregation_fit(self, classif_predictions, labels): + def aggregation_fit(self, classif_predictions, labels): """ Trains the aggregation function of EMQ. This comes down to recalibrating the posterior probabilities ir requested. @@ -1379,14 +1417,14 @@ requires_predictions = (self.calib is not None) or (not self.exact_train_prev) if P is None and requires_predictions: # classifier predictions were not generated because val_split=None - raise ArgumentError(self.val_split, self.__class__.__name__ + - ": Classifier predictions for the aggregative fit were not generated because " - "val_split=None. This usually happens when you enable calibrations or heuristics " - "during model selection but left val_split set to its default value (None). " - "Please provide one of the following values for val_split: (i) an integer >1 " - "(e.g. val_split=5) for k-fold cross-validation; (ii) a float in (0,1) (e.g. " - "val_split=0.3) for a proportion split; or (iii) a tuple (X, y) with explicit " - "validation data") + raise ValueError(self.__class__.__name__ + + ": Classifier predictions for the aggregative fit were not generated because " + "val_split=None. This usually happens when you enable calibrations or heuristics " + "during model selection but left val_split set to its default value (None). " + "Please provide one of the following values for val_split: (i) an integer >1 " + "(e.g. val_split=5) for k-fold cross-validation; (ii) a float in (0,1) (e.g. " + "val_split=0.3) for a proportion split; or (iii) a tuple (X, y) with explicit " + "validation data") if self.calib is not None: calibrator = _get_abstention_calibrators().get(self.calib, None) @@ -1403,7 +1441,7 @@ [docs] - def aggregate(self, classif_posteriors, epsilon=EPSILON): + def aggregate(self, classif_posteriors, epsilon=EPSILON): classif_posteriors = self._calibrate_if_requested(classif_posteriors) priors, posteriors = self.EM(self.train_prevalence, classif_posteriors, epsilon) return priors @@ -1411,7 +1449,7 @@ [docs] - def predict_proba(self, instances, epsilon=EPSILON): + def predict_proba(self, instances, epsilon=EPSILON): """ Returns the posterior probabilities updated by the EM algorithm. @@ -1428,7 +1466,7 @@ [docs] @classmethod - def EM(cls, tr_prev, posterior_probabilities, epsilon=EPSILON): + def EM(cls, tr_prev, posterior_probabilities, epsilon=EPSILON): """ Computes the `Expectation Maximization` routine. @@ -1466,7 +1504,7 @@ s += 1 if not converged: - print('[warning] the method has reached the maximum number of iterations; it might have not converged') + warnings.warn('the method has reached the maximum number of iterations; it might have not converged') return qs, ps @@ -1475,7 +1513,7 @@ [docs] -class HDy(AggregativeSoftQuantifier, BinaryAggregativeQuantifier): +class HDy(AggregativeSoftQuantifier, BinaryAggregativeQuantifier): """ `Hellinger Distance y <https://www.sciencedirect.com/science/article/pii/S0020025512004069>`_ (HDy). HDy is a probabilistic method for training binary quantifiers, that models quantification as the problem of @@ -1502,12 +1540,12 @@ for `k`); or as a tuple (X,y) defining the specific set of data to use for validation. """ - def __init__(self, classifier: BaseEstimator = None, fit_classifier=True, val_split=5): + def __init__(self, classifier: BaseEstimator = None, fit_classifier=True, val_split=5): super().__init__(classifier, fit_classifier, val_split) [docs] - def aggregation_fit(self, classif_predictions, labels): + def aggregation_fit(self, classif_predictions, labels): """ Trains the aggregation function of HDy. @@ -1522,7 +1560,7 @@ # pre-compute the histogram for positive and negative examples self.bins = np.linspace(10, 110, 11, dtype=int) # [10, 20, 30, ..., 100, 110] - def hist(P, bins): + def hist(P, bins): h = np.histogram(P, bins=bins, range=(0, 1), density=True)[0] return h / h.sum() @@ -1532,7 +1570,7 @@ [docs] - def aggregate(self, classif_posteriors): + def aggregate(self, classif_posteriors): # "In this work, the number of bins b used in HDx and HDy was chosen from 10 to 110 in steps of 10, # and the final estimated a priori probability was taken as the median of these 11 estimates." # (González-Castro, et al., 2013). @@ -1549,7 +1587,7 @@ # the authors proposed to search for the prevalence yielding the best matching as a linear search # at small steps (modern implementations resort to an optimization procedure, # see class DistributionMatching) - def loss(prev): + def loss(prev): class1_prev = prev[1] Px_train = class1_prev * Pxy1_density + (1 - class1_prev) * Pxy0_density return F.HellingerDistance(Px_train, Px_test) @@ -1564,7 +1602,7 @@ [docs] -class DyS(AggregativeSoftQuantifier, BinaryAggregativeQuantifier): +class DyS(AggregativeSoftQuantifier, BinaryAggregativeQuantifier): """ `DyS framework <https://ojs.aaai.org/index.php/AAAI/article/view/4376>`_ (DyS). DyS is a generalization of HDy method, using a Ternary Search in order to find the prevalence that @@ -1593,15 +1631,15 @@ :param n_jobs: number of parallel workers. """ - def __init__(self, classifier: BaseEstimator = None, fit_classifier=True, val_split=5, n_bins=8, + def __init__(self, classifier: BaseEstimator = None, fit_classifier=True, val_split=5, n_bins=8, divergence: Union[str, Callable] = 'HD', tol=1e-05, n_jobs=None): super().__init__(classifier, fit_classifier, val_split) self.tol = tol self.divergence = divergence self.n_bins = n_bins - self.n_jobs = n_jobs + self.n_jobs = qp._get_njobs(n_jobs) - def _ternary_search(self, f, left, right, tol): + def _ternary_search(self, f, left, right, tol): """ Find maximum of unimodal function f() within [left, right] """ @@ -1619,7 +1657,7 @@ [docs] - def aggregation_fit(self, classif_predictions, labels): + def aggregation_fit(self, classif_predictions, labels): """ Trains the aggregation function of DyS. @@ -1637,13 +1675,13 @@ [docs] - def aggregate(self, classif_posteriors): + def aggregate(self, classif_posteriors): Px = classif_posteriors[:, self.pos_label] # takes only the P(y=+1|x) Px_test = np.histogram(Px, bins=self.n_bins, range=(0, 1), density=True)[0] divergence = get_divergence(self.divergence) - def distribution_distance(prev): + def distribution_distance(prev): Px_train = prev * self.Pxy1_density + (1 - prev) * self.Pxy0_density return divergence(Px_train, Px_test) @@ -1655,7 +1693,7 @@ [docs] -class SMM(AggregativeSoftQuantifier, BinaryAggregativeQuantifier): +class SMM(AggregativeSoftQuantifier, BinaryAggregativeQuantifier): """ `SMM method <https://ieeexplore.ieee.org/document/9260028>`_ (SMM). SMM is a simplification of matching distribution methods where the representation of the examples @@ -1674,12 +1712,12 @@ for `k`); or as a tuple (X,y) defining the specific set of data to use for validation. """ - def __init__(self, classifier: BaseEstimator = None, fit_classifier=True, val_split=5): + def __init__(self, classifier: BaseEstimator = None, fit_classifier=True, val_split=5): super().__init__(classifier, fit_classifier, val_split) [docs] - def aggregation_fit(self, classif_predictions, labels): + def aggregation_fit(self, classif_predictions, labels): """ Trains the aggregation function of SMM. @@ -1697,7 +1735,7 @@ [docs] - def aggregate(self, classif_posteriors): + def aggregate(self, classif_posteriors): Px = classif_posteriors[:, self.pos_label] # takes only the P(y=+1|x) Px_mean = np.mean(Px) @@ -1709,7 +1747,7 @@ [docs] -class DMy(AggregativeSoftQuantifier): +class DMy(AggregativeSoftQuantifier): """ Generic Distribution Matching quantifier for binary or multiclass quantification based on the space of posterior probabilities. This implementation takes the number of bins, the divergence, and the possibility to work on CDF @@ -1742,19 +1780,19 @@ :param n_jobs: number of parallel workers (default None) """ - def __init__(self, classifier: BaseEstimator = None, fit_classifier=True, val_split=5, nbins=8, + def __init__(self, classifier: BaseEstimator = None, fit_classifier=True, val_split=5, nbins=8, divergence: Union[str, Callable] = 'HD', cdf=False, search='optim_minimize', n_jobs=None): super().__init__(classifier, fit_classifier, val_split) self.nbins = nbins self.divergence = divergence self.cdf = cdf self.search = search - self.n_jobs = n_jobs + self.n_jobs = qp._get_njobs(n_jobs) [docs] @classmethod - def HDy(cls, classifier: BaseEstimator = None, fit_classifier=True, val_split=5, n_jobs=None): + def HDy(cls, classifier: BaseEstimator = None, fit_classifier=True, val_split=5, n_jobs=None): """ Historical HDy preset expressed as a configuration of :class:`DMy`. @@ -1783,7 +1821,7 @@ return AggregativeMedianEstimator(base_quantifier=base, param_grid=param_grid, n_jobs=n_jobs) - def _get_distributions(self, posteriors): + def _get_distributions(self, posteriors): histograms = [] post_dims = posteriors.shape[1] if post_dims == 2: @@ -1801,7 +1839,7 @@ [docs] - def aggregation_fit(self, classif_predictions, labels): + def aggregation_fit(self, classif_predictions, labels): """ Trains the aggregation function of a distribution matching method. This comes down to generating the validation distributions out of the training data. @@ -1829,7 +1867,7 @@ [docs] - def aggregate(self, posteriors: np.ndarray): + def aggregate(self, posteriors: np.ndarray): """ Searches for the mixture model parameter (the sought prevalence values) that yields a validation distribution (the mixture) that best matches the test distribution, in terms of the divergence measure of choice. @@ -1844,7 +1882,7 @@ divergence = get_divergence(self.divergence) n_classes, n_channels, nbins = self.validation_distribution.shape - def loss(prev): + def loss(prev): prev = np.expand_dims(prev, axis=0) mixture_distribution = (prev @ self.validation_distribution.reshape(n_classes, -1)).reshape(n_channels, -1) divs = [divergence(test_distribution[ch], mixture_distribution[ch]) for ch in range(n_channels)] @@ -1857,7 +1895,7 @@ [docs] -def newELM(svmperf_base=None, loss='01', C=1): +def newELM(svmperf_base=None, loss='01', C=1): """ Explicit Loss Minimization (ELM) quantifiers. Quantifiers based on ELM represent a family of methods based on structured output learning; @@ -1887,7 +1925,7 @@ [docs] -def newSVMQ(svmperf_base=None, C=1): +def newSVMQ(svmperf_base=None, C=1): """ SVM(Q) is an Explicit Loss Minimization (ELM) quantifier set to optimize for the `Q` loss combining a classification-oriented loss and a quantification-oriented loss, as proposed by @@ -1914,7 +1952,9 @@ -def newSVMKLD(svmperf_base=None, C=1): + +[docs] +def newSVMKLD(svmperf_base=None, C=1): """ SVM(KLD) is an Explicit Loss Minimization (ELM) quantifier set to optimize for the Kullback-Leibler Divergence as proposed by `Esuli et al. 2015 <https://dl.acm.org/doi/abs/10.1145/2700406>`_. @@ -1936,14 +1976,15 @@ :return: returns an instance of CC set to work with SVMperf (with loss and C set properly) as the underlying classifier """ - return newELM(svmperf_base, loss='kld', C=C) + return newELM(svmperf_base, loss='kld', C=C) - -[docs] -def newSVMKLD(svmperf_base=None, C=1): + + +[docs] +def newSVMNKLD(svmperf_base=None, C=1): """ - SVM(KLD) is an Explicit Loss Minimization (ELM) quantifier set to optimize for the Kullback-Leibler Divergence + SVM(NKLD) is an Explicit Loss Minimization (ELM) quantifier set to optimize for the Kullback-Leibler Divergence normalized via the logistic function, as proposed by `Esuli et al. 2015 <https://dl.acm.org/doi/abs/10.1145/2700406>`_. Equivalent to: @@ -1970,7 +2011,7 @@ [docs] -def newSVMAE(svmperf_base=None, C=1): +def newSVMAE(svmperf_base=None, C=1): """ SVM(KLD) is an Explicit Loss Minimization (ELM) quantifier set to optimize for the Absolute Error as first used by `Moreo and Sebastiani, 2021 <https://arxiv.org/abs/2011.02552>`_. @@ -1998,7 +2039,7 @@ [docs] -def newSVMRAE(svmperf_base=None, C=1): +def newSVMRAE(svmperf_base=None, C=1): """ SVM(KLD) is an Explicit Loss Minimization (ELM) quantifier set to optimize for the Relative Absolute Error as first used by `Moreo and Sebastiani, 2021 <https://arxiv.org/abs/2011.02552>`_. @@ -2026,7 +2067,7 @@ [docs] -class OneVsAllAggregative(OneVsAllGeneric, AggregativeQuantifier): +class OneVsAllAggregative(OneVsAllGeneric, AggregativeQuantifier): """ Allows any binary quantifier to perform quantification on single-label datasets. The method maintains one binary quantifier for each class, and then l1-normalizes the outputs so that the @@ -2042,7 +2083,7 @@ is removed and no longer available at predict time. """ - def __init__(self, binary_quantifier=None, n_jobs=None, parallel_backend='multiprocessing'): + def __init__(self, binary_quantifier=None, n_jobs=None, parallel_backend='multiprocessing'): if binary_quantifier is None: binary_quantifier = PACC() assert isinstance(binary_quantifier, BaseQuantifier), \ @@ -2055,7 +2096,7 @@ [docs] - def classify(self, X): + def classify(self, X): """ If the base quantifier is not probabilistic, returns a matrix of shape `(n,m,)` with `n` the number of instances and `m` the number of classes. The entry `(i,j)` is a binary value indicating whether instance @@ -2079,34 +2120,34 @@ [docs] - def aggregate(self, classif_predictions): + def aggregate(self, classif_predictions): prevalences = self._parallel(self._delayed_binary_aggregate, classif_predictions) return F.normalize_prevalence(prevalences) [docs] - def aggregation_fit(self, classif_predictions, labels): - self._parallel(self._delayed_binary_aggregate_fit(c, classif_predictions, labels)) + def aggregation_fit(self, classif_predictions, labels): + self._parallel(self._delayed_binary_aggregate_fit, classif_predictions, labels) return self - def _delayed_binary_classification(self, c, X): + def _delayed_binary_classification(self, c, X): return self.dict_binary_quantifiers[c].classify(X) - def _delayed_binary_aggregate(self, c, classif_predictions): + def _delayed_binary_aggregate(self, c, classif_predictions): # the estimation for the positive class prevalence return self.dict_binary_quantifiers[c].aggregate(classif_predictions[:, c])[1] - def _delayed_binary_aggregate_fit(self, c, classif_predictions, labels): + def _delayed_binary_aggregate_fit(self, c, classif_predictions, labels): # trains the aggregation function of the cth quantifier - return self.dict_binary_quantifiers[c].aggregate_fit(classif_predictions[:, c], labels) + return self.dict_binary_quantifiers[c].aggregation_fit(classif_predictions[:, c], labels == c) [docs] -class AggregativeMedianEstimator(BinaryQuantifier): +class AggregativeMedianEstimator(BinaryQuantifier): """ This method is a meta-quantifier that returns, as the estimated class prevalence values, the median of the estimation returned by differently (hyper)parameterized base quantifiers. @@ -2119,7 +2160,7 @@ :param n_jobs: number of parallel workers """ - def __init__(self, base_quantifier: AggregativeQuantifier, param_grid: dict, random_state=None, n_jobs=None): + def __init__(self, base_quantifier: AggregativeQuantifier, param_grid: dict, random_state=None, n_jobs=None): self.base_quantifier = base_quantifier self.param_grid = param_grid self.random_state = random_state @@ -2127,17 +2168,17 @@ [docs] - def get_params(self, deep=True): + def get_params(self, deep=True): return self.base_quantifier.get_params(deep) [docs] - def set_params(self, **params): + def set_params(self, **params): self.base_quantifier.set_params(**params) - def _delayed_fit(self, args): + def _delayed_fit(self, args): with qp.util.temp_seed(self.random_state): params, X, y = args model = deepcopy(self.base_quantifier) @@ -2145,7 +2186,7 @@ model.fit(X, y) return model - def _delayed_fit_classifier(self, args): + def _delayed_fit_classifier(self, args): with qp.util.temp_seed(self.random_state): cls_params, X, y = args model = deepcopy(self.base_quantifier) @@ -2153,7 +2194,7 @@ predictions, labels = model.classifier_fit_predict(X, y) return (model, predictions, labels) - def _delayed_fit_aggregation(self, args): + def _delayed_fit_aggregation(self, args): with qp.util.temp_seed(self.random_state): ((model, predictions, y), q_params) = args model = deepcopy(model) @@ -2163,8 +2204,8 @@ [docs] - def fit(self, X, y): - import itertools + def fit(self, X, y): + import itertools self._check_binary(y, self.__class__.__name__) @@ -2177,8 +2218,7 @@ ((params, X, y) for params in cls_configs), seed=qp.environ.get('_R_SEED', None), n_jobs=self.n_jobs, - asarray=False, - backend='threading' + asarray=False ) else: model = self.base_quantifier @@ -2190,8 +2230,7 @@ self._delayed_fit_aggregation, itertools.product(models_preds, q_configs), seed=qp.environ.get('_R_SEED', None), - n_jobs=self.n_jobs, - backend='threading' + n_jobs=self.n_jobs ) else: configs = qp.model_selection.expand_grid(self.param_grid) @@ -2199,25 +2238,23 @@ self._delayed_fit, ((params, X, y) for params in configs), seed=qp.environ.get('_R_SEED', None), - n_jobs=self.n_jobs, - backend='threading' + n_jobs=self.n_jobs ) return self - def _delayed_predict(self, args): + def _delayed_predict(self, args): model, instances = args return model.predict(instances) [docs] - def predict(self, instances): + def predict(self, instances): prev_preds = qp.util.parallel( self._delayed_predict, ((model, instances) for model in self.models), seed=qp.environ.get('_R_SEED', None), - n_jobs=self.n_jobs, - backend='threading' + n_jobs=self.n_jobs ) return np.median(prev_preds, axis=0) @@ -2228,7 +2265,7 @@ # imports # --------------------------------------------------------------- -from . import _threshold_optim +from . import _threshold_optim T50 = _threshold_optim.T50 MAX = _threshold_optim.MAX @@ -2236,7 +2273,7 @@ MS = _threshold_optim.MS MS2 = _threshold_optim.MS2 -from . import _kdey +from . import _kdey KDEyML = _kdey.KDEyML KDEyHD = _kdey.KDEyHD diff --git a/docs/build/html/_modules/quapy/method/base.html b/docs/build/html/_modules/quapy/method/base.html index e649c5b..5519615 100644 --- a/docs/build/html/_modules/quapy/method/base.html +++ b/docs/build/html/_modules/quapy/method/base.html @@ -1,95 +1,392 @@ - - - - - - quapy.method.base — QuaPy: A Python-based open-source framework for quantification 0.2.1 documentation - - + + + + + + + + quapy.method.base — QuaPy: A Python-based open-source framework for quantification 0.2.1 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Skip to main content - - - - - - - - - + + + + Back to top + + + + + + + + + + Ctrl+K + + - - - - - - - - - - QuaPy: A Python-based open-source framework for quantification - - - - - - - + + - - -
+ Source code for quapy.classification.svmperf -import random -import shutil -import subprocess -import tempfile -from os import remove, makedirs -from os.path import join, exists -from subprocess import PIPE, STDOUT -import numpy as np -from sklearn.base import BaseEstimator, ClassifierMixin -from sklearn.datasets import dump_svmlight_file +import logging +import random +import shutil +import subprocess +import tempfile +from os import remove, makedirs +from os.path import join, exists +from subprocess import PIPE, STDOUT +import numpy as np +from sklearn.base import BaseEstimator, ClassifierMixin +from sklearn.datasets import dump_svmlight_file [docs] -class SVMperf(BaseEstimator, ClassifierMixin): +class SVMperf(BaseEstimator, ClassifierMixin): """A wrapper for the `SVM-perf package <https://www.cs.cornell.edu/people/tj/svm_light/svm_perf.html>`__ by Thorsten Joachims. When using losses for quantification, the source code has to be patched. See the `installation documentation <https://hlt-isti.github.io/QuaPy/build/html/Installation.html#svm-perf-with-quantification-oriented-losses>`__ @@ -110,7 +407,7 @@ # losses with their respective codes in svm_perf implementation valid_losses = {'01':0, 'f1':1, 'kld':12, 'nkld':13, 'q':22, 'qacc':23, 'qf1':24, 'qgm':25, 'mae':26, 'mrae':27} - def __init__(self, svmperf_base, C=0.01, verbose=False, loss='01', host_folder=None): + def __init__(self, svmperf_base, C=0.01, verbose=False, loss='01', host_folder=None): assert exists(svmperf_base), \ (f'path {svmperf_base} does not seem to point to a valid path;' f'did you install svm-perf? ' @@ -123,7 +420,7 @@ [docs] - def fit(self, X, y): + def fit(self, X, y): """ Trains the SVM for the multivariate performance loss @@ -146,8 +443,7 @@ # this would allow to run parallel instances of predict random_code = 'svmperfprocess'+'-'.join(str(local_random.randint(0, 1000000)) for _ in range(5)) if self.host_folder is None: - # tmp dir are removed after the fit terminates in multiprocessing... - self.tmpdir = tempfile.TemporaryDirectory(suffix=random_code).name + self.tmpdir = join(tempfile.gettempdir(), random_code) else: self.tmpdir = join(self.host_folder, '.' + random_code) makedirs(self.tmpdir, exist_ok=True) @@ -159,21 +455,21 @@ cmd = ' '.join([self.svmperf_learn, self.c_cmd, self.loss_cmd, traindat, self.model]) if self.verbose: - print('[Running]', cmd) - p = subprocess.run(cmd.split(), stdout=PIPE, stderr=STDOUT) + logging.getLogger(__name__).info(f'[Running] {cmd}') + p = subprocess.run(cmd.split(), stdout=PIPE, stderr=PIPE) if not exists(self.model): - print(p.stderr.decode('utf-8')) + logging.getLogger(__name__).error(p.stderr.decode('utf-8')) remove(traindat) if self.verbose: - print(p.stdout.decode('utf-8')) + logging.getLogger(__name__).info(p.stdout.decode('utf-8')) return self [docs] - def predict(self, X): + def predict(self, X): """ Predicts labels for the instances `X` @@ -188,7 +484,7 @@ [docs] - def decision_function(self, X, y=None): + def decision_function(self, X, y=None): """ Evaluate the decision function for the samples in `X`. @@ -211,11 +507,11 @@ cmd = ' '.join([self.svmperf_classify, testdat, self.model, predictions_path]) if self.verbose: - print('[Running]', cmd) + logging.getLogger(__name__).info(f'[Running] {cmd}') p = subprocess.run(cmd.split(), stdout=PIPE, stderr=STDOUT) if self.verbose: - print(p.stdout.decode('utf-8')) + logging.getLogger(__name__).info(p.stdout.decode('utf-8')) scores = np.loadtxt(predictions_path) remove(testdat) @@ -224,38 +520,82 @@ return scores - def __del__(self): + def __del__(self): if hasattr(self, 'tmpdir'): shutil.rmtree(self.tmpdir, ignore_errors=True) - +
+ + Source code for quapy.data.base +import itertools +from functools import cached_property +from typing import Iterable + +import numpy as np +from scipy.sparse import issparse +from scipy.sparse import vstack +from sklearn.model_selection import train_test_split, RepeatedStratifiedKFold +from numpy.random import RandomState +from quapy.functional import strprev +from quapy.util import temp_seed +import quapy.functional as F [docs] -class LabelledCollection: +class LabelledCollection: """ A LabelledCollection is a set of objects each with a label attached to each of them. This class implements several sampling routines and other utilities. @@ -102,7 +398,7 @@ (i.e., a prevalence of 0) """ - def __init__(self, instances, labels, classes=None): + def __init__(self, instances, labels, classes=None): if issparse(instances): self.instances = instances elif isinstance(instances, list) and len(instances) > 0 and isinstance(instances[0], str): @@ -121,7 +417,7 @@ self._index = None @property - def index(self): + def index(self): if not hasattr(self, '_index') or self._index is None: self._index = {class_: np.arange(len(self))[self.labels == class_] for class_ in self.classes_} return self._index @@ -129,7 +425,7 @@ [docs] @classmethod - def load(cls, path: str, loader_func: callable, classes=None, **loader_kwargs): + def load(cls, path: str, loader_func: callable, classes=None, **loader_kwargs): """ Loads a labelled set of data and convert it into a :class:`LabelledCollection` instance. The function in charge of reading the instances must be specified. This function can be a custom one, or any of the reading functions @@ -146,7 +442,7 @@ return LabelledCollection(*loader_func(path, **loader_kwargs), classes) - def __len__(self): + def __len__(self): """ Returns the length of this collection (number of labelled instances) @@ -156,7 +452,7 @@ [docs] - def prevalence(self): + def prevalence(self): """ Returns the prevalence, or relative frequency, of the classes in the codeframe. @@ -168,7 +464,7 @@ [docs] - def counts(self): + def counts(self): """ Returns the number of instances for each of the classes in the codeframe. @@ -179,7 +475,7 @@ @property - def n_classes(self): + def n_classes(self): """ The number of classes @@ -188,7 +484,7 @@ return len(self.classes_) @property - def n_instances(self): + def n_instances(self): """ The number of instances @@ -197,7 +493,7 @@ return len(self.labels) @property - def binary(self): + def binary(self): """ Returns True if the number of classes is 2 @@ -207,7 +503,7 @@ [docs] - def sampling_index(self, size, *prevs, shuffle=True, random_state=None): + def sampling_index(self, size, *prevs, shuffle=True, random_state=None): """ Returns an index to be used to extract a random sample of desired size and desired prevalence values. If the prevalence values are not specified, then returns the index of a uniform sampling. @@ -270,7 +566,7 @@ [docs] - def uniform_sampling_index(self, size, random_state=None): + def uniform_sampling_index(self, size, random_state=None): """ Returns an index to be used to extract a uniform sample of desired size. The sampling is drawn with replacement. @@ -288,7 +584,7 @@ [docs] - def sampling(self, size, *prevs, shuffle=True, random_state=None): + def sampling(self, size, *prevs, shuffle=True, random_state=None): """ Return a random sample (an instance of :class:`LabelledCollection`) of desired size and desired prevalence values. For each class, the sampling is drawn with replacement. @@ -308,7 +604,7 @@ [docs] - def uniform_sampling(self, size, random_state=None): + def uniform_sampling(self, size, random_state=None): """ Returns a uniform sample (an instance of :class:`LabelledCollection`) of desired size. The sampling is drawn with replacement. @@ -323,7 +619,7 @@ [docs] - def sampling_from_index(self, index): + def sampling_from_index(self, index): """ Returns an instance of :class:`LabelledCollection` whose elements are sampled from this collection using the index. @@ -338,7 +634,7 @@ [docs] - def split_stratified(self, train_prop=0.6, random_state=None): + def split_stratified(self, train_prop=0.6, random_state=None): """ Returns two instances of :class:`LabelledCollection` split with stratification from this collection, at desired proportion. @@ -360,7 +656,7 @@ [docs] - def split_random(self, train_prop=0.6, random_state=None): + def split_random(self, train_prop=0.6, random_state=None): """ Returns two instances of :class:`LabelledCollection` split randomly from this collection, at desired proportion. @@ -387,7 +683,7 @@ return training, test - def __add__(self, other): + def __add__(self, other): """ Returns a new :class:`LabelledCollection` as the union of this collection with another collection. Both labelled collections must have the same classes. @@ -403,7 +699,7 @@ [docs] @classmethod - def join(cls, *args: Iterable['LabelledCollection']): + def join(cls, *args: Iterable['LabelledCollection']): """ Returns a new :class:`LabelledCollection` as the union of the collections given in input. @@ -439,12 +735,14 @@ else: raise NotImplementedError('unsupported operation for collection types') labels = np.concatenate([lc.labels for lc in args]) - classes = np.unique(labels).sort() + # union of each collection's own classes_, so a class declared but absent from + # this particular join (e.g. an empty fold) is preserved at zero prevalence + classes = np.unique(np.concatenate([lc.classes_ for lc in args])) return LabelledCollection(instances, labels, classes=classes) @property - def classes(self): + def classes(self): """ Gets an array-like with the classes used in this collection @@ -453,7 +751,7 @@ return self.classes_ @property - def Xy(self): + def Xy(self): """ Gets the instances and labels. This is useful when working with `sklearn` estimators, e.g.: @@ -464,7 +762,7 @@ return self.instances, self.labels @property - def Xp(self): + def Xp(self): """ Gets the instances and the true prevalence. This is useful when implementing evaluation protocols from a :class:`LabelledCollection` object. @@ -474,7 +772,7 @@ return self.instances, self.prevalence() @property - def X(self): + def X(self): """ An alias to self.instances @@ -483,7 +781,7 @@ return self.instances @property - def y(self): + def y(self): """ An alias to self.labels @@ -492,7 +790,7 @@ return self.labels @property - def p(self): + def p(self): """ An alias to self.prevalence() @@ -503,7 +801,7 @@ [docs] - def stats(self, show=True): + def stats(self, show=True): """ Returns (and eventually prints) a dictionary with some stats of this collection. E.g.,: @@ -538,7 +836,7 @@ [docs] - def kFCV(self, nfolds=5, nrepeats=1, random_state=None): + def kFCV(self, nfolds=5, nrepeats=1, random_state=None): """ Generator of stratified folds to be used in k-fold cross validation. @@ -554,7 +852,7 @@ yield train, test - def __repr__(self): + def __repr__(self): repr=f'<{self.n_instances} instances (dtype={type(self.instances[0])}), ' repr+=f'n_classes={self.n_classes} {self.classes_}, prevalence={F.strprev(self.prevalence())}>' return repr @@ -563,7 +861,7 @@ [docs] -class Dataset: +class Dataset: """ Abstraction of training and test :class:`LabelledCollection` objects. @@ -573,7 +871,7 @@ :param name: a string representing the name of the dataset """ - def __init__(self, training: LabelledCollection, test: LabelledCollection, vocabulary: dict = None, name=''): + def __init__(self, training: LabelledCollection, test: LabelledCollection, vocabulary: dict = None, name=''): assert set(training.classes_) == set(test.classes_), 'incompatible labels in training and test collections' self.training = training self.test = test @@ -583,7 +881,7 @@ [docs] @classmethod - def SplitStratified(cls, collection: LabelledCollection, train_size=0.6): + def SplitStratified(cls, collection: LabelledCollection, train_size=0.6): """ Generates a :class:`Dataset` from a stratified split of a :class:`LabelledCollection` instance. See :meth:`LabelledCollection.split_stratified` @@ -596,7 +894,7 @@ @property - def classes_(self): + def classes_(self): """ The classes according to which the training collection is labelled @@ -605,7 +903,7 @@ return self.training.classes_ @property - def n_classes(self): + def n_classes(self): """ The number of classes according to which the training collection is labelled @@ -614,7 +912,7 @@ return self.training.n_classes @property - def binary(self): + def binary(self): """ Returns True if the training collection is labelled according to two classes @@ -625,7 +923,7 @@ [docs] @classmethod - def load(cls, train_path, test_path, loader_func: callable, classes=None, **loader_kwargs): + def load(cls, train_path, test_path, loader_func: callable, classes=None, **loader_kwargs): """ Loads a training and a test labelled set of data and convert it into a :class:`Dataset` instance. The function in charge of reading the instances must be specified. This function can be a custom one, or any of @@ -647,7 +945,7 @@ @property - def vocabulary_size(self): + def vocabulary_size(self): """ If the dataset is textual, and the vocabulary was indicated, returns the size of the vocabulary @@ -656,7 +954,7 @@ return len(self.vocabulary) @property - def train_test(self): + def train_test(self): """ Alias to `self.training` and `self.test` @@ -667,7 +965,7 @@ [docs] - def stats(self, show=True): + def stats(self, show=True): """ Returns (and eventually prints) a dictionary with some stats of this dataset. E.g.,: @@ -694,7 +992,7 @@ [docs] @classmethod - def kFCV(cls, data: LabelledCollection, nfolds=5, nrepeats=1, random_state=0): + def kFCV(cls, data: LabelledCollection, nfolds=5, nrepeats=1, random_state=0): """ Generator of stratified folds to be used in k-fold cross validation. This function is only a wrapper around :meth:`LabelledCollection.kFCV` that returns :class:`Dataset` instances made of training and test folds. @@ -711,7 +1009,7 @@ [docs] - def reduce(self, n_train=100, n_test=100, random_state=None): + def reduce(self, n_train=100, n_test=100, random_state=None): """ Reduce the number of instances in place for quick experiments. Preserves the prevalence of each set. @@ -732,36 +1030,80 @@ return self - def __repr__(self): + def __repr__(self): return f'training={self.training}; test={self.test}' - + + + + + + + + + + + + + - + + + + - Built with Sphinx using a - theme - provided by Read the Docs. - + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/build/html/_modules/quapy/data/datasets.html b/docs/build/html/_modules/quapy/data/datasets.html index 20d9588..cc6a18b 100644 --- a/docs/build/html/_modules/quapy/data/datasets.html +++ b/docs/build/html/_modules/quapy/data/datasets.html @@ -29,7 +29,7 @@ - + @@ -370,27 +370,28 @@ Source code for quapy.data.datasets -import os -from contextlib import contextmanager -import zipfile -from os.path import join -import pandas as pd -from quapy.data.base import Dataset, LabelledCollection -from quapy.data.preprocessing import text2tfidf, reduce_columns -from quapy.data.preprocessing import standardize as standardizer -from quapy.data.reader import * -from quapy.util import download_file_if_not_exists, download_file, get_quapy_home, pickled_resource -from sklearn.preprocessing import StandardScaler +import logging +import os +from contextlib import contextmanager +import zipfile +from os.path import join +import pandas as pd +from quapy.data.base import Dataset, LabelledCollection +from quapy.data.preprocessing import text2tfidf, reduce_columns +from quapy.data.preprocessing import standardize as standardizer +from quapy.data.reader import * +from quapy.util import download_file_if_not_exists, download_file, get_quapy_home, pickled_resource +from sklearn.preprocessing import StandardScaler -def _fetch_ucirepo(*args, **kwargs): +def _fetch_ucirepo(*args, **kwargs): try: - from ucimlrepo import fetch_ucirepo + from ucimlrepo import fetch_ucirepo except ImportError as exc: raise ImportError( "UCI dataset fetching requires the optional 'ucimlrepo' package. " "Install it to use fetch_UCIBinaryDataset or fetch_UCIMulticlassDataset." - ) from exc + ) from exc return fetch_ucirepo(*args, **kwargs) @@ -497,7 +498,7 @@ [docs] -def fetch_reviews(dataset_name, tfidf=False, min_df=None, data_home=None, pickle=False) -> Dataset: +def fetch_reviews(dataset_name, tfidf=False, min_df=None, data_home=None, pickle=False) -> Dataset: """ Loads a Reviews dataset as a Dataset instance, as used in `Esuli, A., Moreo, A., and Sebastiani, F. "A recurrent neural network for sentiment quantification." @@ -547,7 +548,7 @@ [docs] -def fetch_twitter(dataset_name, for_model_selection=False, min_df=None, data_home=None, pickle=False) -> Dataset: +def fetch_twitter(dataset_name, for_model_selection=False, min_df=None, data_home=None, pickle=False) -> Dataset: """ Loads a Twitter dataset as a :class:`quapy.data.base.Dataset` instance, as used in: `Gao, W., Sebastiani, F.: From classification to quantification in tweet sentiment analysis. @@ -589,8 +590,9 @@ if dataset_name in {'semeval13', 'semeval14', 'semeval15'}: trainset_name = 'semeval' testset_name = 'semeval' if for_model_selection else dataset_name - print(f"the training and development sets for datasets 'semeval13', 'semeval14', 'semeval15' are common " - f"(called 'semeval'); returning trainin-set='{trainset_name}' and test-set={testset_name}") + logging.getLogger(__name__).info( + f"the training and development sets for datasets 'semeval13', 'semeval14', 'semeval15' are common " + f"(called 'semeval'); returning trainin-set='{trainset_name}' and test-set={testset_name}") else: if dataset_name == 'semeval' and for_model_selection==False: raise ValueError('dataset "semeval" can only be used for model selection. ' @@ -624,7 +626,7 @@ [docs] -def fetch_UCIBinaryDataset(dataset_name, data_home=None, test_split=0.3, standardize=True, verbose=False) -> Dataset: +def fetch_UCIBinaryDataset(dataset_name, data_home=None, test_split=0.3, standardize=True, verbose=False) -> Dataset: """ Loads a UCI dataset as an instance of :class:`quapy.data.base.Dataset`, as used in `Pérez-Gállego, P., Quevedo, J. R., & del Coz, J. J. (2017). @@ -658,7 +660,7 @@ [docs] -def fetch_UCIBinaryLabelledCollection(dataset_name, data_home=None, standardize=True, verbose=False) -> LabelledCollection: +def fetch_UCIBinaryLabelledCollection(dataset_name, data_home=None, standardize=True, verbose=False) -> LabelledCollection: """ Loads a UCI collection as an instance of :class:`quapy.data.base.LabelledCollection`, as used in `Pérez-Gállego, P., Quevedo, J. R., & del Coz, J. J. (2017). @@ -851,7 +853,7 @@ file = join(data_home, "uci_datasets", dataset_group + ".pkl") @contextmanager - def download_tmp_file(url_group: str, filename: str): + def download_tmp_file(url_group: str, filename: str): """ Download a data file for a group of datasets temporarely. When used as a context, the file is removed once the context exits. @@ -869,7 +871,7 @@ finally: os.remove(data_path) - def download(id: int | None, group: str) -> dict: + def download(id: int | None, group: str) -> dict: """ Download the data to be pickled for a dataset group. Use the `fetch_ucirepo` api when possible. @@ -935,7 +937,7 @@ return data - def binarize_data(name, data: dict) -> LabelledCollection: + def binarize_data(name, data: dict) -> LabelledCollection: """ Filter and transform data to extract a binary dataset. @@ -980,7 +982,7 @@ [docs] -def fetch_UCIMulticlassDataset( +def fetch_UCIMulticlassDataset( dataset_name, data_home=None, min_test_split=0.3, @@ -1043,7 +1045,7 @@ [docs] -def fetch_UCIMulticlassLabelledCollection(dataset_name, data_home=None, min_class_support=100, standardize=True, verbose=False) -> LabelledCollection: +def fetch_UCIMulticlassLabelledCollection(dataset_name, data_home=None, min_class_support=100, standardize=True, verbose=False) -> LabelledCollection: """ Loads a UCI multiclass collection as an instance of :class:`quapy.data.base.LabelledCollection`. @@ -1146,7 +1148,7 @@ file = join(data_home, 'uci_multiclass', dataset_name+'.pkl') - def dummify_categorical_features(df_features, dataset_id): + def dummify_categorical_features(df_features, dataset_id): categorical_features = { 158: ["S1", "C1", "S2", "C2", "S3", "C3", "S4", "C4", "S5", "C5"], # poker_hand } @@ -1160,7 +1162,7 @@ return X - def download(id, name): + def download(id, name): df = _fetch_ucirepo(id=id) X_df = dummify_categorical_features(df.data.features, id) @@ -1173,7 +1175,7 @@ y = np.searchsorted(classes, y) return LabelledCollection(X, y) - def filter_classes(data: LabelledCollection, min_class_support): + def filter_classes(data: LabelledCollection, min_class_support): if min_class_support is None or min_class_support == 0.: return data if isinstance(min_class_support, float): @@ -1207,18 +1209,18 @@ -def _df_replace(df, col, repl={'yes': 1, 'no':0}, astype=float): +def _df_replace(df, col, repl={'yes': 1, 'no':0}, astype=float): df[col] = df[col].apply(lambda x:repl[x]).astype(astype, copy=False) -def _array_replace(arr, repl={"yes": 1, "no": 0}): +def _array_replace(arr, repl={"yes": 1, "no": 0}): for k, v in repl.items(): arr[arr == k] = v [docs] -def fetch_lequa2022(task, data_home=None): +def fetch_lequa2022(task, data_home=None): """ Loads the official datasets provided for the `LeQua 2022 <https://lequa2022.github.io/index>`_ competition. In brief, there are 4 tasks (T1A, T1B, T2A, T2B) having to do with text quantification @@ -1244,7 +1246,7 @@ that return a series of samples stored in a directory which are labelled by prevalence. """ - from quapy.data._lequa import load_raw_documents, load_vector_documents_2022, SamplesFromDir + from quapy.data._lequa import load_raw_documents, load_vector_documents_2022, SamplesFromDir assert task in LEQUA2022_TASKS, \ f'Unknown task {task}. Valid ones are {LEQUA2022_TASKS}' @@ -1258,7 +1260,7 @@ lequa_dir = join(data_home, 'lequa2022') os.makedirs(lequa_dir, exist_ok=True) - def download_unzip_and_remove(unzipped_path, url): + def download_unzip_and_remove(unzipped_path, url): tmp_path = join(lequa_dir, task + '_tmp.zip') download_file_if_not_exists(url, tmp_path) with zipfile.ZipFile(tmp_path) as file: @@ -1294,7 +1296,7 @@ [docs] -def fetch_lequa2024(task, data_home=None, merge_T3=False): +def fetch_lequa2024(task, data_home=None, merge_T3=False): """ Loads the official datasets provided for the `LeQua 2024 <https://lequa2024.github.io/index>`_ competition. LeQua 2024 defines four tasks (T1, T2, T3, T4) related to the problem of quantification; @@ -1325,7 +1327,7 @@ that return a series of samples stored in a directory which are labelled by prevalence. """ - from quapy.data._lequa import load_vector_documents_2024, SamplesFromDir, LabelledCollectionsFromDir + from quapy.data._lequa import load_vector_documents_2024, SamplesFromDir, LabelledCollectionsFromDir assert task in LEQUA2024_TASKS, \ f'Unknown task {task}. Valid ones are {LEQUA2024_TASKS}' @@ -1344,7 +1346,7 @@ lequa_dir = join(data_home, 'lequa2024') os.makedirs(lequa_dir, exist_ok=True) - def download_unzip_and_remove(unzipped_path, url): + def download_unzip_and_remove(unzipped_path, url): tmp_path = join(lequa_dir, task + '_tmp.zip') download_file_if_not_exists(url, tmp_path) with zipfile.ZipFile(tmp_path) as file: @@ -1384,7 +1386,7 @@ [docs] -def fetch_IFCB(single_sample_train=True, for_model_selection=False, data_home=None): +def fetch_IFCB(single_sample_train=True, for_model_selection=False, data_home=None): """ Loads the IFCB dataset for quantification from `Zenodo <https://zenodo.org/records/10036244>`_ (for more information on this dataset, please follow the zenodo link). @@ -1409,7 +1411,7 @@ i.e., a sampling protocol that returns a series of samples labelled by prevalence. """ - from quapy.data._ifcb import IFCBTrainSamplesFromDir, IFCBTestSamples, get_sample_list, generate_modelselection_split + from quapy.data._ifcb import IFCBTrainSamplesFromDir, IFCBTestSamples, get_sample_list, generate_modelselection_split if data_home is None: data_home = get_quapy_home() @@ -1421,7 +1423,7 @@ ifcb_dir = join(data_home, 'ifcb') os.makedirs(ifcb_dir, exist_ok=True) - def download_unzip_and_remove(unzipped_path, url): + def download_unzip_and_remove(unzipped_path, url): tmp_path = join(ifcb_dir, 'ifcb_tmp.zip') download_file_if_not_exists(url, tmp_path) with zipfile.ZipFile(tmp_path) as file: @@ -1466,7 +1468,7 @@ -def _fetch_image_embedding_splits(dataset_name, embedding, data_home=None) -> tuple[LabelledCollection,LabelledCollection,LabelledCollection]: +def _fetch_image_embedding_splits(dataset_name, embedding, data_home=None) -> tuple[LabelledCollection,LabelledCollection,LabelledCollection]: """ Loads a pre-generated embedding set (train, val, or test) of an image dataset from `Zenodo <https://zenodo.org/records/21131944>`_. @@ -1496,7 +1498,7 @@ trained_network = dataset_network[dataset_name] - def download_embedding_npz(dataset_name, trained_network, embedding): + def download_embedding_npz(dataset_name, trained_network, embedding): target_file = f'{dataset_name}_{trained_network}_{embedding}.npz' URL = f'https://zenodo.org/records/21131944/files/{target_file}' os.makedirs(join(data_home, 'image'), exist_ok=True) @@ -1512,14 +1514,12 @@ val = LabelledCollection(embedding_dict['val'], labels_dict['val'], classes=train.classes) test = LabelledCollection(embedding_dict['test'], labels_dict['test'], classes=train.classes) - print(f'{len(train)} | {len(val)} | {len(test)} | {train.X.shape[1]} | {train.n_classes} | {train.n_classes}') - return train, val, test [docs] -def fetch_image_embeddings(dataset_name, embedding, heldout_only=True, data_home=None) -> Dataset: +def fetch_image_embeddings(dataset_name, embedding, heldout_only=True, data_home=None) -> Dataset: """ Loads an image dataset with pre-generated embeddings. Available datasets include: @@ -1556,14 +1556,6 @@ -if __name__ == '__main__': - #train, val, test = _fetch_image_embedding_splits(dataset_name='mnist', embedding='logits') - #print(train) - #print(val) - #print(test) - - dataset = fetch_image_embeddings(dataset_name='svhn', embedding='features', heldout_only=True) - print(dataset) diff --git a/docs/build/html/_modules/quapy/data/reader.html b/docs/build/html/_modules/quapy/data/reader.html index 827d348..617442d 100644 --- a/docs/build/html/_modules/quapy/data/reader.html +++ b/docs/build/html/_modules/quapy/data/reader.html @@ -1,87 +1,385 @@ - - - - - - quapy.data.reader — QuaPy: A Python-based open-source framework for quantification 0.2.1 documentation - - + + + + + + + + quapy.data.reader — QuaPy: A Python-based open-source framework for quantification 0.2.1 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Skip to main content - - - - - - - - - + + + + Back to top + + + + + + + + + + Ctrl+K + + - - - - - - - - - - QuaPy: A Python-based open-source framework for quantification - - - - - - - + + - - -Quickstart - - -Manuals - - -API - - - - + + + + + + + + + + + - - - QuaPy: A Python-based open-source framework for quantification - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Home + + + + + + + Manuals + + + + + + + API + + - - - - - - Module code - quapy.data.reader - - - + + + + + + + + + + + + + Search + Ctrl+K + + + + + + + + + + + + + + System Settings + Light + Dark + + + + + + + + + + + + + + GitHub + + + + + + + + + + + + + Search + Ctrl+K + + + + + - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Home + + + + + + + Manuals + + + + + + + API + + + + + + + + + + + + + + + + + + + + + + + System Settings + Light + Dark + + + + + + + + + + + + + + GitHub + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Module code + + quapy.data.reader + + + + + + + + + + + + + + + + Source code for quapy.data.reader -import numpy as np -from scipy.sparse import dok_matrix -from tqdm import tqdm +import logging + +import numpy as np +from scipy.sparse import dok_matrix +from tqdm import tqdm [docs] -def from_text(path, encoding='utf-8', verbose=1, class2int=True): +def from_text(path, encoding='utf-8', verbose=1, class2int=True): """ Reads a labelled colletion of documents. File fomart <0 or 1>\t<document>\n @@ -108,14 +406,14 @@ all_sentences.append(sentence) all_labels.append(label) except ValueError: - print(f'format error in {line}') + logging.getLogger(__name__).warning(f'format error in {line}') return all_sentences, all_labels [docs] -def from_sparse(path): +def from_sparse(path): """ Reads a labelled collection of real-valued instances expressed in sparse format File format <-1 or 0 or 1>[\s col(int):val(float)]\n @@ -124,7 +422,7 @@ :return: a `csr_matrix` containing the instances (rows), and a ndarray containing the labels """ - def split_col_val(col_val): + def split_col_val(col_val): col, val = col_val.split(':') col, val = int(col) - 1, float(val) return col, val @@ -152,7 +450,7 @@ [docs] -def from_csv(path, encoding='utf-8'): +def from_csv(path, encoding='utf-8'): """ Reads a csv file in which columns are separated by ','. File format <label>,<feat1>,<feat2>,...,<featn>\n @@ -175,7 +473,7 @@ [docs] -def reindex_labels(y): +def reindex_labels(y): """ Re-indexes a list of labels as a list of indexes, and returns the classnames corresponding to the indexes. E.g.: @@ -198,7 +496,7 @@ [docs] -def binarize(y, pos_class): +def binarize(y, pos_class): """ Binarizes a categorical array-like collection of labels towards the positive class `pos_class`. E.g.,: @@ -218,31 +516,75 @@ - + + + + + + + + + + + + + - + + + + - Built with Sphinx using a - theme - provided by Read the Docs. - + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/build/html/_modules/quapy/error.html b/docs/build/html/_modules/quapy/error.html index 73a9a53..9b93ed4 100644 --- a/docs/build/html/_modules/quapy/error.html +++ b/docs/build/html/_modules/quapy/error.html @@ -1,89 +1,385 @@ - - - - - - quapy.error — QuaPy: A Python-based open-source framework for quantification 0.2.1 documentation - - + + + + + + + + quapy.error — QuaPy: A Python-based open-source framework for quantification 0.2.1 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Skip to main content - - - - - - - - - + + + + Back to top + + + + + + + + + + Ctrl+K + + - - - - - - - - - - QuaPy: A Python-based open-source framework for quantification - - - - - - - + + - - -Quickstart - - -Manuals - - -API - - - - + + + + + + + + + + + - - - QuaPy: A Python-based open-source framework for quantification - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Home + + + + + + + Manuals + + + + + + + API + + - - - - - - Module code - quapy.error - - - + + + + + + + + + + + + + Search + Ctrl+K + + + + + + + + + + + + + + System Settings + Light + Dark + + + + + + + + + + + + + + GitHub + + + + + + + + + + + + + Search + Ctrl+K + + + + + - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Home + + + + + + + Manuals + + + + + + + API + + + + + + + + + + + + + + + + + + + + + + + System Settings + Light + Dark + + + + + + + + + + + + + + GitHub + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Module code + + quapy.error + + + + + + + + + + + + + + + + Source code for quapy.error """Implementation of error measures used for quantification""" -import numpy as np -from sklearn.metrics import f1_score -import quapy as qp +import numpy as np +from sklearn.metrics import f1_score +import quapy as qp [docs] -def from_name(err_name): +def from_name(err_name): """Gets an error function from its name. E.g., `from_name("mae")` will return function :meth:`quapy.error.mae` @@ -98,7 +394,7 @@ [docs] -def f1e(y_true, y_pred): +def f1e(y_true, y_pred): """F1 error: simply computes the error in terms of macro :math:`F_1`, i.e., :math:`1-F_1^M`, where :math:`F_1` is the harmonic mean of precision and recall, defined as :math:`\\frac{2tp}{2tp+fp+fn}`, with `tp`, `fp`, and `fn` standing @@ -116,7 +412,7 @@ [docs] -def acce(y_true, y_pred): +def acce(y_true, y_pred): """Computes the error in terms of 1-accuracy. The accuracy is computed as :math:`\\frac{tp+tn}{tp+fp+fn+tn}`, with `tp`, `fp`, `fn`, and `tn` standing for true positives, false positives, false negatives, and true negatives, @@ -132,7 +428,7 @@ [docs] -def mae(prevs_true, prevs_hat): +def mae(prevs_true, prevs_hat): """Computes the mean absolute error (see :meth:`quapy.error.ae`) across the sample pairs. :param prevs_true: array-like of shape `(n_samples, n_classes,)` with the true prevalence values @@ -146,7 +442,7 @@ [docs] -def ae(prevs_true, prevs_hat): +def ae(prevs_true, prevs_hat): """Computes the absolute error between the two prevalence vectors. Absolute error between two prevalence vectors :math:`p` and :math:`\\hat{p}` is computed as :math:`AE(p,\\hat{p})=\\frac{1}{|\\mathcal{Y}|}\\sum_{y\\in \\mathcal{Y}}|\\hat{p}(y)-p(y)|`, @@ -165,7 +461,7 @@ [docs] -def nae(prevs_true, prevs_hat): +def nae(prevs_true, prevs_hat): """Computes the normalized absolute error between the two prevalence vectors. Normalized absolute error between two prevalence vectors :math:`p` and :math:`\\hat{p}` is computed as :math:`NAE(p,\\hat{p})=\\frac{AE(p,\\hat{p})}{z_{AE}}`, @@ -185,7 +481,7 @@ [docs] -def mnae(prevs_true, prevs_hat): +def mnae(prevs_true, prevs_hat): """Computes the mean normalized absolute error (see :meth:`quapy.error.nae`) across the sample pairs. :param prevs_true: array-like of shape `(n_samples, n_classes,)` with the true prevalence values @@ -199,7 +495,7 @@ [docs] -def mse(prevs_true, prevs_hat): +def mse(prevs_true, prevs_hat): """Computes the mean squared error (see :meth:`quapy.error.se`) across the sample pairs. :param prevs_true: array-like of shape `(n_samples, n_classes,)` with the @@ -214,7 +510,7 @@ [docs] -def se(prevs_true, prevs_hat): +def se(prevs_true, prevs_hat): """Computes the squared error between the two prevalence vectors. Squared error between two prevalence vectors :math:`p` and :math:`\\hat{p}` is computed as :math:`SE(p,\\hat{p})=\\frac{1}{|\\mathcal{Y}|}\\sum_{y\\in \\mathcal{Y}}(\\hat{p}(y)-p(y))^2`, @@ -233,7 +529,7 @@ [docs] -def sre(prevs_true, prevs_hat, prevs_train, eps=0.): +def sre(prevs_true, prevs_hat, prevs_train, eps=0.): """ Computes the squared ratio error between two prevalence vectors. The squared ratio error between prevalence vectors :math:`p` and @@ -269,7 +565,7 @@ [docs] -def msre(prevs_true, prevs_hat, prevs_train, eps=0.): +def msre(prevs_true, prevs_hat, prevs_train, eps=0.): """ Computes the mean squared ratio error (see :meth:`quapy.error.sre`) across the sample pairs. @@ -285,7 +581,7 @@ [docs] -def aitchisondist(prevs_true, prevs_hat): +def aitchisondist(prevs_true, prevs_hat): """ Computes the Aitchison distance between two prevalence vectors. The Aitchison distance between prevalence vectors :math:`p` and @@ -298,7 +594,7 @@ :param prevs_hat: array-like with the predicted prevalence values :return: Aitchison distance """ - from quapy.functional import CLRtransformation + from quapy.functional import CLRtransformation clr = CLRtransformation() return np.linalg.norm(clr(prevs_true) - clr(prevs_hat), axis=-1) @@ -307,7 +603,7 @@ [docs] -def maitchisondist(prevs_true, prevs_hat): +def maitchisondist(prevs_true, prevs_hat): """ Computes the mean Aitchison distance (see :meth:`quapy.error.aitchisondist`) across the sample pairs, i.e., @@ -324,7 +620,7 @@ [docs] -def mkld(prevs_true, prevs_hat, eps=None): +def mkld(prevs_true, prevs_hat, eps=None): """Computes the mean Kullback-Leibler divergence (see :meth:`quapy.error.kld`) across the sample pairs. The distributions are smoothed using the `eps` factor (see :meth:`quapy.error.smooth`). @@ -345,7 +641,7 @@ [docs] -def kld(prevs_true, prevs_hat, eps=None): +def kld(prevs_true, prevs_hat, eps=None): """Computes the Kullback-Leibler divergence between the two prevalence distributions. Kullback-Leibler divergence between two prevalence distributions :math:`p` and :math:`\\hat{p}` is computed as @@ -371,7 +667,7 @@ [docs] -def mnkld(prevs_true, prevs_hat, eps=None): +def mnkld(prevs_true, prevs_hat, eps=None): """Computes the mean Normalized Kullback-Leibler divergence (see :meth:`quapy.error.nkld`) across the sample pairs. The distributions are smoothed using the `eps` factor (see :meth:`quapy.error.smooth`). @@ -391,7 +687,7 @@ [docs] -def nkld(prevs_true, prevs_hat, eps=None): +def nkld(prevs_true, prevs_hat, eps=None): """Computes the Normalized Kullback-Leibler divergence between the two prevalence distributions. Normalized Kullback-Leibler divergence between two prevalence distributions :math:`p` and :math:`\\hat{p}` is computed as @@ -415,7 +711,7 @@ [docs] -def mrae(prevs_true, prevs_hat, eps=None): +def mrae(prevs_true, prevs_hat, eps=None): """Computes the mean relative absolute error (see :meth:`quapy.error.rae`) across the sample pairs. The distributions are smoothed using the `eps` factor (see :meth:`quapy.error.smooth`). @@ -436,7 +732,7 @@ [docs] -def rae(prevs_true, prevs_hat, eps=None): +def rae(prevs_true, prevs_hat, eps=None): """Computes the absolute relative error between the two prevalence vectors. Relative absolute error between two prevalence vectors :math:`p` and :math:`\\hat{p}` is computed as @@ -462,7 +758,7 @@ [docs] -def nrae(prevs_true, prevs_hat, eps=None): +def nrae(prevs_true, prevs_hat, eps=None): """Computes the normalized absolute relative error between the two prevalence vectors. Relative absolute error between two prevalence vectors :math:`p` and :math:`\\hat{p}` is computed as @@ -490,7 +786,7 @@ [docs] -def mnrae(prevs_true, prevs_hat, eps=None): +def mnrae(prevs_true, prevs_hat, eps=None): """Computes the mean normalized relative absolute error (see :meth:`quapy.error.nrae`) across the sample pairs. The distributions are smoothed using the `eps` factor (see :meth:`quapy.error.smooth`). @@ -511,7 +807,7 @@ [docs] -def nmd(prevs_true, prevs_hat): +def nmd(prevs_true, prevs_hat): """ Computes the Normalized Match Distance; which is the Normalized Distance multiplied by the factor `1/(n-1)` to guarantee the measure ranges between 0 (best prediction) and 1 (worst prediction). @@ -529,7 +825,7 @@ [docs] -def bias_binary(prevs_true, prevs_hat): +def bias_binary(prevs_true, prevs_hat): """ Computes the (positive) bias in a binary problem. The bias is simply the difference between the predicted positive value and the true positive value, so that a positive such value indicates the @@ -549,7 +845,7 @@ [docs] -def mean_bias_binary(prevs_true, prevs_hat): +def mean_bias_binary(prevs_true, prevs_hat): """ Computes the mean of the (positive) bias in a binary problem. :param prevs_true: array-like of shape `(n_classes,)` with the true prevalence values @@ -562,7 +858,7 @@ [docs] -def md(prevs_true, prevs_hat, ERROR_TOL=1E-3): +def md(prevs_true, prevs_hat, ERROR_TOL=1E-3): """ Computes the Match Distance, under the assumption that the cost in mistaking class i with class i+1 is 1 in all cases. @@ -582,7 +878,7 @@ [docs] -def smooth(prevs, eps): +def smooth(prevs, eps): """ Smooths a prevalence distribution with :math:`\\epsilon` (`eps`) as: :math:`\\underline{p}(y)=\\frac{\\epsilon+p(y)}{\\epsilon|\\mathcal{Y}|+ \\displaystyle\\sum_{y\\in \\mathcal{Y}}p(y)}` @@ -597,7 +893,7 @@ -def __check_eps(eps=None): +def __check_eps(eps=None): if eps is None: sample_size = qp.environ['SAMPLE_SIZE'] if sample_size is None: @@ -634,31 +930,75 @@ match_distance = md - + + + + + + + + + + + + + - + + + + - Built with Sphinx using a - theme - provided by Read the Docs. - + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/build/html/_modules/quapy/functional.html b/docs/build/html/_modules/quapy/functional.html index b7743b8..c275ca4 100644 --- a/docs/build/html/_modules/quapy/functional.html +++ b/docs/build/html/_modules/quapy/functional.html @@ -29,8 +29,9 @@ - - + + + @@ -41,6 +42,7 @@ + @@ -114,8 +116,14 @@ + + + + + + + - QuaPy @@ -131,7 +139,7 @@ - Quickstart + Home @@ -183,6 +191,21 @@ + + + + + + + + + + + GitHub + + + @@ -229,7 +252,7 @@ - Quickstart + Home @@ -272,6 +295,21 @@ + + + + + + + + + + + GitHub + + + @@ -332,17 +370,17 @@ Source code for quapy.functional -import warnings -from abc import ABC, abstractmethod -from collections import defaultdict -from functools import lru_cache -from typing import Literal, Union, Callable -from numpy.typing import ArrayLike +import warnings +from abc import ABC, abstractmethod +from collections import defaultdict +from functools import lru_cache +from typing import Literal, Union, Callable +from numpy.typing import ArrayLike -import scipy -import numpy as np +import scipy +import numpy as np -import quapy as qp +import quapy as qp # ------------------------------------------------------------------------------------------ @@ -351,7 +389,7 @@ [docs] -def classes_from_labels(labels): +def classes_from_labels(labels): """ Obtains a np.ndarray with the (sorted) classes :param labels: array-like with the instances' labels @@ -365,7 +403,7 @@ [docs] -def num_classes_from_labels(labels): +def num_classes_from_labels(labels): """ Obtains the number of classes from an array-like of instance's labels :param labels: array-like with the instances' labels @@ -380,7 +418,7 @@ [docs] -def counts_from_labels(labels: ArrayLike, classes: ArrayLike) -> np.ndarray: +def counts_from_labels(labels: ArrayLike, classes: ArrayLike) -> np.ndarray: """ Computes the raw count values from a vector of labels. @@ -401,7 +439,7 @@ [docs] -def prevalence_from_labels(labels: ArrayLike, classes: ArrayLike): +def prevalence_from_labels(labels: ArrayLike, classes: ArrayLike): """ Computes the prevalence values from a vector of labels. @@ -419,7 +457,7 @@ [docs] -def prevalence_from_probabilities(posteriors: ArrayLike, binarize: bool = False): +def prevalence_from_probabilities(posteriors: ArrayLike, binarize: bool = False): """ Returns a vector of prevalence values from a matrix of posterior probabilities. @@ -443,7 +481,7 @@ [docs] -def num_prevalence_combinations(n_prevpoints:int, n_classes:int, n_repeats:int=1) -> int: +def num_prevalence_combinations(n_prevpoints:int, n_classes:int, n_repeats:int=1) -> int: """ Computes the number of valid prevalence combinations in the n_classes-dimensional simplex if `n_prevpoints` equally distant prevalence values are generated and `n_repeats` repetitions are requested. @@ -472,7 +510,7 @@ [docs] -def get_nprevpoints_approximation(combinations_budget:int, n_classes:int, n_repeats:int=1) -> int: +def get_nprevpoints_approximation(combinations_budget:int, n_classes:int, n_repeats:int=1) -> int: """ Searches for the largest number of (equidistant) prevalence points to define for each of the `n_classes` classes so that the number of valid prevalence values generated as combinations of prevalence points (points in a @@ -500,7 +538,7 @@ [docs] -def as_binary_prevalence(positive_prevalence: Union[float, ArrayLike], clip_if_necessary: bool=False) -> np.ndarray: +def as_binary_prevalence(positive_prevalence: Union[float, ArrayLike], clip_if_necessary: bool=False) -> np.ndarray: """ Helper that, given a float representing the prevalence for the positive class, returns a np.ndarray of two values representing a binary distribution. @@ -522,7 +560,7 @@ [docs] -def strprev(prevalences: ArrayLike, prec: int=3) -> str: +def strprev(prevalences: ArrayLike, prec: int=3) -> str: """ Returns a string representation for a prevalence vector. E.g., @@ -539,7 +577,7 @@ [docs] -def check_prevalence_vector(prevalences: ArrayLike, raise_exception: bool=False, tolerance: float=1e-08, aggr=True): +def check_prevalence_vector(prevalences: ArrayLike, raise_exception: bool=False, tolerance: float=1e-08, aggr=True): """ Checks that `prevalences` is a valid prevalence vector, i.e., it contains values in [0,1] and the values sum up to 1. In other words, verifies that the `prevalences` vectors lies in the @@ -581,7 +619,7 @@ [docs] -def uniform_prevalence(n_classes): +def uniform_prevalence(n_classes): """ Returns a vector representing the uniform distribution for `n_classes` @@ -597,7 +635,7 @@ [docs] -def normalize_prevalence(prevalences: ArrayLike, method='l1'): +def normalize_prevalence(prevalences: ArrayLike, method='l1'): """ Normalizes a vector or matrix of prevalence values. The normalization consists of applying a L1 normalization in cases in which the prevalence values are not all-zeros, and to convert the prevalence values into `1/n_classes` in @@ -640,7 +678,7 @@ [docs] -def l1_norm(prevalences: ArrayLike) -> np.ndarray: +def l1_norm(prevalences: ArrayLike) -> np.ndarray: """ Applies L1 normalization to the `unnormalized_arr` so that it becomes a valid prevalence vector. Zero vectors are mapped onto the uniform distribution. Raises an exception if @@ -666,7 +704,7 @@ [docs] -def clip(prevalences: ArrayLike) -> np.ndarray: +def clip(prevalences: ArrayLike) -> np.ndarray: """ Clips the values in [0,1] and then applies the L1 normalization. @@ -681,7 +719,7 @@ [docs] -def projection_simplex_sort(unnormalized_arr: ArrayLike) -> np.ndarray: +def projection_simplex_sort(unnormalized_arr: ArrayLike) -> np.ndarray: """Projects a point onto the probability simplex. The code is adapted from Mathieu Blondel's BSD-licensed @@ -709,7 +747,7 @@ [docs] -def softmax(prevalences: ArrayLike) -> np.ndarray: +def softmax(prevalences: ArrayLike) -> np.ndarray: """ Applies the softmax function to all vectors even if the original vectors were valid distributions. If you want to leave valid vectors untouched, use condsoftmax instead. @@ -724,7 +762,7 @@ [docs] -def condsoftmax(prevalences: ArrayLike) -> np.ndarray: +def condsoftmax(prevalences: ArrayLike) -> np.ndarray: """ Applies the softmax function only to vectors that do not represent valid distributions. @@ -749,7 +787,7 @@ [docs] -def HellingerDistance(P: np.ndarray, Q: np.ndarray) -> float: +def HellingerDistance(P: np.ndarray, Q: np.ndarray) -> float: """ Computes the Hellingher Distance (HD) between (discretized) distributions `P` and `Q`. The HD for two discrete distributions of `k` bins is defined as: @@ -767,7 +805,7 @@ [docs] -def TopsoeDistance(P: np.ndarray, Q: np.ndarray, epsilon: float=1e-20): +def TopsoeDistance(P: np.ndarray, Q: np.ndarray, epsilon: float=1e-20): """ Topsoe distance between two (discretized) distributions `P` and `Q`. The Topsoe distance for two discrete distributions of `k` bins is defined as: @@ -786,7 +824,7 @@ [docs] -def get_divergence(divergence: Union[str, Callable]): +def get_divergence(divergence: Union[str, Callable]): """ Guarantees that the divergence received as argument is a function. That is, if this argument is already a callable, then it is returned, if it is instead a string, then tries to instantiate the corresponding @@ -815,7 +853,7 @@ [docs] -def argmin_prevalence(loss: Callable, +def argmin_prevalence(loss: Callable, n_classes: int, method: Literal["optim_minimize", "linear_search", "ternary_search"]='optim_minimize'): """ @@ -842,7 +880,7 @@ [docs] -def optim_minimize(loss: Callable, n_classes: int, return_loss=False): +def optim_minimize(loss: Callable, n_classes: int, return_loss=False): """ Searches for the optimal prevalence values, i.e., an `n_classes`-dimensional vector of the (`n_classes`-1)-simplex that yields the smallest lost. This optimization is carried out by means of a constrained search using scipy's @@ -854,7 +892,7 @@ :return: (ndarray) the best prevalence vector found or a tuple which also contains the value of the loss if return_loss=True """ - from scipy import optimize + from scipy import optimize # the initial point is set as the uniform distribution uniform_distribution = uniform_prevalence(n_classes=n_classes) @@ -873,7 +911,7 @@ [docs] -def linear_search(loss: Callable, n_classes: int): +def linear_search(loss: Callable, n_classes: int): """ Performs a linear search for the best prevalence value in binary problems. The search is carried out by exploring the range [0,1] stepping by 0.01. This search is inefficient, and is added only for completeness (some of the @@ -897,7 +935,7 @@ [docs] -def ternary_search(loss: Callable, n_classes: int): +def ternary_search(loss: Callable, n_classes: int): """ Performs a ternary search for the best prevalence value in binary problems. This search assumes the loss is unimodal over the interval [0,1]. @@ -933,7 +971,7 @@ [docs] -def prevalence_linspace(grid_points:int=21, repeats:int=1, smooth_limits_epsilon:float=0.01) -> np.ndarray: +def prevalence_linspace(grid_points:int=21, repeats:int=1, smooth_limits_epsilon:float=0.01) -> np.ndarray: """ Produces an array of uniformly separated values of prevalence. By default, produces an array of 21 prevalence values, with @@ -958,7 +996,7 @@ [docs] -def uniform_prevalence_sampling(n_classes: int, size: int=1) -> np.ndarray: +def uniform_prevalence_sampling(n_classes: int, size: int=1) -> np.ndarray: """ Implements the `Kraemer algorithm <http://www.cs.cmu.edu/~nasmith/papers/smith+tromble.tr04.pdf>`_ for sampling uniformly at random from the unit simplex. This implementation is adapted from this @@ -994,7 +1032,7 @@ [docs] -def solve_adjustment_binary(prevalence_estim: ArrayLike, tpr: float, fpr: float, clip: bool=True): +def solve_adjustment_binary(prevalence_estim: ArrayLike, tpr: float, fpr: float, clip: bool=True): """ Implements the adjustment of ACC and PACC for the binary case. The adjustment for a prevalence estimate of the positive class `p` comes down to computing: @@ -1021,7 +1059,7 @@ [docs] -def solve_adjustment( +def solve_adjustment( class_conditional_rates: np.ndarray, unadjusted_counts: np.ndarray, method: Literal["inversion", "invariant-ratio"], @@ -1067,14 +1105,18 @@ if method == "inversion": pass # We leave A and B unchanged elif method == "invariant-ratio": - # Change the last equation to replace it with the normalization condition + # Change the last equation to replace it with the normalization condition; + # copy first so this does not mutate the caller's arrays (np.asarray above + # returns the same object, not a copy, when the input is already float64) + A = A.copy() + B = B.copy() A[-1, :] = 1.0 B[-1] = 1.0 else: raise ValueError(f"unknown {method=}") if solver == "minimize": - def loss(prev): + def loss(prev): return np.linalg.norm(A @ prev - B) return optim_minimize(loss, n_classes=A.shape[0]) elif solver in ["exact-raise", "exact-cc"]: @@ -1102,7 +1144,7 @@ [docs] -class CompositionalTransformation(ABC): +class CompositionalTransformation(ABC): """ Abstract class of transformations for compositional data. """ @@ -1110,13 +1152,13 @@ EPSILON = 1e-12 @abstractmethod - def __call__(self, X): + def __call__(self, X): ... [docs] @abstractmethod - def inverse(self, Z): + def inverse(self, Z): ... @@ -1124,12 +1166,12 @@ [docs] -class CLRtransformation(CompositionalTransformation): +class CLRtransformation(CompositionalTransformation): """ Centered log-ratio (CLR) transformation. """ - def __call__(self, X): + def __call__(self, X): X = np.asarray(X) X = qp.error.smooth(X, self.EPSILON) geometric_mean = np.exp(np.mean(np.log(X), axis=-1, keepdims=True)) @@ -1137,7 +1179,7 @@ [docs] - def inverse(self, Z): + def inverse(self, Z): return scipy.special.softmax(Z, axis=-1) @@ -1145,12 +1187,12 @@ [docs] -class ILRtransformation(CompositionalTransformation): +class ILRtransformation(CompositionalTransformation): """ Isometric log-ratio (ILR) transformation. """ - def __call__(self, X): + def __call__(self, X): X = np.asarray(X) X = qp.error.smooth(X, self.EPSILON) basis = self.get_V(X.shape[-1]) @@ -1158,7 +1200,7 @@ [docs] - def inverse(self, Z): + def inverse(self, Z): Z = np.asarray(Z) basis = self.get_V(Z.shape[-1] + 1) logp = Z @ basis @@ -1169,7 +1211,7 @@ [docs] @lru_cache(maxsize=None) - def get_V(self, k): + def get_V(self, k): helmert = np.zeros((k, k)) for i in range(1, k): helmert[i, :i] = 1 @@ -1182,7 +1224,7 @@ [docs] -def normalized_entropy(p): +def normalized_entropy(p): """ Computes the normalized Shannon entropy of a prevalence vector. @@ -1198,7 +1240,7 @@ [docs] -def antagonistic_prevalence(p, strength=1): +def antagonistic_prevalence(p, strength=1): """ Reflects a prevalence vector in ILR space and maps it back to the simplex. @@ -1215,7 +1257,7 @@ [docs] -def in_simplex(x, atol=1e-8): +def in_simplex(x, atol=1e-8): """ Checks whether points lie in the probability simplex. diff --git a/docs/build/html/_modules/quapy/method/_kdey.html b/docs/build/html/_modules/quapy/method/_kdey.html index 6664ea9..3367861 100644 --- a/docs/build/html/_modules/quapy/method/_kdey.html +++ b/docs/build/html/_modules/quapy/method/_kdey.html @@ -29,8 +29,9 @@ - - + + + @@ -41,6 +42,7 @@ + @@ -114,8 +116,14 @@ + + + + + + + - QuaPy @@ -131,7 +139,7 @@ - Quickstart + Home @@ -183,6 +191,21 @@ + + + + + + + + + + + GitHub + + + @@ -229,7 +252,7 @@ - Quickstart + Home @@ -272,6 +295,21 @@ + + + + + + + + + + + GitHub + + + @@ -332,22 +370,22 @@ Source code for quapy.method._kdey -import numpy as np -from numbers import Real -from sklearn.base import BaseEstimator -from sklearn.neighbors import KernelDensity +import numpy as np +from numbers import Real +from sklearn.base import BaseEstimator +from sklearn.neighbors import KernelDensity -import quapy as qp -from quapy.method._helper import _labels_to_indices -from quapy.method.aggregative import AggregativeSoftQuantifier -import quapy.functional as F -from scipy.special import logsumexp -from sklearn.metrics.pairwise import rbf_kernel +import quapy as qp +from quapy.method._helper import _labels_to_indices +from quapy.method.aggregative import AggregativeSoftQuantifier +import quapy.functional as F +from scipy.special import logsumexp +from sklearn.metrics.pairwise import rbf_kernel [docs] -class KDEBase: +class KDEBase: """ Common ancestor for KDE-based methods. Implements some common routines. """ @@ -356,7 +394,7 @@ KERNELS = ['gaussian', 'aitchison', 'ilr'] @classmethod - def _check_bandwidth(cls, bandwidth, kernel): + def _check_bandwidth(cls, bandwidth, kernel): """ Checks that the bandwidth parameter is correct @@ -370,13 +408,13 @@ return bandwidth @classmethod - def _check_kernel(cls, kernel): + def _check_kernel(cls, kernel): assert kernel in KDEBase.KERNELS, f'unknown {kernel=}' return kernel [docs] - def get_kde_function(self, X, bandwidth, kernel): + def get_kde_function(self, X, bandwidth, kernel): """ Wraps the KDE function from scikit-learn. @@ -392,7 +430,7 @@ [docs] - def pdf(self, kde, X, kernel, log_densities=False): + def pdf(self, kde, X, kernel, log_densities=False): """ Wraps the density evalution of scikit-learn's KDE. Scikit-learn returns log-scores (s), so this function returns :math:`e^{s}` @@ -411,7 +449,7 @@ [docs] - def get_mixture_components(self, X, y, classes, bandwidth, kernel): + def get_mixture_components(self, X, y, classes, bandwidth, kernel): """ Returns an array containing the mixture components, i.e., the KDE functions for each class. @@ -433,7 +471,7 @@ [docs] - def transform_posteriors(self, X, kernel): + def transform_posteriors(self, X, kernel): if kernel in {'aitchison', 'ilr'}: X = self.shrink_posteriors(X) if kernel == 'aitchison': @@ -445,7 +483,7 @@ [docs] - def shrink_posteriors(self, X): + def shrink_posteriors(self, X): shrinkage = getattr(self, 'shrinkage', 0.0) if shrinkage <= 0: return X @@ -457,7 +495,7 @@ [docs] - def effective_bandwidth(self, bandwidth, kernel): + def effective_bandwidth(self, bandwidth, kernel): shrinkage = getattr(self, 'shrinkage', 0.0) if shrinkage > 0 and kernel in {'aitchison', 'ilr'} and isinstance(bandwidth, Real): return (1.0 - shrinkage) * float(bandwidth) @@ -466,7 +504,7 @@ [docs] - def clr_transform(self, X): + def clr_transform(self, X): if not hasattr(self, 'clr'): self.clr = F.CLRtransformation() return self.clr(X) @@ -474,7 +512,7 @@ [docs] - def ilr_transform(self, X): + def ilr_transform(self, X): if not hasattr(self, 'ilr'): self.ilr = F.ILRtransformation() return self.ilr(X) @@ -484,7 +522,7 @@ [docs] -class KDEyML(AggregativeSoftQuantifier, KDEBase): +class KDEyML(AggregativeSoftQuantifier, KDEBase): """ Kernel Density Estimation model for quantification (KDEy) relying on the Kullback-Leibler divergence (KLD) as the divergence measure to be minimized. This method was first proposed in the paper @@ -526,7 +564,7 @@ :param random_state: a seed to be set before fitting any base quantifier (default None) """ - def __init__(self, classifier: BaseEstimator=None, fit_classifier=True, val_split=5, bandwidth=0.1, + def __init__(self, classifier: BaseEstimator=None, fit_classifier=True, val_split=5, bandwidth=0.1, kernel='gaussian', shrinkage=0.0, random_state=None): super().__init__(classifier, fit_classifier, val_split) self.bandwidth = KDEBase._check_bandwidth(bandwidth, kernel) @@ -539,7 +577,7 @@ [docs] - def aggregation_fit(self, classif_predictions, labels): + def aggregation_fit(self, classif_predictions, labels): self.mix_densities = self.get_mixture_components( classif_predictions, labels, @@ -552,7 +590,7 @@ [docs] - def aggregate(self, posteriors: np.ndarray): + def aggregate(self, posteriors: np.ndarray): """ Searches for the mixture model parameter (the sought prevalence values) that maximizes the likelihood of the data (i.e., that minimizes the negative log-likelihood) @@ -569,14 +607,14 @@ for kde_i in self.mix_densities ] - def neg_loglikelihood(prev): + def neg_loglikelihood(prev): prev = qp.error.smooth(prev, eps=epsilon) test_loglikelihood = logsumexp(np.log(prev)[:, None] + test_log_densities, axis=0) return -np.sum(test_loglikelihood) else: test_densities = [self.pdf(kde_i, posteriors, self.kernel) for kde_i in self.mix_densities] - def neg_loglikelihood(prev): + def neg_loglikelihood(prev): test_mixture_likelihood = prev @ test_densities test_loglikelihood = np.log(test_mixture_likelihood + epsilon) return -np.sum(test_loglikelihood) @@ -588,7 +626,7 @@ [docs] -class KDEyHD(AggregativeSoftQuantifier, KDEBase): +class KDEyHD(AggregativeSoftQuantifier, KDEBase): """ Kernel Density Estimation model for quantification (KDEy) relying on the squared Hellinger Disntace (HD) as the divergence measure to be minimized. This method was first proposed in the paper @@ -633,7 +671,7 @@ :param montecarlo_trials: number of Monte Carlo trials (default 10000) """ - def __init__(self, classifier: BaseEstimator=None, fit_classifier=True, val_split=5, divergence: str='HD', + def __init__(self, classifier: BaseEstimator=None, fit_classifier=True, val_split=5, divergence: str='HD', bandwidth=0.1, random_state=None, montecarlo_trials=10000): super().__init__(classifier, fit_classifier, val_split) @@ -644,7 +682,7 @@ [docs] - def aggregation_fit(self, classif_predictions, labels): + def aggregation_fit(self, classif_predictions, labels): self.mix_densities = self.get_mixture_components( classif_predictions, labels, self.classes_, self.bandwidth, 'gaussian' ) @@ -663,7 +701,7 @@ [docs] - def aggregate(self, posteriors: np.ndarray): + def aggregate(self, posteriors: np.ndarray): # we retain all n*N examples (sampled from a mixture with uniform parameter), and then # apply importance sampling (IS). In this version we compute D(p_alpha||q) with IS n_classes = len(self.mix_densities) @@ -671,7 +709,7 @@ test_kde = self.get_kde_function(posteriors, self.bandwidth, 'gaussian') test_densities = self.pdf(test_kde, self.reference_samples, 'gaussian') - def f_squared_hellinger(u): + def f_squared_hellinger(u): return (np.sqrt(u)-1)**2 # todo: this will fail when self.divergence is a callable, and is not the right place to do it anyway @@ -687,7 +725,7 @@ p_class = self.reference_classwise_densities + epsilon fracs = p_class/qs - def divergence(prev): + def divergence(prev): # ps / qs = (prev @ p_class) / qs = prev @ (p_class / qs) = prev @ fracs ps_div_qs = prev @ fracs return np.mean( f(ps_div_qs) * iw ) @@ -699,7 +737,7 @@ [docs] -class KDEyCS(AggregativeSoftQuantifier): +class KDEyCS(AggregativeSoftQuantifier): """ Kernel Density Estimation model for quantification (KDEy) relying on the Cauchy-Schwarz divergence (CS) as the divergence measure to be minimized. This method was first proposed in the paper @@ -736,13 +774,13 @@ :param bandwidth: float, the bandwidth of the Kernel """ - def __init__(self, classifier: BaseEstimator=None, fit_classifier=True, val_split=5, bandwidth=0.1): + def __init__(self, classifier: BaseEstimator=None, fit_classifier=True, val_split=5, bandwidth=0.1): super().__init__(classifier, fit_classifier, val_split) self.bandwidth = KDEBase._check_bandwidth(bandwidth, kernel='gaussian') [docs] - def gram_matrix_mix_sum(self, X, Y=None): + def gram_matrix_mix_sum(self, X, Y=None): # this adapts the output of the rbf_kernel function (pairwise evaluations of Gaussian kernels k(x,y)) # to contain pairwise evaluations of N(x|mu,Sigma1+Sigma2) with mu=y and Sigma1 and Sigma2 are # two "scalar matrices" (h^2)*I each, so Sigma1+Sigma2 has scalar 2(h^2) (h is the bandwidth) @@ -757,7 +795,7 @@ [docs] - def aggregation_fit(self, classif_predictions, labels): + def aggregation_fit(self, classif_predictions, labels): P, y = classif_predictions, labels n = len(self.classes_) @@ -789,7 +827,7 @@ [docs] - def aggregate(self, posteriors: np.ndarray): + def aggregate(self, posteriors: np.ndarray): Ptr = self.Ptr Pte = posteriors y = self.ytr @@ -809,7 +847,7 @@ for i in range(n): tr_te_sums[i] = self.gram_matrix_mix_sum(Ptr[y==i], Pte) - def divergence(alpha): + def divergence(alpha): # called \overline{r} in the paper alpha_ratio = alpha * self.counts_inv diff --git a/docs/build/html/_modules/quapy/method/_neural.html b/docs/build/html/_modules/quapy/method/_neural.html index 3e6c7c3..3c8215e 100644 --- a/docs/build/html/_modules/quapy/method/_neural.html +++ b/docs/build/html/_modules/quapy/method/_neural.html @@ -29,7 +29,7 @@ - + @@ -370,23 +370,24 @@ Source code for quapy.method._neural -import os -from pathlib import Path -import random +import logging +import os +from pathlib import Path +import random -import torch -from torch.nn import MSELoss -from torch.nn.functional import relu +import torch +from torch.nn import MSELoss +from torch.nn.functional import relu -from quapy.protocol import UPP -from quapy.method.aggregative import * -from quapy.util import EarlyStop -from tqdm import tqdm +from quapy.protocol import UPP +from quapy.method.aggregative import * +from quapy.util import EarlyStop +from tqdm import tqdm [docs] -class QuaNetTrainer(BaseQuantifier): +class QuaNetTrainer(BaseQuantifier): """ Implementation of `QuaNet <https://dl.acm.org/doi/abs/10.1145/3269206.3269287>`_, a neural network for quantification. This implementation uses `PyTorch <https://pytorch.org/>`_ and can take advantage of GPU @@ -438,7 +439,7 @@ :param device: string, indicate "cpu" or "cuda" """ - def __init__(self, + def __init__(self, classifier, fit_classifier=True, sample_size=None, @@ -491,7 +492,7 @@ [docs] - def fit(self, X, y): + def fit(self, X, y): """ Trains QuaNet. @@ -549,7 +550,7 @@ order_by=0 if data.binary else None, **self.quanet_params ).to(self.device) - print(self.quanet) + logging.getLogger(__name__).debug(self.quanet) self.optim = torch.optim.Adam(self.quanet.parameters(), lr=self.lr) early_stop = EarlyStop(self.patience, lower_is_better=True) @@ -564,15 +565,16 @@ if early_stop.IMPROVED: torch.save(self.quanet.state_dict(), checkpoint) elif early_stop.STOP: - print(f'training ended by patience exhausted; loading best model parameters in {checkpoint} ' - f'for epoch {early_stop.best_epoch}') + logging.getLogger(__name__).info( + f'training ended by patience exhausted; loading best model parameters in {checkpoint} ' + f'for epoch {early_stop.best_epoch}') self.quanet.load_state_dict(torch.load(checkpoint)) break return self - def _get_aggregative_estims(self, posteriors): + def _get_aggregative_estims(self, posteriors): label_predictions = np.argmax(posteriors, axis=-1) prevs_estim = [] for quantifier in self.quantifiers.values(): @@ -585,7 +587,7 @@ [docs] - def predict(self, X): + def predict(self, X): posteriors = self.classifier.predict_proba(X) embeddings = self.classifier.transform(X) quant_estims = self._get_aggregative_estims(posteriors) @@ -598,7 +600,7 @@ return prevalence - def _epoch(self, data: LabelledCollection, posteriors, iterations, epoch, early_stop, train): + def _epoch(self, data: LabelledCollection, posteriors, iterations, epoch, early_stop, train): mse_loss = MSELoss() self.quanet.train(mode=train) @@ -650,7 +652,7 @@ [docs] - def get_params(self, deep=True): + def get_params(self, deep=True): classifier_params = self.classifier.get_params() classifier_params = {'classifier__'+k:v for k,v in classifier_params.items()} return {**classifier_params, **self.quanet_params} @@ -658,7 +660,7 @@ [docs] - def set_params(self, **parameters): + def set_params(self, **parameters): learner_params = {} for key, val in parameters.items(): if key in self.quanet_params: @@ -670,7 +672,7 @@ self.classifier.set_params(**learner_params) - def __check_params_colision(self, quanet_params, learner_params): + def __check_params_colision(self, quanet_params, learner_params): quanet_keys = set(quanet_params.keys()) learner_keys = set(learner_params.keys()) intersection = quanet_keys.intersection(learner_keys) @@ -680,7 +682,7 @@ [docs] - def clean_checkpoint(self): + def clean_checkpoint(self): """ Removes the checkpoint """ @@ -689,23 +691,23 @@ [docs] - def clean_checkpoint_dir(self): + def clean_checkpoint_dir(self): """ Removes anything contained in the checkpoint directory """ - import shutil + import shutil shutil.rmtree(self.checkpointdir, ignore_errors=True) @property - def classes_(self): + def classes_(self): return self._classes_ [docs] -def mae_loss(output, target): +def mae_loss(output, target): """ Torch-like wrapper for the Mean Absolute Error @@ -719,7 +721,7 @@ [docs] -class QuaNetModule(torch.nn.Module): +class QuaNetModule(torch.nn.Module): """ Implements the `QuaNet <https://dl.acm.org/doi/abs/10.1145/3269206.3269287>`_ forward pass. See :class:`QuaNetTrainer` for training QuaNet. @@ -736,7 +738,7 @@ :param order_by: integer, class for which the document embeddings are to be sorted """ - def __init__(self, + def __init__(self, doc_embedding_size, n_classes, stats_size, @@ -771,10 +773,10 @@ self.output = torch.nn.Linear(prev_size, n_classes) @property - def device(self): + def device(self): return torch.device('cuda') if next(self.parameters()).is_cuda else torch.device('cpu') - def _init_hidden(self): + def _init_hidden(self): directions = 2 if self.bidirectional else 1 var_hidden = torch.zeros(self.nlayers * directions, 1, self.hidden_size) var_cell = torch.zeros(self.nlayers * directions, 1, self.hidden_size) @@ -784,7 +786,7 @@ [docs] - def forward(self, doc_embeddings, doc_posteriors, statistics): + def forward(self, doc_embeddings, doc_posteriors, statistics): device = self.device doc_embeddings = torch.as_tensor(doc_embeddings, dtype=torch.float, device=device) doc_posteriors = torch.as_tensor(doc_posteriors, dtype=torch.float, device=device) diff --git a/docs/build/html/_modules/quapy/method/_threshold_optim.html b/docs/build/html/_modules/quapy/method/_threshold_optim.html index 6bb8003..304e087 100644 --- a/docs/build/html/_modules/quapy/method/_threshold_optim.html +++ b/docs/build/html/_modules/quapy/method/_threshold_optim.html @@ -1,92 +1,388 @@ - - - - - - quapy.method._threshold_optim — QuaPy: A Python-based open-source framework for quantification 0.2.1 documentation - - + + + + + + + + quapy.method._threshold_optim — QuaPy: A Python-based open-source framework for quantification 0.2.1 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Skip to main content - - - - - - - - - + + + + Back to top + + + + + + + + + + Ctrl+K + + - - - - - - - - - - QuaPy: A Python-based open-source framework for quantification - - - - - - - + + - - -Quickstart - - -Manuals - - -API - - - - + + + + + + + + + + + - - - QuaPy: A Python-based open-source framework for quantification - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Home + + + + + + + Manuals + + + + + + + API + + - - - - - - Module code - quapy.method._threshold_optim - - - - - - - - Source code for quapy.method._threshold_optim -from abc import abstractmethod + + + + + + + + + -import numpy as np -from sklearn.base import BaseEstimator -import quapy as qp -import quapy.functional as F -from quapy.data import LabelledCollection -from quapy.method.aggregative import BinaryAggregativeQuantifier + + + Search + Ctrl+K + + + + + + + + + + + + + + System Settings + Light + Dark + + + + + + + + + + + + + + GitHub + + + + + + + + + + + + + Search + Ctrl+K + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Home + + + + + + + Manuals + + + + + + + API + + + + + + + + + + + + + + + + + + + + + + + System Settings + Light + Dark + + + + + + + + + + + + + + GitHub + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Module code + + quapy.method._threshold_optim + + + + + + + + + + + + + + + + + Source code for quapy.method._threshold_optim +from abc import abstractmethod + +import numpy as np +from sklearn.base import BaseEstimator +import quapy as qp +import quapy.functional as F +from quapy.data import LabelledCollection +from quapy.method.aggregative import BinaryAggregativeQuantifier [docs] -class ThresholdOptimization(BinaryAggregativeQuantifier): +class ThresholdOptimization(BinaryAggregativeQuantifier): """ Abstract class of Threshold Optimization variants for :class:`ACC` as proposed by `Forman 2006 <https://dl.acm.org/doi/abs/10.1145/1150402.1150423>`_ and @@ -111,14 +407,14 @@ :param n_jobs: number of parallel workers """ - def __init__(self, classifier: BaseEstimator=None, fit_classifier=True, val_split=None, n_jobs=None): + def __init__(self, classifier: BaseEstimator=None, fit_classifier=True, val_split=None, n_jobs=None): super().__init__(classifier, fit_classifier, val_split) self.n_jobs = qp._get_njobs(n_jobs) [docs] @abstractmethod - def condition(self, tpr, fpr) -> float: + def condition(self, tpr, fpr) -> float: """ Implements the criterion according to which the threshold should be selected. This function should return the (float) score to be minimized. @@ -132,7 +428,7 @@ [docs] - def discard(self, tpr, fpr) -> bool: + def discard(self, tpr, fpr) -> bool: """ Indicates whether a combination of tpr and fpr should be discarded @@ -144,7 +440,7 @@ - def _eval_candidate_thresholds(self, decision_scores, y): + def _eval_candidate_thresholds(self, decision_scores, y): """ Seeks for the best `tpr` and `fpr` according to the score obtained at different decision thresholds. The scoring function is implemented in function `_condition`. @@ -181,7 +477,7 @@ [docs] - def aggregate_with_threshold(self, classif_predictions, tprs, fprs, thresholds): + def aggregate_with_threshold(self, classif_predictions, tprs, fprs, thresholds): # This function performs the adjusted count for given tpr, fpr, and threshold. # Note that, due to broadcasting, tprs, fprs, and thresholds could be arrays of length > 1 prevs_estims = np.mean(classif_predictions[:, None] >= thresholds, axis=0) @@ -190,26 +486,26 @@ return prevs_estims.squeeze() - def _compute_table(self, y, y_): + def _compute_table(self, y, y_): TP = np.logical_and(y == y_, y == self.pos_label).sum() FP = np.logical_and(y != y_, y == self.neg_label).sum() FN = np.logical_and(y != y_, y == self.pos_label).sum() TN = np.logical_and(y == y_, y == self.neg_label).sum() return TP, FP, FN, TN - def _compute_tpr(self, TP, FP): - if TP + FP == 0: + def _compute_tpr(self, TP, FN): + if TP + FN == 0: return 1 - return TP / (TP + FP) + return TP / (TP + FN) - def _compute_fpr(self, FP, TN): + def _compute_fpr(self, FP, TN): if FP + TN == 0: return 0 return FP / (FP + TN) [docs] - def aggregation_fit(self, classif_predictions, labels): + def aggregation_fit(self, classif_predictions, labels): decision_scores, y = classif_predictions, labels # the standard behavior is to keep the best threshold only self.tpr, self.fpr, self.threshold = self._eval_candidate_thresholds(decision_scores, y)[0] @@ -218,7 +514,7 @@ [docs] - def aggregate(self, classif_predictions: np.ndarray): + def aggregate(self, classif_predictions: np.ndarray): # the standard behavior is to compute the adjusted count using the best threshold found return self.aggregate_with_threshold(classif_predictions, self.tpr, self.fpr, self.threshold) @@ -227,7 +523,7 @@ [docs] -class T50(ThresholdOptimization): +class T50(ThresholdOptimization): """ Threshold Optimization variant for :class:`ACC` as proposed by `Forman 2006 <https://dl.acm.org/doi/abs/10.1145/1150402.1150423>`_ and @@ -249,12 +545,12 @@ """ - def __init__(self, classifier: BaseEstimator=None, fit_classifier=True, val_split=5): + def __init__(self, classifier: BaseEstimator=None, fit_classifier=True, val_split=5): super().__init__(classifier, fit_classifier, val_split) [docs] - def condition(self, tpr, fpr) -> float: + def condition(self, tpr, fpr) -> float: return abs(tpr - 0.5) @@ -262,7 +558,7 @@ [docs] -class MAX(ThresholdOptimization): +class MAX(ThresholdOptimization): """ Threshold Optimization variant for :class:`ACC` as proposed by `Forman 2006 <https://dl.acm.org/doi/abs/10.1145/1150402.1150423>`_ and @@ -282,12 +578,12 @@ """ - def __init__(self, classifier: BaseEstimator=None, fit_classifier=True, val_split=5): + def __init__(self, classifier: BaseEstimator=None, fit_classifier=True, val_split=5): super().__init__(classifier, fit_classifier, val_split) [docs] - def condition(self, tpr, fpr) -> float: + def condition(self, tpr, fpr) -> float: # MAX strives to maximize (tpr - fpr), which is equivalent to minimize (fpr - tpr) return (fpr - tpr) @@ -296,7 +592,7 @@ [docs] -class X(ThresholdOptimization): +class X(ThresholdOptimization): """ Threshold Optimization variant for :class:`ACC` as proposed by `Forman 2006 <https://dl.acm.org/doi/abs/10.1145/1150402.1150423>`_ and @@ -316,12 +612,12 @@ """ - def __init__(self, classifier: BaseEstimator=None, fit_classifier=True, val_split=5): + def __init__(self, classifier: BaseEstimator=None, fit_classifier=True, val_split=5): super().__init__(classifier, fit_classifier, val_split) [docs] - def condition(self, tpr, fpr) -> float: + def condition(self, tpr, fpr) -> float: return abs(1 - (tpr + fpr)) @@ -329,7 +625,7 @@ [docs] -class MS(ThresholdOptimization): +class MS(ThresholdOptimization): """ Median Sweep. Threshold Optimization variant for :class:`ACC` as proposed by `Forman 2006 <https://dl.acm.org/doi/abs/10.1145/1150402.1150423>`_ and @@ -348,18 +644,18 @@ for `k`); or as a tuple (X,y) defining the specific set of data to use for validation. """ - def __init__(self, classifier: BaseEstimator=None, fit_classifier=True, val_split=5): + def __init__(self, classifier: BaseEstimator=None, fit_classifier=True, val_split=5): super().__init__(classifier, fit_classifier, val_split) [docs] - def condition(self, tpr, fpr) -> float: + def condition(self, tpr, fpr) -> float: return 1 [docs] - def aggregation_fit(self, classif_predictions, labels): + def aggregation_fit(self, classif_predictions, labels): decision_scores, y = classif_predictions, labels # keeps all candidates tprs_fprs_thresholds = self._eval_candidate_thresholds(decision_scores, y) @@ -371,7 +667,7 @@ [docs] - def aggregate(self, classif_predictions: np.ndarray): + def aggregate(self, classif_predictions: np.ndarray): prevalences = self.aggregate_with_threshold(classif_predictions, self.tprs, self.fprs, self.thresholds) if prevalences.ndim==2: prevalences = np.median(prevalences, axis=0) @@ -382,7 +678,7 @@ [docs] -class MS2(MS): +class MS2(MS): """ Median Sweep 2. Threshold Optimization variant for :class:`ACC` as proposed by `Forman 2006 <https://dl.acm.org/doi/abs/10.1145/1150402.1150423>`_ and @@ -402,42 +698,86 @@ for `k`); or as a tuple (X,y) defining the specific set of data to use for validation. """ - def __init__(self, classifier: BaseEstimator=None, fit_classifier=True, val_split=5): + def __init__(self, classifier: BaseEstimator=None, fit_classifier=True, val_split=5): super().__init__(classifier, fit_classifier, val_split) [docs] - def discard(self, tpr, fpr) -> bool: + def discard(self, tpr, fpr) -> bool: return (tpr-fpr) <= 0.25 - + + + + + + + + + + + + + - + + + + - Built with Sphinx using a - theme - provided by Read the Docs. - + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/build/html/_modules/quapy/method/aggregative.html b/docs/build/html/_modules/quapy/method/aggregative.html index a6b98d4..0680ac3 100644 --- a/docs/build/html/_modules/quapy/method/aggregative.html +++ b/docs/build/html/_modules/quapy/method/aggregative.html @@ -29,8 +29,9 @@ - - + + + @@ -41,6 +42,7 @@ + @@ -114,8 +116,14 @@ + + + + + + + - QuaPy @@ -131,7 +139,7 @@ - Quickstart + Home @@ -183,6 +191,21 @@ + + + + + + + + + + + GitHub + + + @@ -229,7 +252,7 @@ - Quickstart + Home @@ -272,6 +295,21 @@ + + + + + + + + + + + GitHub + + + @@ -332,25 +370,25 @@ Source code for quapy.method.aggregative -from abc import ABC, abstractmethod -from argparse import ArgumentError -from copy import deepcopy -from typing import Callable, Literal, Union -import numpy as np -from sklearn.base import BaseEstimator -from sklearn.calibration import CalibratedClassifierCV -from sklearn.exceptions import NotFittedError -from sklearn.metrics import confusion_matrix -from sklearn.model_selection import cross_val_predict, train_test_split -from sklearn.utils.validation import check_is_fitted +import warnings +from abc import ABC, abstractmethod +from copy import deepcopy +from typing import Callable, Literal, Union +import numpy as np +from sklearn.base import BaseEstimator +from sklearn.calibration import CalibratedClassifierCV +from sklearn.exceptions import NotFittedError +from sklearn.metrics import confusion_matrix +from sklearn.model_selection import cross_val_predict, train_test_split +from sklearn.utils.validation import check_is_fitted -import quapy as qp -import quapy.functional as F -from quapy.functional import get_divergence -from quapy.classification.svmperf import SVMperf -from quapy.data import LabelledCollection -from quapy.method.base import BaseQuantifier, BinaryQuantifier, OneVsAllGeneric -from quapy.method._helper import ( +import quapy as qp +import quapy.functional as F +from quapy.functional import get_divergence +from quapy.classification.svmperf import SVMperf +from quapy.data import LabelledCollection +from quapy.method.base import BaseQuantifier, BinaryQuantifier, OneVsAllGeneric +from quapy.method._helper import ( _get_abstention_calibrators, _get_cvxpy, _rlls_check_mode, @@ -368,7 +406,7 @@ [docs] -class AggregativeQuantifier(BaseQuantifier, ABC): +class AggregativeQuantifier(BaseQuantifier, ABC): """ Abstract class for quantification methods that base their estimations on the aggregation of classification results. Aggregative quantifiers implement a pipeline that consists of generating classification predictions @@ -396,7 +434,7 @@ the training data be wasted. """ - def __init__(self, + def __init__(self, classifier: Union[None,BaseEstimator], fit_classifier:bool=True, val_split:Union[int,float,tuple,None]=5): @@ -418,9 +456,9 @@ (f'when {val_split=} is indicated as an integer, it represents the number of folds in a kFCV ' f'and must thus be >1') if val_split==5 and not fit_classifier: - print(f'Warning: {val_split=} will be ignored when the classifier is already trained ' - f'({fit_classifier=}). Parameter {self.val_split=} will be set to None. Set {val_split=} ' - f'to None to avoid this warning.') + warnings.warn(f'{val_split=} will be ignored when the classifier is already trained ' + f'({fit_classifier=}). Parameter {self.val_split=} will be set to None. Set {val_split=} ' + f'to None to avoid this warning.') self.val_split=None if val_split!=5: assert fit_classifier, (f'Parameter {val_split=} has been modified, but {fit_classifier=} ' @@ -457,7 +495,7 @@ assert fitted, (f'{fit_classifier=} requires the classifier to be already trained, ' f'but this does not seem to be') - def _check_init_parameters(self): + def _check_init_parameters(self): """ Implements any check to be performed in the parameters of the init method before undertaking the training of the quantifier. This is made as to allow for a quick execution stop when the @@ -467,7 +505,7 @@ """ pass - def _check_non_empty_classes(self, y): + def _check_non_empty_classes(self, y): """ Asserts all classes have positive instances. @@ -484,7 +522,7 @@ [docs] - def fit(self, X, y): + def fit(self, X, y): """ Trains the aggregative quantifier. This comes down to training a classifier (if requested) and an aggregation function. @@ -501,7 +539,7 @@ [docs] - def classifier_fit_predict(self, X, y): + def classifier_fit_predict(self, X, y): """ Trains the classifier if requested (`fit_classifier=True`) and generate the necessary predictions to train the aggregation function. @@ -551,7 +589,7 @@ [docs] @abstractmethod - def aggregation_fit(self, classif_predictions, labels): + def aggregation_fit(self, classif_predictions, labels): """ Trains the aggregation function. @@ -563,7 +601,7 @@ @property - def classifier(self): + def classifier(self): """ Gives access to the classifier @@ -572,7 +610,7 @@ return self.classifier_ @classifier.setter - def classifier(self, classifier): + def classifier(self, classifier): """ Setter for the classifier @@ -582,7 +620,7 @@ [docs] - def classify(self, X): + def classify(self, X): """ Provides the label predictions for the given instances. The predictions should respect the format expected by :meth:`aggregate`, e.g., posterior probabilities for probabilistic quantifiers, or crisp predictions for @@ -594,7 +632,7 @@ return getattr(self.classifier, self._classifier_method())(X) - def _classifier_method(self): + def _classifier_method(self): """ Name of the method that must be used for issuing label predictions. The default one is "decision_function". @@ -602,7 +640,7 @@ """ return 'decision_function' - def _check_classifier(self, adapt_if_necessary=False): + def _check_classifier(self, adapt_if_necessary=False): """ Guarantees that the underlying classifier implements the method required for issuing predictions, i.e., the method indicated by the :meth:`_classifier_method` @@ -614,7 +652,7 @@ [docs] - def predict(self, X): + def predict(self, X): """ Generate class prevalence estimates for the sample's instances by aggregating the label predictions generated by the classifier. @@ -629,7 +667,7 @@ [docs] @abstractmethod - def aggregate(self, classif_predictions: np.ndarray): + def aggregate(self, classif_predictions: np.ndarray): """ Implements the aggregation of the classifier predictions. @@ -640,7 +678,7 @@ @property - def classes_(self): + def classes_(self): """ Class labels, in the same order in which class prevalence values are to be computed. This default implementation actually returns the class labels of the learner. @@ -653,14 +691,14 @@ [docs] -class AggregativeCrispQuantifier(AggregativeQuantifier, ABC): +class AggregativeCrispQuantifier(AggregativeQuantifier, ABC): """ Abstract class for quantification methods that base their estimations on the aggregation of crisp decisions as returned by a hard classifier. Aggregative crisp quantifiers thus extend Aggregative Quantifiers by implementing specifications about crisp predictions. """ - def _classifier_method(self): + def _classifier_method(self): """ Name of the method that must be used for issuing label predictions. For crisp quantifiers, the method is 'predict', that returns an array of shape `(n_instances,)` of label predictions. @@ -673,7 +711,7 @@ [docs] -class AggregativeSoftQuantifier(AggregativeQuantifier, ABC): +class AggregativeSoftQuantifier(AggregativeQuantifier, ABC): """ Abstract class for quantification methods that base their estimations on the aggregation of posterior probabilities as returned by a probabilistic classifier. @@ -681,7 +719,7 @@ about soft predictions. """ - def _classifier_method(self): + def _classifier_method(self): """ Name of the method that must be used for issuing label predictions. For probabilistic quantifiers, the method is 'predict_proba', that returns an array of shape `(n_instances, n_dimensions,)` with posterior @@ -691,7 +729,7 @@ """ return 'predict_proba' - def _check_classifier(self, adapt_if_necessary=False): + def _check_classifier(self, adapt_if_necessary=False): """ Guarantees that the underlying classifier implements the method indicated by the :meth:`_classifier_method`. In case it does not, the classifier is calibrated (by means of the Platt's calibration method implemented by @@ -703,8 +741,8 @@ """ if not hasattr(self.classifier, self._classifier_method()): if adapt_if_necessary: - print(f'warning: The learner {self.classifier.__class__.__name__} does not seem to be ' - f'probabilistic. The learner will be calibrated (using CalibratedClassifierCV).') + warnings.warn(f'The learner {self.classifier.__class__.__name__} does not seem to be ' + f'probabilistic. The learner will be calibrated (using CalibratedClassifierCV).') self.classifier = CalibratedClassifierCV(self.classifier, cv=5) else: raise AssertionError(f'error: The learner {self.classifier.__class__.__name__} does not ' @@ -715,19 +753,19 @@ [docs] -class BinaryAggregativeQuantifier(AggregativeQuantifier, BinaryQuantifier): +class BinaryAggregativeQuantifier(AggregativeQuantifier, BinaryQuantifier): @property - def pos_label(self): + def pos_label(self): return self.classifier.classes_[1] @property - def neg_label(self): + def neg_label(self): return self.classifier.classes_[0] [docs] - def fit(self, X, y): + def fit(self, X, y): self._check_binary(y, self.__class__.__name__) return super().fit(X, y) @@ -738,19 +776,19 @@ # ------------------------------------ [docs] -class CC(AggregativeCrispQuantifier): +class CC(AggregativeCrispQuantifier): """ The most basic Quantification method. One that simply classifies all instances and counts how many have been attributed to each of the classes in order to compute class prevalence estimates. :param classifier: a sklearn's Estimator that generates a classifier """ - def __init__(self, classifier: BaseEstimator = None, fit_classifier: bool = True): + def __init__(self, classifier: BaseEstimator = None, fit_classifier: bool = True): super().__init__(classifier, fit_classifier, val_split=None) [docs] - def aggregation_fit(self, classif_predictions, labels): + def aggregation_fit(self, classif_predictions, labels): """ Nothing to do here! @@ -762,7 +800,7 @@ [docs] - def aggregate(self, classif_predictions: np.ndarray): + def aggregate(self, classif_predictions: np.ndarray): """ Computes class prevalence estimates by counting the prevalence of each of the predicted labels. @@ -776,7 +814,7 @@ [docs] -class PCC(AggregativeSoftQuantifier): +class PCC(AggregativeSoftQuantifier): """ `Probabilistic Classify & Count <https://ieeexplore.ieee.org/abstract/document/5694031>`_, the probabilistic variant of CC that relies on the posterior probabilities returned by a probabilistic classifier. @@ -784,12 +822,12 @@ :param classifier: a sklearn's Estimator that generates a classifier """ - def __init__(self, classifier: BaseEstimator = None, fit_classifier: bool = True, val_split=None): + def __init__(self, classifier: BaseEstimator = None, fit_classifier: bool = True, val_split=None): super().__init__(classifier, fit_classifier, val_split=val_split) [docs] - def aggregation_fit(self, classif_predictions, labels): + def aggregation_fit(self, classif_predictions, labels): """ Nothing to do here! @@ -801,7 +839,7 @@ [docs] - def aggregate(self, classif_posteriors): + def aggregate(self, classif_posteriors): return F.prevalence_from_probabilities(classif_posteriors, binarize=False) @@ -809,7 +847,7 @@ [docs] -class ACC(AggregativeCrispQuantifier): +class ACC(AggregativeCrispQuantifier): """ `Adjusted Classify & Count <https://link.springer.com/article/10.1007/s10618-008-0097-y>`_, the "adjusted" variant of :class:`CC`, that corrects the predictions of CC @@ -857,7 +895,7 @@ :param n_jobs: number of parallel workers """ - def __init__( + def __init__( self, classifier: BaseEstimator = None, fit_classifier = True, @@ -880,7 +918,7 @@ [docs] @classmethod - def newInvariantRatioEstimation(cls, classifier: BaseEstimator, fit_classifier=True, val_split=5, n_jobs=None): + def newInvariantRatioEstimation(cls, classifier: BaseEstimator, fit_classifier=True, val_split=5, n_jobs=None): """ Constructs a quantifier that implements the Invariant Ratio Estimator of `Vaz et al. 2018 <https://jmlr.org/papers/v20/18-456.html>`_. This amounts @@ -905,7 +943,7 @@ return ACC(classifier, fit_classifier=fit_classifier, val_split=val_split, method='invariant-ratio', norm='mapsimplex', n_jobs=n_jobs) - def _check_init_parameters(self): + def _check_init_parameters(self): if self.solver not in ACC.SOLVERS: raise ValueError(f"unknown solver; valid ones are {ACC.SOLVERS}") if self.method not in ACC.METHODS: @@ -915,7 +953,7 @@ [docs] - def aggregation_fit(self, classif_predictions, labels): + def aggregation_fit(self, classif_predictions, labels): """ Estimates the misclassification rates. :param classif_predictions: array-like with the predicted labels @@ -930,7 +968,7 @@ [docs] @classmethod - def getPteCondEstim(cls, classes, y, y_): + def getPteCondEstim(cls, classes, y, y_): """ Estimate the matrix with entry (i,j) being the estimate of P(hat_yi|yj), that is, the probability that a document that belongs to yj ends up being classified as belonging to yi @@ -953,7 +991,7 @@ [docs] - def aggregate(self, classif_predictions): + def aggregate(self, classif_predictions): prevs_estim = self.cc.aggregate(classif_predictions) estimate = F.solve_adjustment( class_conditional_rates=self.Pte_cond_estim_, @@ -968,7 +1006,7 @@ [docs] -class PACC(AggregativeSoftQuantifier): +class PACC(AggregativeSoftQuantifier): """ `Probabilistic Adjusted Classify & Count <https://ieeexplore.ieee.org/abstract/document/5694031>`_, the probabilistic variant of ACC that relies on the posterior probabilities returned by a probabilistic classifier. @@ -1015,7 +1053,7 @@ :param n_jobs: number of parallel workers """ - def __init__( + def __init__( self, classifier: BaseEstimator = None, fit_classifier=True, @@ -1031,7 +1069,7 @@ self.method = method self.norm = norm - def _check_init_parameters(self): + def _check_init_parameters(self): if self.solver not in ACC.SOLVERS: raise ValueError(f"unknown solver; valid ones are {ACC.SOLVERS}") if self.method not in ACC.METHODS: @@ -1041,7 +1079,7 @@ [docs] - def aggregation_fit(self, classif_predictions, labels): + def aggregation_fit(self, classif_predictions, labels): """ Estimates the misclassification rates @@ -1056,7 +1094,7 @@ [docs] - def aggregate(self, classif_posteriors): + def aggregate(self, classif_posteriors): prevs_estim = self.pcc.aggregate(classif_posteriors) estimate = F.solve_adjustment( @@ -1071,7 +1109,7 @@ [docs] @classmethod - def getPteCondEstim(cls, classes, y, y_): + def getPteCondEstim(cls, classes, y, y_): # estimate the matrix with entry (i,j) being the estimate of P(hat_yi|yj), that is, the probability that a # document that belongs to yj ends up being classified as belonging to yi n_classes = len(classes) @@ -1088,7 +1126,7 @@ [docs] -class RLLS(AggregativeSoftQuantifier): +class RLLS(AggregativeSoftQuantifier): """ `Regularized Learning for Domain Adaptation under Label Shifts <https://arxiv.org/abs/1903.09734>`_, used here as an aggregative @@ -1128,7 +1166,7 @@ :func:`quapy.functional.normalize_prevalence` """ - def __init__( + def __init__( self, classifier: BaseEstimator = None, fit_classifier=True, @@ -1147,7 +1185,7 @@ self.norm = norm self.last_w_ = None - def _check_init_parameters(self): + def _check_init_parameters(self): _get_cvxpy() _rlls_check_mode(self.mode) if not isinstance(self.alpha, (int, float)) or self.alpha < 0: @@ -1164,7 +1202,7 @@ [docs] - def aggregation_fit(self, classif_predictions, labels): + def aggregation_fit(self, classif_predictions, labels): if classif_predictions is None or labels is None: raise ValueError('RLLS requires source posterior predictions and source labels') @@ -1181,7 +1219,7 @@ [docs] - def aggregate(self, classif_posteriors): + def aggregate(self, classif_posteriors): qz = _rlls_predicted_marginal(classif_posteriors, mode=self.mode) w = _rlls_compute_weights( self.C_zy_, @@ -1199,7 +1237,7 @@ [docs] -class EMQ(AggregativeSoftQuantifier): +class EMQ(AggregativeSoftQuantifier): """ `Expectation Maximization for Quantification <https://ieeexplore.ieee.org/abstract/document/6789744>`_ (EMQ), aka `Saerens-Latinne-Decaestecker` (SLD) algorithm. @@ -1249,7 +1287,7 @@ ON_CALIB_ERROR_VALUES = ['raise', 'backup'] CALIB_OPTIONS = [None, 'nbvs', 'bcts', 'ts', 'vs'] - def __init__(self, classifier: BaseEstimator = None, fit_classifier=True, val_split=None, exact_train_prev=True, + def __init__(self, classifier: BaseEstimator = None, fit_classifier=True, val_split=None, exact_train_prev=True, calib=None, on_calib_error='raise', n_jobs=None): assert calib in EMQ.CALIB_OPTIONS, \ @@ -1261,12 +1299,12 @@ self.exact_train_prev = exact_train_prev self.calib = calib self.on_calib_error = on_calib_error - self.n_jobs = n_jobs + self.n_jobs = qp._get_njobs(n_jobs) [docs] @classmethod - def EMQ_BCTS(cls, classifier: BaseEstimator, fit_classifier=True, val_split=5, on_calib_error="raise", n_jobs=None): + def EMQ_BCTS(cls, classifier: BaseEstimator, fit_classifier=True, val_split=5, on_calib_error="raise", n_jobs=None): """ Constructs an instance of EMQ using the best configuration found in the `Alexandari et al. paper <http://proceedings.mlr.press/v119/alexandari20a.html>`_, i.e., one that relies on Bias-Corrected Temperature @@ -1298,23 +1336,23 @@ calib='bcts', on_calib_error=on_calib_error, n_jobs=n_jobs) - def _check_init_parameters(self): + def _check_init_parameters(self): if self.val_split is not None: if self.exact_train_prev and self.calib is None: - raise RuntimeWarning(f'The parameter {self.val_split=} was specified for EMQ, while the parameters ' - f'{self.exact_train_prev=} and {self.calib=}. This has no effect and causes an ' - f'unnecessary overload.') + warnings.warn(f'The parameter {self.val_split=} was specified for EMQ, while the parameters ' + f'{self.exact_train_prev=} and {self.calib=}. This has no effect and causes an ' + f'unnecessary overload.', RuntimeWarning) else: if self.calib is not None: - print(f'[warning] The parameter {self.calib=} requires the val_split be different from None. ' - f'This parameter will be set to 5. To avoid this warning, set this value to a float value ' - f'indicating the proportion of training data to be used as validation, or to an integer ' - f'indicating the number of folds for kFCV.') + warnings.warn(f'The parameter {self.calib=} requires the val_split be different from None. ' + f'This parameter will be set to 5. To avoid this warning, set this value to a float value ' + f'indicating the proportion of training data to be used as validation, or to an integer ' + f'indicating the number of folds for kFCV.') self.val_split = 5 [docs] - def classify(self, X): + def classify(self, X): """ Provides the posterior probabilities for the given instances. The calibration function, if required, has no effect in this step, and is only involved in the aggregate method. @@ -1327,13 +1365,13 @@ [docs] - def classifier_fit_predict(self, X, y): + def classifier_fit_predict(self, X, y): classif_predictions = super().classifier_fit_predict(X, y) self.train_prevalence = F.prevalence_from_labels(y, classes=self.classes_) return classif_predictions - def _fit_calibration(self, calibrator, P, y): + def _fit_calibration(self, calibrator, P, y): n_classes = len(self.classes_) if not np.issubdtype(y.dtype, np.number): @@ -1347,7 +1385,7 @@ elif self.on_calib_error == 'backup': self.calibration_function = lambda P: P - def _calibrate_if_requested(self, uncalib_posteriors): + def _calibrate_if_requested(self, uncalib_posteriors): if hasattr(self, 'calibration_function') and self.calibration_function is not None: try: calib_posteriors = self.calibration_function(uncalib_posteriors) @@ -1364,7 +1402,7 @@ [docs] - def aggregation_fit(self, classif_predictions, labels): + def aggregation_fit(self, classif_predictions, labels): """ Trains the aggregation function of EMQ. This comes down to recalibrating the posterior probabilities ir requested. @@ -1379,14 +1417,14 @@ requires_predictions = (self.calib is not None) or (not self.exact_train_prev) if P is None and requires_predictions: # classifier predictions were not generated because val_split=None - raise ArgumentError(self.val_split, self.__class__.__name__ + - ": Classifier predictions for the aggregative fit were not generated because " - "val_split=None. This usually happens when you enable calibrations or heuristics " - "during model selection but left val_split set to its default value (None). " - "Please provide one of the following values for val_split: (i) an integer >1 " - "(e.g. val_split=5) for k-fold cross-validation; (ii) a float in (0,1) (e.g. " - "val_split=0.3) for a proportion split; or (iii) a tuple (X, y) with explicit " - "validation data") + raise ValueError(self.__class__.__name__ + + ": Classifier predictions for the aggregative fit were not generated because " + "val_split=None. This usually happens when you enable calibrations or heuristics " + "during model selection but left val_split set to its default value (None). " + "Please provide one of the following values for val_split: (i) an integer >1 " + "(e.g. val_split=5) for k-fold cross-validation; (ii) a float in (0,1) (e.g. " + "val_split=0.3) for a proportion split; or (iii) a tuple (X, y) with explicit " + "validation data") if self.calib is not None: calibrator = _get_abstention_calibrators().get(self.calib, None) @@ -1403,7 +1441,7 @@ [docs] - def aggregate(self, classif_posteriors, epsilon=EPSILON): + def aggregate(self, classif_posteriors, epsilon=EPSILON): classif_posteriors = self._calibrate_if_requested(classif_posteriors) priors, posteriors = self.EM(self.train_prevalence, classif_posteriors, epsilon) return priors @@ -1411,7 +1449,7 @@ [docs] - def predict_proba(self, instances, epsilon=EPSILON): + def predict_proba(self, instances, epsilon=EPSILON): """ Returns the posterior probabilities updated by the EM algorithm. @@ -1428,7 +1466,7 @@ [docs] @classmethod - def EM(cls, tr_prev, posterior_probabilities, epsilon=EPSILON): + def EM(cls, tr_prev, posterior_probabilities, epsilon=EPSILON): """ Computes the `Expectation Maximization` routine. @@ -1466,7 +1504,7 @@ s += 1 if not converged: - print('[warning] the method has reached the maximum number of iterations; it might have not converged') + warnings.warn('the method has reached the maximum number of iterations; it might have not converged') return qs, ps @@ -1475,7 +1513,7 @@ [docs] -class HDy(AggregativeSoftQuantifier, BinaryAggregativeQuantifier): +class HDy(AggregativeSoftQuantifier, BinaryAggregativeQuantifier): """ `Hellinger Distance y <https://www.sciencedirect.com/science/article/pii/S0020025512004069>`_ (HDy). HDy is a probabilistic method for training binary quantifiers, that models quantification as the problem of @@ -1502,12 +1540,12 @@ for `k`); or as a tuple (X,y) defining the specific set of data to use for validation. """ - def __init__(self, classifier: BaseEstimator = None, fit_classifier=True, val_split=5): + def __init__(self, classifier: BaseEstimator = None, fit_classifier=True, val_split=5): super().__init__(classifier, fit_classifier, val_split) [docs] - def aggregation_fit(self, classif_predictions, labels): + def aggregation_fit(self, classif_predictions, labels): """ Trains the aggregation function of HDy. @@ -1522,7 +1560,7 @@ # pre-compute the histogram for positive and negative examples self.bins = np.linspace(10, 110, 11, dtype=int) # [10, 20, 30, ..., 100, 110] - def hist(P, bins): + def hist(P, bins): h = np.histogram(P, bins=bins, range=(0, 1), density=True)[0] return h / h.sum() @@ -1532,7 +1570,7 @@ [docs] - def aggregate(self, classif_posteriors): + def aggregate(self, classif_posteriors): # "In this work, the number of bins b used in HDx and HDy was chosen from 10 to 110 in steps of 10, # and the final estimated a priori probability was taken as the median of these 11 estimates." # (González-Castro, et al., 2013). @@ -1549,7 +1587,7 @@ # the authors proposed to search for the prevalence yielding the best matching as a linear search # at small steps (modern implementations resort to an optimization procedure, # see class DistributionMatching) - def loss(prev): + def loss(prev): class1_prev = prev[1] Px_train = class1_prev * Pxy1_density + (1 - class1_prev) * Pxy0_density return F.HellingerDistance(Px_train, Px_test) @@ -1564,7 +1602,7 @@ [docs] -class DyS(AggregativeSoftQuantifier, BinaryAggregativeQuantifier): +class DyS(AggregativeSoftQuantifier, BinaryAggregativeQuantifier): """ `DyS framework <https://ojs.aaai.org/index.php/AAAI/article/view/4376>`_ (DyS). DyS is a generalization of HDy method, using a Ternary Search in order to find the prevalence that @@ -1593,15 +1631,15 @@ :param n_jobs: number of parallel workers. """ - def __init__(self, classifier: BaseEstimator = None, fit_classifier=True, val_split=5, n_bins=8, + def __init__(self, classifier: BaseEstimator = None, fit_classifier=True, val_split=5, n_bins=8, divergence: Union[str, Callable] = 'HD', tol=1e-05, n_jobs=None): super().__init__(classifier, fit_classifier, val_split) self.tol = tol self.divergence = divergence self.n_bins = n_bins - self.n_jobs = n_jobs + self.n_jobs = qp._get_njobs(n_jobs) - def _ternary_search(self, f, left, right, tol): + def _ternary_search(self, f, left, right, tol): """ Find maximum of unimodal function f() within [left, right] """ @@ -1619,7 +1657,7 @@ [docs] - def aggregation_fit(self, classif_predictions, labels): + def aggregation_fit(self, classif_predictions, labels): """ Trains the aggregation function of DyS. @@ -1637,13 +1675,13 @@ [docs] - def aggregate(self, classif_posteriors): + def aggregate(self, classif_posteriors): Px = classif_posteriors[:, self.pos_label] # takes only the P(y=+1|x) Px_test = np.histogram(Px, bins=self.n_bins, range=(0, 1), density=True)[0] divergence = get_divergence(self.divergence) - def distribution_distance(prev): + def distribution_distance(prev): Px_train = prev * self.Pxy1_density + (1 - prev) * self.Pxy0_density return divergence(Px_train, Px_test) @@ -1655,7 +1693,7 @@ [docs] -class SMM(AggregativeSoftQuantifier, BinaryAggregativeQuantifier): +class SMM(AggregativeSoftQuantifier, BinaryAggregativeQuantifier): """ `SMM method <https://ieeexplore.ieee.org/document/9260028>`_ (SMM). SMM is a simplification of matching distribution methods where the representation of the examples @@ -1674,12 +1712,12 @@ for `k`); or as a tuple (X,y) defining the specific set of data to use for validation. """ - def __init__(self, classifier: BaseEstimator = None, fit_classifier=True, val_split=5): + def __init__(self, classifier: BaseEstimator = None, fit_classifier=True, val_split=5): super().__init__(classifier, fit_classifier, val_split) [docs] - def aggregation_fit(self, classif_predictions, labels): + def aggregation_fit(self, classif_predictions, labels): """ Trains the aggregation function of SMM. @@ -1697,7 +1735,7 @@ [docs] - def aggregate(self, classif_posteriors): + def aggregate(self, classif_posteriors): Px = classif_posteriors[:, self.pos_label] # takes only the P(y=+1|x) Px_mean = np.mean(Px) @@ -1709,7 +1747,7 @@ [docs] -class DMy(AggregativeSoftQuantifier): +class DMy(AggregativeSoftQuantifier): """ Generic Distribution Matching quantifier for binary or multiclass quantification based on the space of posterior probabilities. This implementation takes the number of bins, the divergence, and the possibility to work on CDF @@ -1742,19 +1780,19 @@ :param n_jobs: number of parallel workers (default None) """ - def __init__(self, classifier: BaseEstimator = None, fit_classifier=True, val_split=5, nbins=8, + def __init__(self, classifier: BaseEstimator = None, fit_classifier=True, val_split=5, nbins=8, divergence: Union[str, Callable] = 'HD', cdf=False, search='optim_minimize', n_jobs=None): super().__init__(classifier, fit_classifier, val_split) self.nbins = nbins self.divergence = divergence self.cdf = cdf self.search = search - self.n_jobs = n_jobs + self.n_jobs = qp._get_njobs(n_jobs) [docs] @classmethod - def HDy(cls, classifier: BaseEstimator = None, fit_classifier=True, val_split=5, n_jobs=None): + def HDy(cls, classifier: BaseEstimator = None, fit_classifier=True, val_split=5, n_jobs=None): """ Historical HDy preset expressed as a configuration of :class:`DMy`. @@ -1783,7 +1821,7 @@ return AggregativeMedianEstimator(base_quantifier=base, param_grid=param_grid, n_jobs=n_jobs) - def _get_distributions(self, posteriors): + def _get_distributions(self, posteriors): histograms = [] post_dims = posteriors.shape[1] if post_dims == 2: @@ -1801,7 +1839,7 @@ [docs] - def aggregation_fit(self, classif_predictions, labels): + def aggregation_fit(self, classif_predictions, labels): """ Trains the aggregation function of a distribution matching method. This comes down to generating the validation distributions out of the training data. @@ -1829,7 +1867,7 @@ [docs] - def aggregate(self, posteriors: np.ndarray): + def aggregate(self, posteriors: np.ndarray): """ Searches for the mixture model parameter (the sought prevalence values) that yields a validation distribution (the mixture) that best matches the test distribution, in terms of the divergence measure of choice. @@ -1844,7 +1882,7 @@ divergence = get_divergence(self.divergence) n_classes, n_channels, nbins = self.validation_distribution.shape - def loss(prev): + def loss(prev): prev = np.expand_dims(prev, axis=0) mixture_distribution = (prev @ self.validation_distribution.reshape(n_classes, -1)).reshape(n_channels, -1) divs = [divergence(test_distribution[ch], mixture_distribution[ch]) for ch in range(n_channels)] @@ -1857,7 +1895,7 @@ [docs] -def newELM(svmperf_base=None, loss='01', C=1): +def newELM(svmperf_base=None, loss='01', C=1): """ Explicit Loss Minimization (ELM) quantifiers. Quantifiers based on ELM represent a family of methods based on structured output learning; @@ -1887,7 +1925,7 @@ [docs] -def newSVMQ(svmperf_base=None, C=1): +def newSVMQ(svmperf_base=None, C=1): """ SVM(Q) is an Explicit Loss Minimization (ELM) quantifier set to optimize for the `Q` loss combining a classification-oriented loss and a quantification-oriented loss, as proposed by @@ -1914,7 +1952,9 @@ -def newSVMKLD(svmperf_base=None, C=1): + +[docs] +def newSVMKLD(svmperf_base=None, C=1): """ SVM(KLD) is an Explicit Loss Minimization (ELM) quantifier set to optimize for the Kullback-Leibler Divergence as proposed by `Esuli et al. 2015 <https://dl.acm.org/doi/abs/10.1145/2700406>`_. @@ -1936,14 +1976,15 @@ :return: returns an instance of CC set to work with SVMperf (with loss and C set properly) as the underlying classifier """ - return newELM(svmperf_base, loss='kld', C=C) + return newELM(svmperf_base, loss='kld', C=C) - -[docs] -def newSVMKLD(svmperf_base=None, C=1): + + +[docs] +def newSVMNKLD(svmperf_base=None, C=1): """ - SVM(KLD) is an Explicit Loss Minimization (ELM) quantifier set to optimize for the Kullback-Leibler Divergence + SVM(NKLD) is an Explicit Loss Minimization (ELM) quantifier set to optimize for the Kullback-Leibler Divergence normalized via the logistic function, as proposed by `Esuli et al. 2015 <https://dl.acm.org/doi/abs/10.1145/2700406>`_. Equivalent to: @@ -1970,7 +2011,7 @@ [docs] -def newSVMAE(svmperf_base=None, C=1): +def newSVMAE(svmperf_base=None, C=1): """ SVM(KLD) is an Explicit Loss Minimization (ELM) quantifier set to optimize for the Absolute Error as first used by `Moreo and Sebastiani, 2021 <https://arxiv.org/abs/2011.02552>`_. @@ -1998,7 +2039,7 @@ [docs] -def newSVMRAE(svmperf_base=None, C=1): +def newSVMRAE(svmperf_base=None, C=1): """ SVM(KLD) is an Explicit Loss Minimization (ELM) quantifier set to optimize for the Relative Absolute Error as first used by `Moreo and Sebastiani, 2021 <https://arxiv.org/abs/2011.02552>`_. @@ -2026,7 +2067,7 @@ [docs] -class OneVsAllAggregative(OneVsAllGeneric, AggregativeQuantifier): +class OneVsAllAggregative(OneVsAllGeneric, AggregativeQuantifier): """ Allows any binary quantifier to perform quantification on single-label datasets. The method maintains one binary quantifier for each class, and then l1-normalizes the outputs so that the @@ -2042,7 +2083,7 @@ is removed and no longer available at predict time. """ - def __init__(self, binary_quantifier=None, n_jobs=None, parallel_backend='multiprocessing'): + def __init__(self, binary_quantifier=None, n_jobs=None, parallel_backend='multiprocessing'): if binary_quantifier is None: binary_quantifier = PACC() assert isinstance(binary_quantifier, BaseQuantifier), \ @@ -2055,7 +2096,7 @@ [docs] - def classify(self, X): + def classify(self, X): """ If the base quantifier is not probabilistic, returns a matrix of shape `(n,m,)` with `n` the number of instances and `m` the number of classes. The entry `(i,j)` is a binary value indicating whether instance @@ -2079,34 +2120,34 @@ [docs] - def aggregate(self, classif_predictions): + def aggregate(self, classif_predictions): prevalences = self._parallel(self._delayed_binary_aggregate, classif_predictions) return F.normalize_prevalence(prevalences) [docs] - def aggregation_fit(self, classif_predictions, labels): - self._parallel(self._delayed_binary_aggregate_fit(c, classif_predictions, labels)) + def aggregation_fit(self, classif_predictions, labels): + self._parallel(self._delayed_binary_aggregate_fit, classif_predictions, labels) return self - def _delayed_binary_classification(self, c, X): + def _delayed_binary_classification(self, c, X): return self.dict_binary_quantifiers[c].classify(X) - def _delayed_binary_aggregate(self, c, classif_predictions): + def _delayed_binary_aggregate(self, c, classif_predictions): # the estimation for the positive class prevalence return self.dict_binary_quantifiers[c].aggregate(classif_predictions[:, c])[1] - def _delayed_binary_aggregate_fit(self, c, classif_predictions, labels): + def _delayed_binary_aggregate_fit(self, c, classif_predictions, labels): # trains the aggregation function of the cth quantifier - return self.dict_binary_quantifiers[c].aggregate_fit(classif_predictions[:, c], labels) + return self.dict_binary_quantifiers[c].aggregation_fit(classif_predictions[:, c], labels == c) [docs] -class AggregativeMedianEstimator(BinaryQuantifier): +class AggregativeMedianEstimator(BinaryQuantifier): """ This method is a meta-quantifier that returns, as the estimated class prevalence values, the median of the estimation returned by differently (hyper)parameterized base quantifiers. @@ -2119,7 +2160,7 @@ :param n_jobs: number of parallel workers """ - def __init__(self, base_quantifier: AggregativeQuantifier, param_grid: dict, random_state=None, n_jobs=None): + def __init__(self, base_quantifier: AggregativeQuantifier, param_grid: dict, random_state=None, n_jobs=None): self.base_quantifier = base_quantifier self.param_grid = param_grid self.random_state = random_state @@ -2127,17 +2168,17 @@ [docs] - def get_params(self, deep=True): + def get_params(self, deep=True): return self.base_quantifier.get_params(deep) [docs] - def set_params(self, **params): + def set_params(self, **params): self.base_quantifier.set_params(**params) - def _delayed_fit(self, args): + def _delayed_fit(self, args): with qp.util.temp_seed(self.random_state): params, X, y = args model = deepcopy(self.base_quantifier) @@ -2145,7 +2186,7 @@ model.fit(X, y) return model - def _delayed_fit_classifier(self, args): + def _delayed_fit_classifier(self, args): with qp.util.temp_seed(self.random_state): cls_params, X, y = args model = deepcopy(self.base_quantifier) @@ -2153,7 +2194,7 @@ predictions, labels = model.classifier_fit_predict(X, y) return (model, predictions, labels) - def _delayed_fit_aggregation(self, args): + def _delayed_fit_aggregation(self, args): with qp.util.temp_seed(self.random_state): ((model, predictions, y), q_params) = args model = deepcopy(model) @@ -2163,8 +2204,8 @@ [docs] - def fit(self, X, y): - import itertools + def fit(self, X, y): + import itertools self._check_binary(y, self.__class__.__name__) @@ -2177,8 +2218,7 @@ ((params, X, y) for params in cls_configs), seed=qp.environ.get('_R_SEED', None), n_jobs=self.n_jobs, - asarray=False, - backend='threading' + asarray=False ) else: model = self.base_quantifier @@ -2190,8 +2230,7 @@ self._delayed_fit_aggregation, itertools.product(models_preds, q_configs), seed=qp.environ.get('_R_SEED', None), - n_jobs=self.n_jobs, - backend='threading' + n_jobs=self.n_jobs ) else: configs = qp.model_selection.expand_grid(self.param_grid) @@ -2199,25 +2238,23 @@ self._delayed_fit, ((params, X, y) for params in configs), seed=qp.environ.get('_R_SEED', None), - n_jobs=self.n_jobs, - backend='threading' + n_jobs=self.n_jobs ) return self - def _delayed_predict(self, args): + def _delayed_predict(self, args): model, instances = args return model.predict(instances) [docs] - def predict(self, instances): + def predict(self, instances): prev_preds = qp.util.parallel( self._delayed_predict, ((model, instances) for model in self.models), seed=qp.environ.get('_R_SEED', None), - n_jobs=self.n_jobs, - backend='threading' + n_jobs=self.n_jobs ) return np.median(prev_preds, axis=0) @@ -2228,7 +2265,7 @@ # imports # --------------------------------------------------------------- -from . import _threshold_optim +from . import _threshold_optim T50 = _threshold_optim.T50 MAX = _threshold_optim.MAX @@ -2236,7 +2273,7 @@ MS = _threshold_optim.MS MS2 = _threshold_optim.MS2 -from . import _kdey +from . import _kdey KDEyML = _kdey.KDEyML KDEyHD = _kdey.KDEyHD diff --git a/docs/build/html/_modules/quapy/method/base.html b/docs/build/html/_modules/quapy/method/base.html index e649c5b..5519615 100644 --- a/docs/build/html/_modules/quapy/method/base.html +++ b/docs/build/html/_modules/quapy/method/base.html @@ -1,95 +1,392 @@ - - - - - - quapy.method.base — QuaPy: A Python-based open-source framework for quantification 0.2.1 documentation - - + + + + + + + + quapy.method.base — QuaPy: A Python-based open-source framework for quantification 0.2.1 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Skip to main content - - - - - - - - - + + + + Back to top + + + + + + + + + + Ctrl+K + + - - - - - - - - - - QuaPy: A Python-based open-source framework for quantification - - - - - - - + + - - -
Source code for quapy.data.datasets -import os -from contextlib import contextmanager -import zipfile -from os.path import join -import pandas as pd -from quapy.data.base import Dataset, LabelledCollection -from quapy.data.preprocessing import text2tfidf, reduce_columns -from quapy.data.preprocessing import standardize as standardizer -from quapy.data.reader import * -from quapy.util import download_file_if_not_exists, download_file, get_quapy_home, pickled_resource -from sklearn.preprocessing import StandardScaler +import logging +import os +from contextlib import contextmanager +import zipfile +from os.path import join +import pandas as pd +from quapy.data.base import Dataset, LabelledCollection +from quapy.data.preprocessing import text2tfidf, reduce_columns +from quapy.data.preprocessing import standardize as standardizer +from quapy.data.reader import * +from quapy.util import download_file_if_not_exists, download_file, get_quapy_home, pickled_resource +from sklearn.preprocessing import StandardScaler -def _fetch_ucirepo(*args, **kwargs): +def _fetch_ucirepo(*args, **kwargs): try: - from ucimlrepo import fetch_ucirepo + from ucimlrepo import fetch_ucirepo except ImportError as exc: raise ImportError( "UCI dataset fetching requires the optional 'ucimlrepo' package. " "Install it to use fetch_UCIBinaryDataset or fetch_UCIMulticlassDataset." - ) from exc + ) from exc return fetch_ucirepo(*args, **kwargs) @@ -497,7 +498,7 @@ [docs] -def fetch_reviews(dataset_name, tfidf=False, min_df=None, data_home=None, pickle=False) -> Dataset: +def fetch_reviews(dataset_name, tfidf=False, min_df=None, data_home=None, pickle=False) -> Dataset: """ Loads a Reviews dataset as a Dataset instance, as used in `Esuli, A., Moreo, A., and Sebastiani, F. "A recurrent neural network for sentiment quantification." @@ -547,7 +548,7 @@ [docs] -def fetch_twitter(dataset_name, for_model_selection=False, min_df=None, data_home=None, pickle=False) -> Dataset: +def fetch_twitter(dataset_name, for_model_selection=False, min_df=None, data_home=None, pickle=False) -> Dataset: """ Loads a Twitter dataset as a :class:`quapy.data.base.Dataset` instance, as used in: `Gao, W., Sebastiani, F.: From classification to quantification in tweet sentiment analysis. @@ -589,8 +590,9 @@ if dataset_name in {'semeval13', 'semeval14', 'semeval15'}: trainset_name = 'semeval' testset_name = 'semeval' if for_model_selection else dataset_name - print(f"the training and development sets for datasets 'semeval13', 'semeval14', 'semeval15' are common " - f"(called 'semeval'); returning trainin-set='{trainset_name}' and test-set={testset_name}") + logging.getLogger(__name__).info( + f"the training and development sets for datasets 'semeval13', 'semeval14', 'semeval15' are common " + f"(called 'semeval'); returning trainin-set='{trainset_name}' and test-set={testset_name}") else: if dataset_name == 'semeval' and for_model_selection==False: raise ValueError('dataset "semeval" can only be used for model selection. ' @@ -624,7 +626,7 @@ [docs] -def fetch_UCIBinaryDataset(dataset_name, data_home=None, test_split=0.3, standardize=True, verbose=False) -> Dataset: +def fetch_UCIBinaryDataset(dataset_name, data_home=None, test_split=0.3, standardize=True, verbose=False) -> Dataset: """ Loads a UCI dataset as an instance of :class:`quapy.data.base.Dataset`, as used in `Pérez-Gállego, P., Quevedo, J. R., & del Coz, J. J. (2017). @@ -658,7 +660,7 @@ [docs] -def fetch_UCIBinaryLabelledCollection(dataset_name, data_home=None, standardize=True, verbose=False) -> LabelledCollection: +def fetch_UCIBinaryLabelledCollection(dataset_name, data_home=None, standardize=True, verbose=False) -> LabelledCollection: """ Loads a UCI collection as an instance of :class:`quapy.data.base.LabelledCollection`, as used in `Pérez-Gállego, P., Quevedo, J. R., & del Coz, J. J. (2017). @@ -851,7 +853,7 @@ file = join(data_home, "uci_datasets", dataset_group + ".pkl") @contextmanager - def download_tmp_file(url_group: str, filename: str): + def download_tmp_file(url_group: str, filename: str): """ Download a data file for a group of datasets temporarely. When used as a context, the file is removed once the context exits. @@ -869,7 +871,7 @@ finally: os.remove(data_path) - def download(id: int | None, group: str) -> dict: + def download(id: int | None, group: str) -> dict: """ Download the data to be pickled for a dataset group. Use the `fetch_ucirepo` api when possible. @@ -935,7 +937,7 @@ return data - def binarize_data(name, data: dict) -> LabelledCollection: + def binarize_data(name, data: dict) -> LabelledCollection: """ Filter and transform data to extract a binary dataset. @@ -980,7 +982,7 @@ [docs] -def fetch_UCIMulticlassDataset( +def fetch_UCIMulticlassDataset( dataset_name, data_home=None, min_test_split=0.3, @@ -1043,7 +1045,7 @@ [docs] -def fetch_UCIMulticlassLabelledCollection(dataset_name, data_home=None, min_class_support=100, standardize=True, verbose=False) -> LabelledCollection: +def fetch_UCIMulticlassLabelledCollection(dataset_name, data_home=None, min_class_support=100, standardize=True, verbose=False) -> LabelledCollection: """ Loads a UCI multiclass collection as an instance of :class:`quapy.data.base.LabelledCollection`. @@ -1146,7 +1148,7 @@ file = join(data_home, 'uci_multiclass', dataset_name+'.pkl') - def dummify_categorical_features(df_features, dataset_id): + def dummify_categorical_features(df_features, dataset_id): categorical_features = { 158: ["S1", "C1", "S2", "C2", "S3", "C3", "S4", "C4", "S5", "C5"], # poker_hand } @@ -1160,7 +1162,7 @@ return X - def download(id, name): + def download(id, name): df = _fetch_ucirepo(id=id) X_df = dummify_categorical_features(df.data.features, id) @@ -1173,7 +1175,7 @@ y = np.searchsorted(classes, y) return LabelledCollection(X, y) - def filter_classes(data: LabelledCollection, min_class_support): + def filter_classes(data: LabelledCollection, min_class_support): if min_class_support is None or min_class_support == 0.: return data if isinstance(min_class_support, float): @@ -1207,18 +1209,18 @@ -def _df_replace(df, col, repl={'yes': 1, 'no':0}, astype=float): +def _df_replace(df, col, repl={'yes': 1, 'no':0}, astype=float): df[col] = df[col].apply(lambda x:repl[x]).astype(astype, copy=False) -def _array_replace(arr, repl={"yes": 1, "no": 0}): +def _array_replace(arr, repl={"yes": 1, "no": 0}): for k, v in repl.items(): arr[arr == k] = v [docs] -def fetch_lequa2022(task, data_home=None): +def fetch_lequa2022(task, data_home=None): """ Loads the official datasets provided for the `LeQua 2022 <https://lequa2022.github.io/index>`_ competition. In brief, there are 4 tasks (T1A, T1B, T2A, T2B) having to do with text quantification @@ -1244,7 +1246,7 @@ that return a series of samples stored in a directory which are labelled by prevalence. """ - from quapy.data._lequa import load_raw_documents, load_vector_documents_2022, SamplesFromDir + from quapy.data._lequa import load_raw_documents, load_vector_documents_2022, SamplesFromDir assert task in LEQUA2022_TASKS, \ f'Unknown task {task}. Valid ones are {LEQUA2022_TASKS}' @@ -1258,7 +1260,7 @@ lequa_dir = join(data_home, 'lequa2022') os.makedirs(lequa_dir, exist_ok=True) - def download_unzip_and_remove(unzipped_path, url): + def download_unzip_and_remove(unzipped_path, url): tmp_path = join(lequa_dir, task + '_tmp.zip') download_file_if_not_exists(url, tmp_path) with zipfile.ZipFile(tmp_path) as file: @@ -1294,7 +1296,7 @@ [docs] -def fetch_lequa2024(task, data_home=None, merge_T3=False): +def fetch_lequa2024(task, data_home=None, merge_T3=False): """ Loads the official datasets provided for the `LeQua 2024 <https://lequa2024.github.io/index>`_ competition. LeQua 2024 defines four tasks (T1, T2, T3, T4) related to the problem of quantification; @@ -1325,7 +1327,7 @@ that return a series of samples stored in a directory which are labelled by prevalence. """ - from quapy.data._lequa import load_vector_documents_2024, SamplesFromDir, LabelledCollectionsFromDir + from quapy.data._lequa import load_vector_documents_2024, SamplesFromDir, LabelledCollectionsFromDir assert task in LEQUA2024_TASKS, \ f'Unknown task {task}. Valid ones are {LEQUA2024_TASKS}' @@ -1344,7 +1346,7 @@ lequa_dir = join(data_home, 'lequa2024') os.makedirs(lequa_dir, exist_ok=True) - def download_unzip_and_remove(unzipped_path, url): + def download_unzip_and_remove(unzipped_path, url): tmp_path = join(lequa_dir, task + '_tmp.zip') download_file_if_not_exists(url, tmp_path) with zipfile.ZipFile(tmp_path) as file: @@ -1384,7 +1386,7 @@ [docs] -def fetch_IFCB(single_sample_train=True, for_model_selection=False, data_home=None): +def fetch_IFCB(single_sample_train=True, for_model_selection=False, data_home=None): """ Loads the IFCB dataset for quantification from `Zenodo <https://zenodo.org/records/10036244>`_ (for more information on this dataset, please follow the zenodo link). @@ -1409,7 +1411,7 @@ i.e., a sampling protocol that returns a series of samples labelled by prevalence. """ - from quapy.data._ifcb import IFCBTrainSamplesFromDir, IFCBTestSamples, get_sample_list, generate_modelselection_split + from quapy.data._ifcb import IFCBTrainSamplesFromDir, IFCBTestSamples, get_sample_list, generate_modelselection_split if data_home is None: data_home = get_quapy_home() @@ -1421,7 +1423,7 @@ ifcb_dir = join(data_home, 'ifcb') os.makedirs(ifcb_dir, exist_ok=True) - def download_unzip_and_remove(unzipped_path, url): + def download_unzip_and_remove(unzipped_path, url): tmp_path = join(ifcb_dir, 'ifcb_tmp.zip') download_file_if_not_exists(url, tmp_path) with zipfile.ZipFile(tmp_path) as file: @@ -1466,7 +1468,7 @@ -def _fetch_image_embedding_splits(dataset_name, embedding, data_home=None) -> tuple[LabelledCollection,LabelledCollection,LabelledCollection]: +def _fetch_image_embedding_splits(dataset_name, embedding, data_home=None) -> tuple[LabelledCollection,LabelledCollection,LabelledCollection]: """ Loads a pre-generated embedding set (train, val, or test) of an image dataset from `Zenodo <https://zenodo.org/records/21131944>`_. @@ -1496,7 +1498,7 @@ trained_network = dataset_network[dataset_name] - def download_embedding_npz(dataset_name, trained_network, embedding): + def download_embedding_npz(dataset_name, trained_network, embedding): target_file = f'{dataset_name}_{trained_network}_{embedding}.npz' URL = f'https://zenodo.org/records/21131944/files/{target_file}' os.makedirs(join(data_home, 'image'), exist_ok=True) @@ -1512,14 +1514,12 @@ val = LabelledCollection(embedding_dict['val'], labels_dict['val'], classes=train.classes) test = LabelledCollection(embedding_dict['test'], labels_dict['test'], classes=train.classes) - print(f'{len(train)} | {len(val)} | {len(test)} | {train.X.shape[1]} | {train.n_classes} | {train.n_classes}') - return train, val, test [docs] -def fetch_image_embeddings(dataset_name, embedding, heldout_only=True, data_home=None) -> Dataset: +def fetch_image_embeddings(dataset_name, embedding, heldout_only=True, data_home=None) -> Dataset: """ Loads an image dataset with pre-generated embeddings. Available datasets include: @@ -1556,14 +1556,6 @@ -if __name__ == '__main__': - #train, val, test = _fetch_image_embedding_splits(dataset_name='mnist', embedding='logits') - #print(train) - #print(val) - #print(test) - - dataset = fetch_image_embeddings(dataset_name='svhn', embedding='features', heldout_only=True) - print(dataset) diff --git a/docs/build/html/_modules/quapy/data/reader.html b/docs/build/html/_modules/quapy/data/reader.html index 827d348..617442d 100644 --- a/docs/build/html/_modules/quapy/data/reader.html +++ b/docs/build/html/_modules/quapy/data/reader.html @@ -1,87 +1,385 @@ - - - - - - quapy.data.reader — QuaPy: A Python-based open-source framework for quantification 0.2.1 documentation - - + + + + + + + + quapy.data.reader — QuaPy: A Python-based open-source framework for quantification 0.2.1 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Skip to main content - - - - - - - - - + + + + Back to top + + + + + + + + + + Ctrl+K + + - - - - - - - - - - QuaPy: A Python-based open-source framework for quantification - - - - - - - + + - - -Quickstart - - -Manuals - - -API - - - - + + + + + + + + + + + - - - QuaPy: A Python-based open-source framework for quantification - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Home + + + + + + + Manuals + + + + + + + API + + - - - - - - Module code - quapy.data.reader - - - + + + + + + + + + + + + + Search + Ctrl+K + + + + + + + + + + + + + + System Settings + Light + Dark + + + + + + + + + + + + + + GitHub + + + + + + + + + + + + + Search + Ctrl+K + + + + + - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Home + + + + + + + Manuals + + + + + + + API + + + + + + + + + + + + + + + + + + + + + + + System Settings + Light + Dark + + + + + + + + + + + + + + GitHub + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Module code + + quapy.data.reader + + + + + + + + + + + + + + + + Source code for quapy.data.reader -import numpy as np -from scipy.sparse import dok_matrix -from tqdm import tqdm +import logging + +import numpy as np +from scipy.sparse import dok_matrix +from tqdm import tqdm [docs] -def from_text(path, encoding='utf-8', verbose=1, class2int=True): +def from_text(path, encoding='utf-8', verbose=1, class2int=True): """ Reads a labelled colletion of documents. File fomart <0 or 1>\t<document>\n @@ -108,14 +406,14 @@ all_sentences.append(sentence) all_labels.append(label) except ValueError: - print(f'format error in {line}') + logging.getLogger(__name__).warning(f'format error in {line}') return all_sentences, all_labels [docs] -def from_sparse(path): +def from_sparse(path): """ Reads a labelled collection of real-valued instances expressed in sparse format File format <-1 or 0 or 1>[\s col(int):val(float)]\n @@ -124,7 +422,7 @@ :return: a `csr_matrix` containing the instances (rows), and a ndarray containing the labels """ - def split_col_val(col_val): + def split_col_val(col_val): col, val = col_val.split(':') col, val = int(col) - 1, float(val) return col, val @@ -152,7 +450,7 @@ [docs] -def from_csv(path, encoding='utf-8'): +def from_csv(path, encoding='utf-8'): """ Reads a csv file in which columns are separated by ','. File format <label>,<feat1>,<feat2>,...,<featn>\n @@ -175,7 +473,7 @@ [docs] -def reindex_labels(y): +def reindex_labels(y): """ Re-indexes a list of labels as a list of indexes, and returns the classnames corresponding to the indexes. E.g.: @@ -198,7 +496,7 @@ [docs] -def binarize(y, pos_class): +def binarize(y, pos_class): """ Binarizes a categorical array-like collection of labels towards the positive class `pos_class`. E.g.,: @@ -218,31 +516,75 @@ - + + + + + + + + + + + + + - + + + + - Built with Sphinx using a - theme - provided by Read the Docs. - + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/build/html/_modules/quapy/error.html b/docs/build/html/_modules/quapy/error.html index 73a9a53..9b93ed4 100644 --- a/docs/build/html/_modules/quapy/error.html +++ b/docs/build/html/_modules/quapy/error.html @@ -1,89 +1,385 @@ - - - - - - quapy.error — QuaPy: A Python-based open-source framework for quantification 0.2.1 documentation - - + + + + + + + + quapy.error — QuaPy: A Python-based open-source framework for quantification 0.2.1 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Skip to main content - - - - - - - - - + + + + Back to top + + + + + + + + + + Ctrl+K + + - - - - - - - - - - QuaPy: A Python-based open-source framework for quantification - - - - - - - + + - - -Quickstart - - -Manuals - - -API - - - - + + + + + + + + + + + - - - QuaPy: A Python-based open-source framework for quantification - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Home + + + + + + + Manuals + + + + + + + API + + - - - - - - Module code - quapy.error - - - + + + + + + + + + + + + + Search + Ctrl+K + + + + + + + + + + + + + + System Settings + Light + Dark + + + + + + + + + + + + + + GitHub + + + + + + + + + + + + + Search + Ctrl+K + + + + + - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Home + + + + + + + Manuals + + + + + + + API + + + + + + + + + + + + + + + + + + + + + + + System Settings + Light + Dark + + + + + + + + + + + + + + GitHub + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Module code + + quapy.error + + + + + + + + + + + + + + + + Source code for quapy.error """Implementation of error measures used for quantification""" -import numpy as np -from sklearn.metrics import f1_score -import quapy as qp +import numpy as np +from sklearn.metrics import f1_score +import quapy as qp [docs] -def from_name(err_name): +def from_name(err_name): """Gets an error function from its name. E.g., `from_name("mae")` will return function :meth:`quapy.error.mae` @@ -98,7 +394,7 @@ [docs] -def f1e(y_true, y_pred): +def f1e(y_true, y_pred): """F1 error: simply computes the error in terms of macro :math:`F_1`, i.e., :math:`1-F_1^M`, where :math:`F_1` is the harmonic mean of precision and recall, defined as :math:`\\frac{2tp}{2tp+fp+fn}`, with `tp`, `fp`, and `fn` standing @@ -116,7 +412,7 @@ [docs] -def acce(y_true, y_pred): +def acce(y_true, y_pred): """Computes the error in terms of 1-accuracy. The accuracy is computed as :math:`\\frac{tp+tn}{tp+fp+fn+tn}`, with `tp`, `fp`, `fn`, and `tn` standing for true positives, false positives, false negatives, and true negatives, @@ -132,7 +428,7 @@ [docs] -def mae(prevs_true, prevs_hat): +def mae(prevs_true, prevs_hat): """Computes the mean absolute error (see :meth:`quapy.error.ae`) across the sample pairs. :param prevs_true: array-like of shape `(n_samples, n_classes,)` with the true prevalence values @@ -146,7 +442,7 @@ [docs] -def ae(prevs_true, prevs_hat): +def ae(prevs_true, prevs_hat): """Computes the absolute error between the two prevalence vectors. Absolute error between two prevalence vectors :math:`p` and :math:`\\hat{p}` is computed as :math:`AE(p,\\hat{p})=\\frac{1}{|\\mathcal{Y}|}\\sum_{y\\in \\mathcal{Y}}|\\hat{p}(y)-p(y)|`, @@ -165,7 +461,7 @@ [docs] -def nae(prevs_true, prevs_hat): +def nae(prevs_true, prevs_hat): """Computes the normalized absolute error between the two prevalence vectors. Normalized absolute error between two prevalence vectors :math:`p` and :math:`\\hat{p}` is computed as :math:`NAE(p,\\hat{p})=\\frac{AE(p,\\hat{p})}{z_{AE}}`, @@ -185,7 +481,7 @@ [docs] -def mnae(prevs_true, prevs_hat): +def mnae(prevs_true, prevs_hat): """Computes the mean normalized absolute error (see :meth:`quapy.error.nae`) across the sample pairs. :param prevs_true: array-like of shape `(n_samples, n_classes,)` with the true prevalence values @@ -199,7 +495,7 @@ [docs] -def mse(prevs_true, prevs_hat): +def mse(prevs_true, prevs_hat): """Computes the mean squared error (see :meth:`quapy.error.se`) across the sample pairs. :param prevs_true: array-like of shape `(n_samples, n_classes,)` with the @@ -214,7 +510,7 @@ [docs] -def se(prevs_true, prevs_hat): +def se(prevs_true, prevs_hat): """Computes the squared error between the two prevalence vectors. Squared error between two prevalence vectors :math:`p` and :math:`\\hat{p}` is computed as :math:`SE(p,\\hat{p})=\\frac{1}{|\\mathcal{Y}|}\\sum_{y\\in \\mathcal{Y}}(\\hat{p}(y)-p(y))^2`, @@ -233,7 +529,7 @@ [docs] -def sre(prevs_true, prevs_hat, prevs_train, eps=0.): +def sre(prevs_true, prevs_hat, prevs_train, eps=0.): """ Computes the squared ratio error between two prevalence vectors. The squared ratio error between prevalence vectors :math:`p` and @@ -269,7 +565,7 @@ [docs] -def msre(prevs_true, prevs_hat, prevs_train, eps=0.): +def msre(prevs_true, prevs_hat, prevs_train, eps=0.): """ Computes the mean squared ratio error (see :meth:`quapy.error.sre`) across the sample pairs. @@ -285,7 +581,7 @@ [docs] -def aitchisondist(prevs_true, prevs_hat): +def aitchisondist(prevs_true, prevs_hat): """ Computes the Aitchison distance between two prevalence vectors. The Aitchison distance between prevalence vectors :math:`p` and @@ -298,7 +594,7 @@ :param prevs_hat: array-like with the predicted prevalence values :return: Aitchison distance """ - from quapy.functional import CLRtransformation + from quapy.functional import CLRtransformation clr = CLRtransformation() return np.linalg.norm(clr(prevs_true) - clr(prevs_hat), axis=-1) @@ -307,7 +603,7 @@ [docs] -def maitchisondist(prevs_true, prevs_hat): +def maitchisondist(prevs_true, prevs_hat): """ Computes the mean Aitchison distance (see :meth:`quapy.error.aitchisondist`) across the sample pairs, i.e., @@ -324,7 +620,7 @@ [docs] -def mkld(prevs_true, prevs_hat, eps=None): +def mkld(prevs_true, prevs_hat, eps=None): """Computes the mean Kullback-Leibler divergence (see :meth:`quapy.error.kld`) across the sample pairs. The distributions are smoothed using the `eps` factor (see :meth:`quapy.error.smooth`). @@ -345,7 +641,7 @@ [docs] -def kld(prevs_true, prevs_hat, eps=None): +def kld(prevs_true, prevs_hat, eps=None): """Computes the Kullback-Leibler divergence between the two prevalence distributions. Kullback-Leibler divergence between two prevalence distributions :math:`p` and :math:`\\hat{p}` is computed as @@ -371,7 +667,7 @@ [docs] -def mnkld(prevs_true, prevs_hat, eps=None): +def mnkld(prevs_true, prevs_hat, eps=None): """Computes the mean Normalized Kullback-Leibler divergence (see :meth:`quapy.error.nkld`) across the sample pairs. The distributions are smoothed using the `eps` factor (see :meth:`quapy.error.smooth`). @@ -391,7 +687,7 @@ [docs] -def nkld(prevs_true, prevs_hat, eps=None): +def nkld(prevs_true, prevs_hat, eps=None): """Computes the Normalized Kullback-Leibler divergence between the two prevalence distributions. Normalized Kullback-Leibler divergence between two prevalence distributions :math:`p` and :math:`\\hat{p}` is computed as @@ -415,7 +711,7 @@ [docs] -def mrae(prevs_true, prevs_hat, eps=None): +def mrae(prevs_true, prevs_hat, eps=None): """Computes the mean relative absolute error (see :meth:`quapy.error.rae`) across the sample pairs. The distributions are smoothed using the `eps` factor (see :meth:`quapy.error.smooth`). @@ -436,7 +732,7 @@ [docs] -def rae(prevs_true, prevs_hat, eps=None): +def rae(prevs_true, prevs_hat, eps=None): """Computes the absolute relative error between the two prevalence vectors. Relative absolute error between two prevalence vectors :math:`p` and :math:`\\hat{p}` is computed as @@ -462,7 +758,7 @@ [docs] -def nrae(prevs_true, prevs_hat, eps=None): +def nrae(prevs_true, prevs_hat, eps=None): """Computes the normalized absolute relative error between the two prevalence vectors. Relative absolute error between two prevalence vectors :math:`p` and :math:`\\hat{p}` is computed as @@ -490,7 +786,7 @@ [docs] -def mnrae(prevs_true, prevs_hat, eps=None): +def mnrae(prevs_true, prevs_hat, eps=None): """Computes the mean normalized relative absolute error (see :meth:`quapy.error.nrae`) across the sample pairs. The distributions are smoothed using the `eps` factor (see :meth:`quapy.error.smooth`). @@ -511,7 +807,7 @@ [docs] -def nmd(prevs_true, prevs_hat): +def nmd(prevs_true, prevs_hat): """ Computes the Normalized Match Distance; which is the Normalized Distance multiplied by the factor `1/(n-1)` to guarantee the measure ranges between 0 (best prediction) and 1 (worst prediction). @@ -529,7 +825,7 @@ [docs] -def bias_binary(prevs_true, prevs_hat): +def bias_binary(prevs_true, prevs_hat): """ Computes the (positive) bias in a binary problem. The bias is simply the difference between the predicted positive value and the true positive value, so that a positive such value indicates the @@ -549,7 +845,7 @@ [docs] -def mean_bias_binary(prevs_true, prevs_hat): +def mean_bias_binary(prevs_true, prevs_hat): """ Computes the mean of the (positive) bias in a binary problem. :param prevs_true: array-like of shape `(n_classes,)` with the true prevalence values @@ -562,7 +858,7 @@ [docs] -def md(prevs_true, prevs_hat, ERROR_TOL=1E-3): +def md(prevs_true, prevs_hat, ERROR_TOL=1E-3): """ Computes the Match Distance, under the assumption that the cost in mistaking class i with class i+1 is 1 in all cases. @@ -582,7 +878,7 @@ [docs] -def smooth(prevs, eps): +def smooth(prevs, eps): """ Smooths a prevalence distribution with :math:`\\epsilon` (`eps`) as: :math:`\\underline{p}(y)=\\frac{\\epsilon+p(y)}{\\epsilon|\\mathcal{Y}|+ \\displaystyle\\sum_{y\\in \\mathcal{Y}}p(y)}` @@ -597,7 +893,7 @@ -def __check_eps(eps=None): +def __check_eps(eps=None): if eps is None: sample_size = qp.environ['SAMPLE_SIZE'] if sample_size is None: @@ -634,31 +930,75 @@ match_distance = md - + + + + + + + + + + + + + - + + + + - Built with Sphinx using a - theme - provided by Read the Docs. - + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/build/html/_modules/quapy/functional.html b/docs/build/html/_modules/quapy/functional.html index b7743b8..c275ca4 100644 --- a/docs/build/html/_modules/quapy/functional.html +++ b/docs/build/html/_modules/quapy/functional.html @@ -29,8 +29,9 @@ - - + + + @@ -41,6 +42,7 @@ + @@ -114,8 +116,14 @@ + + + + + + + - QuaPy @@ -131,7 +139,7 @@ - Quickstart + Home @@ -183,6 +191,21 @@ + + + + + + + + + + + GitHub + + + @@ -229,7 +252,7 @@ - Quickstart + Home @@ -272,6 +295,21 @@ + + + + + + + + + + + GitHub + + + @@ -332,17 +370,17 @@ Source code for quapy.functional -import warnings -from abc import ABC, abstractmethod -from collections import defaultdict -from functools import lru_cache -from typing import Literal, Union, Callable -from numpy.typing import ArrayLike +import warnings +from abc import ABC, abstractmethod +from collections import defaultdict +from functools import lru_cache +from typing import Literal, Union, Callable +from numpy.typing import ArrayLike -import scipy -import numpy as np +import scipy +import numpy as np -import quapy as qp +import quapy as qp # ------------------------------------------------------------------------------------------ @@ -351,7 +389,7 @@ [docs] -def classes_from_labels(labels): +def classes_from_labels(labels): """ Obtains a np.ndarray with the (sorted) classes :param labels: array-like with the instances' labels @@ -365,7 +403,7 @@ [docs] -def num_classes_from_labels(labels): +def num_classes_from_labels(labels): """ Obtains the number of classes from an array-like of instance's labels :param labels: array-like with the instances' labels @@ -380,7 +418,7 @@ [docs] -def counts_from_labels(labels: ArrayLike, classes: ArrayLike) -> np.ndarray: +def counts_from_labels(labels: ArrayLike, classes: ArrayLike) -> np.ndarray: """ Computes the raw count values from a vector of labels. @@ -401,7 +439,7 @@ [docs] -def prevalence_from_labels(labels: ArrayLike, classes: ArrayLike): +def prevalence_from_labels(labels: ArrayLike, classes: ArrayLike): """ Computes the prevalence values from a vector of labels. @@ -419,7 +457,7 @@ [docs] -def prevalence_from_probabilities(posteriors: ArrayLike, binarize: bool = False): +def prevalence_from_probabilities(posteriors: ArrayLike, binarize: bool = False): """ Returns a vector of prevalence values from a matrix of posterior probabilities. @@ -443,7 +481,7 @@ [docs] -def num_prevalence_combinations(n_prevpoints:int, n_classes:int, n_repeats:int=1) -> int: +def num_prevalence_combinations(n_prevpoints:int, n_classes:int, n_repeats:int=1) -> int: """ Computes the number of valid prevalence combinations in the n_classes-dimensional simplex if `n_prevpoints` equally distant prevalence values are generated and `n_repeats` repetitions are requested. @@ -472,7 +510,7 @@ [docs] -def get_nprevpoints_approximation(combinations_budget:int, n_classes:int, n_repeats:int=1) -> int: +def get_nprevpoints_approximation(combinations_budget:int, n_classes:int, n_repeats:int=1) -> int: """ Searches for the largest number of (equidistant) prevalence points to define for each of the `n_classes` classes so that the number of valid prevalence values generated as combinations of prevalence points (points in a @@ -500,7 +538,7 @@ [docs] -def as_binary_prevalence(positive_prevalence: Union[float, ArrayLike], clip_if_necessary: bool=False) -> np.ndarray: +def as_binary_prevalence(positive_prevalence: Union[float, ArrayLike], clip_if_necessary: bool=False) -> np.ndarray: """ Helper that, given a float representing the prevalence for the positive class, returns a np.ndarray of two values representing a binary distribution. @@ -522,7 +560,7 @@ [docs] -def strprev(prevalences: ArrayLike, prec: int=3) -> str: +def strprev(prevalences: ArrayLike, prec: int=3) -> str: """ Returns a string representation for a prevalence vector. E.g., @@ -539,7 +577,7 @@ [docs] -def check_prevalence_vector(prevalences: ArrayLike, raise_exception: bool=False, tolerance: float=1e-08, aggr=True): +def check_prevalence_vector(prevalences: ArrayLike, raise_exception: bool=False, tolerance: float=1e-08, aggr=True): """ Checks that `prevalences` is a valid prevalence vector, i.e., it contains values in [0,1] and the values sum up to 1. In other words, verifies that the `prevalences` vectors lies in the @@ -581,7 +619,7 @@ [docs] -def uniform_prevalence(n_classes): +def uniform_prevalence(n_classes): """ Returns a vector representing the uniform distribution for `n_classes` @@ -597,7 +635,7 @@ [docs] -def normalize_prevalence(prevalences: ArrayLike, method='l1'): +def normalize_prevalence(prevalences: ArrayLike, method='l1'): """ Normalizes a vector or matrix of prevalence values. The normalization consists of applying a L1 normalization in cases in which the prevalence values are not all-zeros, and to convert the prevalence values into `1/n_classes` in @@ -640,7 +678,7 @@ [docs] -def l1_norm(prevalences: ArrayLike) -> np.ndarray: +def l1_norm(prevalences: ArrayLike) -> np.ndarray: """ Applies L1 normalization to the `unnormalized_arr` so that it becomes a valid prevalence vector. Zero vectors are mapped onto the uniform distribution. Raises an exception if @@ -666,7 +704,7 @@ [docs] -def clip(prevalences: ArrayLike) -> np.ndarray: +def clip(prevalences: ArrayLike) -> np.ndarray: """ Clips the values in [0,1] and then applies the L1 normalization. @@ -681,7 +719,7 @@ [docs] -def projection_simplex_sort(unnormalized_arr: ArrayLike) -> np.ndarray: +def projection_simplex_sort(unnormalized_arr: ArrayLike) -> np.ndarray: """Projects a point onto the probability simplex. The code is adapted from Mathieu Blondel's BSD-licensed @@ -709,7 +747,7 @@ [docs] -def softmax(prevalences: ArrayLike) -> np.ndarray: +def softmax(prevalences: ArrayLike) -> np.ndarray: """ Applies the softmax function to all vectors even if the original vectors were valid distributions. If you want to leave valid vectors untouched, use condsoftmax instead. @@ -724,7 +762,7 @@ [docs] -def condsoftmax(prevalences: ArrayLike) -> np.ndarray: +def condsoftmax(prevalences: ArrayLike) -> np.ndarray: """ Applies the softmax function only to vectors that do not represent valid distributions. @@ -749,7 +787,7 @@ [docs] -def HellingerDistance(P: np.ndarray, Q: np.ndarray) -> float: +def HellingerDistance(P: np.ndarray, Q: np.ndarray) -> float: """ Computes the Hellingher Distance (HD) between (discretized) distributions `P` and `Q`. The HD for two discrete distributions of `k` bins is defined as: @@ -767,7 +805,7 @@ [docs] -def TopsoeDistance(P: np.ndarray, Q: np.ndarray, epsilon: float=1e-20): +def TopsoeDistance(P: np.ndarray, Q: np.ndarray, epsilon: float=1e-20): """ Topsoe distance between two (discretized) distributions `P` and `Q`. The Topsoe distance for two discrete distributions of `k` bins is defined as: @@ -786,7 +824,7 @@ [docs] -def get_divergence(divergence: Union[str, Callable]): +def get_divergence(divergence: Union[str, Callable]): """ Guarantees that the divergence received as argument is a function. That is, if this argument is already a callable, then it is returned, if it is instead a string, then tries to instantiate the corresponding @@ -815,7 +853,7 @@ [docs] -def argmin_prevalence(loss: Callable, +def argmin_prevalence(loss: Callable, n_classes: int, method: Literal["optim_minimize", "linear_search", "ternary_search"]='optim_minimize'): """ @@ -842,7 +880,7 @@ [docs] -def optim_minimize(loss: Callable, n_classes: int, return_loss=False): +def optim_minimize(loss: Callable, n_classes: int, return_loss=False): """ Searches for the optimal prevalence values, i.e., an `n_classes`-dimensional vector of the (`n_classes`-1)-simplex that yields the smallest lost. This optimization is carried out by means of a constrained search using scipy's @@ -854,7 +892,7 @@ :return: (ndarray) the best prevalence vector found or a tuple which also contains the value of the loss if return_loss=True """ - from scipy import optimize + from scipy import optimize # the initial point is set as the uniform distribution uniform_distribution = uniform_prevalence(n_classes=n_classes) @@ -873,7 +911,7 @@ [docs] -def linear_search(loss: Callable, n_classes: int): +def linear_search(loss: Callable, n_classes: int): """ Performs a linear search for the best prevalence value in binary problems. The search is carried out by exploring the range [0,1] stepping by 0.01. This search is inefficient, and is added only for completeness (some of the @@ -897,7 +935,7 @@ [docs] -def ternary_search(loss: Callable, n_classes: int): +def ternary_search(loss: Callable, n_classes: int): """ Performs a ternary search for the best prevalence value in binary problems. This search assumes the loss is unimodal over the interval [0,1]. @@ -933,7 +971,7 @@ [docs] -def prevalence_linspace(grid_points:int=21, repeats:int=1, smooth_limits_epsilon:float=0.01) -> np.ndarray: +def prevalence_linspace(grid_points:int=21, repeats:int=1, smooth_limits_epsilon:float=0.01) -> np.ndarray: """ Produces an array of uniformly separated values of prevalence. By default, produces an array of 21 prevalence values, with @@ -958,7 +996,7 @@ [docs] -def uniform_prevalence_sampling(n_classes: int, size: int=1) -> np.ndarray: +def uniform_prevalence_sampling(n_classes: int, size: int=1) -> np.ndarray: """ Implements the `Kraemer algorithm <http://www.cs.cmu.edu/~nasmith/papers/smith+tromble.tr04.pdf>`_ for sampling uniformly at random from the unit simplex. This implementation is adapted from this @@ -994,7 +1032,7 @@ [docs] -def solve_adjustment_binary(prevalence_estim: ArrayLike, tpr: float, fpr: float, clip: bool=True): +def solve_adjustment_binary(prevalence_estim: ArrayLike, tpr: float, fpr: float, clip: bool=True): """ Implements the adjustment of ACC and PACC for the binary case. The adjustment for a prevalence estimate of the positive class `p` comes down to computing: @@ -1021,7 +1059,7 @@ [docs] -def solve_adjustment( +def solve_adjustment( class_conditional_rates: np.ndarray, unadjusted_counts: np.ndarray, method: Literal["inversion", "invariant-ratio"], @@ -1067,14 +1105,18 @@ if method == "inversion": pass # We leave A and B unchanged elif method == "invariant-ratio": - # Change the last equation to replace it with the normalization condition + # Change the last equation to replace it with the normalization condition; + # copy first so this does not mutate the caller's arrays (np.asarray above + # returns the same object, not a copy, when the input is already float64) + A = A.copy() + B = B.copy() A[-1, :] = 1.0 B[-1] = 1.0 else: raise ValueError(f"unknown {method=}") if solver == "minimize": - def loss(prev): + def loss(prev): return np.linalg.norm(A @ prev - B) return optim_minimize(loss, n_classes=A.shape[0]) elif solver in ["exact-raise", "exact-cc"]: @@ -1102,7 +1144,7 @@ [docs] -class CompositionalTransformation(ABC): +class CompositionalTransformation(ABC): """ Abstract class of transformations for compositional data. """ @@ -1110,13 +1152,13 @@ EPSILON = 1e-12 @abstractmethod - def __call__(self, X): + def __call__(self, X): ... [docs] @abstractmethod - def inverse(self, Z): + def inverse(self, Z): ... @@ -1124,12 +1166,12 @@ [docs] -class CLRtransformation(CompositionalTransformation): +class CLRtransformation(CompositionalTransformation): """ Centered log-ratio (CLR) transformation. """ - def __call__(self, X): + def __call__(self, X): X = np.asarray(X) X = qp.error.smooth(X, self.EPSILON) geometric_mean = np.exp(np.mean(np.log(X), axis=-1, keepdims=True)) @@ -1137,7 +1179,7 @@ [docs] - def inverse(self, Z): + def inverse(self, Z): return scipy.special.softmax(Z, axis=-1) @@ -1145,12 +1187,12 @@ [docs] -class ILRtransformation(CompositionalTransformation): +class ILRtransformation(CompositionalTransformation): """ Isometric log-ratio (ILR) transformation. """ - def __call__(self, X): + def __call__(self, X): X = np.asarray(X) X = qp.error.smooth(X, self.EPSILON) basis = self.get_V(X.shape[-1]) @@ -1158,7 +1200,7 @@ [docs] - def inverse(self, Z): + def inverse(self, Z): Z = np.asarray(Z) basis = self.get_V(Z.shape[-1] + 1) logp = Z @ basis @@ -1169,7 +1211,7 @@ [docs] @lru_cache(maxsize=None) - def get_V(self, k): + def get_V(self, k): helmert = np.zeros((k, k)) for i in range(1, k): helmert[i, :i] = 1 @@ -1182,7 +1224,7 @@ [docs] -def normalized_entropy(p): +def normalized_entropy(p): """ Computes the normalized Shannon entropy of a prevalence vector. @@ -1198,7 +1240,7 @@ [docs] -def antagonistic_prevalence(p, strength=1): +def antagonistic_prevalence(p, strength=1): """ Reflects a prevalence vector in ILR space and maps it back to the simplex. @@ -1215,7 +1257,7 @@ [docs] -def in_simplex(x, atol=1e-8): +def in_simplex(x, atol=1e-8): """ Checks whether points lie in the probability simplex. diff --git a/docs/build/html/_modules/quapy/method/_kdey.html b/docs/build/html/_modules/quapy/method/_kdey.html index 6664ea9..3367861 100644 --- a/docs/build/html/_modules/quapy/method/_kdey.html +++ b/docs/build/html/_modules/quapy/method/_kdey.html @@ -29,8 +29,9 @@ - - + + + @@ -41,6 +42,7 @@ + @@ -114,8 +116,14 @@ + + + + + + + - QuaPy @@ -131,7 +139,7 @@ - Quickstart + Home @@ -183,6 +191,21 @@ + + + + + + + + + + + GitHub + + + @@ -229,7 +252,7 @@ - Quickstart + Home @@ -272,6 +295,21 @@ + + + + + + + + + + + GitHub + + + @@ -332,22 +370,22 @@ Source code for quapy.method._kdey -import numpy as np -from numbers import Real -from sklearn.base import BaseEstimator -from sklearn.neighbors import KernelDensity +import numpy as np +from numbers import Real +from sklearn.base import BaseEstimator +from sklearn.neighbors import KernelDensity -import quapy as qp -from quapy.method._helper import _labels_to_indices -from quapy.method.aggregative import AggregativeSoftQuantifier -import quapy.functional as F -from scipy.special import logsumexp -from sklearn.metrics.pairwise import rbf_kernel +import quapy as qp +from quapy.method._helper import _labels_to_indices +from quapy.method.aggregative import AggregativeSoftQuantifier +import quapy.functional as F +from scipy.special import logsumexp +from sklearn.metrics.pairwise import rbf_kernel [docs] -class KDEBase: +class KDEBase: """ Common ancestor for KDE-based methods. Implements some common routines. """ @@ -356,7 +394,7 @@ KERNELS = ['gaussian', 'aitchison', 'ilr'] @classmethod - def _check_bandwidth(cls, bandwidth, kernel): + def _check_bandwidth(cls, bandwidth, kernel): """ Checks that the bandwidth parameter is correct @@ -370,13 +408,13 @@ return bandwidth @classmethod - def _check_kernel(cls, kernel): + def _check_kernel(cls, kernel): assert kernel in KDEBase.KERNELS, f'unknown {kernel=}' return kernel [docs] - def get_kde_function(self, X, bandwidth, kernel): + def get_kde_function(self, X, bandwidth, kernel): """ Wraps the KDE function from scikit-learn. @@ -392,7 +430,7 @@ [docs] - def pdf(self, kde, X, kernel, log_densities=False): + def pdf(self, kde, X, kernel, log_densities=False): """ Wraps the density evalution of scikit-learn's KDE. Scikit-learn returns log-scores (s), so this function returns :math:`e^{s}` @@ -411,7 +449,7 @@ [docs] - def get_mixture_components(self, X, y, classes, bandwidth, kernel): + def get_mixture_components(self, X, y, classes, bandwidth, kernel): """ Returns an array containing the mixture components, i.e., the KDE functions for each class. @@ -433,7 +471,7 @@ [docs] - def transform_posteriors(self, X, kernel): + def transform_posteriors(self, X, kernel): if kernel in {'aitchison', 'ilr'}: X = self.shrink_posteriors(X) if kernel == 'aitchison': @@ -445,7 +483,7 @@ [docs] - def shrink_posteriors(self, X): + def shrink_posteriors(self, X): shrinkage = getattr(self, 'shrinkage', 0.0) if shrinkage <= 0: return X @@ -457,7 +495,7 @@ [docs] - def effective_bandwidth(self, bandwidth, kernel): + def effective_bandwidth(self, bandwidth, kernel): shrinkage = getattr(self, 'shrinkage', 0.0) if shrinkage > 0 and kernel in {'aitchison', 'ilr'} and isinstance(bandwidth, Real): return (1.0 - shrinkage) * float(bandwidth) @@ -466,7 +504,7 @@ [docs] - def clr_transform(self, X): + def clr_transform(self, X): if not hasattr(self, 'clr'): self.clr = F.CLRtransformation() return self.clr(X) @@ -474,7 +512,7 @@ [docs] - def ilr_transform(self, X): + def ilr_transform(self, X): if not hasattr(self, 'ilr'): self.ilr = F.ILRtransformation() return self.ilr(X) @@ -484,7 +522,7 @@ [docs] -class KDEyML(AggregativeSoftQuantifier, KDEBase): +class KDEyML(AggregativeSoftQuantifier, KDEBase): """ Kernel Density Estimation model for quantification (KDEy) relying on the Kullback-Leibler divergence (KLD) as the divergence measure to be minimized. This method was first proposed in the paper @@ -526,7 +564,7 @@ :param random_state: a seed to be set before fitting any base quantifier (default None) """ - def __init__(self, classifier: BaseEstimator=None, fit_classifier=True, val_split=5, bandwidth=0.1, + def __init__(self, classifier: BaseEstimator=None, fit_classifier=True, val_split=5, bandwidth=0.1, kernel='gaussian', shrinkage=0.0, random_state=None): super().__init__(classifier, fit_classifier, val_split) self.bandwidth = KDEBase._check_bandwidth(bandwidth, kernel) @@ -539,7 +577,7 @@ [docs] - def aggregation_fit(self, classif_predictions, labels): + def aggregation_fit(self, classif_predictions, labels): self.mix_densities = self.get_mixture_components( classif_predictions, labels, @@ -552,7 +590,7 @@ [docs] - def aggregate(self, posteriors: np.ndarray): + def aggregate(self, posteriors: np.ndarray): """ Searches for the mixture model parameter (the sought prevalence values) that maximizes the likelihood of the data (i.e., that minimizes the negative log-likelihood) @@ -569,14 +607,14 @@ for kde_i in self.mix_densities ] - def neg_loglikelihood(prev): + def neg_loglikelihood(prev): prev = qp.error.smooth(prev, eps=epsilon) test_loglikelihood = logsumexp(np.log(prev)[:, None] + test_log_densities, axis=0) return -np.sum(test_loglikelihood) else: test_densities = [self.pdf(kde_i, posteriors, self.kernel) for kde_i in self.mix_densities] - def neg_loglikelihood(prev): + def neg_loglikelihood(prev): test_mixture_likelihood = prev @ test_densities test_loglikelihood = np.log(test_mixture_likelihood + epsilon) return -np.sum(test_loglikelihood) @@ -588,7 +626,7 @@ [docs] -class KDEyHD(AggregativeSoftQuantifier, KDEBase): +class KDEyHD(AggregativeSoftQuantifier, KDEBase): """ Kernel Density Estimation model for quantification (KDEy) relying on the squared Hellinger Disntace (HD) as the divergence measure to be minimized. This method was first proposed in the paper @@ -633,7 +671,7 @@ :param montecarlo_trials: number of Monte Carlo trials (default 10000) """ - def __init__(self, classifier: BaseEstimator=None, fit_classifier=True, val_split=5, divergence: str='HD', + def __init__(self, classifier: BaseEstimator=None, fit_classifier=True, val_split=5, divergence: str='HD', bandwidth=0.1, random_state=None, montecarlo_trials=10000): super().__init__(classifier, fit_classifier, val_split) @@ -644,7 +682,7 @@ [docs] - def aggregation_fit(self, classif_predictions, labels): + def aggregation_fit(self, classif_predictions, labels): self.mix_densities = self.get_mixture_components( classif_predictions, labels, self.classes_, self.bandwidth, 'gaussian' ) @@ -663,7 +701,7 @@ [docs] - def aggregate(self, posteriors: np.ndarray): + def aggregate(self, posteriors: np.ndarray): # we retain all n*N examples (sampled from a mixture with uniform parameter), and then # apply importance sampling (IS). In this version we compute D(p_alpha||q) with IS n_classes = len(self.mix_densities) @@ -671,7 +709,7 @@ test_kde = self.get_kde_function(posteriors, self.bandwidth, 'gaussian') test_densities = self.pdf(test_kde, self.reference_samples, 'gaussian') - def f_squared_hellinger(u): + def f_squared_hellinger(u): return (np.sqrt(u)-1)**2 # todo: this will fail when self.divergence is a callable, and is not the right place to do it anyway @@ -687,7 +725,7 @@ p_class = self.reference_classwise_densities + epsilon fracs = p_class/qs - def divergence(prev): + def divergence(prev): # ps / qs = (prev @ p_class) / qs = prev @ (p_class / qs) = prev @ fracs ps_div_qs = prev @ fracs return np.mean( f(ps_div_qs) * iw ) @@ -699,7 +737,7 @@ [docs] -class KDEyCS(AggregativeSoftQuantifier): +class KDEyCS(AggregativeSoftQuantifier): """ Kernel Density Estimation model for quantification (KDEy) relying on the Cauchy-Schwarz divergence (CS) as the divergence measure to be minimized. This method was first proposed in the paper @@ -736,13 +774,13 @@ :param bandwidth: float, the bandwidth of the Kernel """ - def __init__(self, classifier: BaseEstimator=None, fit_classifier=True, val_split=5, bandwidth=0.1): + def __init__(self, classifier: BaseEstimator=None, fit_classifier=True, val_split=5, bandwidth=0.1): super().__init__(classifier, fit_classifier, val_split) self.bandwidth = KDEBase._check_bandwidth(bandwidth, kernel='gaussian') [docs] - def gram_matrix_mix_sum(self, X, Y=None): + def gram_matrix_mix_sum(self, X, Y=None): # this adapts the output of the rbf_kernel function (pairwise evaluations of Gaussian kernels k(x,y)) # to contain pairwise evaluations of N(x|mu,Sigma1+Sigma2) with mu=y and Sigma1 and Sigma2 are # two "scalar matrices" (h^2)*I each, so Sigma1+Sigma2 has scalar 2(h^2) (h is the bandwidth) @@ -757,7 +795,7 @@ [docs] - def aggregation_fit(self, classif_predictions, labels): + def aggregation_fit(self, classif_predictions, labels): P, y = classif_predictions, labels n = len(self.classes_) @@ -789,7 +827,7 @@ [docs] - def aggregate(self, posteriors: np.ndarray): + def aggregate(self, posteriors: np.ndarray): Ptr = self.Ptr Pte = posteriors y = self.ytr @@ -809,7 +847,7 @@ for i in range(n): tr_te_sums[i] = self.gram_matrix_mix_sum(Ptr[y==i], Pte) - def divergence(alpha): + def divergence(alpha): # called \overline{r} in the paper alpha_ratio = alpha * self.counts_inv diff --git a/docs/build/html/_modules/quapy/method/_neural.html b/docs/build/html/_modules/quapy/method/_neural.html index 3e6c7c3..3c8215e 100644 --- a/docs/build/html/_modules/quapy/method/_neural.html +++ b/docs/build/html/_modules/quapy/method/_neural.html @@ -29,7 +29,7 @@ - + @@ -370,23 +370,24 @@ Source code for quapy.method._neural -import os -from pathlib import Path -import random +import logging +import os +from pathlib import Path +import random -import torch -from torch.nn import MSELoss -from torch.nn.functional import relu +import torch +from torch.nn import MSELoss +from torch.nn.functional import relu -from quapy.protocol import UPP -from quapy.method.aggregative import * -from quapy.util import EarlyStop -from tqdm import tqdm +from quapy.protocol import UPP +from quapy.method.aggregative import * +from quapy.util import EarlyStop +from tqdm import tqdm [docs] -class QuaNetTrainer(BaseQuantifier): +class QuaNetTrainer(BaseQuantifier): """ Implementation of `QuaNet <https://dl.acm.org/doi/abs/10.1145/3269206.3269287>`_, a neural network for quantification. This implementation uses `PyTorch <https://pytorch.org/>`_ and can take advantage of GPU @@ -438,7 +439,7 @@ :param device: string, indicate "cpu" or "cuda" """ - def __init__(self, + def __init__(self, classifier, fit_classifier=True, sample_size=None, @@ -491,7 +492,7 @@ [docs] - def fit(self, X, y): + def fit(self, X, y): """ Trains QuaNet. @@ -549,7 +550,7 @@ order_by=0 if data.binary else None, **self.quanet_params ).to(self.device) - print(self.quanet) + logging.getLogger(__name__).debug(self.quanet) self.optim = torch.optim.Adam(self.quanet.parameters(), lr=self.lr) early_stop = EarlyStop(self.patience, lower_is_better=True) @@ -564,15 +565,16 @@ if early_stop.IMPROVED: torch.save(self.quanet.state_dict(), checkpoint) elif early_stop.STOP: - print(f'training ended by patience exhausted; loading best model parameters in {checkpoint} ' - f'for epoch {early_stop.best_epoch}') + logging.getLogger(__name__).info( + f'training ended by patience exhausted; loading best model parameters in {checkpoint} ' + f'for epoch {early_stop.best_epoch}') self.quanet.load_state_dict(torch.load(checkpoint)) break return self - def _get_aggregative_estims(self, posteriors): + def _get_aggregative_estims(self, posteriors): label_predictions = np.argmax(posteriors, axis=-1) prevs_estim = [] for quantifier in self.quantifiers.values(): @@ -585,7 +587,7 @@ [docs] - def predict(self, X): + def predict(self, X): posteriors = self.classifier.predict_proba(X) embeddings = self.classifier.transform(X) quant_estims = self._get_aggregative_estims(posteriors) @@ -598,7 +600,7 @@ return prevalence - def _epoch(self, data: LabelledCollection, posteriors, iterations, epoch, early_stop, train): + def _epoch(self, data: LabelledCollection, posteriors, iterations, epoch, early_stop, train): mse_loss = MSELoss() self.quanet.train(mode=train) @@ -650,7 +652,7 @@ [docs] - def get_params(self, deep=True): + def get_params(self, deep=True): classifier_params = self.classifier.get_params() classifier_params = {'classifier__'+k:v for k,v in classifier_params.items()} return {**classifier_params, **self.quanet_params} @@ -658,7 +660,7 @@ [docs] - def set_params(self, **parameters): + def set_params(self, **parameters): learner_params = {} for key, val in parameters.items(): if key in self.quanet_params: @@ -670,7 +672,7 @@ self.classifier.set_params(**learner_params) - def __check_params_colision(self, quanet_params, learner_params): + def __check_params_colision(self, quanet_params, learner_params): quanet_keys = set(quanet_params.keys()) learner_keys = set(learner_params.keys()) intersection = quanet_keys.intersection(learner_keys) @@ -680,7 +682,7 @@ [docs] - def clean_checkpoint(self): + def clean_checkpoint(self): """ Removes the checkpoint """ @@ -689,23 +691,23 @@ [docs] - def clean_checkpoint_dir(self): + def clean_checkpoint_dir(self): """ Removes anything contained in the checkpoint directory """ - import shutil + import shutil shutil.rmtree(self.checkpointdir, ignore_errors=True) @property - def classes_(self): + def classes_(self): return self._classes_ [docs] -def mae_loss(output, target): +def mae_loss(output, target): """ Torch-like wrapper for the Mean Absolute Error @@ -719,7 +721,7 @@ [docs] -class QuaNetModule(torch.nn.Module): +class QuaNetModule(torch.nn.Module): """ Implements the `QuaNet <https://dl.acm.org/doi/abs/10.1145/3269206.3269287>`_ forward pass. See :class:`QuaNetTrainer` for training QuaNet. @@ -736,7 +738,7 @@ :param order_by: integer, class for which the document embeddings are to be sorted """ - def __init__(self, + def __init__(self, doc_embedding_size, n_classes, stats_size, @@ -771,10 +773,10 @@ self.output = torch.nn.Linear(prev_size, n_classes) @property - def device(self): + def device(self): return torch.device('cuda') if next(self.parameters()).is_cuda else torch.device('cpu') - def _init_hidden(self): + def _init_hidden(self): directions = 2 if self.bidirectional else 1 var_hidden = torch.zeros(self.nlayers * directions, 1, self.hidden_size) var_cell = torch.zeros(self.nlayers * directions, 1, self.hidden_size) @@ -784,7 +786,7 @@ [docs] - def forward(self, doc_embeddings, doc_posteriors, statistics): + def forward(self, doc_embeddings, doc_posteriors, statistics): device = self.device doc_embeddings = torch.as_tensor(doc_embeddings, dtype=torch.float, device=device) doc_posteriors = torch.as_tensor(doc_posteriors, dtype=torch.float, device=device) diff --git a/docs/build/html/_modules/quapy/method/_threshold_optim.html b/docs/build/html/_modules/quapy/method/_threshold_optim.html index 6bb8003..304e087 100644 --- a/docs/build/html/_modules/quapy/method/_threshold_optim.html +++ b/docs/build/html/_modules/quapy/method/_threshold_optim.html @@ -1,92 +1,388 @@ - - - - - - quapy.method._threshold_optim — QuaPy: A Python-based open-source framework for quantification 0.2.1 documentation - - + + + + + + + + quapy.method._threshold_optim — QuaPy: A Python-based open-source framework for quantification 0.2.1 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Skip to main content - - - - - - - - - + + + + Back to top + + + + + + + + + + Ctrl+K + + - - - - - - - - - - QuaPy: A Python-based open-source framework for quantification - - - - - - - + + - - -Quickstart - - -Manuals - - -API - - - - + + + + + + + + + + + - - - QuaPy: A Python-based open-source framework for quantification - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Home + + + + + + + Manuals + + + + + + + API + + - - - - - - Module code - quapy.method._threshold_optim - - - - - - - - Source code for quapy.method._threshold_optim -from abc import abstractmethod + + + + + + + + + -import numpy as np -from sklearn.base import BaseEstimator -import quapy as qp -import quapy.functional as F -from quapy.data import LabelledCollection -from quapy.method.aggregative import BinaryAggregativeQuantifier + + + Search + Ctrl+K + + + + + + + + + + + + + + System Settings + Light + Dark + + + + + + + + + + + + + + GitHub + + + + + + + + + + + + + Search + Ctrl+K + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Home + + + + + + + Manuals + + + + + + + API + + + + + + + + + + + + + + + + + + + + + + + System Settings + Light + Dark + + + + + + + + + + + + + + GitHub + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Module code + + quapy.method._threshold_optim + + + + + + + + + + + + + + + + + Source code for quapy.method._threshold_optim +from abc import abstractmethod + +import numpy as np +from sklearn.base import BaseEstimator +import quapy as qp +import quapy.functional as F +from quapy.data import LabelledCollection +from quapy.method.aggregative import BinaryAggregativeQuantifier [docs] -class ThresholdOptimization(BinaryAggregativeQuantifier): +class ThresholdOptimization(BinaryAggregativeQuantifier): """ Abstract class of Threshold Optimization variants for :class:`ACC` as proposed by `Forman 2006 <https://dl.acm.org/doi/abs/10.1145/1150402.1150423>`_ and @@ -111,14 +407,14 @@ :param n_jobs: number of parallel workers """ - def __init__(self, classifier: BaseEstimator=None, fit_classifier=True, val_split=None, n_jobs=None): + def __init__(self, classifier: BaseEstimator=None, fit_classifier=True, val_split=None, n_jobs=None): super().__init__(classifier, fit_classifier, val_split) self.n_jobs = qp._get_njobs(n_jobs) [docs] @abstractmethod - def condition(self, tpr, fpr) -> float: + def condition(self, tpr, fpr) -> float: """ Implements the criterion according to which the threshold should be selected. This function should return the (float) score to be minimized. @@ -132,7 +428,7 @@ [docs] - def discard(self, tpr, fpr) -> bool: + def discard(self, tpr, fpr) -> bool: """ Indicates whether a combination of tpr and fpr should be discarded @@ -144,7 +440,7 @@ - def _eval_candidate_thresholds(self, decision_scores, y): + def _eval_candidate_thresholds(self, decision_scores, y): """ Seeks for the best `tpr` and `fpr` according to the score obtained at different decision thresholds. The scoring function is implemented in function `_condition`. @@ -181,7 +477,7 @@ [docs] - def aggregate_with_threshold(self, classif_predictions, tprs, fprs, thresholds): + def aggregate_with_threshold(self, classif_predictions, tprs, fprs, thresholds): # This function performs the adjusted count for given tpr, fpr, and threshold. # Note that, due to broadcasting, tprs, fprs, and thresholds could be arrays of length > 1 prevs_estims = np.mean(classif_predictions[:, None] >= thresholds, axis=0) @@ -190,26 +486,26 @@ return prevs_estims.squeeze() - def _compute_table(self, y, y_): + def _compute_table(self, y, y_): TP = np.logical_and(y == y_, y == self.pos_label).sum() FP = np.logical_and(y != y_, y == self.neg_label).sum() FN = np.logical_and(y != y_, y == self.pos_label).sum() TN = np.logical_and(y == y_, y == self.neg_label).sum() return TP, FP, FN, TN - def _compute_tpr(self, TP, FP): - if TP + FP == 0: + def _compute_tpr(self, TP, FN): + if TP + FN == 0: return 1 - return TP / (TP + FP) + return TP / (TP + FN) - def _compute_fpr(self, FP, TN): + def _compute_fpr(self, FP, TN): if FP + TN == 0: return 0 return FP / (FP + TN) [docs] - def aggregation_fit(self, classif_predictions, labels): + def aggregation_fit(self, classif_predictions, labels): decision_scores, y = classif_predictions, labels # the standard behavior is to keep the best threshold only self.tpr, self.fpr, self.threshold = self._eval_candidate_thresholds(decision_scores, y)[0] @@ -218,7 +514,7 @@ [docs] - def aggregate(self, classif_predictions: np.ndarray): + def aggregate(self, classif_predictions: np.ndarray): # the standard behavior is to compute the adjusted count using the best threshold found return self.aggregate_with_threshold(classif_predictions, self.tpr, self.fpr, self.threshold) @@ -227,7 +523,7 @@ [docs] -class T50(ThresholdOptimization): +class T50(ThresholdOptimization): """ Threshold Optimization variant for :class:`ACC` as proposed by `Forman 2006 <https://dl.acm.org/doi/abs/10.1145/1150402.1150423>`_ and @@ -249,12 +545,12 @@ """ - def __init__(self, classifier: BaseEstimator=None, fit_classifier=True, val_split=5): + def __init__(self, classifier: BaseEstimator=None, fit_classifier=True, val_split=5): super().__init__(classifier, fit_classifier, val_split) [docs] - def condition(self, tpr, fpr) -> float: + def condition(self, tpr, fpr) -> float: return abs(tpr - 0.5) @@ -262,7 +558,7 @@ [docs] -class MAX(ThresholdOptimization): +class MAX(ThresholdOptimization): """ Threshold Optimization variant for :class:`ACC` as proposed by `Forman 2006 <https://dl.acm.org/doi/abs/10.1145/1150402.1150423>`_ and @@ -282,12 +578,12 @@ """ - def __init__(self, classifier: BaseEstimator=None, fit_classifier=True, val_split=5): + def __init__(self, classifier: BaseEstimator=None, fit_classifier=True, val_split=5): super().__init__(classifier, fit_classifier, val_split) [docs] - def condition(self, tpr, fpr) -> float: + def condition(self, tpr, fpr) -> float: # MAX strives to maximize (tpr - fpr), which is equivalent to minimize (fpr - tpr) return (fpr - tpr) @@ -296,7 +592,7 @@ [docs] -class X(ThresholdOptimization): +class X(ThresholdOptimization): """ Threshold Optimization variant for :class:`ACC` as proposed by `Forman 2006 <https://dl.acm.org/doi/abs/10.1145/1150402.1150423>`_ and @@ -316,12 +612,12 @@ """ - def __init__(self, classifier: BaseEstimator=None, fit_classifier=True, val_split=5): + def __init__(self, classifier: BaseEstimator=None, fit_classifier=True, val_split=5): super().__init__(classifier, fit_classifier, val_split) [docs] - def condition(self, tpr, fpr) -> float: + def condition(self, tpr, fpr) -> float: return abs(1 - (tpr + fpr)) @@ -329,7 +625,7 @@ [docs] -class MS(ThresholdOptimization): +class MS(ThresholdOptimization): """ Median Sweep. Threshold Optimization variant for :class:`ACC` as proposed by `Forman 2006 <https://dl.acm.org/doi/abs/10.1145/1150402.1150423>`_ and @@ -348,18 +644,18 @@ for `k`); or as a tuple (X,y) defining the specific set of data to use for validation. """ - def __init__(self, classifier: BaseEstimator=None, fit_classifier=True, val_split=5): + def __init__(self, classifier: BaseEstimator=None, fit_classifier=True, val_split=5): super().__init__(classifier, fit_classifier, val_split) [docs] - def condition(self, tpr, fpr) -> float: + def condition(self, tpr, fpr) -> float: return 1 [docs] - def aggregation_fit(self, classif_predictions, labels): + def aggregation_fit(self, classif_predictions, labels): decision_scores, y = classif_predictions, labels # keeps all candidates tprs_fprs_thresholds = self._eval_candidate_thresholds(decision_scores, y) @@ -371,7 +667,7 @@ [docs] - def aggregate(self, classif_predictions: np.ndarray): + def aggregate(self, classif_predictions: np.ndarray): prevalences = self.aggregate_with_threshold(classif_predictions, self.tprs, self.fprs, self.thresholds) if prevalences.ndim==2: prevalences = np.median(prevalences, axis=0) @@ -382,7 +678,7 @@ [docs] -class MS2(MS): +class MS2(MS): """ Median Sweep 2. Threshold Optimization variant for :class:`ACC` as proposed by `Forman 2006 <https://dl.acm.org/doi/abs/10.1145/1150402.1150423>`_ and @@ -402,42 +698,86 @@ for `k`); or as a tuple (X,y) defining the specific set of data to use for validation. """ - def __init__(self, classifier: BaseEstimator=None, fit_classifier=True, val_split=5): + def __init__(self, classifier: BaseEstimator=None, fit_classifier=True, val_split=5): super().__init__(classifier, fit_classifier, val_split) [docs] - def discard(self, tpr, fpr) -> bool: + def discard(self, tpr, fpr) -> bool: return (tpr-fpr) <= 0.25 - + + + + + + + + + + + + + - + + + + - Built with Sphinx using a - theme - provided by Read the Docs. - + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/build/html/_modules/quapy/method/aggregative.html b/docs/build/html/_modules/quapy/method/aggregative.html index a6b98d4..0680ac3 100644 --- a/docs/build/html/_modules/quapy/method/aggregative.html +++ b/docs/build/html/_modules/quapy/method/aggregative.html @@ -29,8 +29,9 @@ - - + + + @@ -41,6 +42,7 @@ + @@ -114,8 +116,14 @@ + + + + + + + - QuaPy @@ -131,7 +139,7 @@ - Quickstart + Home @@ -183,6 +191,21 @@ + + + + + + + + + + + GitHub + + + @@ -229,7 +252,7 @@ - Quickstart + Home @@ -272,6 +295,21 @@ + + + + + + + + + + + GitHub + + + @@ -332,25 +370,25 @@ Source code for quapy.method.aggregative -from abc import ABC, abstractmethod -from argparse import ArgumentError -from copy import deepcopy -from typing import Callable, Literal, Union -import numpy as np -from sklearn.base import BaseEstimator -from sklearn.calibration import CalibratedClassifierCV -from sklearn.exceptions import NotFittedError -from sklearn.metrics import confusion_matrix -from sklearn.model_selection import cross_val_predict, train_test_split -from sklearn.utils.validation import check_is_fitted +import warnings +from abc import ABC, abstractmethod +from copy import deepcopy +from typing import Callable, Literal, Union +import numpy as np +from sklearn.base import BaseEstimator +from sklearn.calibration import CalibratedClassifierCV +from sklearn.exceptions import NotFittedError +from sklearn.metrics import confusion_matrix +from sklearn.model_selection import cross_val_predict, train_test_split +from sklearn.utils.validation import check_is_fitted -import quapy as qp -import quapy.functional as F -from quapy.functional import get_divergence -from quapy.classification.svmperf import SVMperf -from quapy.data import LabelledCollection -from quapy.method.base import BaseQuantifier, BinaryQuantifier, OneVsAllGeneric -from quapy.method._helper import ( +import quapy as qp +import quapy.functional as F +from quapy.functional import get_divergence +from quapy.classification.svmperf import SVMperf +from quapy.data import LabelledCollection +from quapy.method.base import BaseQuantifier, BinaryQuantifier, OneVsAllGeneric +from quapy.method._helper import ( _get_abstention_calibrators, _get_cvxpy, _rlls_check_mode, @@ -368,7 +406,7 @@ [docs] -class AggregativeQuantifier(BaseQuantifier, ABC): +class AggregativeQuantifier(BaseQuantifier, ABC): """ Abstract class for quantification methods that base their estimations on the aggregation of classification results. Aggregative quantifiers implement a pipeline that consists of generating classification predictions @@ -396,7 +434,7 @@ the training data be wasted. """ - def __init__(self, + def __init__(self, classifier: Union[None,BaseEstimator], fit_classifier:bool=True, val_split:Union[int,float,tuple,None]=5): @@ -418,9 +456,9 @@ (f'when {val_split=} is indicated as an integer, it represents the number of folds in a kFCV ' f'and must thus be >1') if val_split==5 and not fit_classifier: - print(f'Warning: {val_split=} will be ignored when the classifier is already trained ' - f'({fit_classifier=}). Parameter {self.val_split=} will be set to None. Set {val_split=} ' - f'to None to avoid this warning.') + warnings.warn(f'{val_split=} will be ignored when the classifier is already trained ' + f'({fit_classifier=}). Parameter {self.val_split=} will be set to None. Set {val_split=} ' + f'to None to avoid this warning.') self.val_split=None if val_split!=5: assert fit_classifier, (f'Parameter {val_split=} has been modified, but {fit_classifier=} ' @@ -457,7 +495,7 @@ assert fitted, (f'{fit_classifier=} requires the classifier to be already trained, ' f'but this does not seem to be') - def _check_init_parameters(self): + def _check_init_parameters(self): """ Implements any check to be performed in the parameters of the init method before undertaking the training of the quantifier. This is made as to allow for a quick execution stop when the @@ -467,7 +505,7 @@ """ pass - def _check_non_empty_classes(self, y): + def _check_non_empty_classes(self, y): """ Asserts all classes have positive instances. @@ -484,7 +522,7 @@ [docs] - def fit(self, X, y): + def fit(self, X, y): """ Trains the aggregative quantifier. This comes down to training a classifier (if requested) and an aggregation function. @@ -501,7 +539,7 @@ [docs] - def classifier_fit_predict(self, X, y): + def classifier_fit_predict(self, X, y): """ Trains the classifier if requested (`fit_classifier=True`) and generate the necessary predictions to train the aggregation function. @@ -551,7 +589,7 @@ [docs] @abstractmethod - def aggregation_fit(self, classif_predictions, labels): + def aggregation_fit(self, classif_predictions, labels): """ Trains the aggregation function. @@ -563,7 +601,7 @@ @property - def classifier(self): + def classifier(self): """ Gives access to the classifier @@ -572,7 +610,7 @@ return self.classifier_ @classifier.setter - def classifier(self, classifier): + def classifier(self, classifier): """ Setter for the classifier @@ -582,7 +620,7 @@ [docs] - def classify(self, X): + def classify(self, X): """ Provides the label predictions for the given instances. The predictions should respect the format expected by :meth:`aggregate`, e.g., posterior probabilities for probabilistic quantifiers, or crisp predictions for @@ -594,7 +632,7 @@ return getattr(self.classifier, self._classifier_method())(X) - def _classifier_method(self): + def _classifier_method(self): """ Name of the method that must be used for issuing label predictions. The default one is "decision_function". @@ -602,7 +640,7 @@ """ return 'decision_function' - def _check_classifier(self, adapt_if_necessary=False): + def _check_classifier(self, adapt_if_necessary=False): """ Guarantees that the underlying classifier implements the method required for issuing predictions, i.e., the method indicated by the :meth:`_classifier_method` @@ -614,7 +652,7 @@ [docs] - def predict(self, X): + def predict(self, X): """ Generate class prevalence estimates for the sample's instances by aggregating the label predictions generated by the classifier. @@ -629,7 +667,7 @@ [docs] @abstractmethod - def aggregate(self, classif_predictions: np.ndarray): + def aggregate(self, classif_predictions: np.ndarray): """ Implements the aggregation of the classifier predictions. @@ -640,7 +678,7 @@ @property - def classes_(self): + def classes_(self): """ Class labels, in the same order in which class prevalence values are to be computed. This default implementation actually returns the class labels of the learner. @@ -653,14 +691,14 @@ [docs] -class AggregativeCrispQuantifier(AggregativeQuantifier, ABC): +class AggregativeCrispQuantifier(AggregativeQuantifier, ABC): """ Abstract class for quantification methods that base their estimations on the aggregation of crisp decisions as returned by a hard classifier. Aggregative crisp quantifiers thus extend Aggregative Quantifiers by implementing specifications about crisp predictions. """ - def _classifier_method(self): + def _classifier_method(self): """ Name of the method that must be used for issuing label predictions. For crisp quantifiers, the method is 'predict', that returns an array of shape `(n_instances,)` of label predictions. @@ -673,7 +711,7 @@ [docs] -class AggregativeSoftQuantifier(AggregativeQuantifier, ABC): +class AggregativeSoftQuantifier(AggregativeQuantifier, ABC): """ Abstract class for quantification methods that base their estimations on the aggregation of posterior probabilities as returned by a probabilistic classifier. @@ -681,7 +719,7 @@ about soft predictions. """ - def _classifier_method(self): + def _classifier_method(self): """ Name of the method that must be used for issuing label predictions. For probabilistic quantifiers, the method is 'predict_proba', that returns an array of shape `(n_instances, n_dimensions,)` with posterior @@ -691,7 +729,7 @@ """ return 'predict_proba' - def _check_classifier(self, adapt_if_necessary=False): + def _check_classifier(self, adapt_if_necessary=False): """ Guarantees that the underlying classifier implements the method indicated by the :meth:`_classifier_method`. In case it does not, the classifier is calibrated (by means of the Platt's calibration method implemented by @@ -703,8 +741,8 @@ """ if not hasattr(self.classifier, self._classifier_method()): if adapt_if_necessary: - print(f'warning: The learner {self.classifier.__class__.__name__} does not seem to be ' - f'probabilistic. The learner will be calibrated (using CalibratedClassifierCV).') + warnings.warn(f'The learner {self.classifier.__class__.__name__} does not seem to be ' + f'probabilistic. The learner will be calibrated (using CalibratedClassifierCV).') self.classifier = CalibratedClassifierCV(self.classifier, cv=5) else: raise AssertionError(f'error: The learner {self.classifier.__class__.__name__} does not ' @@ -715,19 +753,19 @@ [docs] -class BinaryAggregativeQuantifier(AggregativeQuantifier, BinaryQuantifier): +class BinaryAggregativeQuantifier(AggregativeQuantifier, BinaryQuantifier): @property - def pos_label(self): + def pos_label(self): return self.classifier.classes_[1] @property - def neg_label(self): + def neg_label(self): return self.classifier.classes_[0] [docs] - def fit(self, X, y): + def fit(self, X, y): self._check_binary(y, self.__class__.__name__) return super().fit(X, y) @@ -738,19 +776,19 @@ # ------------------------------------ [docs] -class CC(AggregativeCrispQuantifier): +class CC(AggregativeCrispQuantifier): """ The most basic Quantification method. One that simply classifies all instances and counts how many have been attributed to each of the classes in order to compute class prevalence estimates. :param classifier: a sklearn's Estimator that generates a classifier """ - def __init__(self, classifier: BaseEstimator = None, fit_classifier: bool = True): + def __init__(self, classifier: BaseEstimator = None, fit_classifier: bool = True): super().__init__(classifier, fit_classifier, val_split=None) [docs] - def aggregation_fit(self, classif_predictions, labels): + def aggregation_fit(self, classif_predictions, labels): """ Nothing to do here! @@ -762,7 +800,7 @@ [docs] - def aggregate(self, classif_predictions: np.ndarray): + def aggregate(self, classif_predictions: np.ndarray): """ Computes class prevalence estimates by counting the prevalence of each of the predicted labels. @@ -776,7 +814,7 @@ [docs] -class PCC(AggregativeSoftQuantifier): +class PCC(AggregativeSoftQuantifier): """ `Probabilistic Classify & Count <https://ieeexplore.ieee.org/abstract/document/5694031>`_, the probabilistic variant of CC that relies on the posterior probabilities returned by a probabilistic classifier. @@ -784,12 +822,12 @@ :param classifier: a sklearn's Estimator that generates a classifier """ - def __init__(self, classifier: BaseEstimator = None, fit_classifier: bool = True, val_split=None): + def __init__(self, classifier: BaseEstimator = None, fit_classifier: bool = True, val_split=None): super().__init__(classifier, fit_classifier, val_split=val_split) [docs] - def aggregation_fit(self, classif_predictions, labels): + def aggregation_fit(self, classif_predictions, labels): """ Nothing to do here! @@ -801,7 +839,7 @@ [docs] - def aggregate(self, classif_posteriors): + def aggregate(self, classif_posteriors): return F.prevalence_from_probabilities(classif_posteriors, binarize=False) @@ -809,7 +847,7 @@ [docs] -class ACC(AggregativeCrispQuantifier): +class ACC(AggregativeCrispQuantifier): """ `Adjusted Classify & Count <https://link.springer.com/article/10.1007/s10618-008-0097-y>`_, the "adjusted" variant of :class:`CC`, that corrects the predictions of CC @@ -857,7 +895,7 @@ :param n_jobs: number of parallel workers """ - def __init__( + def __init__( self, classifier: BaseEstimator = None, fit_classifier = True, @@ -880,7 +918,7 @@ [docs] @classmethod - def newInvariantRatioEstimation(cls, classifier: BaseEstimator, fit_classifier=True, val_split=5, n_jobs=None): + def newInvariantRatioEstimation(cls, classifier: BaseEstimator, fit_classifier=True, val_split=5, n_jobs=None): """ Constructs a quantifier that implements the Invariant Ratio Estimator of `Vaz et al. 2018 <https://jmlr.org/papers/v20/18-456.html>`_. This amounts @@ -905,7 +943,7 @@ return ACC(classifier, fit_classifier=fit_classifier, val_split=val_split, method='invariant-ratio', norm='mapsimplex', n_jobs=n_jobs) - def _check_init_parameters(self): + def _check_init_parameters(self): if self.solver not in ACC.SOLVERS: raise ValueError(f"unknown solver; valid ones are {ACC.SOLVERS}") if self.method not in ACC.METHODS: @@ -915,7 +953,7 @@ [docs] - def aggregation_fit(self, classif_predictions, labels): + def aggregation_fit(self, classif_predictions, labels): """ Estimates the misclassification rates. :param classif_predictions: array-like with the predicted labels @@ -930,7 +968,7 @@ [docs] @classmethod - def getPteCondEstim(cls, classes, y, y_): + def getPteCondEstim(cls, classes, y, y_): """ Estimate the matrix with entry (i,j) being the estimate of P(hat_yi|yj), that is, the probability that a document that belongs to yj ends up being classified as belonging to yi @@ -953,7 +991,7 @@ [docs] - def aggregate(self, classif_predictions): + def aggregate(self, classif_predictions): prevs_estim = self.cc.aggregate(classif_predictions) estimate = F.solve_adjustment( class_conditional_rates=self.Pte_cond_estim_, @@ -968,7 +1006,7 @@ [docs] -class PACC(AggregativeSoftQuantifier): +class PACC(AggregativeSoftQuantifier): """ `Probabilistic Adjusted Classify & Count <https://ieeexplore.ieee.org/abstract/document/5694031>`_, the probabilistic variant of ACC that relies on the posterior probabilities returned by a probabilistic classifier. @@ -1015,7 +1053,7 @@ :param n_jobs: number of parallel workers """ - def __init__( + def __init__( self, classifier: BaseEstimator = None, fit_classifier=True, @@ -1031,7 +1069,7 @@ self.method = method self.norm = norm - def _check_init_parameters(self): + def _check_init_parameters(self): if self.solver not in ACC.SOLVERS: raise ValueError(f"unknown solver; valid ones are {ACC.SOLVERS}") if self.method not in ACC.METHODS: @@ -1041,7 +1079,7 @@ [docs] - def aggregation_fit(self, classif_predictions, labels): + def aggregation_fit(self, classif_predictions, labels): """ Estimates the misclassification rates @@ -1056,7 +1094,7 @@ [docs] - def aggregate(self, classif_posteriors): + def aggregate(self, classif_posteriors): prevs_estim = self.pcc.aggregate(classif_posteriors) estimate = F.solve_adjustment( @@ -1071,7 +1109,7 @@ [docs] @classmethod - def getPteCondEstim(cls, classes, y, y_): + def getPteCondEstim(cls, classes, y, y_): # estimate the matrix with entry (i,j) being the estimate of P(hat_yi|yj), that is, the probability that a # document that belongs to yj ends up being classified as belonging to yi n_classes = len(classes) @@ -1088,7 +1126,7 @@ [docs] -class RLLS(AggregativeSoftQuantifier): +class RLLS(AggregativeSoftQuantifier): """ `Regularized Learning for Domain Adaptation under Label Shifts <https://arxiv.org/abs/1903.09734>`_, used here as an aggregative @@ -1128,7 +1166,7 @@ :func:`quapy.functional.normalize_prevalence` """ - def __init__( + def __init__( self, classifier: BaseEstimator = None, fit_classifier=True, @@ -1147,7 +1185,7 @@ self.norm = norm self.last_w_ = None - def _check_init_parameters(self): + def _check_init_parameters(self): _get_cvxpy() _rlls_check_mode(self.mode) if not isinstance(self.alpha, (int, float)) or self.alpha < 0: @@ -1164,7 +1202,7 @@ [docs] - def aggregation_fit(self, classif_predictions, labels): + def aggregation_fit(self, classif_predictions, labels): if classif_predictions is None or labels is None: raise ValueError('RLLS requires source posterior predictions and source labels') @@ -1181,7 +1219,7 @@ [docs] - def aggregate(self, classif_posteriors): + def aggregate(self, classif_posteriors): qz = _rlls_predicted_marginal(classif_posteriors, mode=self.mode) w = _rlls_compute_weights( self.C_zy_, @@ -1199,7 +1237,7 @@ [docs] -class EMQ(AggregativeSoftQuantifier): +class EMQ(AggregativeSoftQuantifier): """ `Expectation Maximization for Quantification <https://ieeexplore.ieee.org/abstract/document/6789744>`_ (EMQ), aka `Saerens-Latinne-Decaestecker` (SLD) algorithm. @@ -1249,7 +1287,7 @@ ON_CALIB_ERROR_VALUES = ['raise', 'backup'] CALIB_OPTIONS = [None, 'nbvs', 'bcts', 'ts', 'vs'] - def __init__(self, classifier: BaseEstimator = None, fit_classifier=True, val_split=None, exact_train_prev=True, + def __init__(self, classifier: BaseEstimator = None, fit_classifier=True, val_split=None, exact_train_prev=True, calib=None, on_calib_error='raise', n_jobs=None): assert calib in EMQ.CALIB_OPTIONS, \ @@ -1261,12 +1299,12 @@ self.exact_train_prev = exact_train_prev self.calib = calib self.on_calib_error = on_calib_error - self.n_jobs = n_jobs + self.n_jobs = qp._get_njobs(n_jobs) [docs] @classmethod - def EMQ_BCTS(cls, classifier: BaseEstimator, fit_classifier=True, val_split=5, on_calib_error="raise", n_jobs=None): + def EMQ_BCTS(cls, classifier: BaseEstimator, fit_classifier=True, val_split=5, on_calib_error="raise", n_jobs=None): """ Constructs an instance of EMQ using the best configuration found in the `Alexandari et al. paper <http://proceedings.mlr.press/v119/alexandari20a.html>`_, i.e., one that relies on Bias-Corrected Temperature @@ -1298,23 +1336,23 @@ calib='bcts', on_calib_error=on_calib_error, n_jobs=n_jobs) - def _check_init_parameters(self): + def _check_init_parameters(self): if self.val_split is not None: if self.exact_train_prev and self.calib is None: - raise RuntimeWarning(f'The parameter {self.val_split=} was specified for EMQ, while the parameters ' - f'{self.exact_train_prev=} and {self.calib=}. This has no effect and causes an ' - f'unnecessary overload.') + warnings.warn(f'The parameter {self.val_split=} was specified for EMQ, while the parameters ' + f'{self.exact_train_prev=} and {self.calib=}. This has no effect and causes an ' + f'unnecessary overload.', RuntimeWarning) else: if self.calib is not None: - print(f'[warning] The parameter {self.calib=} requires the val_split be different from None. ' - f'This parameter will be set to 5. To avoid this warning, set this value to a float value ' - f'indicating the proportion of training data to be used as validation, or to an integer ' - f'indicating the number of folds for kFCV.') + warnings.warn(f'The parameter {self.calib=} requires the val_split be different from None. ' + f'This parameter will be set to 5. To avoid this warning, set this value to a float value ' + f'indicating the proportion of training data to be used as validation, or to an integer ' + f'indicating the number of folds for kFCV.') self.val_split = 5 [docs] - def classify(self, X): + def classify(self, X): """ Provides the posterior probabilities for the given instances. The calibration function, if required, has no effect in this step, and is only involved in the aggregate method. @@ -1327,13 +1365,13 @@ [docs] - def classifier_fit_predict(self, X, y): + def classifier_fit_predict(self, X, y): classif_predictions = super().classifier_fit_predict(X, y) self.train_prevalence = F.prevalence_from_labels(y, classes=self.classes_) return classif_predictions - def _fit_calibration(self, calibrator, P, y): + def _fit_calibration(self, calibrator, P, y): n_classes = len(self.classes_) if not np.issubdtype(y.dtype, np.number): @@ -1347,7 +1385,7 @@ elif self.on_calib_error == 'backup': self.calibration_function = lambda P: P - def _calibrate_if_requested(self, uncalib_posteriors): + def _calibrate_if_requested(self, uncalib_posteriors): if hasattr(self, 'calibration_function') and self.calibration_function is not None: try: calib_posteriors = self.calibration_function(uncalib_posteriors) @@ -1364,7 +1402,7 @@ [docs] - def aggregation_fit(self, classif_predictions, labels): + def aggregation_fit(self, classif_predictions, labels): """ Trains the aggregation function of EMQ. This comes down to recalibrating the posterior probabilities ir requested. @@ -1379,14 +1417,14 @@ requires_predictions = (self.calib is not None) or (not self.exact_train_prev) if P is None and requires_predictions: # classifier predictions were not generated because val_split=None - raise ArgumentError(self.val_split, self.__class__.__name__ + - ": Classifier predictions for the aggregative fit were not generated because " - "val_split=None. This usually happens when you enable calibrations or heuristics " - "during model selection but left val_split set to its default value (None). " - "Please provide one of the following values for val_split: (i) an integer >1 " - "(e.g. val_split=5) for k-fold cross-validation; (ii) a float in (0,1) (e.g. " - "val_split=0.3) for a proportion split; or (iii) a tuple (X, y) with explicit " - "validation data") + raise ValueError(self.__class__.__name__ + + ": Classifier predictions for the aggregative fit were not generated because " + "val_split=None. This usually happens when you enable calibrations or heuristics " + "during model selection but left val_split set to its default value (None). " + "Please provide one of the following values for val_split: (i) an integer >1 " + "(e.g. val_split=5) for k-fold cross-validation; (ii) a float in (0,1) (e.g. " + "val_split=0.3) for a proportion split; or (iii) a tuple (X, y) with explicit " + "validation data") if self.calib is not None: calibrator = _get_abstention_calibrators().get(self.calib, None) @@ -1403,7 +1441,7 @@ [docs] - def aggregate(self, classif_posteriors, epsilon=EPSILON): + def aggregate(self, classif_posteriors, epsilon=EPSILON): classif_posteriors = self._calibrate_if_requested(classif_posteriors) priors, posteriors = self.EM(self.train_prevalence, classif_posteriors, epsilon) return priors @@ -1411,7 +1449,7 @@ [docs] - def predict_proba(self, instances, epsilon=EPSILON): + def predict_proba(self, instances, epsilon=EPSILON): """ Returns the posterior probabilities updated by the EM algorithm. @@ -1428,7 +1466,7 @@ [docs] @classmethod - def EM(cls, tr_prev, posterior_probabilities, epsilon=EPSILON): + def EM(cls, tr_prev, posterior_probabilities, epsilon=EPSILON): """ Computes the `Expectation Maximization` routine. @@ -1466,7 +1504,7 @@ s += 1 if not converged: - print('[warning] the method has reached the maximum number of iterations; it might have not converged') + warnings.warn('the method has reached the maximum number of iterations; it might have not converged') return qs, ps @@ -1475,7 +1513,7 @@ [docs] -class HDy(AggregativeSoftQuantifier, BinaryAggregativeQuantifier): +class HDy(AggregativeSoftQuantifier, BinaryAggregativeQuantifier): """ `Hellinger Distance y <https://www.sciencedirect.com/science/article/pii/S0020025512004069>`_ (HDy). HDy is a probabilistic method for training binary quantifiers, that models quantification as the problem of @@ -1502,12 +1540,12 @@ for `k`); or as a tuple (X,y) defining the specific set of data to use for validation. """ - def __init__(self, classifier: BaseEstimator = None, fit_classifier=True, val_split=5): + def __init__(self, classifier: BaseEstimator = None, fit_classifier=True, val_split=5): super().__init__(classifier, fit_classifier, val_split) [docs] - def aggregation_fit(self, classif_predictions, labels): + def aggregation_fit(self, classif_predictions, labels): """ Trains the aggregation function of HDy. @@ -1522,7 +1560,7 @@ # pre-compute the histogram for positive and negative examples self.bins = np.linspace(10, 110, 11, dtype=int) # [10, 20, 30, ..., 100, 110] - def hist(P, bins): + def hist(P, bins): h = np.histogram(P, bins=bins, range=(0, 1), density=True)[0] return h / h.sum() @@ -1532,7 +1570,7 @@ [docs] - def aggregate(self, classif_posteriors): + def aggregate(self, classif_posteriors): # "In this work, the number of bins b used in HDx and HDy was chosen from 10 to 110 in steps of 10, # and the final estimated a priori probability was taken as the median of these 11 estimates." # (González-Castro, et al., 2013). @@ -1549,7 +1587,7 @@ # the authors proposed to search for the prevalence yielding the best matching as a linear search # at small steps (modern implementations resort to an optimization procedure, # see class DistributionMatching) - def loss(prev): + def loss(prev): class1_prev = prev[1] Px_train = class1_prev * Pxy1_density + (1 - class1_prev) * Pxy0_density return F.HellingerDistance(Px_train, Px_test) @@ -1564,7 +1602,7 @@ [docs] -class DyS(AggregativeSoftQuantifier, BinaryAggregativeQuantifier): +class DyS(AggregativeSoftQuantifier, BinaryAggregativeQuantifier): """ `DyS framework <https://ojs.aaai.org/index.php/AAAI/article/view/4376>`_ (DyS). DyS is a generalization of HDy method, using a Ternary Search in order to find the prevalence that @@ -1593,15 +1631,15 @@ :param n_jobs: number of parallel workers. """ - def __init__(self, classifier: BaseEstimator = None, fit_classifier=True, val_split=5, n_bins=8, + def __init__(self, classifier: BaseEstimator = None, fit_classifier=True, val_split=5, n_bins=8, divergence: Union[str, Callable] = 'HD', tol=1e-05, n_jobs=None): super().__init__(classifier, fit_classifier, val_split) self.tol = tol self.divergence = divergence self.n_bins = n_bins - self.n_jobs = n_jobs + self.n_jobs = qp._get_njobs(n_jobs) - def _ternary_search(self, f, left, right, tol): + def _ternary_search(self, f, left, right, tol): """ Find maximum of unimodal function f() within [left, right] """ @@ -1619,7 +1657,7 @@ [docs] - def aggregation_fit(self, classif_predictions, labels): + def aggregation_fit(self, classif_predictions, labels): """ Trains the aggregation function of DyS. @@ -1637,13 +1675,13 @@ [docs] - def aggregate(self, classif_posteriors): + def aggregate(self, classif_posteriors): Px = classif_posteriors[:, self.pos_label] # takes only the P(y=+1|x) Px_test = np.histogram(Px, bins=self.n_bins, range=(0, 1), density=True)[0] divergence = get_divergence(self.divergence) - def distribution_distance(prev): + def distribution_distance(prev): Px_train = prev * self.Pxy1_density + (1 - prev) * self.Pxy0_density return divergence(Px_train, Px_test) @@ -1655,7 +1693,7 @@ [docs] -class SMM(AggregativeSoftQuantifier, BinaryAggregativeQuantifier): +class SMM(AggregativeSoftQuantifier, BinaryAggregativeQuantifier): """ `SMM method <https://ieeexplore.ieee.org/document/9260028>`_ (SMM). SMM is a simplification of matching distribution methods where the representation of the examples @@ -1674,12 +1712,12 @@ for `k`); or as a tuple (X,y) defining the specific set of data to use for validation. """ - def __init__(self, classifier: BaseEstimator = None, fit_classifier=True, val_split=5): + def __init__(self, classifier: BaseEstimator = None, fit_classifier=True, val_split=5): super().__init__(classifier, fit_classifier, val_split) [docs] - def aggregation_fit(self, classif_predictions, labels): + def aggregation_fit(self, classif_predictions, labels): """ Trains the aggregation function of SMM. @@ -1697,7 +1735,7 @@ [docs] - def aggregate(self, classif_posteriors): + def aggregate(self, classif_posteriors): Px = classif_posteriors[:, self.pos_label] # takes only the P(y=+1|x) Px_mean = np.mean(Px) @@ -1709,7 +1747,7 @@ [docs] -class DMy(AggregativeSoftQuantifier): +class DMy(AggregativeSoftQuantifier): """ Generic Distribution Matching quantifier for binary or multiclass quantification based on the space of posterior probabilities. This implementation takes the number of bins, the divergence, and the possibility to work on CDF @@ -1742,19 +1780,19 @@ :param n_jobs: number of parallel workers (default None) """ - def __init__(self, classifier: BaseEstimator = None, fit_classifier=True, val_split=5, nbins=8, + def __init__(self, classifier: BaseEstimator = None, fit_classifier=True, val_split=5, nbins=8, divergence: Union[str, Callable] = 'HD', cdf=False, search='optim_minimize', n_jobs=None): super().__init__(classifier, fit_classifier, val_split) self.nbins = nbins self.divergence = divergence self.cdf = cdf self.search = search - self.n_jobs = n_jobs + self.n_jobs = qp._get_njobs(n_jobs) [docs] @classmethod - def HDy(cls, classifier: BaseEstimator = None, fit_classifier=True, val_split=5, n_jobs=None): + def HDy(cls, classifier: BaseEstimator = None, fit_classifier=True, val_split=5, n_jobs=None): """ Historical HDy preset expressed as a configuration of :class:`DMy`. @@ -1783,7 +1821,7 @@ return AggregativeMedianEstimator(base_quantifier=base, param_grid=param_grid, n_jobs=n_jobs) - def _get_distributions(self, posteriors): + def _get_distributions(self, posteriors): histograms = [] post_dims = posteriors.shape[1] if post_dims == 2: @@ -1801,7 +1839,7 @@ [docs] - def aggregation_fit(self, classif_predictions, labels): + def aggregation_fit(self, classif_predictions, labels): """ Trains the aggregation function of a distribution matching method. This comes down to generating the validation distributions out of the training data. @@ -1829,7 +1867,7 @@ [docs] - def aggregate(self, posteriors: np.ndarray): + def aggregate(self, posteriors: np.ndarray): """ Searches for the mixture model parameter (the sought prevalence values) that yields a validation distribution (the mixture) that best matches the test distribution, in terms of the divergence measure of choice. @@ -1844,7 +1882,7 @@ divergence = get_divergence(self.divergence) n_classes, n_channels, nbins = self.validation_distribution.shape - def loss(prev): + def loss(prev): prev = np.expand_dims(prev, axis=0) mixture_distribution = (prev @ self.validation_distribution.reshape(n_classes, -1)).reshape(n_channels, -1) divs = [divergence(test_distribution[ch], mixture_distribution[ch]) for ch in range(n_channels)] @@ -1857,7 +1895,7 @@ [docs] -def newELM(svmperf_base=None, loss='01', C=1): +def newELM(svmperf_base=None, loss='01', C=1): """ Explicit Loss Minimization (ELM) quantifiers. Quantifiers based on ELM represent a family of methods based on structured output learning; @@ -1887,7 +1925,7 @@ [docs] -def newSVMQ(svmperf_base=None, C=1): +def newSVMQ(svmperf_base=None, C=1): """ SVM(Q) is an Explicit Loss Minimization (ELM) quantifier set to optimize for the `Q` loss combining a classification-oriented loss and a quantification-oriented loss, as proposed by @@ -1914,7 +1952,9 @@ -def newSVMKLD(svmperf_base=None, C=1): + +[docs] +def newSVMKLD(svmperf_base=None, C=1): """ SVM(KLD) is an Explicit Loss Minimization (ELM) quantifier set to optimize for the Kullback-Leibler Divergence as proposed by `Esuli et al. 2015 <https://dl.acm.org/doi/abs/10.1145/2700406>`_. @@ -1936,14 +1976,15 @@ :return: returns an instance of CC set to work with SVMperf (with loss and C set properly) as the underlying classifier """ - return newELM(svmperf_base, loss='kld', C=C) + return newELM(svmperf_base, loss='kld', C=C) - -[docs] -def newSVMKLD(svmperf_base=None, C=1): + + +[docs] +def newSVMNKLD(svmperf_base=None, C=1): """ - SVM(KLD) is an Explicit Loss Minimization (ELM) quantifier set to optimize for the Kullback-Leibler Divergence + SVM(NKLD) is an Explicit Loss Minimization (ELM) quantifier set to optimize for the Kullback-Leibler Divergence normalized via the logistic function, as proposed by `Esuli et al. 2015 <https://dl.acm.org/doi/abs/10.1145/2700406>`_. Equivalent to: @@ -1970,7 +2011,7 @@ [docs] -def newSVMAE(svmperf_base=None, C=1): +def newSVMAE(svmperf_base=None, C=1): """ SVM(KLD) is an Explicit Loss Minimization (ELM) quantifier set to optimize for the Absolute Error as first used by `Moreo and Sebastiani, 2021 <https://arxiv.org/abs/2011.02552>`_. @@ -1998,7 +2039,7 @@ [docs] -def newSVMRAE(svmperf_base=None, C=1): +def newSVMRAE(svmperf_base=None, C=1): """ SVM(KLD) is an Explicit Loss Minimization (ELM) quantifier set to optimize for the Relative Absolute Error as first used by `Moreo and Sebastiani, 2021 <https://arxiv.org/abs/2011.02552>`_. @@ -2026,7 +2067,7 @@ [docs] -class OneVsAllAggregative(OneVsAllGeneric, AggregativeQuantifier): +class OneVsAllAggregative(OneVsAllGeneric, AggregativeQuantifier): """ Allows any binary quantifier to perform quantification on single-label datasets. The method maintains one binary quantifier for each class, and then l1-normalizes the outputs so that the @@ -2042,7 +2083,7 @@ is removed and no longer available at predict time. """ - def __init__(self, binary_quantifier=None, n_jobs=None, parallel_backend='multiprocessing'): + def __init__(self, binary_quantifier=None, n_jobs=None, parallel_backend='multiprocessing'): if binary_quantifier is None: binary_quantifier = PACC() assert isinstance(binary_quantifier, BaseQuantifier), \ @@ -2055,7 +2096,7 @@ [docs] - def classify(self, X): + def classify(self, X): """ If the base quantifier is not probabilistic, returns a matrix of shape `(n,m,)` with `n` the number of instances and `m` the number of classes. The entry `(i,j)` is a binary value indicating whether instance @@ -2079,34 +2120,34 @@ [docs] - def aggregate(self, classif_predictions): + def aggregate(self, classif_predictions): prevalences = self._parallel(self._delayed_binary_aggregate, classif_predictions) return F.normalize_prevalence(prevalences) [docs] - def aggregation_fit(self, classif_predictions, labels): - self._parallel(self._delayed_binary_aggregate_fit(c, classif_predictions, labels)) + def aggregation_fit(self, classif_predictions, labels): + self._parallel(self._delayed_binary_aggregate_fit, classif_predictions, labels) return self - def _delayed_binary_classification(self, c, X): + def _delayed_binary_classification(self, c, X): return self.dict_binary_quantifiers[c].classify(X) - def _delayed_binary_aggregate(self, c, classif_predictions): + def _delayed_binary_aggregate(self, c, classif_predictions): # the estimation for the positive class prevalence return self.dict_binary_quantifiers[c].aggregate(classif_predictions[:, c])[1] - def _delayed_binary_aggregate_fit(self, c, classif_predictions, labels): + def _delayed_binary_aggregate_fit(self, c, classif_predictions, labels): # trains the aggregation function of the cth quantifier - return self.dict_binary_quantifiers[c].aggregate_fit(classif_predictions[:, c], labels) + return self.dict_binary_quantifiers[c].aggregation_fit(classif_predictions[:, c], labels == c) [docs] -class AggregativeMedianEstimator(BinaryQuantifier): +class AggregativeMedianEstimator(BinaryQuantifier): """ This method is a meta-quantifier that returns, as the estimated class prevalence values, the median of the estimation returned by differently (hyper)parameterized base quantifiers. @@ -2119,7 +2160,7 @@ :param n_jobs: number of parallel workers """ - def __init__(self, base_quantifier: AggregativeQuantifier, param_grid: dict, random_state=None, n_jobs=None): + def __init__(self, base_quantifier: AggregativeQuantifier, param_grid: dict, random_state=None, n_jobs=None): self.base_quantifier = base_quantifier self.param_grid = param_grid self.random_state = random_state @@ -2127,17 +2168,17 @@ [docs] - def get_params(self, deep=True): + def get_params(self, deep=True): return self.base_quantifier.get_params(deep) [docs] - def set_params(self, **params): + def set_params(self, **params): self.base_quantifier.set_params(**params) - def _delayed_fit(self, args): + def _delayed_fit(self, args): with qp.util.temp_seed(self.random_state): params, X, y = args model = deepcopy(self.base_quantifier) @@ -2145,7 +2186,7 @@ model.fit(X, y) return model - def _delayed_fit_classifier(self, args): + def _delayed_fit_classifier(self, args): with qp.util.temp_seed(self.random_state): cls_params, X, y = args model = deepcopy(self.base_quantifier) @@ -2153,7 +2194,7 @@ predictions, labels = model.classifier_fit_predict(X, y) return (model, predictions, labels) - def _delayed_fit_aggregation(self, args): + def _delayed_fit_aggregation(self, args): with qp.util.temp_seed(self.random_state): ((model, predictions, y), q_params) = args model = deepcopy(model) @@ -2163,8 +2204,8 @@ [docs] - def fit(self, X, y): - import itertools + def fit(self, X, y): + import itertools self._check_binary(y, self.__class__.__name__) @@ -2177,8 +2218,7 @@ ((params, X, y) for params in cls_configs), seed=qp.environ.get('_R_SEED', None), n_jobs=self.n_jobs, - asarray=False, - backend='threading' + asarray=False ) else: model = self.base_quantifier @@ -2190,8 +2230,7 @@ self._delayed_fit_aggregation, itertools.product(models_preds, q_configs), seed=qp.environ.get('_R_SEED', None), - n_jobs=self.n_jobs, - backend='threading' + n_jobs=self.n_jobs ) else: configs = qp.model_selection.expand_grid(self.param_grid) @@ -2199,25 +2238,23 @@ self._delayed_fit, ((params, X, y) for params in configs), seed=qp.environ.get('_R_SEED', None), - n_jobs=self.n_jobs, - backend='threading' + n_jobs=self.n_jobs ) return self - def _delayed_predict(self, args): + def _delayed_predict(self, args): model, instances = args return model.predict(instances) [docs] - def predict(self, instances): + def predict(self, instances): prev_preds = qp.util.parallel( self._delayed_predict, ((model, instances) for model in self.models), seed=qp.environ.get('_R_SEED', None), - n_jobs=self.n_jobs, - backend='threading' + n_jobs=self.n_jobs ) return np.median(prev_preds, axis=0) @@ -2228,7 +2265,7 @@ # imports # --------------------------------------------------------------- -from . import _threshold_optim +from . import _threshold_optim T50 = _threshold_optim.T50 MAX = _threshold_optim.MAX @@ -2236,7 +2273,7 @@ MS = _threshold_optim.MS MS2 = _threshold_optim.MS2 -from . import _kdey +from . import _kdey KDEyML = _kdey.KDEyML KDEyHD = _kdey.KDEyHD diff --git a/docs/build/html/_modules/quapy/method/base.html b/docs/build/html/_modules/quapy/method/base.html index e649c5b..5519615 100644 --- a/docs/build/html/_modules/quapy/method/base.html +++ b/docs/build/html/_modules/quapy/method/base.html @@ -1,95 +1,392 @@ - - - - - - quapy.method.base — QuaPy: A Python-based open-source framework for quantification 0.2.1 documentation - - + + + + + + + + quapy.method.base — QuaPy: A Python-based open-source framework for quantification 0.2.1 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Skip to main content - - - - - - - - - + + + + Back to top + + + + + + + + + + Ctrl+K + + - - - - - - - - - - QuaPy: A Python-based open-source framework for quantification - - - - - - - + + - - -
+ Source code for quapy.data.reader -import numpy as np -from scipy.sparse import dok_matrix -from tqdm import tqdm +import logging + +import numpy as np +from scipy.sparse import dok_matrix +from tqdm import tqdm [docs] -def from_text(path, encoding='utf-8', verbose=1, class2int=True): +def from_text(path, encoding='utf-8', verbose=1, class2int=True): """ Reads a labelled colletion of documents. File fomart <0 or 1>\t<document>\n @@ -108,14 +406,14 @@ all_sentences.append(sentence) all_labels.append(label) except ValueError: - print(f'format error in {line}') + logging.getLogger(__name__).warning(f'format error in {line}') return all_sentences, all_labels [docs] -def from_sparse(path): +def from_sparse(path): """ Reads a labelled collection of real-valued instances expressed in sparse format File format <-1 or 0 or 1>[\s col(int):val(float)]\n @@ -124,7 +422,7 @@ :return: a `csr_matrix` containing the instances (rows), and a ndarray containing the labels """ - def split_col_val(col_val): + def split_col_val(col_val): col, val = col_val.split(':') col, val = int(col) - 1, float(val) return col, val @@ -152,7 +450,7 @@ [docs] -def from_csv(path, encoding='utf-8'): +def from_csv(path, encoding='utf-8'): """ Reads a csv file in which columns are separated by ','. File format <label>,<feat1>,<feat2>,...,<featn>\n @@ -175,7 +473,7 @@ [docs] -def reindex_labels(y): +def reindex_labels(y): """ Re-indexes a list of labels as a list of indexes, and returns the classnames corresponding to the indexes. E.g.: @@ -198,7 +496,7 @@ [docs] -def binarize(y, pos_class): +def binarize(y, pos_class): """ Binarizes a categorical array-like collection of labels towards the positive class `pos_class`. E.g.,: @@ -218,31 +516,75 @@ - + + + + + + + + + + + + + - + + + + - Built with Sphinx using a - theme - provided by Read the Docs. - +
+ Source code for quapy.error """Implementation of error measures used for quantification""" -import numpy as np -from sklearn.metrics import f1_score -import quapy as qp +import numpy as np +from sklearn.metrics import f1_score +import quapy as qp [docs] -def from_name(err_name): +def from_name(err_name): """Gets an error function from its name. E.g., `from_name("mae")` will return function :meth:`quapy.error.mae` @@ -98,7 +394,7 @@ [docs] -def f1e(y_true, y_pred): +def f1e(y_true, y_pred): """F1 error: simply computes the error in terms of macro :math:`F_1`, i.e., :math:`1-F_1^M`, where :math:`F_1` is the harmonic mean of precision and recall, defined as :math:`\\frac{2tp}{2tp+fp+fn}`, with `tp`, `fp`, and `fn` standing @@ -116,7 +412,7 @@ [docs] -def acce(y_true, y_pred): +def acce(y_true, y_pred): """Computes the error in terms of 1-accuracy. The accuracy is computed as :math:`\\frac{tp+tn}{tp+fp+fn+tn}`, with `tp`, `fp`, `fn`, and `tn` standing for true positives, false positives, false negatives, and true negatives, @@ -132,7 +428,7 @@ [docs] -def mae(prevs_true, prevs_hat): +def mae(prevs_true, prevs_hat): """Computes the mean absolute error (see :meth:`quapy.error.ae`) across the sample pairs. :param prevs_true: array-like of shape `(n_samples, n_classes,)` with the true prevalence values @@ -146,7 +442,7 @@ [docs] -def ae(prevs_true, prevs_hat): +def ae(prevs_true, prevs_hat): """Computes the absolute error between the two prevalence vectors. Absolute error between two prevalence vectors :math:`p` and :math:`\\hat{p}` is computed as :math:`AE(p,\\hat{p})=\\frac{1}{|\\mathcal{Y}|}\\sum_{y\\in \\mathcal{Y}}|\\hat{p}(y)-p(y)|`, @@ -165,7 +461,7 @@ [docs] -def nae(prevs_true, prevs_hat): +def nae(prevs_true, prevs_hat): """Computes the normalized absolute error between the two prevalence vectors. Normalized absolute error between two prevalence vectors :math:`p` and :math:`\\hat{p}` is computed as :math:`NAE(p,\\hat{p})=\\frac{AE(p,\\hat{p})}{z_{AE}}`, @@ -185,7 +481,7 @@ [docs] -def mnae(prevs_true, prevs_hat): +def mnae(prevs_true, prevs_hat): """Computes the mean normalized absolute error (see :meth:`quapy.error.nae`) across the sample pairs. :param prevs_true: array-like of shape `(n_samples, n_classes,)` with the true prevalence values @@ -199,7 +495,7 @@ [docs] -def mse(prevs_true, prevs_hat): +def mse(prevs_true, prevs_hat): """Computes the mean squared error (see :meth:`quapy.error.se`) across the sample pairs. :param prevs_true: array-like of shape `(n_samples, n_classes,)` with the @@ -214,7 +510,7 @@ [docs] -def se(prevs_true, prevs_hat): +def se(prevs_true, prevs_hat): """Computes the squared error between the two prevalence vectors. Squared error between two prevalence vectors :math:`p` and :math:`\\hat{p}` is computed as :math:`SE(p,\\hat{p})=\\frac{1}{|\\mathcal{Y}|}\\sum_{y\\in \\mathcal{Y}}(\\hat{p}(y)-p(y))^2`, @@ -233,7 +529,7 @@ [docs] -def sre(prevs_true, prevs_hat, prevs_train, eps=0.): +def sre(prevs_true, prevs_hat, prevs_train, eps=0.): """ Computes the squared ratio error between two prevalence vectors. The squared ratio error between prevalence vectors :math:`p` and @@ -269,7 +565,7 @@ [docs] -def msre(prevs_true, prevs_hat, prevs_train, eps=0.): +def msre(prevs_true, prevs_hat, prevs_train, eps=0.): """ Computes the mean squared ratio error (see :meth:`quapy.error.sre`) across the sample pairs. @@ -285,7 +581,7 @@ [docs] -def aitchisondist(prevs_true, prevs_hat): +def aitchisondist(prevs_true, prevs_hat): """ Computes the Aitchison distance between two prevalence vectors. The Aitchison distance between prevalence vectors :math:`p` and @@ -298,7 +594,7 @@ :param prevs_hat: array-like with the predicted prevalence values :return: Aitchison distance """ - from quapy.functional import CLRtransformation + from quapy.functional import CLRtransformation clr = CLRtransformation() return np.linalg.norm(clr(prevs_true) - clr(prevs_hat), axis=-1) @@ -307,7 +603,7 @@ [docs] -def maitchisondist(prevs_true, prevs_hat): +def maitchisondist(prevs_true, prevs_hat): """ Computes the mean Aitchison distance (see :meth:`quapy.error.aitchisondist`) across the sample pairs, i.e., @@ -324,7 +620,7 @@ [docs] -def mkld(prevs_true, prevs_hat, eps=None): +def mkld(prevs_true, prevs_hat, eps=None): """Computes the mean Kullback-Leibler divergence (see :meth:`quapy.error.kld`) across the sample pairs. The distributions are smoothed using the `eps` factor (see :meth:`quapy.error.smooth`). @@ -345,7 +641,7 @@ [docs] -def kld(prevs_true, prevs_hat, eps=None): +def kld(prevs_true, prevs_hat, eps=None): """Computes the Kullback-Leibler divergence between the two prevalence distributions. Kullback-Leibler divergence between two prevalence distributions :math:`p` and :math:`\\hat{p}` is computed as @@ -371,7 +667,7 @@ [docs] -def mnkld(prevs_true, prevs_hat, eps=None): +def mnkld(prevs_true, prevs_hat, eps=None): """Computes the mean Normalized Kullback-Leibler divergence (see :meth:`quapy.error.nkld`) across the sample pairs. The distributions are smoothed using the `eps` factor (see :meth:`quapy.error.smooth`). @@ -391,7 +687,7 @@ [docs] -def nkld(prevs_true, prevs_hat, eps=None): +def nkld(prevs_true, prevs_hat, eps=None): """Computes the Normalized Kullback-Leibler divergence between the two prevalence distributions. Normalized Kullback-Leibler divergence between two prevalence distributions :math:`p` and :math:`\\hat{p}` is computed as @@ -415,7 +711,7 @@ [docs] -def mrae(prevs_true, prevs_hat, eps=None): +def mrae(prevs_true, prevs_hat, eps=None): """Computes the mean relative absolute error (see :meth:`quapy.error.rae`) across the sample pairs. The distributions are smoothed using the `eps` factor (see :meth:`quapy.error.smooth`). @@ -436,7 +732,7 @@ [docs] -def rae(prevs_true, prevs_hat, eps=None): +def rae(prevs_true, prevs_hat, eps=None): """Computes the absolute relative error between the two prevalence vectors. Relative absolute error between two prevalence vectors :math:`p` and :math:`\\hat{p}` is computed as @@ -462,7 +758,7 @@ [docs] -def nrae(prevs_true, prevs_hat, eps=None): +def nrae(prevs_true, prevs_hat, eps=None): """Computes the normalized absolute relative error between the two prevalence vectors. Relative absolute error between two prevalence vectors :math:`p` and :math:`\\hat{p}` is computed as @@ -490,7 +786,7 @@ [docs] -def mnrae(prevs_true, prevs_hat, eps=None): +def mnrae(prevs_true, prevs_hat, eps=None): """Computes the mean normalized relative absolute error (see :meth:`quapy.error.nrae`) across the sample pairs. The distributions are smoothed using the `eps` factor (see :meth:`quapy.error.smooth`). @@ -511,7 +807,7 @@ [docs] -def nmd(prevs_true, prevs_hat): +def nmd(prevs_true, prevs_hat): """ Computes the Normalized Match Distance; which is the Normalized Distance multiplied by the factor `1/(n-1)` to guarantee the measure ranges between 0 (best prediction) and 1 (worst prediction). @@ -529,7 +825,7 @@ [docs] -def bias_binary(prevs_true, prevs_hat): +def bias_binary(prevs_true, prevs_hat): """ Computes the (positive) bias in a binary problem. The bias is simply the difference between the predicted positive value and the true positive value, so that a positive such value indicates the @@ -549,7 +845,7 @@ [docs] -def mean_bias_binary(prevs_true, prevs_hat): +def mean_bias_binary(prevs_true, prevs_hat): """ Computes the mean of the (positive) bias in a binary problem. :param prevs_true: array-like of shape `(n_classes,)` with the true prevalence values @@ -562,7 +858,7 @@ [docs] -def md(prevs_true, prevs_hat, ERROR_TOL=1E-3): +def md(prevs_true, prevs_hat, ERROR_TOL=1E-3): """ Computes the Match Distance, under the assumption that the cost in mistaking class i with class i+1 is 1 in all cases. @@ -582,7 +878,7 @@ [docs] -def smooth(prevs, eps): +def smooth(prevs, eps): """ Smooths a prevalence distribution with :math:`\\epsilon` (`eps`) as: :math:`\\underline{p}(y)=\\frac{\\epsilon+p(y)}{\\epsilon|\\mathcal{Y}|+ \\displaystyle\\sum_{y\\in \\mathcal{Y}}p(y)}` @@ -597,7 +893,7 @@ -def __check_eps(eps=None): +def __check_eps(eps=None): if eps is None: sample_size = qp.environ['SAMPLE_SIZE'] if sample_size is None: @@ -634,31 +930,75 @@ match_distance = md - + + + + + + + + + + + + + - + + + + - Built with Sphinx using a - theme - provided by Read the Docs. - + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/build/html/_modules/quapy/functional.html b/docs/build/html/_modules/quapy/functional.html index b7743b8..c275ca4 100644 --- a/docs/build/html/_modules/quapy/functional.html +++ b/docs/build/html/_modules/quapy/functional.html @@ -29,8 +29,9 @@ - - + + + @@ -41,6 +42,7 @@ + @@ -114,8 +116,14 @@ + + + + + + + - QuaPy @@ -131,7 +139,7 @@ - Quickstart + Home @@ -183,6 +191,21 @@ + + + + + + + + + + + GitHub + + + @@ -229,7 +252,7 @@ - Quickstart + Home @@ -272,6 +295,21 @@ + + + + + + + + + + + GitHub + + + @@ -332,17 +370,17 @@ Source code for quapy.functional -import warnings -from abc import ABC, abstractmethod -from collections import defaultdict -from functools import lru_cache -from typing import Literal, Union, Callable -from numpy.typing import ArrayLike +import warnings +from abc import ABC, abstractmethod +from collections import defaultdict +from functools import lru_cache +from typing import Literal, Union, Callable +from numpy.typing import ArrayLike -import scipy -import numpy as np +import scipy +import numpy as np -import quapy as qp +import quapy as qp # ------------------------------------------------------------------------------------------ @@ -351,7 +389,7 @@ [docs] -def classes_from_labels(labels): +def classes_from_labels(labels): """ Obtains a np.ndarray with the (sorted) classes :param labels: array-like with the instances' labels @@ -365,7 +403,7 @@ [docs] -def num_classes_from_labels(labels): +def num_classes_from_labels(labels): """ Obtains the number of classes from an array-like of instance's labels :param labels: array-like with the instances' labels @@ -380,7 +418,7 @@ [docs] -def counts_from_labels(labels: ArrayLike, classes: ArrayLike) -> np.ndarray: +def counts_from_labels(labels: ArrayLike, classes: ArrayLike) -> np.ndarray: """ Computes the raw count values from a vector of labels. @@ -401,7 +439,7 @@ [docs] -def prevalence_from_labels(labels: ArrayLike, classes: ArrayLike): +def prevalence_from_labels(labels: ArrayLike, classes: ArrayLike): """ Computes the prevalence values from a vector of labels. @@ -419,7 +457,7 @@ [docs] -def prevalence_from_probabilities(posteriors: ArrayLike, binarize: bool = False): +def prevalence_from_probabilities(posteriors: ArrayLike, binarize: bool = False): """ Returns a vector of prevalence values from a matrix of posterior probabilities. @@ -443,7 +481,7 @@ [docs] -def num_prevalence_combinations(n_prevpoints:int, n_classes:int, n_repeats:int=1) -> int: +def num_prevalence_combinations(n_prevpoints:int, n_classes:int, n_repeats:int=1) -> int: """ Computes the number of valid prevalence combinations in the n_classes-dimensional simplex if `n_prevpoints` equally distant prevalence values are generated and `n_repeats` repetitions are requested. @@ -472,7 +510,7 @@ [docs] -def get_nprevpoints_approximation(combinations_budget:int, n_classes:int, n_repeats:int=1) -> int: +def get_nprevpoints_approximation(combinations_budget:int, n_classes:int, n_repeats:int=1) -> int: """ Searches for the largest number of (equidistant) prevalence points to define for each of the `n_classes` classes so that the number of valid prevalence values generated as combinations of prevalence points (points in a @@ -500,7 +538,7 @@ [docs] -def as_binary_prevalence(positive_prevalence: Union[float, ArrayLike], clip_if_necessary: bool=False) -> np.ndarray: +def as_binary_prevalence(positive_prevalence: Union[float, ArrayLike], clip_if_necessary: bool=False) -> np.ndarray: """ Helper that, given a float representing the prevalence for the positive class, returns a np.ndarray of two values representing a binary distribution. @@ -522,7 +560,7 @@ [docs] -def strprev(prevalences: ArrayLike, prec: int=3) -> str: +def strprev(prevalences: ArrayLike, prec: int=3) -> str: """ Returns a string representation for a prevalence vector. E.g., @@ -539,7 +577,7 @@ [docs] -def check_prevalence_vector(prevalences: ArrayLike, raise_exception: bool=False, tolerance: float=1e-08, aggr=True): +def check_prevalence_vector(prevalences: ArrayLike, raise_exception: bool=False, tolerance: float=1e-08, aggr=True): """ Checks that `prevalences` is a valid prevalence vector, i.e., it contains values in [0,1] and the values sum up to 1. In other words, verifies that the `prevalences` vectors lies in the @@ -581,7 +619,7 @@ [docs] -def uniform_prevalence(n_classes): +def uniform_prevalence(n_classes): """ Returns a vector representing the uniform distribution for `n_classes` @@ -597,7 +635,7 @@ [docs] -def normalize_prevalence(prevalences: ArrayLike, method='l1'): +def normalize_prevalence(prevalences: ArrayLike, method='l1'): """ Normalizes a vector or matrix of prevalence values. The normalization consists of applying a L1 normalization in cases in which the prevalence values are not all-zeros, and to convert the prevalence values into `1/n_classes` in @@ -640,7 +678,7 @@ [docs] -def l1_norm(prevalences: ArrayLike) -> np.ndarray: +def l1_norm(prevalences: ArrayLike) -> np.ndarray: """ Applies L1 normalization to the `unnormalized_arr` so that it becomes a valid prevalence vector. Zero vectors are mapped onto the uniform distribution. Raises an exception if @@ -666,7 +704,7 @@ [docs] -def clip(prevalences: ArrayLike) -> np.ndarray: +def clip(prevalences: ArrayLike) -> np.ndarray: """ Clips the values in [0,1] and then applies the L1 normalization. @@ -681,7 +719,7 @@ [docs] -def projection_simplex_sort(unnormalized_arr: ArrayLike) -> np.ndarray: +def projection_simplex_sort(unnormalized_arr: ArrayLike) -> np.ndarray: """Projects a point onto the probability simplex. The code is adapted from Mathieu Blondel's BSD-licensed @@ -709,7 +747,7 @@ [docs] -def softmax(prevalences: ArrayLike) -> np.ndarray: +def softmax(prevalences: ArrayLike) -> np.ndarray: """ Applies the softmax function to all vectors even if the original vectors were valid distributions. If you want to leave valid vectors untouched, use condsoftmax instead. @@ -724,7 +762,7 @@ [docs] -def condsoftmax(prevalences: ArrayLike) -> np.ndarray: +def condsoftmax(prevalences: ArrayLike) -> np.ndarray: """ Applies the softmax function only to vectors that do not represent valid distributions. @@ -749,7 +787,7 @@ [docs] -def HellingerDistance(P: np.ndarray, Q: np.ndarray) -> float: +def HellingerDistance(P: np.ndarray, Q: np.ndarray) -> float: """ Computes the Hellingher Distance (HD) between (discretized) distributions `P` and `Q`. The HD for two discrete distributions of `k` bins is defined as: @@ -767,7 +805,7 @@ [docs] -def TopsoeDistance(P: np.ndarray, Q: np.ndarray, epsilon: float=1e-20): +def TopsoeDistance(P: np.ndarray, Q: np.ndarray, epsilon: float=1e-20): """ Topsoe distance between two (discretized) distributions `P` and `Q`. The Topsoe distance for two discrete distributions of `k` bins is defined as: @@ -786,7 +824,7 @@ [docs] -def get_divergence(divergence: Union[str, Callable]): +def get_divergence(divergence: Union[str, Callable]): """ Guarantees that the divergence received as argument is a function. That is, if this argument is already a callable, then it is returned, if it is instead a string, then tries to instantiate the corresponding @@ -815,7 +853,7 @@ [docs] -def argmin_prevalence(loss: Callable, +def argmin_prevalence(loss: Callable, n_classes: int, method: Literal["optim_minimize", "linear_search", "ternary_search"]='optim_minimize'): """ @@ -842,7 +880,7 @@ [docs] -def optim_minimize(loss: Callable, n_classes: int, return_loss=False): +def optim_minimize(loss: Callable, n_classes: int, return_loss=False): """ Searches for the optimal prevalence values, i.e., an `n_classes`-dimensional vector of the (`n_classes`-1)-simplex that yields the smallest lost. This optimization is carried out by means of a constrained search using scipy's @@ -854,7 +892,7 @@ :return: (ndarray) the best prevalence vector found or a tuple which also contains the value of the loss if return_loss=True """ - from scipy import optimize + from scipy import optimize # the initial point is set as the uniform distribution uniform_distribution = uniform_prevalence(n_classes=n_classes) @@ -873,7 +911,7 @@ [docs] -def linear_search(loss: Callable, n_classes: int): +def linear_search(loss: Callable, n_classes: int): """ Performs a linear search for the best prevalence value in binary problems. The search is carried out by exploring the range [0,1] stepping by 0.01. This search is inefficient, and is added only for completeness (some of the @@ -897,7 +935,7 @@ [docs] -def ternary_search(loss: Callable, n_classes: int): +def ternary_search(loss: Callable, n_classes: int): """ Performs a ternary search for the best prevalence value in binary problems. This search assumes the loss is unimodal over the interval [0,1]. @@ -933,7 +971,7 @@ [docs] -def prevalence_linspace(grid_points:int=21, repeats:int=1, smooth_limits_epsilon:float=0.01) -> np.ndarray: +def prevalence_linspace(grid_points:int=21, repeats:int=1, smooth_limits_epsilon:float=0.01) -> np.ndarray: """ Produces an array of uniformly separated values of prevalence. By default, produces an array of 21 prevalence values, with @@ -958,7 +996,7 @@ [docs] -def uniform_prevalence_sampling(n_classes: int, size: int=1) -> np.ndarray: +def uniform_prevalence_sampling(n_classes: int, size: int=1) -> np.ndarray: """ Implements the `Kraemer algorithm <http://www.cs.cmu.edu/~nasmith/papers/smith+tromble.tr04.pdf>`_ for sampling uniformly at random from the unit simplex. This implementation is adapted from this @@ -994,7 +1032,7 @@ [docs] -def solve_adjustment_binary(prevalence_estim: ArrayLike, tpr: float, fpr: float, clip: bool=True): +def solve_adjustment_binary(prevalence_estim: ArrayLike, tpr: float, fpr: float, clip: bool=True): """ Implements the adjustment of ACC and PACC for the binary case. The adjustment for a prevalence estimate of the positive class `p` comes down to computing: @@ -1021,7 +1059,7 @@ [docs] -def solve_adjustment( +def solve_adjustment( class_conditional_rates: np.ndarray, unadjusted_counts: np.ndarray, method: Literal["inversion", "invariant-ratio"], @@ -1067,14 +1105,18 @@ if method == "inversion": pass # We leave A and B unchanged elif method == "invariant-ratio": - # Change the last equation to replace it with the normalization condition + # Change the last equation to replace it with the normalization condition; + # copy first so this does not mutate the caller's arrays (np.asarray above + # returns the same object, not a copy, when the input is already float64) + A = A.copy() + B = B.copy() A[-1, :] = 1.0 B[-1] = 1.0 else: raise ValueError(f"unknown {method=}") if solver == "minimize": - def loss(prev): + def loss(prev): return np.linalg.norm(A @ prev - B) return optim_minimize(loss, n_classes=A.shape[0]) elif solver in ["exact-raise", "exact-cc"]: @@ -1102,7 +1144,7 @@ [docs] -class CompositionalTransformation(ABC): +class CompositionalTransformation(ABC): """ Abstract class of transformations for compositional data. """ @@ -1110,13 +1152,13 @@ EPSILON = 1e-12 @abstractmethod - def __call__(self, X): + def __call__(self, X): ... [docs] @abstractmethod - def inverse(self, Z): + def inverse(self, Z): ... @@ -1124,12 +1166,12 @@ [docs] -class CLRtransformation(CompositionalTransformation): +class CLRtransformation(CompositionalTransformation): """ Centered log-ratio (CLR) transformation. """ - def __call__(self, X): + def __call__(self, X): X = np.asarray(X) X = qp.error.smooth(X, self.EPSILON) geometric_mean = np.exp(np.mean(np.log(X), axis=-1, keepdims=True)) @@ -1137,7 +1179,7 @@ [docs] - def inverse(self, Z): + def inverse(self, Z): return scipy.special.softmax(Z, axis=-1) @@ -1145,12 +1187,12 @@ [docs] -class ILRtransformation(CompositionalTransformation): +class ILRtransformation(CompositionalTransformation): """ Isometric log-ratio (ILR) transformation. """ - def __call__(self, X): + def __call__(self, X): X = np.asarray(X) X = qp.error.smooth(X, self.EPSILON) basis = self.get_V(X.shape[-1]) @@ -1158,7 +1200,7 @@ [docs] - def inverse(self, Z): + def inverse(self, Z): Z = np.asarray(Z) basis = self.get_V(Z.shape[-1] + 1) logp = Z @ basis @@ -1169,7 +1211,7 @@ [docs] @lru_cache(maxsize=None) - def get_V(self, k): + def get_V(self, k): helmert = np.zeros((k, k)) for i in range(1, k): helmert[i, :i] = 1 @@ -1182,7 +1224,7 @@ [docs] -def normalized_entropy(p): +def normalized_entropy(p): """ Computes the normalized Shannon entropy of a prevalence vector. @@ -1198,7 +1240,7 @@ [docs] -def antagonistic_prevalence(p, strength=1): +def antagonistic_prevalence(p, strength=1): """ Reflects a prevalence vector in ILR space and maps it back to the simplex. @@ -1215,7 +1257,7 @@ [docs] -def in_simplex(x, atol=1e-8): +def in_simplex(x, atol=1e-8): """ Checks whether points lie in the probability simplex. diff --git a/docs/build/html/_modules/quapy/method/_kdey.html b/docs/build/html/_modules/quapy/method/_kdey.html index 6664ea9..3367861 100644 --- a/docs/build/html/_modules/quapy/method/_kdey.html +++ b/docs/build/html/_modules/quapy/method/_kdey.html @@ -29,8 +29,9 @@ - - + + + @@ -41,6 +42,7 @@ + @@ -114,8 +116,14 @@ + + + + + + + - QuaPy @@ -131,7 +139,7 @@ - Quickstart + Home @@ -183,6 +191,21 @@ + + + + + + + + + + + GitHub + + + @@ -229,7 +252,7 @@ - Quickstart + Home @@ -272,6 +295,21 @@ + + + + + + + + + + + GitHub + + + @@ -332,22 +370,22 @@ Source code for quapy.method._kdey -import numpy as np -from numbers import Real -from sklearn.base import BaseEstimator -from sklearn.neighbors import KernelDensity +import numpy as np +from numbers import Real +from sklearn.base import BaseEstimator +from sklearn.neighbors import KernelDensity -import quapy as qp -from quapy.method._helper import _labels_to_indices -from quapy.method.aggregative import AggregativeSoftQuantifier -import quapy.functional as F -from scipy.special import logsumexp -from sklearn.metrics.pairwise import rbf_kernel +import quapy as qp +from quapy.method._helper import _labels_to_indices +from quapy.method.aggregative import AggregativeSoftQuantifier +import quapy.functional as F +from scipy.special import logsumexp +from sklearn.metrics.pairwise import rbf_kernel [docs] -class KDEBase: +class KDEBase: """ Common ancestor for KDE-based methods. Implements some common routines. """ @@ -356,7 +394,7 @@ KERNELS = ['gaussian', 'aitchison', 'ilr'] @classmethod - def _check_bandwidth(cls, bandwidth, kernel): + def _check_bandwidth(cls, bandwidth, kernel): """ Checks that the bandwidth parameter is correct @@ -370,13 +408,13 @@ return bandwidth @classmethod - def _check_kernel(cls, kernel): + def _check_kernel(cls, kernel): assert kernel in KDEBase.KERNELS, f'unknown {kernel=}' return kernel [docs] - def get_kde_function(self, X, bandwidth, kernel): + def get_kde_function(self, X, bandwidth, kernel): """ Wraps the KDE function from scikit-learn. @@ -392,7 +430,7 @@ [docs] - def pdf(self, kde, X, kernel, log_densities=False): + def pdf(self, kde, X, kernel, log_densities=False): """ Wraps the density evalution of scikit-learn's KDE. Scikit-learn returns log-scores (s), so this function returns :math:`e^{s}` @@ -411,7 +449,7 @@ [docs] - def get_mixture_components(self, X, y, classes, bandwidth, kernel): + def get_mixture_components(self, X, y, classes, bandwidth, kernel): """ Returns an array containing the mixture components, i.e., the KDE functions for each class. @@ -433,7 +471,7 @@ [docs] - def transform_posteriors(self, X, kernel): + def transform_posteriors(self, X, kernel): if kernel in {'aitchison', 'ilr'}: X = self.shrink_posteriors(X) if kernel == 'aitchison': @@ -445,7 +483,7 @@ [docs] - def shrink_posteriors(self, X): + def shrink_posteriors(self, X): shrinkage = getattr(self, 'shrinkage', 0.0) if shrinkage <= 0: return X @@ -457,7 +495,7 @@ [docs] - def effective_bandwidth(self, bandwidth, kernel): + def effective_bandwidth(self, bandwidth, kernel): shrinkage = getattr(self, 'shrinkage', 0.0) if shrinkage > 0 and kernel in {'aitchison', 'ilr'} and isinstance(bandwidth, Real): return (1.0 - shrinkage) * float(bandwidth) @@ -466,7 +504,7 @@ [docs] - def clr_transform(self, X): + def clr_transform(self, X): if not hasattr(self, 'clr'): self.clr = F.CLRtransformation() return self.clr(X) @@ -474,7 +512,7 @@ [docs] - def ilr_transform(self, X): + def ilr_transform(self, X): if not hasattr(self, 'ilr'): self.ilr = F.ILRtransformation() return self.ilr(X) @@ -484,7 +522,7 @@ [docs] -class KDEyML(AggregativeSoftQuantifier, KDEBase): +class KDEyML(AggregativeSoftQuantifier, KDEBase): """ Kernel Density Estimation model for quantification (KDEy) relying on the Kullback-Leibler divergence (KLD) as the divergence measure to be minimized. This method was first proposed in the paper @@ -526,7 +564,7 @@ :param random_state: a seed to be set before fitting any base quantifier (default None) """ - def __init__(self, classifier: BaseEstimator=None, fit_classifier=True, val_split=5, bandwidth=0.1, + def __init__(self, classifier: BaseEstimator=None, fit_classifier=True, val_split=5, bandwidth=0.1, kernel='gaussian', shrinkage=0.0, random_state=None): super().__init__(classifier, fit_classifier, val_split) self.bandwidth = KDEBase._check_bandwidth(bandwidth, kernel) @@ -539,7 +577,7 @@ [docs] - def aggregation_fit(self, classif_predictions, labels): + def aggregation_fit(self, classif_predictions, labels): self.mix_densities = self.get_mixture_components( classif_predictions, labels, @@ -552,7 +590,7 @@ [docs] - def aggregate(self, posteriors: np.ndarray): + def aggregate(self, posteriors: np.ndarray): """ Searches for the mixture model parameter (the sought prevalence values) that maximizes the likelihood of the data (i.e., that minimizes the negative log-likelihood) @@ -569,14 +607,14 @@ for kde_i in self.mix_densities ] - def neg_loglikelihood(prev): + def neg_loglikelihood(prev): prev = qp.error.smooth(prev, eps=epsilon) test_loglikelihood = logsumexp(np.log(prev)[:, None] + test_log_densities, axis=0) return -np.sum(test_loglikelihood) else: test_densities = [self.pdf(kde_i, posteriors, self.kernel) for kde_i in self.mix_densities] - def neg_loglikelihood(prev): + def neg_loglikelihood(prev): test_mixture_likelihood = prev @ test_densities test_loglikelihood = np.log(test_mixture_likelihood + epsilon) return -np.sum(test_loglikelihood) @@ -588,7 +626,7 @@ [docs] -class KDEyHD(AggregativeSoftQuantifier, KDEBase): +class KDEyHD(AggregativeSoftQuantifier, KDEBase): """ Kernel Density Estimation model for quantification (KDEy) relying on the squared Hellinger Disntace (HD) as the divergence measure to be minimized. This method was first proposed in the paper @@ -633,7 +671,7 @@ :param montecarlo_trials: number of Monte Carlo trials (default 10000) """ - def __init__(self, classifier: BaseEstimator=None, fit_classifier=True, val_split=5, divergence: str='HD', + def __init__(self, classifier: BaseEstimator=None, fit_classifier=True, val_split=5, divergence: str='HD', bandwidth=0.1, random_state=None, montecarlo_trials=10000): super().__init__(classifier, fit_classifier, val_split) @@ -644,7 +682,7 @@ [docs] - def aggregation_fit(self, classif_predictions, labels): + def aggregation_fit(self, classif_predictions, labels): self.mix_densities = self.get_mixture_components( classif_predictions, labels, self.classes_, self.bandwidth, 'gaussian' ) @@ -663,7 +701,7 @@ [docs] - def aggregate(self, posteriors: np.ndarray): + def aggregate(self, posteriors: np.ndarray): # we retain all n*N examples (sampled from a mixture with uniform parameter), and then # apply importance sampling (IS). In this version we compute D(p_alpha||q) with IS n_classes = len(self.mix_densities) @@ -671,7 +709,7 @@ test_kde = self.get_kde_function(posteriors, self.bandwidth, 'gaussian') test_densities = self.pdf(test_kde, self.reference_samples, 'gaussian') - def f_squared_hellinger(u): + def f_squared_hellinger(u): return (np.sqrt(u)-1)**2 # todo: this will fail when self.divergence is a callable, and is not the right place to do it anyway @@ -687,7 +725,7 @@ p_class = self.reference_classwise_densities + epsilon fracs = p_class/qs - def divergence(prev): + def divergence(prev): # ps / qs = (prev @ p_class) / qs = prev @ (p_class / qs) = prev @ fracs ps_div_qs = prev @ fracs return np.mean( f(ps_div_qs) * iw ) @@ -699,7 +737,7 @@ [docs] -class KDEyCS(AggregativeSoftQuantifier): +class KDEyCS(AggregativeSoftQuantifier): """ Kernel Density Estimation model for quantification (KDEy) relying on the Cauchy-Schwarz divergence (CS) as the divergence measure to be minimized. This method was first proposed in the paper @@ -736,13 +774,13 @@ :param bandwidth: float, the bandwidth of the Kernel """ - def __init__(self, classifier: BaseEstimator=None, fit_classifier=True, val_split=5, bandwidth=0.1): + def __init__(self, classifier: BaseEstimator=None, fit_classifier=True, val_split=5, bandwidth=0.1): super().__init__(classifier, fit_classifier, val_split) self.bandwidth = KDEBase._check_bandwidth(bandwidth, kernel='gaussian') [docs] - def gram_matrix_mix_sum(self, X, Y=None): + def gram_matrix_mix_sum(self, X, Y=None): # this adapts the output of the rbf_kernel function (pairwise evaluations of Gaussian kernels k(x,y)) # to contain pairwise evaluations of N(x|mu,Sigma1+Sigma2) with mu=y and Sigma1 and Sigma2 are # two "scalar matrices" (h^2)*I each, so Sigma1+Sigma2 has scalar 2(h^2) (h is the bandwidth) @@ -757,7 +795,7 @@ [docs] - def aggregation_fit(self, classif_predictions, labels): + def aggregation_fit(self, classif_predictions, labels): P, y = classif_predictions, labels n = len(self.classes_) @@ -789,7 +827,7 @@ [docs] - def aggregate(self, posteriors: np.ndarray): + def aggregate(self, posteriors: np.ndarray): Ptr = self.Ptr Pte = posteriors y = self.ytr @@ -809,7 +847,7 @@ for i in range(n): tr_te_sums[i] = self.gram_matrix_mix_sum(Ptr[y==i], Pte) - def divergence(alpha): + def divergence(alpha): # called \overline{r} in the paper alpha_ratio = alpha * self.counts_inv diff --git a/docs/build/html/_modules/quapy/method/_neural.html b/docs/build/html/_modules/quapy/method/_neural.html index 3e6c7c3..3c8215e 100644 --- a/docs/build/html/_modules/quapy/method/_neural.html +++ b/docs/build/html/_modules/quapy/method/_neural.html @@ -29,7 +29,7 @@ - + @@ -370,23 +370,24 @@ Source code for quapy.method._neural -import os -from pathlib import Path -import random +import logging +import os +from pathlib import Path +import random -import torch -from torch.nn import MSELoss -from torch.nn.functional import relu +import torch +from torch.nn import MSELoss +from torch.nn.functional import relu -from quapy.protocol import UPP -from quapy.method.aggregative import * -from quapy.util import EarlyStop -from tqdm import tqdm +from quapy.protocol import UPP +from quapy.method.aggregative import * +from quapy.util import EarlyStop +from tqdm import tqdm [docs] -class QuaNetTrainer(BaseQuantifier): +class QuaNetTrainer(BaseQuantifier): """ Implementation of `QuaNet <https://dl.acm.org/doi/abs/10.1145/3269206.3269287>`_, a neural network for quantification. This implementation uses `PyTorch <https://pytorch.org/>`_ and can take advantage of GPU @@ -438,7 +439,7 @@ :param device: string, indicate "cpu" or "cuda" """ - def __init__(self, + def __init__(self, classifier, fit_classifier=True, sample_size=None, @@ -491,7 +492,7 @@ [docs] - def fit(self, X, y): + def fit(self, X, y): """ Trains QuaNet. @@ -549,7 +550,7 @@ order_by=0 if data.binary else None, **self.quanet_params ).to(self.device) - print(self.quanet) + logging.getLogger(__name__).debug(self.quanet) self.optim = torch.optim.Adam(self.quanet.parameters(), lr=self.lr) early_stop = EarlyStop(self.patience, lower_is_better=True) @@ -564,15 +565,16 @@ if early_stop.IMPROVED: torch.save(self.quanet.state_dict(), checkpoint) elif early_stop.STOP: - print(f'training ended by patience exhausted; loading best model parameters in {checkpoint} ' - f'for epoch {early_stop.best_epoch}') + logging.getLogger(__name__).info( + f'training ended by patience exhausted; loading best model parameters in {checkpoint} ' + f'for epoch {early_stop.best_epoch}') self.quanet.load_state_dict(torch.load(checkpoint)) break return self - def _get_aggregative_estims(self, posteriors): + def _get_aggregative_estims(self, posteriors): label_predictions = np.argmax(posteriors, axis=-1) prevs_estim = [] for quantifier in self.quantifiers.values(): @@ -585,7 +587,7 @@ [docs] - def predict(self, X): + def predict(self, X): posteriors = self.classifier.predict_proba(X) embeddings = self.classifier.transform(X) quant_estims = self._get_aggregative_estims(posteriors) @@ -598,7 +600,7 @@ return prevalence - def _epoch(self, data: LabelledCollection, posteriors, iterations, epoch, early_stop, train): + def _epoch(self, data: LabelledCollection, posteriors, iterations, epoch, early_stop, train): mse_loss = MSELoss() self.quanet.train(mode=train) @@ -650,7 +652,7 @@ [docs] - def get_params(self, deep=True): + def get_params(self, deep=True): classifier_params = self.classifier.get_params() classifier_params = {'classifier__'+k:v for k,v in classifier_params.items()} return {**classifier_params, **self.quanet_params} @@ -658,7 +660,7 @@ [docs] - def set_params(self, **parameters): + def set_params(self, **parameters): learner_params = {} for key, val in parameters.items(): if key in self.quanet_params: @@ -670,7 +672,7 @@ self.classifier.set_params(**learner_params) - def __check_params_colision(self, quanet_params, learner_params): + def __check_params_colision(self, quanet_params, learner_params): quanet_keys = set(quanet_params.keys()) learner_keys = set(learner_params.keys()) intersection = quanet_keys.intersection(learner_keys) @@ -680,7 +682,7 @@ [docs] - def clean_checkpoint(self): + def clean_checkpoint(self): """ Removes the checkpoint """ @@ -689,23 +691,23 @@ [docs] - def clean_checkpoint_dir(self): + def clean_checkpoint_dir(self): """ Removes anything contained in the checkpoint directory """ - import shutil + import shutil shutil.rmtree(self.checkpointdir, ignore_errors=True) @property - def classes_(self): + def classes_(self): return self._classes_ [docs] -def mae_loss(output, target): +def mae_loss(output, target): """ Torch-like wrapper for the Mean Absolute Error @@ -719,7 +721,7 @@ [docs] -class QuaNetModule(torch.nn.Module): +class QuaNetModule(torch.nn.Module): """ Implements the `QuaNet <https://dl.acm.org/doi/abs/10.1145/3269206.3269287>`_ forward pass. See :class:`QuaNetTrainer` for training QuaNet. @@ -736,7 +738,7 @@ :param order_by: integer, class for which the document embeddings are to be sorted """ - def __init__(self, + def __init__(self, doc_embedding_size, n_classes, stats_size, @@ -771,10 +773,10 @@ self.output = torch.nn.Linear(prev_size, n_classes) @property - def device(self): + def device(self): return torch.device('cuda') if next(self.parameters()).is_cuda else torch.device('cpu') - def _init_hidden(self): + def _init_hidden(self): directions = 2 if self.bidirectional else 1 var_hidden = torch.zeros(self.nlayers * directions, 1, self.hidden_size) var_cell = torch.zeros(self.nlayers * directions, 1, self.hidden_size) @@ -784,7 +786,7 @@ [docs] - def forward(self, doc_embeddings, doc_posteriors, statistics): + def forward(self, doc_embeddings, doc_posteriors, statistics): device = self.device doc_embeddings = torch.as_tensor(doc_embeddings, dtype=torch.float, device=device) doc_posteriors = torch.as_tensor(doc_posteriors, dtype=torch.float, device=device) diff --git a/docs/build/html/_modules/quapy/method/_threshold_optim.html b/docs/build/html/_modules/quapy/method/_threshold_optim.html index 6bb8003..304e087 100644 --- a/docs/build/html/_modules/quapy/method/_threshold_optim.html +++ b/docs/build/html/_modules/quapy/method/_threshold_optim.html @@ -1,92 +1,388 @@ - - - - - - quapy.method._threshold_optim — QuaPy: A Python-based open-source framework for quantification 0.2.1 documentation - - + + + + + + + + quapy.method._threshold_optim — QuaPy: A Python-based open-source framework for quantification 0.2.1 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Skip to main content - - - - - - - - - + + + + Back to top + + + + + + + + + + Ctrl+K + + - - - - - - - - - - QuaPy: A Python-based open-source framework for quantification - - - - - - - + + - - -Quickstart - - -Manuals - - -API - - - - + + + + + + + + + + + - - - QuaPy: A Python-based open-source framework for quantification - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Home + + + + + + + Manuals + + + + + + + API + + - - - - - - Module code - quapy.method._threshold_optim - - - - - - - - Source code for quapy.method._threshold_optim -from abc import abstractmethod + + + + + + + + + -import numpy as np -from sklearn.base import BaseEstimator -import quapy as qp -import quapy.functional as F -from quapy.data import LabelledCollection -from quapy.method.aggregative import BinaryAggregativeQuantifier + + + Search + Ctrl+K + + + + + + + + + + + + + + System Settings + Light + Dark + + + + + + + + + + + + + + GitHub + + + + + + + + + + + + + Search + Ctrl+K + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Home + + + + + + + Manuals + + + + + + + API + + + + + + + + + + + + + + + + + + + + + + + System Settings + Light + Dark + + + + + + + + + + + + + + GitHub + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Module code + + quapy.method._threshold_optim + + + + + + + + + + + + + + + + + Source code for quapy.method._threshold_optim +from abc import abstractmethod + +import numpy as np +from sklearn.base import BaseEstimator +import quapy as qp +import quapy.functional as F +from quapy.data import LabelledCollection +from quapy.method.aggregative import BinaryAggregativeQuantifier [docs] -class ThresholdOptimization(BinaryAggregativeQuantifier): +class ThresholdOptimization(BinaryAggregativeQuantifier): """ Abstract class of Threshold Optimization variants for :class:`ACC` as proposed by `Forman 2006 <https://dl.acm.org/doi/abs/10.1145/1150402.1150423>`_ and @@ -111,14 +407,14 @@ :param n_jobs: number of parallel workers """ - def __init__(self, classifier: BaseEstimator=None, fit_classifier=True, val_split=None, n_jobs=None): + def __init__(self, classifier: BaseEstimator=None, fit_classifier=True, val_split=None, n_jobs=None): super().__init__(classifier, fit_classifier, val_split) self.n_jobs = qp._get_njobs(n_jobs) [docs] @abstractmethod - def condition(self, tpr, fpr) -> float: + def condition(self, tpr, fpr) -> float: """ Implements the criterion according to which the threshold should be selected. This function should return the (float) score to be minimized. @@ -132,7 +428,7 @@ [docs] - def discard(self, tpr, fpr) -> bool: + def discard(self, tpr, fpr) -> bool: """ Indicates whether a combination of tpr and fpr should be discarded @@ -144,7 +440,7 @@ - def _eval_candidate_thresholds(self, decision_scores, y): + def _eval_candidate_thresholds(self, decision_scores, y): """ Seeks for the best `tpr` and `fpr` according to the score obtained at different decision thresholds. The scoring function is implemented in function `_condition`. @@ -181,7 +477,7 @@ [docs] - def aggregate_with_threshold(self, classif_predictions, tprs, fprs, thresholds): + def aggregate_with_threshold(self, classif_predictions, tprs, fprs, thresholds): # This function performs the adjusted count for given tpr, fpr, and threshold. # Note that, due to broadcasting, tprs, fprs, and thresholds could be arrays of length > 1 prevs_estims = np.mean(classif_predictions[:, None] >= thresholds, axis=0) @@ -190,26 +486,26 @@ return prevs_estims.squeeze() - def _compute_table(self, y, y_): + def _compute_table(self, y, y_): TP = np.logical_and(y == y_, y == self.pos_label).sum() FP = np.logical_and(y != y_, y == self.neg_label).sum() FN = np.logical_and(y != y_, y == self.pos_label).sum() TN = np.logical_and(y == y_, y == self.neg_label).sum() return TP, FP, FN, TN - def _compute_tpr(self, TP, FP): - if TP + FP == 0: + def _compute_tpr(self, TP, FN): + if TP + FN == 0: return 1 - return TP / (TP + FP) + return TP / (TP + FN) - def _compute_fpr(self, FP, TN): + def _compute_fpr(self, FP, TN): if FP + TN == 0: return 0 return FP / (FP + TN) [docs] - def aggregation_fit(self, classif_predictions, labels): + def aggregation_fit(self, classif_predictions, labels): decision_scores, y = classif_predictions, labels # the standard behavior is to keep the best threshold only self.tpr, self.fpr, self.threshold = self._eval_candidate_thresholds(decision_scores, y)[0] @@ -218,7 +514,7 @@ [docs] - def aggregate(self, classif_predictions: np.ndarray): + def aggregate(self, classif_predictions: np.ndarray): # the standard behavior is to compute the adjusted count using the best threshold found return self.aggregate_with_threshold(classif_predictions, self.tpr, self.fpr, self.threshold) @@ -227,7 +523,7 @@ [docs] -class T50(ThresholdOptimization): +class T50(ThresholdOptimization): """ Threshold Optimization variant for :class:`ACC` as proposed by `Forman 2006 <https://dl.acm.org/doi/abs/10.1145/1150402.1150423>`_ and @@ -249,12 +545,12 @@ """ - def __init__(self, classifier: BaseEstimator=None, fit_classifier=True, val_split=5): + def __init__(self, classifier: BaseEstimator=None, fit_classifier=True, val_split=5): super().__init__(classifier, fit_classifier, val_split) [docs] - def condition(self, tpr, fpr) -> float: + def condition(self, tpr, fpr) -> float: return abs(tpr - 0.5) @@ -262,7 +558,7 @@ [docs] -class MAX(ThresholdOptimization): +class MAX(ThresholdOptimization): """ Threshold Optimization variant for :class:`ACC` as proposed by `Forman 2006 <https://dl.acm.org/doi/abs/10.1145/1150402.1150423>`_ and @@ -282,12 +578,12 @@ """ - def __init__(self, classifier: BaseEstimator=None, fit_classifier=True, val_split=5): + def __init__(self, classifier: BaseEstimator=None, fit_classifier=True, val_split=5): super().__init__(classifier, fit_classifier, val_split) [docs] - def condition(self, tpr, fpr) -> float: + def condition(self, tpr, fpr) -> float: # MAX strives to maximize (tpr - fpr), which is equivalent to minimize (fpr - tpr) return (fpr - tpr) @@ -296,7 +592,7 @@ [docs] -class X(ThresholdOptimization): +class X(ThresholdOptimization): """ Threshold Optimization variant for :class:`ACC` as proposed by `Forman 2006 <https://dl.acm.org/doi/abs/10.1145/1150402.1150423>`_ and @@ -316,12 +612,12 @@ """ - def __init__(self, classifier: BaseEstimator=None, fit_classifier=True, val_split=5): + def __init__(self, classifier: BaseEstimator=None, fit_classifier=True, val_split=5): super().__init__(classifier, fit_classifier, val_split) [docs] - def condition(self, tpr, fpr) -> float: + def condition(self, tpr, fpr) -> float: return abs(1 - (tpr + fpr)) @@ -329,7 +625,7 @@ [docs] -class MS(ThresholdOptimization): +class MS(ThresholdOptimization): """ Median Sweep. Threshold Optimization variant for :class:`ACC` as proposed by `Forman 2006 <https://dl.acm.org/doi/abs/10.1145/1150402.1150423>`_ and @@ -348,18 +644,18 @@ for `k`); or as a tuple (X,y) defining the specific set of data to use for validation. """ - def __init__(self, classifier: BaseEstimator=None, fit_classifier=True, val_split=5): + def __init__(self, classifier: BaseEstimator=None, fit_classifier=True, val_split=5): super().__init__(classifier, fit_classifier, val_split) [docs] - def condition(self, tpr, fpr) -> float: + def condition(self, tpr, fpr) -> float: return 1 [docs] - def aggregation_fit(self, classif_predictions, labels): + def aggregation_fit(self, classif_predictions, labels): decision_scores, y = classif_predictions, labels # keeps all candidates tprs_fprs_thresholds = self._eval_candidate_thresholds(decision_scores, y) @@ -371,7 +667,7 @@ [docs] - def aggregate(self, classif_predictions: np.ndarray): + def aggregate(self, classif_predictions: np.ndarray): prevalences = self.aggregate_with_threshold(classif_predictions, self.tprs, self.fprs, self.thresholds) if prevalences.ndim==2: prevalences = np.median(prevalences, axis=0) @@ -382,7 +678,7 @@ [docs] -class MS2(MS): +class MS2(MS): """ Median Sweep 2. Threshold Optimization variant for :class:`ACC` as proposed by `Forman 2006 <https://dl.acm.org/doi/abs/10.1145/1150402.1150423>`_ and @@ -402,42 +698,86 @@ for `k`); or as a tuple (X,y) defining the specific set of data to use for validation. """ - def __init__(self, classifier: BaseEstimator=None, fit_classifier=True, val_split=5): + def __init__(self, classifier: BaseEstimator=None, fit_classifier=True, val_split=5): super().__init__(classifier, fit_classifier, val_split) [docs] - def discard(self, tpr, fpr) -> bool: + def discard(self, tpr, fpr) -> bool: return (tpr-fpr) <= 0.25 - + + + + + + + + + + + + + - + + + + - Built with Sphinx using a - theme - provided by Read the Docs. - + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/build/html/_modules/quapy/method/aggregative.html b/docs/build/html/_modules/quapy/method/aggregative.html index a6b98d4..0680ac3 100644 --- a/docs/build/html/_modules/quapy/method/aggregative.html +++ b/docs/build/html/_modules/quapy/method/aggregative.html @@ -29,8 +29,9 @@ - - + + + @@ -41,6 +42,7 @@ + @@ -114,8 +116,14 @@ + + + + + + + - QuaPy @@ -131,7 +139,7 @@ - Quickstart + Home @@ -183,6 +191,21 @@ + + + + + + + + + + + GitHub + + + @@ -229,7 +252,7 @@ - Quickstart + Home @@ -272,6 +295,21 @@ + + + + + + + + + + + GitHub + + + @@ -332,25 +370,25 @@ Source code for quapy.method.aggregative -from abc import ABC, abstractmethod -from argparse import ArgumentError -from copy import deepcopy -from typing import Callable, Literal, Union -import numpy as np -from sklearn.base import BaseEstimator -from sklearn.calibration import CalibratedClassifierCV -from sklearn.exceptions import NotFittedError -from sklearn.metrics import confusion_matrix -from sklearn.model_selection import cross_val_predict, train_test_split -from sklearn.utils.validation import check_is_fitted +import warnings +from abc import ABC, abstractmethod +from copy import deepcopy +from typing import Callable, Literal, Union +import numpy as np +from sklearn.base import BaseEstimator +from sklearn.calibration import CalibratedClassifierCV +from sklearn.exceptions import NotFittedError +from sklearn.metrics import confusion_matrix +from sklearn.model_selection import cross_val_predict, train_test_split +from sklearn.utils.validation import check_is_fitted -import quapy as qp -import quapy.functional as F -from quapy.functional import get_divergence -from quapy.classification.svmperf import SVMperf -from quapy.data import LabelledCollection -from quapy.method.base import BaseQuantifier, BinaryQuantifier, OneVsAllGeneric -from quapy.method._helper import ( +import quapy as qp +import quapy.functional as F +from quapy.functional import get_divergence +from quapy.classification.svmperf import SVMperf +from quapy.data import LabelledCollection +from quapy.method.base import BaseQuantifier, BinaryQuantifier, OneVsAllGeneric +from quapy.method._helper import ( _get_abstention_calibrators, _get_cvxpy, _rlls_check_mode, @@ -368,7 +406,7 @@ [docs] -class AggregativeQuantifier(BaseQuantifier, ABC): +class AggregativeQuantifier(BaseQuantifier, ABC): """ Abstract class for quantification methods that base their estimations on the aggregation of classification results. Aggregative quantifiers implement a pipeline that consists of generating classification predictions @@ -396,7 +434,7 @@ the training data be wasted. """ - def __init__(self, + def __init__(self, classifier: Union[None,BaseEstimator], fit_classifier:bool=True, val_split:Union[int,float,tuple,None]=5): @@ -418,9 +456,9 @@ (f'when {val_split=} is indicated as an integer, it represents the number of folds in a kFCV ' f'and must thus be >1') if val_split==5 and not fit_classifier: - print(f'Warning: {val_split=} will be ignored when the classifier is already trained ' - f'({fit_classifier=}). Parameter {self.val_split=} will be set to None. Set {val_split=} ' - f'to None to avoid this warning.') + warnings.warn(f'{val_split=} will be ignored when the classifier is already trained ' + f'({fit_classifier=}). Parameter {self.val_split=} will be set to None. Set {val_split=} ' + f'to None to avoid this warning.') self.val_split=None if val_split!=5: assert fit_classifier, (f'Parameter {val_split=} has been modified, but {fit_classifier=} ' @@ -457,7 +495,7 @@ assert fitted, (f'{fit_classifier=} requires the classifier to be already trained, ' f'but this does not seem to be') - def _check_init_parameters(self): + def _check_init_parameters(self): """ Implements any check to be performed in the parameters of the init method before undertaking the training of the quantifier. This is made as to allow for a quick execution stop when the @@ -467,7 +505,7 @@ """ pass - def _check_non_empty_classes(self, y): + def _check_non_empty_classes(self, y): """ Asserts all classes have positive instances. @@ -484,7 +522,7 @@ [docs] - def fit(self, X, y): + def fit(self, X, y): """ Trains the aggregative quantifier. This comes down to training a classifier (if requested) and an aggregation function. @@ -501,7 +539,7 @@ [docs] - def classifier_fit_predict(self, X, y): + def classifier_fit_predict(self, X, y): """ Trains the classifier if requested (`fit_classifier=True`) and generate the necessary predictions to train the aggregation function. @@ -551,7 +589,7 @@ [docs] @abstractmethod - def aggregation_fit(self, classif_predictions, labels): + def aggregation_fit(self, classif_predictions, labels): """ Trains the aggregation function. @@ -563,7 +601,7 @@ @property - def classifier(self): + def classifier(self): """ Gives access to the classifier @@ -572,7 +610,7 @@ return self.classifier_ @classifier.setter - def classifier(self, classifier): + def classifier(self, classifier): """ Setter for the classifier @@ -582,7 +620,7 @@ [docs] - def classify(self, X): + def classify(self, X): """ Provides the label predictions for the given instances. The predictions should respect the format expected by :meth:`aggregate`, e.g., posterior probabilities for probabilistic quantifiers, or crisp predictions for @@ -594,7 +632,7 @@ return getattr(self.classifier, self._classifier_method())(X) - def _classifier_method(self): + def _classifier_method(self): """ Name of the method that must be used for issuing label predictions. The default one is "decision_function". @@ -602,7 +640,7 @@ """ return 'decision_function' - def _check_classifier(self, adapt_if_necessary=False): + def _check_classifier(self, adapt_if_necessary=False): """ Guarantees that the underlying classifier implements the method required for issuing predictions, i.e., the method indicated by the :meth:`_classifier_method` @@ -614,7 +652,7 @@ [docs] - def predict(self, X): + def predict(self, X): """ Generate class prevalence estimates for the sample's instances by aggregating the label predictions generated by the classifier. @@ -629,7 +667,7 @@ [docs] @abstractmethod - def aggregate(self, classif_predictions: np.ndarray): + def aggregate(self, classif_predictions: np.ndarray): """ Implements the aggregation of the classifier predictions. @@ -640,7 +678,7 @@ @property - def classes_(self): + def classes_(self): """ Class labels, in the same order in which class prevalence values are to be computed. This default implementation actually returns the class labels of the learner. @@ -653,14 +691,14 @@ [docs] -class AggregativeCrispQuantifier(AggregativeQuantifier, ABC): +class AggregativeCrispQuantifier(AggregativeQuantifier, ABC): """ Abstract class for quantification methods that base their estimations on the aggregation of crisp decisions as returned by a hard classifier. Aggregative crisp quantifiers thus extend Aggregative Quantifiers by implementing specifications about crisp predictions. """ - def _classifier_method(self): + def _classifier_method(self): """ Name of the method that must be used for issuing label predictions. For crisp quantifiers, the method is 'predict', that returns an array of shape `(n_instances,)` of label predictions. @@ -673,7 +711,7 @@ [docs] -class AggregativeSoftQuantifier(AggregativeQuantifier, ABC): +class AggregativeSoftQuantifier(AggregativeQuantifier, ABC): """ Abstract class for quantification methods that base their estimations on the aggregation of posterior probabilities as returned by a probabilistic classifier. @@ -681,7 +719,7 @@ about soft predictions. """ - def _classifier_method(self): + def _classifier_method(self): """ Name of the method that must be used for issuing label predictions. For probabilistic quantifiers, the method is 'predict_proba', that returns an array of shape `(n_instances, n_dimensions,)` with posterior @@ -691,7 +729,7 @@ """ return 'predict_proba' - def _check_classifier(self, adapt_if_necessary=False): + def _check_classifier(self, adapt_if_necessary=False): """ Guarantees that the underlying classifier implements the method indicated by the :meth:`_classifier_method`. In case it does not, the classifier is calibrated (by means of the Platt's calibration method implemented by @@ -703,8 +741,8 @@ """ if not hasattr(self.classifier, self._classifier_method()): if adapt_if_necessary: - print(f'warning: The learner {self.classifier.__class__.__name__} does not seem to be ' - f'probabilistic. The learner will be calibrated (using CalibratedClassifierCV).') + warnings.warn(f'The learner {self.classifier.__class__.__name__} does not seem to be ' + f'probabilistic. The learner will be calibrated (using CalibratedClassifierCV).') self.classifier = CalibratedClassifierCV(self.classifier, cv=5) else: raise AssertionError(f'error: The learner {self.classifier.__class__.__name__} does not ' @@ -715,19 +753,19 @@ [docs] -class BinaryAggregativeQuantifier(AggregativeQuantifier, BinaryQuantifier): +class BinaryAggregativeQuantifier(AggregativeQuantifier, BinaryQuantifier): @property - def pos_label(self): + def pos_label(self): return self.classifier.classes_[1] @property - def neg_label(self): + def neg_label(self): return self.classifier.classes_[0] [docs] - def fit(self, X, y): + def fit(self, X, y): self._check_binary(y, self.__class__.__name__) return super().fit(X, y) @@ -738,19 +776,19 @@ # ------------------------------------ [docs] -class CC(AggregativeCrispQuantifier): +class CC(AggregativeCrispQuantifier): """ The most basic Quantification method. One that simply classifies all instances and counts how many have been attributed to each of the classes in order to compute class prevalence estimates. :param classifier: a sklearn's Estimator that generates a classifier """ - def __init__(self, classifier: BaseEstimator = None, fit_classifier: bool = True): + def __init__(self, classifier: BaseEstimator = None, fit_classifier: bool = True): super().__init__(classifier, fit_classifier, val_split=None) [docs] - def aggregation_fit(self, classif_predictions, labels): + def aggregation_fit(self, classif_predictions, labels): """ Nothing to do here! @@ -762,7 +800,7 @@ [docs] - def aggregate(self, classif_predictions: np.ndarray): + def aggregate(self, classif_predictions: np.ndarray): """ Computes class prevalence estimates by counting the prevalence of each of the predicted labels. @@ -776,7 +814,7 @@ [docs] -class PCC(AggregativeSoftQuantifier): +class PCC(AggregativeSoftQuantifier): """ `Probabilistic Classify & Count <https://ieeexplore.ieee.org/abstract/document/5694031>`_, the probabilistic variant of CC that relies on the posterior probabilities returned by a probabilistic classifier. @@ -784,12 +822,12 @@ :param classifier: a sklearn's Estimator that generates a classifier """ - def __init__(self, classifier: BaseEstimator = None, fit_classifier: bool = True, val_split=None): + def __init__(self, classifier: BaseEstimator = None, fit_classifier: bool = True, val_split=None): super().__init__(classifier, fit_classifier, val_split=val_split) [docs] - def aggregation_fit(self, classif_predictions, labels): + def aggregation_fit(self, classif_predictions, labels): """ Nothing to do here! @@ -801,7 +839,7 @@ [docs] - def aggregate(self, classif_posteriors): + def aggregate(self, classif_posteriors): return F.prevalence_from_probabilities(classif_posteriors, binarize=False) @@ -809,7 +847,7 @@ [docs] -class ACC(AggregativeCrispQuantifier): +class ACC(AggregativeCrispQuantifier): """ `Adjusted Classify & Count <https://link.springer.com/article/10.1007/s10618-008-0097-y>`_, the "adjusted" variant of :class:`CC`, that corrects the predictions of CC @@ -857,7 +895,7 @@ :param n_jobs: number of parallel workers """ - def __init__( + def __init__( self, classifier: BaseEstimator = None, fit_classifier = True, @@ -880,7 +918,7 @@ [docs] @classmethod - def newInvariantRatioEstimation(cls, classifier: BaseEstimator, fit_classifier=True, val_split=5, n_jobs=None): + def newInvariantRatioEstimation(cls, classifier: BaseEstimator, fit_classifier=True, val_split=5, n_jobs=None): """ Constructs a quantifier that implements the Invariant Ratio Estimator of `Vaz et al. 2018 <https://jmlr.org/papers/v20/18-456.html>`_. This amounts @@ -905,7 +943,7 @@ return ACC(classifier, fit_classifier=fit_classifier, val_split=val_split, method='invariant-ratio', norm='mapsimplex', n_jobs=n_jobs) - def _check_init_parameters(self): + def _check_init_parameters(self): if self.solver not in ACC.SOLVERS: raise ValueError(f"unknown solver; valid ones are {ACC.SOLVERS}") if self.method not in ACC.METHODS: @@ -915,7 +953,7 @@ [docs] - def aggregation_fit(self, classif_predictions, labels): + def aggregation_fit(self, classif_predictions, labels): """ Estimates the misclassification rates. :param classif_predictions: array-like with the predicted labels @@ -930,7 +968,7 @@ [docs] @classmethod - def getPteCondEstim(cls, classes, y, y_): + def getPteCondEstim(cls, classes, y, y_): """ Estimate the matrix with entry (i,j) being the estimate of P(hat_yi|yj), that is, the probability that a document that belongs to yj ends up being classified as belonging to yi @@ -953,7 +991,7 @@ [docs] - def aggregate(self, classif_predictions): + def aggregate(self, classif_predictions): prevs_estim = self.cc.aggregate(classif_predictions) estimate = F.solve_adjustment( class_conditional_rates=self.Pte_cond_estim_, @@ -968,7 +1006,7 @@ [docs] -class PACC(AggregativeSoftQuantifier): +class PACC(AggregativeSoftQuantifier): """ `Probabilistic Adjusted Classify & Count <https://ieeexplore.ieee.org/abstract/document/5694031>`_, the probabilistic variant of ACC that relies on the posterior probabilities returned by a probabilistic classifier. @@ -1015,7 +1053,7 @@ :param n_jobs: number of parallel workers """ - def __init__( + def __init__( self, classifier: BaseEstimator = None, fit_classifier=True, @@ -1031,7 +1069,7 @@ self.method = method self.norm = norm - def _check_init_parameters(self): + def _check_init_parameters(self): if self.solver not in ACC.SOLVERS: raise ValueError(f"unknown solver; valid ones are {ACC.SOLVERS}") if self.method not in ACC.METHODS: @@ -1041,7 +1079,7 @@ [docs] - def aggregation_fit(self, classif_predictions, labels): + def aggregation_fit(self, classif_predictions, labels): """ Estimates the misclassification rates @@ -1056,7 +1094,7 @@ [docs] - def aggregate(self, classif_posteriors): + def aggregate(self, classif_posteriors): prevs_estim = self.pcc.aggregate(classif_posteriors) estimate = F.solve_adjustment( @@ -1071,7 +1109,7 @@ [docs] @classmethod - def getPteCondEstim(cls, classes, y, y_): + def getPteCondEstim(cls, classes, y, y_): # estimate the matrix with entry (i,j) being the estimate of P(hat_yi|yj), that is, the probability that a # document that belongs to yj ends up being classified as belonging to yi n_classes = len(classes) @@ -1088,7 +1126,7 @@ [docs] -class RLLS(AggregativeSoftQuantifier): +class RLLS(AggregativeSoftQuantifier): """ `Regularized Learning for Domain Adaptation under Label Shifts <https://arxiv.org/abs/1903.09734>`_, used here as an aggregative @@ -1128,7 +1166,7 @@ :func:`quapy.functional.normalize_prevalence` """ - def __init__( + def __init__( self, classifier: BaseEstimator = None, fit_classifier=True, @@ -1147,7 +1185,7 @@ self.norm = norm self.last_w_ = None - def _check_init_parameters(self): + def _check_init_parameters(self): _get_cvxpy() _rlls_check_mode(self.mode) if not isinstance(self.alpha, (int, float)) or self.alpha < 0: @@ -1164,7 +1202,7 @@ [docs] - def aggregation_fit(self, classif_predictions, labels): + def aggregation_fit(self, classif_predictions, labels): if classif_predictions is None or labels is None: raise ValueError('RLLS requires source posterior predictions and source labels') @@ -1181,7 +1219,7 @@ [docs] - def aggregate(self, classif_posteriors): + def aggregate(self, classif_posteriors): qz = _rlls_predicted_marginal(classif_posteriors, mode=self.mode) w = _rlls_compute_weights( self.C_zy_, @@ -1199,7 +1237,7 @@ [docs] -class EMQ(AggregativeSoftQuantifier): +class EMQ(AggregativeSoftQuantifier): """ `Expectation Maximization for Quantification <https://ieeexplore.ieee.org/abstract/document/6789744>`_ (EMQ), aka `Saerens-Latinne-Decaestecker` (SLD) algorithm. @@ -1249,7 +1287,7 @@ ON_CALIB_ERROR_VALUES = ['raise', 'backup'] CALIB_OPTIONS = [None, 'nbvs', 'bcts', 'ts', 'vs'] - def __init__(self, classifier: BaseEstimator = None, fit_classifier=True, val_split=None, exact_train_prev=True, + def __init__(self, classifier: BaseEstimator = None, fit_classifier=True, val_split=None, exact_train_prev=True, calib=None, on_calib_error='raise', n_jobs=None): assert calib in EMQ.CALIB_OPTIONS, \ @@ -1261,12 +1299,12 @@ self.exact_train_prev = exact_train_prev self.calib = calib self.on_calib_error = on_calib_error - self.n_jobs = n_jobs + self.n_jobs = qp._get_njobs(n_jobs) [docs] @classmethod - def EMQ_BCTS(cls, classifier: BaseEstimator, fit_classifier=True, val_split=5, on_calib_error="raise", n_jobs=None): + def EMQ_BCTS(cls, classifier: BaseEstimator, fit_classifier=True, val_split=5, on_calib_error="raise", n_jobs=None): """ Constructs an instance of EMQ using the best configuration found in the `Alexandari et al. paper <http://proceedings.mlr.press/v119/alexandari20a.html>`_, i.e., one that relies on Bias-Corrected Temperature @@ -1298,23 +1336,23 @@ calib='bcts', on_calib_error=on_calib_error, n_jobs=n_jobs) - def _check_init_parameters(self): + def _check_init_parameters(self): if self.val_split is not None: if self.exact_train_prev and self.calib is None: - raise RuntimeWarning(f'The parameter {self.val_split=} was specified for EMQ, while the parameters ' - f'{self.exact_train_prev=} and {self.calib=}. This has no effect and causes an ' - f'unnecessary overload.') + warnings.warn(f'The parameter {self.val_split=} was specified for EMQ, while the parameters ' + f'{self.exact_train_prev=} and {self.calib=}. This has no effect and causes an ' + f'unnecessary overload.', RuntimeWarning) else: if self.calib is not None: - print(f'[warning] The parameter {self.calib=} requires the val_split be different from None. ' - f'This parameter will be set to 5. To avoid this warning, set this value to a float value ' - f'indicating the proportion of training data to be used as validation, or to an integer ' - f'indicating the number of folds for kFCV.') + warnings.warn(f'The parameter {self.calib=} requires the val_split be different from None. ' + f'This parameter will be set to 5. To avoid this warning, set this value to a float value ' + f'indicating the proportion of training data to be used as validation, or to an integer ' + f'indicating the number of folds for kFCV.') self.val_split = 5 [docs] - def classify(self, X): + def classify(self, X): """ Provides the posterior probabilities for the given instances. The calibration function, if required, has no effect in this step, and is only involved in the aggregate method. @@ -1327,13 +1365,13 @@ [docs] - def classifier_fit_predict(self, X, y): + def classifier_fit_predict(self, X, y): classif_predictions = super().classifier_fit_predict(X, y) self.train_prevalence = F.prevalence_from_labels(y, classes=self.classes_) return classif_predictions - def _fit_calibration(self, calibrator, P, y): + def _fit_calibration(self, calibrator, P, y): n_classes = len(self.classes_) if not np.issubdtype(y.dtype, np.number): @@ -1347,7 +1385,7 @@ elif self.on_calib_error == 'backup': self.calibration_function = lambda P: P - def _calibrate_if_requested(self, uncalib_posteriors): + def _calibrate_if_requested(self, uncalib_posteriors): if hasattr(self, 'calibration_function') and self.calibration_function is not None: try: calib_posteriors = self.calibration_function(uncalib_posteriors) @@ -1364,7 +1402,7 @@ [docs] - def aggregation_fit(self, classif_predictions, labels): + def aggregation_fit(self, classif_predictions, labels): """ Trains the aggregation function of EMQ. This comes down to recalibrating the posterior probabilities ir requested. @@ -1379,14 +1417,14 @@ requires_predictions = (self.calib is not None) or (not self.exact_train_prev) if P is None and requires_predictions: # classifier predictions were not generated because val_split=None - raise ArgumentError(self.val_split, self.__class__.__name__ + - ": Classifier predictions for the aggregative fit were not generated because " - "val_split=None. This usually happens when you enable calibrations or heuristics " - "during model selection but left val_split set to its default value (None). " - "Please provide one of the following values for val_split: (i) an integer >1 " - "(e.g. val_split=5) for k-fold cross-validation; (ii) a float in (0,1) (e.g. " - "val_split=0.3) for a proportion split; or (iii) a tuple (X, y) with explicit " - "validation data") + raise ValueError(self.__class__.__name__ + + ": Classifier predictions for the aggregative fit were not generated because " + "val_split=None. This usually happens when you enable calibrations or heuristics " + "during model selection but left val_split set to its default value (None). " + "Please provide one of the following values for val_split: (i) an integer >1 " + "(e.g. val_split=5) for k-fold cross-validation; (ii) a float in (0,1) (e.g. " + "val_split=0.3) for a proportion split; or (iii) a tuple (X, y) with explicit " + "validation data") if self.calib is not None: calibrator = _get_abstention_calibrators().get(self.calib, None) @@ -1403,7 +1441,7 @@ [docs] - def aggregate(self, classif_posteriors, epsilon=EPSILON): + def aggregate(self, classif_posteriors, epsilon=EPSILON): classif_posteriors = self._calibrate_if_requested(classif_posteriors) priors, posteriors = self.EM(self.train_prevalence, classif_posteriors, epsilon) return priors @@ -1411,7 +1449,7 @@ [docs] - def predict_proba(self, instances, epsilon=EPSILON): + def predict_proba(self, instances, epsilon=EPSILON): """ Returns the posterior probabilities updated by the EM algorithm. @@ -1428,7 +1466,7 @@ [docs] @classmethod - def EM(cls, tr_prev, posterior_probabilities, epsilon=EPSILON): + def EM(cls, tr_prev, posterior_probabilities, epsilon=EPSILON): """ Computes the `Expectation Maximization` routine. @@ -1466,7 +1504,7 @@ s += 1 if not converged: - print('[warning] the method has reached the maximum number of iterations; it might have not converged') + warnings.warn('the method has reached the maximum number of iterations; it might have not converged') return qs, ps @@ -1475,7 +1513,7 @@ [docs] -class HDy(AggregativeSoftQuantifier, BinaryAggregativeQuantifier): +class HDy(AggregativeSoftQuantifier, BinaryAggregativeQuantifier): """ `Hellinger Distance y <https://www.sciencedirect.com/science/article/pii/S0020025512004069>`_ (HDy). HDy is a probabilistic method for training binary quantifiers, that models quantification as the problem of @@ -1502,12 +1540,12 @@ for `k`); or as a tuple (X,y) defining the specific set of data to use for validation. """ - def __init__(self, classifier: BaseEstimator = None, fit_classifier=True, val_split=5): + def __init__(self, classifier: BaseEstimator = None, fit_classifier=True, val_split=5): super().__init__(classifier, fit_classifier, val_split) [docs] - def aggregation_fit(self, classif_predictions, labels): + def aggregation_fit(self, classif_predictions, labels): """ Trains the aggregation function of HDy. @@ -1522,7 +1560,7 @@ # pre-compute the histogram for positive and negative examples self.bins = np.linspace(10, 110, 11, dtype=int) # [10, 20, 30, ..., 100, 110] - def hist(P, bins): + def hist(P, bins): h = np.histogram(P, bins=bins, range=(0, 1), density=True)[0] return h / h.sum() @@ -1532,7 +1570,7 @@ [docs] - def aggregate(self, classif_posteriors): + def aggregate(self, classif_posteriors): # "In this work, the number of bins b used in HDx and HDy was chosen from 10 to 110 in steps of 10, # and the final estimated a priori probability was taken as the median of these 11 estimates." # (González-Castro, et al., 2013). @@ -1549,7 +1587,7 @@ # the authors proposed to search for the prevalence yielding the best matching as a linear search # at small steps (modern implementations resort to an optimization procedure, # see class DistributionMatching) - def loss(prev): + def loss(prev): class1_prev = prev[1] Px_train = class1_prev * Pxy1_density + (1 - class1_prev) * Pxy0_density return F.HellingerDistance(Px_train, Px_test) @@ -1564,7 +1602,7 @@ [docs] -class DyS(AggregativeSoftQuantifier, BinaryAggregativeQuantifier): +class DyS(AggregativeSoftQuantifier, BinaryAggregativeQuantifier): """ `DyS framework <https://ojs.aaai.org/index.php/AAAI/article/view/4376>`_ (DyS). DyS is a generalization of HDy method, using a Ternary Search in order to find the prevalence that @@ -1593,15 +1631,15 @@ :param n_jobs: number of parallel workers. """ - def __init__(self, classifier: BaseEstimator = None, fit_classifier=True, val_split=5, n_bins=8, + def __init__(self, classifier: BaseEstimator = None, fit_classifier=True, val_split=5, n_bins=8, divergence: Union[str, Callable] = 'HD', tol=1e-05, n_jobs=None): super().__init__(classifier, fit_classifier, val_split) self.tol = tol self.divergence = divergence self.n_bins = n_bins - self.n_jobs = n_jobs + self.n_jobs = qp._get_njobs(n_jobs) - def _ternary_search(self, f, left, right, tol): + def _ternary_search(self, f, left, right, tol): """ Find maximum of unimodal function f() within [left, right] """ @@ -1619,7 +1657,7 @@ [docs] - def aggregation_fit(self, classif_predictions, labels): + def aggregation_fit(self, classif_predictions, labels): """ Trains the aggregation function of DyS. @@ -1637,13 +1675,13 @@ [docs] - def aggregate(self, classif_posteriors): + def aggregate(self, classif_posteriors): Px = classif_posteriors[:, self.pos_label] # takes only the P(y=+1|x) Px_test = np.histogram(Px, bins=self.n_bins, range=(0, 1), density=True)[0] divergence = get_divergence(self.divergence) - def distribution_distance(prev): + def distribution_distance(prev): Px_train = prev * self.Pxy1_density + (1 - prev) * self.Pxy0_density return divergence(Px_train, Px_test) @@ -1655,7 +1693,7 @@ [docs] -class SMM(AggregativeSoftQuantifier, BinaryAggregativeQuantifier): +class SMM(AggregativeSoftQuantifier, BinaryAggregativeQuantifier): """ `SMM method <https://ieeexplore.ieee.org/document/9260028>`_ (SMM). SMM is a simplification of matching distribution methods where the representation of the examples @@ -1674,12 +1712,12 @@ for `k`); or as a tuple (X,y) defining the specific set of data to use for validation. """ - def __init__(self, classifier: BaseEstimator = None, fit_classifier=True, val_split=5): + def __init__(self, classifier: BaseEstimator = None, fit_classifier=True, val_split=5): super().__init__(classifier, fit_classifier, val_split) [docs] - def aggregation_fit(self, classif_predictions, labels): + def aggregation_fit(self, classif_predictions, labels): """ Trains the aggregation function of SMM. @@ -1697,7 +1735,7 @@ [docs] - def aggregate(self, classif_posteriors): + def aggregate(self, classif_posteriors): Px = classif_posteriors[:, self.pos_label] # takes only the P(y=+1|x) Px_mean = np.mean(Px) @@ -1709,7 +1747,7 @@ [docs] -class DMy(AggregativeSoftQuantifier): +class DMy(AggregativeSoftQuantifier): """ Generic Distribution Matching quantifier for binary or multiclass quantification based on the space of posterior probabilities. This implementation takes the number of bins, the divergence, and the possibility to work on CDF @@ -1742,19 +1780,19 @@ :param n_jobs: number of parallel workers (default None) """ - def __init__(self, classifier: BaseEstimator = None, fit_classifier=True, val_split=5, nbins=8, + def __init__(self, classifier: BaseEstimator = None, fit_classifier=True, val_split=5, nbins=8, divergence: Union[str, Callable] = 'HD', cdf=False, search='optim_minimize', n_jobs=None): super().__init__(classifier, fit_classifier, val_split) self.nbins = nbins self.divergence = divergence self.cdf = cdf self.search = search - self.n_jobs = n_jobs + self.n_jobs = qp._get_njobs(n_jobs) [docs] @classmethod - def HDy(cls, classifier: BaseEstimator = None, fit_classifier=True, val_split=5, n_jobs=None): + def HDy(cls, classifier: BaseEstimator = None, fit_classifier=True, val_split=5, n_jobs=None): """ Historical HDy preset expressed as a configuration of :class:`DMy`. @@ -1783,7 +1821,7 @@ return AggregativeMedianEstimator(base_quantifier=base, param_grid=param_grid, n_jobs=n_jobs) - def _get_distributions(self, posteriors): + def _get_distributions(self, posteriors): histograms = [] post_dims = posteriors.shape[1] if post_dims == 2: @@ -1801,7 +1839,7 @@ [docs] - def aggregation_fit(self, classif_predictions, labels): + def aggregation_fit(self, classif_predictions, labels): """ Trains the aggregation function of a distribution matching method. This comes down to generating the validation distributions out of the training data. @@ -1829,7 +1867,7 @@ [docs] - def aggregate(self, posteriors: np.ndarray): + def aggregate(self, posteriors: np.ndarray): """ Searches for the mixture model parameter (the sought prevalence values) that yields a validation distribution (the mixture) that best matches the test distribution, in terms of the divergence measure of choice. @@ -1844,7 +1882,7 @@ divergence = get_divergence(self.divergence) n_classes, n_channels, nbins = self.validation_distribution.shape - def loss(prev): + def loss(prev): prev = np.expand_dims(prev, axis=0) mixture_distribution = (prev @ self.validation_distribution.reshape(n_classes, -1)).reshape(n_channels, -1) divs = [divergence(test_distribution[ch], mixture_distribution[ch]) for ch in range(n_channels)] @@ -1857,7 +1895,7 @@ [docs] -def newELM(svmperf_base=None, loss='01', C=1): +def newELM(svmperf_base=None, loss='01', C=1): """ Explicit Loss Minimization (ELM) quantifiers. Quantifiers based on ELM represent a family of methods based on structured output learning; @@ -1887,7 +1925,7 @@ [docs] -def newSVMQ(svmperf_base=None, C=1): +def newSVMQ(svmperf_base=None, C=1): """ SVM(Q) is an Explicit Loss Minimization (ELM) quantifier set to optimize for the `Q` loss combining a classification-oriented loss and a quantification-oriented loss, as proposed by @@ -1914,7 +1952,9 @@ -def newSVMKLD(svmperf_base=None, C=1): + +[docs] +def newSVMKLD(svmperf_base=None, C=1): """ SVM(KLD) is an Explicit Loss Minimization (ELM) quantifier set to optimize for the Kullback-Leibler Divergence as proposed by `Esuli et al. 2015 <https://dl.acm.org/doi/abs/10.1145/2700406>`_. @@ -1936,14 +1976,15 @@ :return: returns an instance of CC set to work with SVMperf (with loss and C set properly) as the underlying classifier """ - return newELM(svmperf_base, loss='kld', C=C) + return newELM(svmperf_base, loss='kld', C=C) - -[docs] -def newSVMKLD(svmperf_base=None, C=1): + + +[docs] +def newSVMNKLD(svmperf_base=None, C=1): """ - SVM(KLD) is an Explicit Loss Minimization (ELM) quantifier set to optimize for the Kullback-Leibler Divergence + SVM(NKLD) is an Explicit Loss Minimization (ELM) quantifier set to optimize for the Kullback-Leibler Divergence normalized via the logistic function, as proposed by `Esuli et al. 2015 <https://dl.acm.org/doi/abs/10.1145/2700406>`_. Equivalent to: @@ -1970,7 +2011,7 @@ [docs] -def newSVMAE(svmperf_base=None, C=1): +def newSVMAE(svmperf_base=None, C=1): """ SVM(KLD) is an Explicit Loss Minimization (ELM) quantifier set to optimize for the Absolute Error as first used by `Moreo and Sebastiani, 2021 <https://arxiv.org/abs/2011.02552>`_. @@ -1998,7 +2039,7 @@ [docs] -def newSVMRAE(svmperf_base=None, C=1): +def newSVMRAE(svmperf_base=None, C=1): """ SVM(KLD) is an Explicit Loss Minimization (ELM) quantifier set to optimize for the Relative Absolute Error as first used by `Moreo and Sebastiani, 2021 <https://arxiv.org/abs/2011.02552>`_. @@ -2026,7 +2067,7 @@ [docs] -class OneVsAllAggregative(OneVsAllGeneric, AggregativeQuantifier): +class OneVsAllAggregative(OneVsAllGeneric, AggregativeQuantifier): """ Allows any binary quantifier to perform quantification on single-label datasets. The method maintains one binary quantifier for each class, and then l1-normalizes the outputs so that the @@ -2042,7 +2083,7 @@ is removed and no longer available at predict time. """ - def __init__(self, binary_quantifier=None, n_jobs=None, parallel_backend='multiprocessing'): + def __init__(self, binary_quantifier=None, n_jobs=None, parallel_backend='multiprocessing'): if binary_quantifier is None: binary_quantifier = PACC() assert isinstance(binary_quantifier, BaseQuantifier), \ @@ -2055,7 +2096,7 @@ [docs] - def classify(self, X): + def classify(self, X): """ If the base quantifier is not probabilistic, returns a matrix of shape `(n,m,)` with `n` the number of instances and `m` the number of classes. The entry `(i,j)` is a binary value indicating whether instance @@ -2079,34 +2120,34 @@ [docs] - def aggregate(self, classif_predictions): + def aggregate(self, classif_predictions): prevalences = self._parallel(self._delayed_binary_aggregate, classif_predictions) return F.normalize_prevalence(prevalences) [docs] - def aggregation_fit(self, classif_predictions, labels): - self._parallel(self._delayed_binary_aggregate_fit(c, classif_predictions, labels)) + def aggregation_fit(self, classif_predictions, labels): + self._parallel(self._delayed_binary_aggregate_fit, classif_predictions, labels) return self - def _delayed_binary_classification(self, c, X): + def _delayed_binary_classification(self, c, X): return self.dict_binary_quantifiers[c].classify(X) - def _delayed_binary_aggregate(self, c, classif_predictions): + def _delayed_binary_aggregate(self, c, classif_predictions): # the estimation for the positive class prevalence return self.dict_binary_quantifiers[c].aggregate(classif_predictions[:, c])[1] - def _delayed_binary_aggregate_fit(self, c, classif_predictions, labels): + def _delayed_binary_aggregate_fit(self, c, classif_predictions, labels): # trains the aggregation function of the cth quantifier - return self.dict_binary_quantifiers[c].aggregate_fit(classif_predictions[:, c], labels) + return self.dict_binary_quantifiers[c].aggregation_fit(classif_predictions[:, c], labels == c) [docs] -class AggregativeMedianEstimator(BinaryQuantifier): +class AggregativeMedianEstimator(BinaryQuantifier): """ This method is a meta-quantifier that returns, as the estimated class prevalence values, the median of the estimation returned by differently (hyper)parameterized base quantifiers. @@ -2119,7 +2160,7 @@ :param n_jobs: number of parallel workers """ - def __init__(self, base_quantifier: AggregativeQuantifier, param_grid: dict, random_state=None, n_jobs=None): + def __init__(self, base_quantifier: AggregativeQuantifier, param_grid: dict, random_state=None, n_jobs=None): self.base_quantifier = base_quantifier self.param_grid = param_grid self.random_state = random_state @@ -2127,17 +2168,17 @@ [docs] - def get_params(self, deep=True): + def get_params(self, deep=True): return self.base_quantifier.get_params(deep) [docs] - def set_params(self, **params): + def set_params(self, **params): self.base_quantifier.set_params(**params) - def _delayed_fit(self, args): + def _delayed_fit(self, args): with qp.util.temp_seed(self.random_state): params, X, y = args model = deepcopy(self.base_quantifier) @@ -2145,7 +2186,7 @@ model.fit(X, y) return model - def _delayed_fit_classifier(self, args): + def _delayed_fit_classifier(self, args): with qp.util.temp_seed(self.random_state): cls_params, X, y = args model = deepcopy(self.base_quantifier) @@ -2153,7 +2194,7 @@ predictions, labels = model.classifier_fit_predict(X, y) return (model, predictions, labels) - def _delayed_fit_aggregation(self, args): + def _delayed_fit_aggregation(self, args): with qp.util.temp_seed(self.random_state): ((model, predictions, y), q_params) = args model = deepcopy(model) @@ -2163,8 +2204,8 @@ [docs] - def fit(self, X, y): - import itertools + def fit(self, X, y): + import itertools self._check_binary(y, self.__class__.__name__) @@ -2177,8 +2218,7 @@ ((params, X, y) for params in cls_configs), seed=qp.environ.get('_R_SEED', None), n_jobs=self.n_jobs, - asarray=False, - backend='threading' + asarray=False ) else: model = self.base_quantifier @@ -2190,8 +2230,7 @@ self._delayed_fit_aggregation, itertools.product(models_preds, q_configs), seed=qp.environ.get('_R_SEED', None), - n_jobs=self.n_jobs, - backend='threading' + n_jobs=self.n_jobs ) else: configs = qp.model_selection.expand_grid(self.param_grid) @@ -2199,25 +2238,23 @@ self._delayed_fit, ((params, X, y) for params in configs), seed=qp.environ.get('_R_SEED', None), - n_jobs=self.n_jobs, - backend='threading' + n_jobs=self.n_jobs ) return self - def _delayed_predict(self, args): + def _delayed_predict(self, args): model, instances = args return model.predict(instances) [docs] - def predict(self, instances): + def predict(self, instances): prev_preds = qp.util.parallel( self._delayed_predict, ((model, instances) for model in self.models), seed=qp.environ.get('_R_SEED', None), - n_jobs=self.n_jobs, - backend='threading' + n_jobs=self.n_jobs ) return np.median(prev_preds, axis=0) @@ -2228,7 +2265,7 @@ # imports # --------------------------------------------------------------- -from . import _threshold_optim +from . import _threshold_optim T50 = _threshold_optim.T50 MAX = _threshold_optim.MAX @@ -2236,7 +2273,7 @@ MS = _threshold_optim.MS MS2 = _threshold_optim.MS2 -from . import _kdey +from . import _kdey KDEyML = _kdey.KDEyML KDEyHD = _kdey.KDEyHD diff --git a/docs/build/html/_modules/quapy/method/base.html b/docs/build/html/_modules/quapy/method/base.html index e649c5b..5519615 100644 --- a/docs/build/html/_modules/quapy/method/base.html +++ b/docs/build/html/_modules/quapy/method/base.html @@ -1,95 +1,392 @@ - - - - - - quapy.method.base — QuaPy: A Python-based open-source framework for quantification 0.2.1 documentation - - + + + + + + + + quapy.method.base — QuaPy: A Python-based open-source framework for quantification 0.2.1 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Skip to main content - - - - - - - - - + + + + Back to top + + + + + + + + + + Ctrl+K + + - - - - - - - - - - QuaPy: A Python-based open-source framework for quantification - - - - - - - + + - - -
Source code for quapy.functional -import warnings -from abc import ABC, abstractmethod -from collections import defaultdict -from functools import lru_cache -from typing import Literal, Union, Callable -from numpy.typing import ArrayLike +import warnings +from abc import ABC, abstractmethod +from collections import defaultdict +from functools import lru_cache +from typing import Literal, Union, Callable +from numpy.typing import ArrayLike -import scipy -import numpy as np +import scipy +import numpy as np -import quapy as qp +import quapy as qp # ------------------------------------------------------------------------------------------ @@ -351,7 +389,7 @@ [docs] -def classes_from_labels(labels): +def classes_from_labels(labels): """ Obtains a np.ndarray with the (sorted) classes :param labels: array-like with the instances' labels @@ -365,7 +403,7 @@ [docs] -def num_classes_from_labels(labels): +def num_classes_from_labels(labels): """ Obtains the number of classes from an array-like of instance's labels :param labels: array-like with the instances' labels @@ -380,7 +418,7 @@ [docs] -def counts_from_labels(labels: ArrayLike, classes: ArrayLike) -> np.ndarray: +def counts_from_labels(labels: ArrayLike, classes: ArrayLike) -> np.ndarray: """ Computes the raw count values from a vector of labels. @@ -401,7 +439,7 @@ [docs] -def prevalence_from_labels(labels: ArrayLike, classes: ArrayLike): +def prevalence_from_labels(labels: ArrayLike, classes: ArrayLike): """ Computes the prevalence values from a vector of labels. @@ -419,7 +457,7 @@ [docs] -def prevalence_from_probabilities(posteriors: ArrayLike, binarize: bool = False): +def prevalence_from_probabilities(posteriors: ArrayLike, binarize: bool = False): """ Returns a vector of prevalence values from a matrix of posterior probabilities. @@ -443,7 +481,7 @@ [docs] -def num_prevalence_combinations(n_prevpoints:int, n_classes:int, n_repeats:int=1) -> int: +def num_prevalence_combinations(n_prevpoints:int, n_classes:int, n_repeats:int=1) -> int: """ Computes the number of valid prevalence combinations in the n_classes-dimensional simplex if `n_prevpoints` equally distant prevalence values are generated and `n_repeats` repetitions are requested. @@ -472,7 +510,7 @@ [docs] -def get_nprevpoints_approximation(combinations_budget:int, n_classes:int, n_repeats:int=1) -> int: +def get_nprevpoints_approximation(combinations_budget:int, n_classes:int, n_repeats:int=1) -> int: """ Searches for the largest number of (equidistant) prevalence points to define for each of the `n_classes` classes so that the number of valid prevalence values generated as combinations of prevalence points (points in a @@ -500,7 +538,7 @@ [docs] -def as_binary_prevalence(positive_prevalence: Union[float, ArrayLike], clip_if_necessary: bool=False) -> np.ndarray: +def as_binary_prevalence(positive_prevalence: Union[float, ArrayLike], clip_if_necessary: bool=False) -> np.ndarray: """ Helper that, given a float representing the prevalence for the positive class, returns a np.ndarray of two values representing a binary distribution. @@ -522,7 +560,7 @@ [docs] -def strprev(prevalences: ArrayLike, prec: int=3) -> str: +def strprev(prevalences: ArrayLike, prec: int=3) -> str: """ Returns a string representation for a prevalence vector. E.g., @@ -539,7 +577,7 @@ [docs] -def check_prevalence_vector(prevalences: ArrayLike, raise_exception: bool=False, tolerance: float=1e-08, aggr=True): +def check_prevalence_vector(prevalences: ArrayLike, raise_exception: bool=False, tolerance: float=1e-08, aggr=True): """ Checks that `prevalences` is a valid prevalence vector, i.e., it contains values in [0,1] and the values sum up to 1. In other words, verifies that the `prevalences` vectors lies in the @@ -581,7 +619,7 @@ [docs] -def uniform_prevalence(n_classes): +def uniform_prevalence(n_classes): """ Returns a vector representing the uniform distribution for `n_classes` @@ -597,7 +635,7 @@ [docs] -def normalize_prevalence(prevalences: ArrayLike, method='l1'): +def normalize_prevalence(prevalences: ArrayLike, method='l1'): """ Normalizes a vector or matrix of prevalence values. The normalization consists of applying a L1 normalization in cases in which the prevalence values are not all-zeros, and to convert the prevalence values into `1/n_classes` in @@ -640,7 +678,7 @@ [docs] -def l1_norm(prevalences: ArrayLike) -> np.ndarray: +def l1_norm(prevalences: ArrayLike) -> np.ndarray: """ Applies L1 normalization to the `unnormalized_arr` so that it becomes a valid prevalence vector. Zero vectors are mapped onto the uniform distribution. Raises an exception if @@ -666,7 +704,7 @@ [docs] -def clip(prevalences: ArrayLike) -> np.ndarray: +def clip(prevalences: ArrayLike) -> np.ndarray: """ Clips the values in [0,1] and then applies the L1 normalization. @@ -681,7 +719,7 @@ [docs] -def projection_simplex_sort(unnormalized_arr: ArrayLike) -> np.ndarray: +def projection_simplex_sort(unnormalized_arr: ArrayLike) -> np.ndarray: """Projects a point onto the probability simplex. The code is adapted from Mathieu Blondel's BSD-licensed @@ -709,7 +747,7 @@ [docs] -def softmax(prevalences: ArrayLike) -> np.ndarray: +def softmax(prevalences: ArrayLike) -> np.ndarray: """ Applies the softmax function to all vectors even if the original vectors were valid distributions. If you want to leave valid vectors untouched, use condsoftmax instead. @@ -724,7 +762,7 @@ [docs] -def condsoftmax(prevalences: ArrayLike) -> np.ndarray: +def condsoftmax(prevalences: ArrayLike) -> np.ndarray: """ Applies the softmax function only to vectors that do not represent valid distributions. @@ -749,7 +787,7 @@ [docs] -def HellingerDistance(P: np.ndarray, Q: np.ndarray) -> float: +def HellingerDistance(P: np.ndarray, Q: np.ndarray) -> float: """ Computes the Hellingher Distance (HD) between (discretized) distributions `P` and `Q`. The HD for two discrete distributions of `k` bins is defined as: @@ -767,7 +805,7 @@ [docs] -def TopsoeDistance(P: np.ndarray, Q: np.ndarray, epsilon: float=1e-20): +def TopsoeDistance(P: np.ndarray, Q: np.ndarray, epsilon: float=1e-20): """ Topsoe distance between two (discretized) distributions `P` and `Q`. The Topsoe distance for two discrete distributions of `k` bins is defined as: @@ -786,7 +824,7 @@ [docs] -def get_divergence(divergence: Union[str, Callable]): +def get_divergence(divergence: Union[str, Callable]): """ Guarantees that the divergence received as argument is a function. That is, if this argument is already a callable, then it is returned, if it is instead a string, then tries to instantiate the corresponding @@ -815,7 +853,7 @@ [docs] -def argmin_prevalence(loss: Callable, +def argmin_prevalence(loss: Callable, n_classes: int, method: Literal["optim_minimize", "linear_search", "ternary_search"]='optim_minimize'): """ @@ -842,7 +880,7 @@ [docs] -def optim_minimize(loss: Callable, n_classes: int, return_loss=False): +def optim_minimize(loss: Callable, n_classes: int, return_loss=False): """ Searches for the optimal prevalence values, i.e., an `n_classes`-dimensional vector of the (`n_classes`-1)-simplex that yields the smallest lost. This optimization is carried out by means of a constrained search using scipy's @@ -854,7 +892,7 @@ :return: (ndarray) the best prevalence vector found or a tuple which also contains the value of the loss if return_loss=True """ - from scipy import optimize + from scipy import optimize # the initial point is set as the uniform distribution uniform_distribution = uniform_prevalence(n_classes=n_classes) @@ -873,7 +911,7 @@ [docs] -def linear_search(loss: Callable, n_classes: int): +def linear_search(loss: Callable, n_classes: int): """ Performs a linear search for the best prevalence value in binary problems. The search is carried out by exploring the range [0,1] stepping by 0.01. This search is inefficient, and is added only for completeness (some of the @@ -897,7 +935,7 @@ [docs] -def ternary_search(loss: Callable, n_classes: int): +def ternary_search(loss: Callable, n_classes: int): """ Performs a ternary search for the best prevalence value in binary problems. This search assumes the loss is unimodal over the interval [0,1]. @@ -933,7 +971,7 @@ [docs] -def prevalence_linspace(grid_points:int=21, repeats:int=1, smooth_limits_epsilon:float=0.01) -> np.ndarray: +def prevalence_linspace(grid_points:int=21, repeats:int=1, smooth_limits_epsilon:float=0.01) -> np.ndarray: """ Produces an array of uniformly separated values of prevalence. By default, produces an array of 21 prevalence values, with @@ -958,7 +996,7 @@ [docs] -def uniform_prevalence_sampling(n_classes: int, size: int=1) -> np.ndarray: +def uniform_prevalence_sampling(n_classes: int, size: int=1) -> np.ndarray: """ Implements the `Kraemer algorithm <http://www.cs.cmu.edu/~nasmith/papers/smith+tromble.tr04.pdf>`_ for sampling uniformly at random from the unit simplex. This implementation is adapted from this @@ -994,7 +1032,7 @@ [docs] -def solve_adjustment_binary(prevalence_estim: ArrayLike, tpr: float, fpr: float, clip: bool=True): +def solve_adjustment_binary(prevalence_estim: ArrayLike, tpr: float, fpr: float, clip: bool=True): """ Implements the adjustment of ACC and PACC for the binary case. The adjustment for a prevalence estimate of the positive class `p` comes down to computing: @@ -1021,7 +1059,7 @@ [docs] -def solve_adjustment( +def solve_adjustment( class_conditional_rates: np.ndarray, unadjusted_counts: np.ndarray, method: Literal["inversion", "invariant-ratio"], @@ -1067,14 +1105,18 @@ if method == "inversion": pass # We leave A and B unchanged elif method == "invariant-ratio": - # Change the last equation to replace it with the normalization condition + # Change the last equation to replace it with the normalization condition; + # copy first so this does not mutate the caller's arrays (np.asarray above + # returns the same object, not a copy, when the input is already float64) + A = A.copy() + B = B.copy() A[-1, :] = 1.0 B[-1] = 1.0 else: raise ValueError(f"unknown {method=}") if solver == "minimize": - def loss(prev): + def loss(prev): return np.linalg.norm(A @ prev - B) return optim_minimize(loss, n_classes=A.shape[0]) elif solver in ["exact-raise", "exact-cc"]: @@ -1102,7 +1144,7 @@ [docs] -class CompositionalTransformation(ABC): +class CompositionalTransformation(ABC): """ Abstract class of transformations for compositional data. """ @@ -1110,13 +1152,13 @@ EPSILON = 1e-12 @abstractmethod - def __call__(self, X): + def __call__(self, X): ... [docs] @abstractmethod - def inverse(self, Z): + def inverse(self, Z): ... @@ -1124,12 +1166,12 @@ [docs] -class CLRtransformation(CompositionalTransformation): +class CLRtransformation(CompositionalTransformation): """ Centered log-ratio (CLR) transformation. """ - def __call__(self, X): + def __call__(self, X): X = np.asarray(X) X = qp.error.smooth(X, self.EPSILON) geometric_mean = np.exp(np.mean(np.log(X), axis=-1, keepdims=True)) @@ -1137,7 +1179,7 @@ [docs] - def inverse(self, Z): + def inverse(self, Z): return scipy.special.softmax(Z, axis=-1) @@ -1145,12 +1187,12 @@ [docs] -class ILRtransformation(CompositionalTransformation): +class ILRtransformation(CompositionalTransformation): """ Isometric log-ratio (ILR) transformation. """ - def __call__(self, X): + def __call__(self, X): X = np.asarray(X) X = qp.error.smooth(X, self.EPSILON) basis = self.get_V(X.shape[-1]) @@ -1158,7 +1200,7 @@ [docs] - def inverse(self, Z): + def inverse(self, Z): Z = np.asarray(Z) basis = self.get_V(Z.shape[-1] + 1) logp = Z @ basis @@ -1169,7 +1211,7 @@ [docs] @lru_cache(maxsize=None) - def get_V(self, k): + def get_V(self, k): helmert = np.zeros((k, k)) for i in range(1, k): helmert[i, :i] = 1 @@ -1182,7 +1224,7 @@ [docs] -def normalized_entropy(p): +def normalized_entropy(p): """ Computes the normalized Shannon entropy of a prevalence vector. @@ -1198,7 +1240,7 @@ [docs] -def antagonistic_prevalence(p, strength=1): +def antagonistic_prevalence(p, strength=1): """ Reflects a prevalence vector in ILR space and maps it back to the simplex. @@ -1215,7 +1257,7 @@ [docs] -def in_simplex(x, atol=1e-8): +def in_simplex(x, atol=1e-8): """ Checks whether points lie in the probability simplex. diff --git a/docs/build/html/_modules/quapy/method/_kdey.html b/docs/build/html/_modules/quapy/method/_kdey.html index 6664ea9..3367861 100644 --- a/docs/build/html/_modules/quapy/method/_kdey.html +++ b/docs/build/html/_modules/quapy/method/_kdey.html @@ -29,8 +29,9 @@ - - + + + @@ -41,6 +42,7 @@ + @@ -114,8 +116,14 @@ + + + + + + + - QuaPy @@ -131,7 +139,7 @@ - Quickstart + Home @@ -183,6 +191,21 @@ + + + + + + + + + + + GitHub + + + @@ -229,7 +252,7 @@ - Quickstart + Home @@ -272,6 +295,21 @@ + + + + + + + + + + + GitHub + + + @@ -332,22 +370,22 @@ Source code for quapy.method._kdey -import numpy as np -from numbers import Real -from sklearn.base import BaseEstimator -from sklearn.neighbors import KernelDensity +import numpy as np +from numbers import Real +from sklearn.base import BaseEstimator +from sklearn.neighbors import KernelDensity -import quapy as qp -from quapy.method._helper import _labels_to_indices -from quapy.method.aggregative import AggregativeSoftQuantifier -import quapy.functional as F -from scipy.special import logsumexp -from sklearn.metrics.pairwise import rbf_kernel +import quapy as qp +from quapy.method._helper import _labels_to_indices +from quapy.method.aggregative import AggregativeSoftQuantifier +import quapy.functional as F +from scipy.special import logsumexp +from sklearn.metrics.pairwise import rbf_kernel [docs] -class KDEBase: +class KDEBase: """ Common ancestor for KDE-based methods. Implements some common routines. """ @@ -356,7 +394,7 @@ KERNELS = ['gaussian', 'aitchison', 'ilr'] @classmethod - def _check_bandwidth(cls, bandwidth, kernel): + def _check_bandwidth(cls, bandwidth, kernel): """ Checks that the bandwidth parameter is correct @@ -370,13 +408,13 @@ return bandwidth @classmethod - def _check_kernel(cls, kernel): + def _check_kernel(cls, kernel): assert kernel in KDEBase.KERNELS, f'unknown {kernel=}' return kernel [docs] - def get_kde_function(self, X, bandwidth, kernel): + def get_kde_function(self, X, bandwidth, kernel): """ Wraps the KDE function from scikit-learn. @@ -392,7 +430,7 @@ [docs] - def pdf(self, kde, X, kernel, log_densities=False): + def pdf(self, kde, X, kernel, log_densities=False): """ Wraps the density evalution of scikit-learn's KDE. Scikit-learn returns log-scores (s), so this function returns :math:`e^{s}` @@ -411,7 +449,7 @@ [docs] - def get_mixture_components(self, X, y, classes, bandwidth, kernel): + def get_mixture_components(self, X, y, classes, bandwidth, kernel): """ Returns an array containing the mixture components, i.e., the KDE functions for each class. @@ -433,7 +471,7 @@ [docs] - def transform_posteriors(self, X, kernel): + def transform_posteriors(self, X, kernel): if kernel in {'aitchison', 'ilr'}: X = self.shrink_posteriors(X) if kernel == 'aitchison': @@ -445,7 +483,7 @@ [docs] - def shrink_posteriors(self, X): + def shrink_posteriors(self, X): shrinkage = getattr(self, 'shrinkage', 0.0) if shrinkage <= 0: return X @@ -457,7 +495,7 @@ [docs] - def effective_bandwidth(self, bandwidth, kernel): + def effective_bandwidth(self, bandwidth, kernel): shrinkage = getattr(self, 'shrinkage', 0.0) if shrinkage > 0 and kernel in {'aitchison', 'ilr'} and isinstance(bandwidth, Real): return (1.0 - shrinkage) * float(bandwidth) @@ -466,7 +504,7 @@ [docs] - def clr_transform(self, X): + def clr_transform(self, X): if not hasattr(self, 'clr'): self.clr = F.CLRtransformation() return self.clr(X) @@ -474,7 +512,7 @@ [docs] - def ilr_transform(self, X): + def ilr_transform(self, X): if not hasattr(self, 'ilr'): self.ilr = F.ILRtransformation() return self.ilr(X) @@ -484,7 +522,7 @@ [docs] -class KDEyML(AggregativeSoftQuantifier, KDEBase): +class KDEyML(AggregativeSoftQuantifier, KDEBase): """ Kernel Density Estimation model for quantification (KDEy) relying on the Kullback-Leibler divergence (KLD) as the divergence measure to be minimized. This method was first proposed in the paper @@ -526,7 +564,7 @@ :param random_state: a seed to be set before fitting any base quantifier (default None) """ - def __init__(self, classifier: BaseEstimator=None, fit_classifier=True, val_split=5, bandwidth=0.1, + def __init__(self, classifier: BaseEstimator=None, fit_classifier=True, val_split=5, bandwidth=0.1, kernel='gaussian', shrinkage=0.0, random_state=None): super().__init__(classifier, fit_classifier, val_split) self.bandwidth = KDEBase._check_bandwidth(bandwidth, kernel) @@ -539,7 +577,7 @@ [docs] - def aggregation_fit(self, classif_predictions, labels): + def aggregation_fit(self, classif_predictions, labels): self.mix_densities = self.get_mixture_components( classif_predictions, labels, @@ -552,7 +590,7 @@ [docs] - def aggregate(self, posteriors: np.ndarray): + def aggregate(self, posteriors: np.ndarray): """ Searches for the mixture model parameter (the sought prevalence values) that maximizes the likelihood of the data (i.e., that minimizes the negative log-likelihood) @@ -569,14 +607,14 @@ for kde_i in self.mix_densities ] - def neg_loglikelihood(prev): + def neg_loglikelihood(prev): prev = qp.error.smooth(prev, eps=epsilon) test_loglikelihood = logsumexp(np.log(prev)[:, None] + test_log_densities, axis=0) return -np.sum(test_loglikelihood) else: test_densities = [self.pdf(kde_i, posteriors, self.kernel) for kde_i in self.mix_densities] - def neg_loglikelihood(prev): + def neg_loglikelihood(prev): test_mixture_likelihood = prev @ test_densities test_loglikelihood = np.log(test_mixture_likelihood + epsilon) return -np.sum(test_loglikelihood) @@ -588,7 +626,7 @@ [docs] -class KDEyHD(AggregativeSoftQuantifier, KDEBase): +class KDEyHD(AggregativeSoftQuantifier, KDEBase): """ Kernel Density Estimation model for quantification (KDEy) relying on the squared Hellinger Disntace (HD) as the divergence measure to be minimized. This method was first proposed in the paper @@ -633,7 +671,7 @@ :param montecarlo_trials: number of Monte Carlo trials (default 10000) """ - def __init__(self, classifier: BaseEstimator=None, fit_classifier=True, val_split=5, divergence: str='HD', + def __init__(self, classifier: BaseEstimator=None, fit_classifier=True, val_split=5, divergence: str='HD', bandwidth=0.1, random_state=None, montecarlo_trials=10000): super().__init__(classifier, fit_classifier, val_split) @@ -644,7 +682,7 @@ [docs] - def aggregation_fit(self, classif_predictions, labels): + def aggregation_fit(self, classif_predictions, labels): self.mix_densities = self.get_mixture_components( classif_predictions, labels, self.classes_, self.bandwidth, 'gaussian' ) @@ -663,7 +701,7 @@ [docs] - def aggregate(self, posteriors: np.ndarray): + def aggregate(self, posteriors: np.ndarray): # we retain all n*N examples (sampled from a mixture with uniform parameter), and then # apply importance sampling (IS). In this version we compute D(p_alpha||q) with IS n_classes = len(self.mix_densities) @@ -671,7 +709,7 @@ test_kde = self.get_kde_function(posteriors, self.bandwidth, 'gaussian') test_densities = self.pdf(test_kde, self.reference_samples, 'gaussian') - def f_squared_hellinger(u): + def f_squared_hellinger(u): return (np.sqrt(u)-1)**2 # todo: this will fail when self.divergence is a callable, and is not the right place to do it anyway @@ -687,7 +725,7 @@ p_class = self.reference_classwise_densities + epsilon fracs = p_class/qs - def divergence(prev): + def divergence(prev): # ps / qs = (prev @ p_class) / qs = prev @ (p_class / qs) = prev @ fracs ps_div_qs = prev @ fracs return np.mean( f(ps_div_qs) * iw ) @@ -699,7 +737,7 @@ [docs] -class KDEyCS(AggregativeSoftQuantifier): +class KDEyCS(AggregativeSoftQuantifier): """ Kernel Density Estimation model for quantification (KDEy) relying on the Cauchy-Schwarz divergence (CS) as the divergence measure to be minimized. This method was first proposed in the paper @@ -736,13 +774,13 @@ :param bandwidth: float, the bandwidth of the Kernel """ - def __init__(self, classifier: BaseEstimator=None, fit_classifier=True, val_split=5, bandwidth=0.1): + def __init__(self, classifier: BaseEstimator=None, fit_classifier=True, val_split=5, bandwidth=0.1): super().__init__(classifier, fit_classifier, val_split) self.bandwidth = KDEBase._check_bandwidth(bandwidth, kernel='gaussian') [docs] - def gram_matrix_mix_sum(self, X, Y=None): + def gram_matrix_mix_sum(self, X, Y=None): # this adapts the output of the rbf_kernel function (pairwise evaluations of Gaussian kernels k(x,y)) # to contain pairwise evaluations of N(x|mu,Sigma1+Sigma2) with mu=y and Sigma1 and Sigma2 are # two "scalar matrices" (h^2)*I each, so Sigma1+Sigma2 has scalar 2(h^2) (h is the bandwidth) @@ -757,7 +795,7 @@ [docs] - def aggregation_fit(self, classif_predictions, labels): + def aggregation_fit(self, classif_predictions, labels): P, y = classif_predictions, labels n = len(self.classes_) @@ -789,7 +827,7 @@ [docs] - def aggregate(self, posteriors: np.ndarray): + def aggregate(self, posteriors: np.ndarray): Ptr = self.Ptr Pte = posteriors y = self.ytr @@ -809,7 +847,7 @@ for i in range(n): tr_te_sums[i] = self.gram_matrix_mix_sum(Ptr[y==i], Pte) - def divergence(alpha): + def divergence(alpha): # called \overline{r} in the paper alpha_ratio = alpha * self.counts_inv diff --git a/docs/build/html/_modules/quapy/method/_neural.html b/docs/build/html/_modules/quapy/method/_neural.html index 3e6c7c3..3c8215e 100644 --- a/docs/build/html/_modules/quapy/method/_neural.html +++ b/docs/build/html/_modules/quapy/method/_neural.html @@ -29,7 +29,7 @@ - + @@ -370,23 +370,24 @@ Source code for quapy.method._neural -import os -from pathlib import Path -import random +import logging +import os +from pathlib import Path +import random -import torch -from torch.nn import MSELoss -from torch.nn.functional import relu +import torch +from torch.nn import MSELoss +from torch.nn.functional import relu -from quapy.protocol import UPP -from quapy.method.aggregative import * -from quapy.util import EarlyStop -from tqdm import tqdm +from quapy.protocol import UPP +from quapy.method.aggregative import * +from quapy.util import EarlyStop +from tqdm import tqdm [docs] -class QuaNetTrainer(BaseQuantifier): +class QuaNetTrainer(BaseQuantifier): """ Implementation of `QuaNet <https://dl.acm.org/doi/abs/10.1145/3269206.3269287>`_, a neural network for quantification. This implementation uses `PyTorch <https://pytorch.org/>`_ and can take advantage of GPU @@ -438,7 +439,7 @@ :param device: string, indicate "cpu" or "cuda" """ - def __init__(self, + def __init__(self, classifier, fit_classifier=True, sample_size=None, @@ -491,7 +492,7 @@ [docs] - def fit(self, X, y): + def fit(self, X, y): """ Trains QuaNet. @@ -549,7 +550,7 @@ order_by=0 if data.binary else None, **self.quanet_params ).to(self.device) - print(self.quanet) + logging.getLogger(__name__).debug(self.quanet) self.optim = torch.optim.Adam(self.quanet.parameters(), lr=self.lr) early_stop = EarlyStop(self.patience, lower_is_better=True) @@ -564,15 +565,16 @@ if early_stop.IMPROVED: torch.save(self.quanet.state_dict(), checkpoint) elif early_stop.STOP: - print(f'training ended by patience exhausted; loading best model parameters in {checkpoint} ' - f'for epoch {early_stop.best_epoch}') + logging.getLogger(__name__).info( + f'training ended by patience exhausted; loading best model parameters in {checkpoint} ' + f'for epoch {early_stop.best_epoch}') self.quanet.load_state_dict(torch.load(checkpoint)) break return self - def _get_aggregative_estims(self, posteriors): + def _get_aggregative_estims(self, posteriors): label_predictions = np.argmax(posteriors, axis=-1) prevs_estim = [] for quantifier in self.quantifiers.values(): @@ -585,7 +587,7 @@ [docs] - def predict(self, X): + def predict(self, X): posteriors = self.classifier.predict_proba(X) embeddings = self.classifier.transform(X) quant_estims = self._get_aggregative_estims(posteriors) @@ -598,7 +600,7 @@ return prevalence - def _epoch(self, data: LabelledCollection, posteriors, iterations, epoch, early_stop, train): + def _epoch(self, data: LabelledCollection, posteriors, iterations, epoch, early_stop, train): mse_loss = MSELoss() self.quanet.train(mode=train) @@ -650,7 +652,7 @@ [docs] - def get_params(self, deep=True): + def get_params(self, deep=True): classifier_params = self.classifier.get_params() classifier_params = {'classifier__'+k:v for k,v in classifier_params.items()} return {**classifier_params, **self.quanet_params} @@ -658,7 +660,7 @@ [docs] - def set_params(self, **parameters): + def set_params(self, **parameters): learner_params = {} for key, val in parameters.items(): if key in self.quanet_params: @@ -670,7 +672,7 @@ self.classifier.set_params(**learner_params) - def __check_params_colision(self, quanet_params, learner_params): + def __check_params_colision(self, quanet_params, learner_params): quanet_keys = set(quanet_params.keys()) learner_keys = set(learner_params.keys()) intersection = quanet_keys.intersection(learner_keys) @@ -680,7 +682,7 @@ [docs] - def clean_checkpoint(self): + def clean_checkpoint(self): """ Removes the checkpoint """ @@ -689,23 +691,23 @@ [docs] - def clean_checkpoint_dir(self): + def clean_checkpoint_dir(self): """ Removes anything contained in the checkpoint directory """ - import shutil + import shutil shutil.rmtree(self.checkpointdir, ignore_errors=True) @property - def classes_(self): + def classes_(self): return self._classes_ [docs] -def mae_loss(output, target): +def mae_loss(output, target): """ Torch-like wrapper for the Mean Absolute Error @@ -719,7 +721,7 @@ [docs] -class QuaNetModule(torch.nn.Module): +class QuaNetModule(torch.nn.Module): """ Implements the `QuaNet <https://dl.acm.org/doi/abs/10.1145/3269206.3269287>`_ forward pass. See :class:`QuaNetTrainer` for training QuaNet. @@ -736,7 +738,7 @@ :param order_by: integer, class for which the document embeddings are to be sorted """ - def __init__(self, + def __init__(self, doc_embedding_size, n_classes, stats_size, @@ -771,10 +773,10 @@ self.output = torch.nn.Linear(prev_size, n_classes) @property - def device(self): + def device(self): return torch.device('cuda') if next(self.parameters()).is_cuda else torch.device('cpu') - def _init_hidden(self): + def _init_hidden(self): directions = 2 if self.bidirectional else 1 var_hidden = torch.zeros(self.nlayers * directions, 1, self.hidden_size) var_cell = torch.zeros(self.nlayers * directions, 1, self.hidden_size) @@ -784,7 +786,7 @@ [docs] - def forward(self, doc_embeddings, doc_posteriors, statistics): + def forward(self, doc_embeddings, doc_posteriors, statistics): device = self.device doc_embeddings = torch.as_tensor(doc_embeddings, dtype=torch.float, device=device) doc_posteriors = torch.as_tensor(doc_posteriors, dtype=torch.float, device=device) diff --git a/docs/build/html/_modules/quapy/method/_threshold_optim.html b/docs/build/html/_modules/quapy/method/_threshold_optim.html index 6bb8003..304e087 100644 --- a/docs/build/html/_modules/quapy/method/_threshold_optim.html +++ b/docs/build/html/_modules/quapy/method/_threshold_optim.html @@ -1,92 +1,388 @@ - - - - - - quapy.method._threshold_optim — QuaPy: A Python-based open-source framework for quantification 0.2.1 documentation - - + + + + + + + + quapy.method._threshold_optim — QuaPy: A Python-based open-source framework for quantification 0.2.1 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Skip to main content - - - - - - - - - + + + + Back to top + + + + + + + + + + Ctrl+K + + - - - - - - - - - - QuaPy: A Python-based open-source framework for quantification - - - - - - - + + - - -Quickstart - - -Manuals - - -API - - - - + + + + + + + + + + + - - - QuaPy: A Python-based open-source framework for quantification - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Home + + + + + + + Manuals + + + + + + + API + + - - - - - - Module code - quapy.method._threshold_optim - - - - - - - - Source code for quapy.method._threshold_optim -from abc import abstractmethod + + + + + + + + + -import numpy as np -from sklearn.base import BaseEstimator -import quapy as qp -import quapy.functional as F -from quapy.data import LabelledCollection -from quapy.method.aggregative import BinaryAggregativeQuantifier + + + Search + Ctrl+K + + + + + + + + + + + + + + System Settings + Light + Dark + + + + + + + + + + + + + + GitHub + + + + + + + + + + + + + Search + Ctrl+K + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Home + + + + + + + Manuals + + + + + + + API + + + + + + + + + + + + + + + + + + + + + + + System Settings + Light + Dark + + + + + + + + + + + + + + GitHub + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Module code + + quapy.method._threshold_optim + + + + + + + + + + + + + + + + + Source code for quapy.method._threshold_optim +from abc import abstractmethod + +import numpy as np +from sklearn.base import BaseEstimator +import quapy as qp +import quapy.functional as F +from quapy.data import LabelledCollection +from quapy.method.aggregative import BinaryAggregativeQuantifier [docs] -class ThresholdOptimization(BinaryAggregativeQuantifier): +class ThresholdOptimization(BinaryAggregativeQuantifier): """ Abstract class of Threshold Optimization variants for :class:`ACC` as proposed by `Forman 2006 <https://dl.acm.org/doi/abs/10.1145/1150402.1150423>`_ and @@ -111,14 +407,14 @@ :param n_jobs: number of parallel workers """ - def __init__(self, classifier: BaseEstimator=None, fit_classifier=True, val_split=None, n_jobs=None): + def __init__(self, classifier: BaseEstimator=None, fit_classifier=True, val_split=None, n_jobs=None): super().__init__(classifier, fit_classifier, val_split) self.n_jobs = qp._get_njobs(n_jobs) [docs] @abstractmethod - def condition(self, tpr, fpr) -> float: + def condition(self, tpr, fpr) -> float: """ Implements the criterion according to which the threshold should be selected. This function should return the (float) score to be minimized. @@ -132,7 +428,7 @@ [docs] - def discard(self, tpr, fpr) -> bool: + def discard(self, tpr, fpr) -> bool: """ Indicates whether a combination of tpr and fpr should be discarded @@ -144,7 +440,7 @@ - def _eval_candidate_thresholds(self, decision_scores, y): + def _eval_candidate_thresholds(self, decision_scores, y): """ Seeks for the best `tpr` and `fpr` according to the score obtained at different decision thresholds. The scoring function is implemented in function `_condition`. @@ -181,7 +477,7 @@ [docs] - def aggregate_with_threshold(self, classif_predictions, tprs, fprs, thresholds): + def aggregate_with_threshold(self, classif_predictions, tprs, fprs, thresholds): # This function performs the adjusted count for given tpr, fpr, and threshold. # Note that, due to broadcasting, tprs, fprs, and thresholds could be arrays of length > 1 prevs_estims = np.mean(classif_predictions[:, None] >= thresholds, axis=0) @@ -190,26 +486,26 @@ return prevs_estims.squeeze() - def _compute_table(self, y, y_): + def _compute_table(self, y, y_): TP = np.logical_and(y == y_, y == self.pos_label).sum() FP = np.logical_and(y != y_, y == self.neg_label).sum() FN = np.logical_and(y != y_, y == self.pos_label).sum() TN = np.logical_and(y == y_, y == self.neg_label).sum() return TP, FP, FN, TN - def _compute_tpr(self, TP, FP): - if TP + FP == 0: + def _compute_tpr(self, TP, FN): + if TP + FN == 0: return 1 - return TP / (TP + FP) + return TP / (TP + FN) - def _compute_fpr(self, FP, TN): + def _compute_fpr(self, FP, TN): if FP + TN == 0: return 0 return FP / (FP + TN) [docs] - def aggregation_fit(self, classif_predictions, labels): + def aggregation_fit(self, classif_predictions, labels): decision_scores, y = classif_predictions, labels # the standard behavior is to keep the best threshold only self.tpr, self.fpr, self.threshold = self._eval_candidate_thresholds(decision_scores, y)[0] @@ -218,7 +514,7 @@ [docs] - def aggregate(self, classif_predictions: np.ndarray): + def aggregate(self, classif_predictions: np.ndarray): # the standard behavior is to compute the adjusted count using the best threshold found return self.aggregate_with_threshold(classif_predictions, self.tpr, self.fpr, self.threshold) @@ -227,7 +523,7 @@ [docs] -class T50(ThresholdOptimization): +class T50(ThresholdOptimization): """ Threshold Optimization variant for :class:`ACC` as proposed by `Forman 2006 <https://dl.acm.org/doi/abs/10.1145/1150402.1150423>`_ and @@ -249,12 +545,12 @@ """ - def __init__(self, classifier: BaseEstimator=None, fit_classifier=True, val_split=5): + def __init__(self, classifier: BaseEstimator=None, fit_classifier=True, val_split=5): super().__init__(classifier, fit_classifier, val_split) [docs] - def condition(self, tpr, fpr) -> float: + def condition(self, tpr, fpr) -> float: return abs(tpr - 0.5) @@ -262,7 +558,7 @@ [docs] -class MAX(ThresholdOptimization): +class MAX(ThresholdOptimization): """ Threshold Optimization variant for :class:`ACC` as proposed by `Forman 2006 <https://dl.acm.org/doi/abs/10.1145/1150402.1150423>`_ and @@ -282,12 +578,12 @@ """ - def __init__(self, classifier: BaseEstimator=None, fit_classifier=True, val_split=5): + def __init__(self, classifier: BaseEstimator=None, fit_classifier=True, val_split=5): super().__init__(classifier, fit_classifier, val_split) [docs] - def condition(self, tpr, fpr) -> float: + def condition(self, tpr, fpr) -> float: # MAX strives to maximize (tpr - fpr), which is equivalent to minimize (fpr - tpr) return (fpr - tpr) @@ -296,7 +592,7 @@ [docs] -class X(ThresholdOptimization): +class X(ThresholdOptimization): """ Threshold Optimization variant for :class:`ACC` as proposed by `Forman 2006 <https://dl.acm.org/doi/abs/10.1145/1150402.1150423>`_ and @@ -316,12 +612,12 @@ """ - def __init__(self, classifier: BaseEstimator=None, fit_classifier=True, val_split=5): + def __init__(self, classifier: BaseEstimator=None, fit_classifier=True, val_split=5): super().__init__(classifier, fit_classifier, val_split) [docs] - def condition(self, tpr, fpr) -> float: + def condition(self, tpr, fpr) -> float: return abs(1 - (tpr + fpr)) @@ -329,7 +625,7 @@ [docs] -class MS(ThresholdOptimization): +class MS(ThresholdOptimization): """ Median Sweep. Threshold Optimization variant for :class:`ACC` as proposed by `Forman 2006 <https://dl.acm.org/doi/abs/10.1145/1150402.1150423>`_ and @@ -348,18 +644,18 @@ for `k`); or as a tuple (X,y) defining the specific set of data to use for validation. """ - def __init__(self, classifier: BaseEstimator=None, fit_classifier=True, val_split=5): + def __init__(self, classifier: BaseEstimator=None, fit_classifier=True, val_split=5): super().__init__(classifier, fit_classifier, val_split) [docs] - def condition(self, tpr, fpr) -> float: + def condition(self, tpr, fpr) -> float: return 1 [docs] - def aggregation_fit(self, classif_predictions, labels): + def aggregation_fit(self, classif_predictions, labels): decision_scores, y = classif_predictions, labels # keeps all candidates tprs_fprs_thresholds = self._eval_candidate_thresholds(decision_scores, y) @@ -371,7 +667,7 @@ [docs] - def aggregate(self, classif_predictions: np.ndarray): + def aggregate(self, classif_predictions: np.ndarray): prevalences = self.aggregate_with_threshold(classif_predictions, self.tprs, self.fprs, self.thresholds) if prevalences.ndim==2: prevalences = np.median(prevalences, axis=0) @@ -382,7 +678,7 @@ [docs] -class MS2(MS): +class MS2(MS): """ Median Sweep 2. Threshold Optimization variant for :class:`ACC` as proposed by `Forman 2006 <https://dl.acm.org/doi/abs/10.1145/1150402.1150423>`_ and @@ -402,42 +698,86 @@ for `k`); or as a tuple (X,y) defining the specific set of data to use for validation. """ - def __init__(self, classifier: BaseEstimator=None, fit_classifier=True, val_split=5): + def __init__(self, classifier: BaseEstimator=None, fit_classifier=True, val_split=5): super().__init__(classifier, fit_classifier, val_split) [docs] - def discard(self, tpr, fpr) -> bool: + def discard(self, tpr, fpr) -> bool: return (tpr-fpr) <= 0.25 - + + + + + + + + + + + + + - + + + + - Built with Sphinx using a - theme - provided by Read the Docs. - + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/build/html/_modules/quapy/method/aggregative.html b/docs/build/html/_modules/quapy/method/aggregative.html index a6b98d4..0680ac3 100644 --- a/docs/build/html/_modules/quapy/method/aggregative.html +++ b/docs/build/html/_modules/quapy/method/aggregative.html @@ -29,8 +29,9 @@ - - + + + @@ -41,6 +42,7 @@ + @@ -114,8 +116,14 @@ + + + + + + + - QuaPy @@ -131,7 +139,7 @@ - Quickstart + Home @@ -183,6 +191,21 @@ + + + + + + + + + + + GitHub + + + @@ -229,7 +252,7 @@ - Quickstart + Home @@ -272,6 +295,21 @@ + + + + + + + + + + + GitHub + + + @@ -332,25 +370,25 @@ Source code for quapy.method.aggregative -from abc import ABC, abstractmethod -from argparse import ArgumentError -from copy import deepcopy -from typing import Callable, Literal, Union -import numpy as np -from sklearn.base import BaseEstimator -from sklearn.calibration import CalibratedClassifierCV -from sklearn.exceptions import NotFittedError -from sklearn.metrics import confusion_matrix -from sklearn.model_selection import cross_val_predict, train_test_split -from sklearn.utils.validation import check_is_fitted +import warnings +from abc import ABC, abstractmethod +from copy import deepcopy +from typing import Callable, Literal, Union +import numpy as np +from sklearn.base import BaseEstimator +from sklearn.calibration import CalibratedClassifierCV +from sklearn.exceptions import NotFittedError +from sklearn.metrics import confusion_matrix +from sklearn.model_selection import cross_val_predict, train_test_split +from sklearn.utils.validation import check_is_fitted -import quapy as qp -import quapy.functional as F -from quapy.functional import get_divergence -from quapy.classification.svmperf import SVMperf -from quapy.data import LabelledCollection -from quapy.method.base import BaseQuantifier, BinaryQuantifier, OneVsAllGeneric -from quapy.method._helper import ( +import quapy as qp +import quapy.functional as F +from quapy.functional import get_divergence +from quapy.classification.svmperf import SVMperf +from quapy.data import LabelledCollection +from quapy.method.base import BaseQuantifier, BinaryQuantifier, OneVsAllGeneric +from quapy.method._helper import ( _get_abstention_calibrators, _get_cvxpy, _rlls_check_mode, @@ -368,7 +406,7 @@ [docs] -class AggregativeQuantifier(BaseQuantifier, ABC): +class AggregativeQuantifier(BaseQuantifier, ABC): """ Abstract class for quantification methods that base their estimations on the aggregation of classification results. Aggregative quantifiers implement a pipeline that consists of generating classification predictions @@ -396,7 +434,7 @@ the training data be wasted. """ - def __init__(self, + def __init__(self, classifier: Union[None,BaseEstimator], fit_classifier:bool=True, val_split:Union[int,float,tuple,None]=5): @@ -418,9 +456,9 @@ (f'when {val_split=} is indicated as an integer, it represents the number of folds in a kFCV ' f'and must thus be >1') if val_split==5 and not fit_classifier: - print(f'Warning: {val_split=} will be ignored when the classifier is already trained ' - f'({fit_classifier=}). Parameter {self.val_split=} will be set to None. Set {val_split=} ' - f'to None to avoid this warning.') + warnings.warn(f'{val_split=} will be ignored when the classifier is already trained ' + f'({fit_classifier=}). Parameter {self.val_split=} will be set to None. Set {val_split=} ' + f'to None to avoid this warning.') self.val_split=None if val_split!=5: assert fit_classifier, (f'Parameter {val_split=} has been modified, but {fit_classifier=} ' @@ -457,7 +495,7 @@ assert fitted, (f'{fit_classifier=} requires the classifier to be already trained, ' f'but this does not seem to be') - def _check_init_parameters(self): + def _check_init_parameters(self): """ Implements any check to be performed in the parameters of the init method before undertaking the training of the quantifier. This is made as to allow for a quick execution stop when the @@ -467,7 +505,7 @@ """ pass - def _check_non_empty_classes(self, y): + def _check_non_empty_classes(self, y): """ Asserts all classes have positive instances. @@ -484,7 +522,7 @@ [docs] - def fit(self, X, y): + def fit(self, X, y): """ Trains the aggregative quantifier. This comes down to training a classifier (if requested) and an aggregation function. @@ -501,7 +539,7 @@ [docs] - def classifier_fit_predict(self, X, y): + def classifier_fit_predict(self, X, y): """ Trains the classifier if requested (`fit_classifier=True`) and generate the necessary predictions to train the aggregation function. @@ -551,7 +589,7 @@ [docs] @abstractmethod - def aggregation_fit(self, classif_predictions, labels): + def aggregation_fit(self, classif_predictions, labels): """ Trains the aggregation function. @@ -563,7 +601,7 @@ @property - def classifier(self): + def classifier(self): """ Gives access to the classifier @@ -572,7 +610,7 @@ return self.classifier_ @classifier.setter - def classifier(self, classifier): + def classifier(self, classifier): """ Setter for the classifier @@ -582,7 +620,7 @@ [docs] - def classify(self, X): + def classify(self, X): """ Provides the label predictions for the given instances. The predictions should respect the format expected by :meth:`aggregate`, e.g., posterior probabilities for probabilistic quantifiers, or crisp predictions for @@ -594,7 +632,7 @@ return getattr(self.classifier, self._classifier_method())(X) - def _classifier_method(self): + def _classifier_method(self): """ Name of the method that must be used for issuing label predictions. The default one is "decision_function". @@ -602,7 +640,7 @@ """ return 'decision_function' - def _check_classifier(self, adapt_if_necessary=False): + def _check_classifier(self, adapt_if_necessary=False): """ Guarantees that the underlying classifier implements the method required for issuing predictions, i.e., the method indicated by the :meth:`_classifier_method` @@ -614,7 +652,7 @@ [docs] - def predict(self, X): + def predict(self, X): """ Generate class prevalence estimates for the sample's instances by aggregating the label predictions generated by the classifier. @@ -629,7 +667,7 @@ [docs] @abstractmethod - def aggregate(self, classif_predictions: np.ndarray): + def aggregate(self, classif_predictions: np.ndarray): """ Implements the aggregation of the classifier predictions. @@ -640,7 +678,7 @@ @property - def classes_(self): + def classes_(self): """ Class labels, in the same order in which class prevalence values are to be computed. This default implementation actually returns the class labels of the learner. @@ -653,14 +691,14 @@ [docs] -class AggregativeCrispQuantifier(AggregativeQuantifier, ABC): +class AggregativeCrispQuantifier(AggregativeQuantifier, ABC): """ Abstract class for quantification methods that base their estimations on the aggregation of crisp decisions as returned by a hard classifier. Aggregative crisp quantifiers thus extend Aggregative Quantifiers by implementing specifications about crisp predictions. """ - def _classifier_method(self): + def _classifier_method(self): """ Name of the method that must be used for issuing label predictions. For crisp quantifiers, the method is 'predict', that returns an array of shape `(n_instances,)` of label predictions. @@ -673,7 +711,7 @@ [docs] -class AggregativeSoftQuantifier(AggregativeQuantifier, ABC): +class AggregativeSoftQuantifier(AggregativeQuantifier, ABC): """ Abstract class for quantification methods that base their estimations on the aggregation of posterior probabilities as returned by a probabilistic classifier. @@ -681,7 +719,7 @@ about soft predictions. """ - def _classifier_method(self): + def _classifier_method(self): """ Name of the method that must be used for issuing label predictions. For probabilistic quantifiers, the method is 'predict_proba', that returns an array of shape `(n_instances, n_dimensions,)` with posterior @@ -691,7 +729,7 @@ """ return 'predict_proba' - def _check_classifier(self, adapt_if_necessary=False): + def _check_classifier(self, adapt_if_necessary=False): """ Guarantees that the underlying classifier implements the method indicated by the :meth:`_classifier_method`. In case it does not, the classifier is calibrated (by means of the Platt's calibration method implemented by @@ -703,8 +741,8 @@ """ if not hasattr(self.classifier, self._classifier_method()): if adapt_if_necessary: - print(f'warning: The learner {self.classifier.__class__.__name__} does not seem to be ' - f'probabilistic. The learner will be calibrated (using CalibratedClassifierCV).') + warnings.warn(f'The learner {self.classifier.__class__.__name__} does not seem to be ' + f'probabilistic. The learner will be calibrated (using CalibratedClassifierCV).') self.classifier = CalibratedClassifierCV(self.classifier, cv=5) else: raise AssertionError(f'error: The learner {self.classifier.__class__.__name__} does not ' @@ -715,19 +753,19 @@ [docs] -class BinaryAggregativeQuantifier(AggregativeQuantifier, BinaryQuantifier): +class BinaryAggregativeQuantifier(AggregativeQuantifier, BinaryQuantifier): @property - def pos_label(self): + def pos_label(self): return self.classifier.classes_[1] @property - def neg_label(self): + def neg_label(self): return self.classifier.classes_[0] [docs] - def fit(self, X, y): + def fit(self, X, y): self._check_binary(y, self.__class__.__name__) return super().fit(X, y) @@ -738,19 +776,19 @@ # ------------------------------------ [docs] -class CC(AggregativeCrispQuantifier): +class CC(AggregativeCrispQuantifier): """ The most basic Quantification method. One that simply classifies all instances and counts how many have been attributed to each of the classes in order to compute class prevalence estimates. :param classifier: a sklearn's Estimator that generates a classifier """ - def __init__(self, classifier: BaseEstimator = None, fit_classifier: bool = True): + def __init__(self, classifier: BaseEstimator = None, fit_classifier: bool = True): super().__init__(classifier, fit_classifier, val_split=None) [docs] - def aggregation_fit(self, classif_predictions, labels): + def aggregation_fit(self, classif_predictions, labels): """ Nothing to do here! @@ -762,7 +800,7 @@ [docs] - def aggregate(self, classif_predictions: np.ndarray): + def aggregate(self, classif_predictions: np.ndarray): """ Computes class prevalence estimates by counting the prevalence of each of the predicted labels. @@ -776,7 +814,7 @@ [docs] -class PCC(AggregativeSoftQuantifier): +class PCC(AggregativeSoftQuantifier): """ `Probabilistic Classify & Count <https://ieeexplore.ieee.org/abstract/document/5694031>`_, the probabilistic variant of CC that relies on the posterior probabilities returned by a probabilistic classifier. @@ -784,12 +822,12 @@ :param classifier: a sklearn's Estimator that generates a classifier """ - def __init__(self, classifier: BaseEstimator = None, fit_classifier: bool = True, val_split=None): + def __init__(self, classifier: BaseEstimator = None, fit_classifier: bool = True, val_split=None): super().__init__(classifier, fit_classifier, val_split=val_split) [docs] - def aggregation_fit(self, classif_predictions, labels): + def aggregation_fit(self, classif_predictions, labels): """ Nothing to do here! @@ -801,7 +839,7 @@ [docs] - def aggregate(self, classif_posteriors): + def aggregate(self, classif_posteriors): return F.prevalence_from_probabilities(classif_posteriors, binarize=False) @@ -809,7 +847,7 @@ [docs] -class ACC(AggregativeCrispQuantifier): +class ACC(AggregativeCrispQuantifier): """ `Adjusted Classify & Count <https://link.springer.com/article/10.1007/s10618-008-0097-y>`_, the "adjusted" variant of :class:`CC`, that corrects the predictions of CC @@ -857,7 +895,7 @@ :param n_jobs: number of parallel workers """ - def __init__( + def __init__( self, classifier: BaseEstimator = None, fit_classifier = True, @@ -880,7 +918,7 @@ [docs] @classmethod - def newInvariantRatioEstimation(cls, classifier: BaseEstimator, fit_classifier=True, val_split=5, n_jobs=None): + def newInvariantRatioEstimation(cls, classifier: BaseEstimator, fit_classifier=True, val_split=5, n_jobs=None): """ Constructs a quantifier that implements the Invariant Ratio Estimator of `Vaz et al. 2018 <https://jmlr.org/papers/v20/18-456.html>`_. This amounts @@ -905,7 +943,7 @@ return ACC(classifier, fit_classifier=fit_classifier, val_split=val_split, method='invariant-ratio', norm='mapsimplex', n_jobs=n_jobs) - def _check_init_parameters(self): + def _check_init_parameters(self): if self.solver not in ACC.SOLVERS: raise ValueError(f"unknown solver; valid ones are {ACC.SOLVERS}") if self.method not in ACC.METHODS: @@ -915,7 +953,7 @@ [docs] - def aggregation_fit(self, classif_predictions, labels): + def aggregation_fit(self, classif_predictions, labels): """ Estimates the misclassification rates. :param classif_predictions: array-like with the predicted labels @@ -930,7 +968,7 @@ [docs] @classmethod - def getPteCondEstim(cls, classes, y, y_): + def getPteCondEstim(cls, classes, y, y_): """ Estimate the matrix with entry (i,j) being the estimate of P(hat_yi|yj), that is, the probability that a document that belongs to yj ends up being classified as belonging to yi @@ -953,7 +991,7 @@ [docs] - def aggregate(self, classif_predictions): + def aggregate(self, classif_predictions): prevs_estim = self.cc.aggregate(classif_predictions) estimate = F.solve_adjustment( class_conditional_rates=self.Pte_cond_estim_, @@ -968,7 +1006,7 @@ [docs] -class PACC(AggregativeSoftQuantifier): +class PACC(AggregativeSoftQuantifier): """ `Probabilistic Adjusted Classify & Count <https://ieeexplore.ieee.org/abstract/document/5694031>`_, the probabilistic variant of ACC that relies on the posterior probabilities returned by a probabilistic classifier. @@ -1015,7 +1053,7 @@ :param n_jobs: number of parallel workers """ - def __init__( + def __init__( self, classifier: BaseEstimator = None, fit_classifier=True, @@ -1031,7 +1069,7 @@ self.method = method self.norm = norm - def _check_init_parameters(self): + def _check_init_parameters(self): if self.solver not in ACC.SOLVERS: raise ValueError(f"unknown solver; valid ones are {ACC.SOLVERS}") if self.method not in ACC.METHODS: @@ -1041,7 +1079,7 @@ [docs] - def aggregation_fit(self, classif_predictions, labels): + def aggregation_fit(self, classif_predictions, labels): """ Estimates the misclassification rates @@ -1056,7 +1094,7 @@ [docs] - def aggregate(self, classif_posteriors): + def aggregate(self, classif_posteriors): prevs_estim = self.pcc.aggregate(classif_posteriors) estimate = F.solve_adjustment( @@ -1071,7 +1109,7 @@ [docs] @classmethod - def getPteCondEstim(cls, classes, y, y_): + def getPteCondEstim(cls, classes, y, y_): # estimate the matrix with entry (i,j) being the estimate of P(hat_yi|yj), that is, the probability that a # document that belongs to yj ends up being classified as belonging to yi n_classes = len(classes) @@ -1088,7 +1126,7 @@ [docs] -class RLLS(AggregativeSoftQuantifier): +class RLLS(AggregativeSoftQuantifier): """ `Regularized Learning for Domain Adaptation under Label Shifts <https://arxiv.org/abs/1903.09734>`_, used here as an aggregative @@ -1128,7 +1166,7 @@ :func:`quapy.functional.normalize_prevalence` """ - def __init__( + def __init__( self, classifier: BaseEstimator = None, fit_classifier=True, @@ -1147,7 +1185,7 @@ self.norm = norm self.last_w_ = None - def _check_init_parameters(self): + def _check_init_parameters(self): _get_cvxpy() _rlls_check_mode(self.mode) if not isinstance(self.alpha, (int, float)) or self.alpha < 0: @@ -1164,7 +1202,7 @@ [docs] - def aggregation_fit(self, classif_predictions, labels): + def aggregation_fit(self, classif_predictions, labels): if classif_predictions is None or labels is None: raise ValueError('RLLS requires source posterior predictions and source labels') @@ -1181,7 +1219,7 @@ [docs] - def aggregate(self, classif_posteriors): + def aggregate(self, classif_posteriors): qz = _rlls_predicted_marginal(classif_posteriors, mode=self.mode) w = _rlls_compute_weights( self.C_zy_, @@ -1199,7 +1237,7 @@ [docs] -class EMQ(AggregativeSoftQuantifier): +class EMQ(AggregativeSoftQuantifier): """ `Expectation Maximization for Quantification <https://ieeexplore.ieee.org/abstract/document/6789744>`_ (EMQ), aka `Saerens-Latinne-Decaestecker` (SLD) algorithm. @@ -1249,7 +1287,7 @@ ON_CALIB_ERROR_VALUES = ['raise', 'backup'] CALIB_OPTIONS = [None, 'nbvs', 'bcts', 'ts', 'vs'] - def __init__(self, classifier: BaseEstimator = None, fit_classifier=True, val_split=None, exact_train_prev=True, + def __init__(self, classifier: BaseEstimator = None, fit_classifier=True, val_split=None, exact_train_prev=True, calib=None, on_calib_error='raise', n_jobs=None): assert calib in EMQ.CALIB_OPTIONS, \ @@ -1261,12 +1299,12 @@ self.exact_train_prev = exact_train_prev self.calib = calib self.on_calib_error = on_calib_error - self.n_jobs = n_jobs + self.n_jobs = qp._get_njobs(n_jobs) [docs] @classmethod - def EMQ_BCTS(cls, classifier: BaseEstimator, fit_classifier=True, val_split=5, on_calib_error="raise", n_jobs=None): + def EMQ_BCTS(cls, classifier: BaseEstimator, fit_classifier=True, val_split=5, on_calib_error="raise", n_jobs=None): """ Constructs an instance of EMQ using the best configuration found in the `Alexandari et al. paper <http://proceedings.mlr.press/v119/alexandari20a.html>`_, i.e., one that relies on Bias-Corrected Temperature @@ -1298,23 +1336,23 @@ calib='bcts', on_calib_error=on_calib_error, n_jobs=n_jobs) - def _check_init_parameters(self): + def _check_init_parameters(self): if self.val_split is not None: if self.exact_train_prev and self.calib is None: - raise RuntimeWarning(f'The parameter {self.val_split=} was specified for EMQ, while the parameters ' - f'{self.exact_train_prev=} and {self.calib=}. This has no effect and causes an ' - f'unnecessary overload.') + warnings.warn(f'The parameter {self.val_split=} was specified for EMQ, while the parameters ' + f'{self.exact_train_prev=} and {self.calib=}. This has no effect and causes an ' + f'unnecessary overload.', RuntimeWarning) else: if self.calib is not None: - print(f'[warning] The parameter {self.calib=} requires the val_split be different from None. ' - f'This parameter will be set to 5. To avoid this warning, set this value to a float value ' - f'indicating the proportion of training data to be used as validation, or to an integer ' - f'indicating the number of folds for kFCV.') + warnings.warn(f'The parameter {self.calib=} requires the val_split be different from None. ' + f'This parameter will be set to 5. To avoid this warning, set this value to a float value ' + f'indicating the proportion of training data to be used as validation, or to an integer ' + f'indicating the number of folds for kFCV.') self.val_split = 5 [docs] - def classify(self, X): + def classify(self, X): """ Provides the posterior probabilities for the given instances. The calibration function, if required, has no effect in this step, and is only involved in the aggregate method. @@ -1327,13 +1365,13 @@ [docs] - def classifier_fit_predict(self, X, y): + def classifier_fit_predict(self, X, y): classif_predictions = super().classifier_fit_predict(X, y) self.train_prevalence = F.prevalence_from_labels(y, classes=self.classes_) return classif_predictions - def _fit_calibration(self, calibrator, P, y): + def _fit_calibration(self, calibrator, P, y): n_classes = len(self.classes_) if not np.issubdtype(y.dtype, np.number): @@ -1347,7 +1385,7 @@ elif self.on_calib_error == 'backup': self.calibration_function = lambda P: P - def _calibrate_if_requested(self, uncalib_posteriors): + def _calibrate_if_requested(self, uncalib_posteriors): if hasattr(self, 'calibration_function') and self.calibration_function is not None: try: calib_posteriors = self.calibration_function(uncalib_posteriors) @@ -1364,7 +1402,7 @@ [docs] - def aggregation_fit(self, classif_predictions, labels): + def aggregation_fit(self, classif_predictions, labels): """ Trains the aggregation function of EMQ. This comes down to recalibrating the posterior probabilities ir requested. @@ -1379,14 +1417,14 @@ requires_predictions = (self.calib is not None) or (not self.exact_train_prev) if P is None and requires_predictions: # classifier predictions were not generated because val_split=None - raise ArgumentError(self.val_split, self.__class__.__name__ + - ": Classifier predictions for the aggregative fit were not generated because " - "val_split=None. This usually happens when you enable calibrations or heuristics " - "during model selection but left val_split set to its default value (None). " - "Please provide one of the following values for val_split: (i) an integer >1 " - "(e.g. val_split=5) for k-fold cross-validation; (ii) a float in (0,1) (e.g. " - "val_split=0.3) for a proportion split; or (iii) a tuple (X, y) with explicit " - "validation data") + raise ValueError(self.__class__.__name__ + + ": Classifier predictions for the aggregative fit were not generated because " + "val_split=None. This usually happens when you enable calibrations or heuristics " + "during model selection but left val_split set to its default value (None). " + "Please provide one of the following values for val_split: (i) an integer >1 " + "(e.g. val_split=5) for k-fold cross-validation; (ii) a float in (0,1) (e.g. " + "val_split=0.3) for a proportion split; or (iii) a tuple (X, y) with explicit " + "validation data") if self.calib is not None: calibrator = _get_abstention_calibrators().get(self.calib, None) @@ -1403,7 +1441,7 @@ [docs] - def aggregate(self, classif_posteriors, epsilon=EPSILON): + def aggregate(self, classif_posteriors, epsilon=EPSILON): classif_posteriors = self._calibrate_if_requested(classif_posteriors) priors, posteriors = self.EM(self.train_prevalence, classif_posteriors, epsilon) return priors @@ -1411,7 +1449,7 @@ [docs] - def predict_proba(self, instances, epsilon=EPSILON): + def predict_proba(self, instances, epsilon=EPSILON): """ Returns the posterior probabilities updated by the EM algorithm. @@ -1428,7 +1466,7 @@ [docs] @classmethod - def EM(cls, tr_prev, posterior_probabilities, epsilon=EPSILON): + def EM(cls, tr_prev, posterior_probabilities, epsilon=EPSILON): """ Computes the `Expectation Maximization` routine. @@ -1466,7 +1504,7 @@ s += 1 if not converged: - print('[warning] the method has reached the maximum number of iterations; it might have not converged') + warnings.warn('the method has reached the maximum number of iterations; it might have not converged') return qs, ps @@ -1475,7 +1513,7 @@ [docs] -class HDy(AggregativeSoftQuantifier, BinaryAggregativeQuantifier): +class HDy(AggregativeSoftQuantifier, BinaryAggregativeQuantifier): """ `Hellinger Distance y <https://www.sciencedirect.com/science/article/pii/S0020025512004069>`_ (HDy). HDy is a probabilistic method for training binary quantifiers, that models quantification as the problem of @@ -1502,12 +1540,12 @@ for `k`); or as a tuple (X,y) defining the specific set of data to use for validation. """ - def __init__(self, classifier: BaseEstimator = None, fit_classifier=True, val_split=5): + def __init__(self, classifier: BaseEstimator = None, fit_classifier=True, val_split=5): super().__init__(classifier, fit_classifier, val_split) [docs] - def aggregation_fit(self, classif_predictions, labels): + def aggregation_fit(self, classif_predictions, labels): """ Trains the aggregation function of HDy. @@ -1522,7 +1560,7 @@ # pre-compute the histogram for positive and negative examples self.bins = np.linspace(10, 110, 11, dtype=int) # [10, 20, 30, ..., 100, 110] - def hist(P, bins): + def hist(P, bins): h = np.histogram(P, bins=bins, range=(0, 1), density=True)[0] return h / h.sum() @@ -1532,7 +1570,7 @@ [docs] - def aggregate(self, classif_posteriors): + def aggregate(self, classif_posteriors): # "In this work, the number of bins b used in HDx and HDy was chosen from 10 to 110 in steps of 10, # and the final estimated a priori probability was taken as the median of these 11 estimates." # (González-Castro, et al., 2013). @@ -1549,7 +1587,7 @@ # the authors proposed to search for the prevalence yielding the best matching as a linear search # at small steps (modern implementations resort to an optimization procedure, # see class DistributionMatching) - def loss(prev): + def loss(prev): class1_prev = prev[1] Px_train = class1_prev * Pxy1_density + (1 - class1_prev) * Pxy0_density return F.HellingerDistance(Px_train, Px_test) @@ -1564,7 +1602,7 @@ [docs] -class DyS(AggregativeSoftQuantifier, BinaryAggregativeQuantifier): +class DyS(AggregativeSoftQuantifier, BinaryAggregativeQuantifier): """ `DyS framework <https://ojs.aaai.org/index.php/AAAI/article/view/4376>`_ (DyS). DyS is a generalization of HDy method, using a Ternary Search in order to find the prevalence that @@ -1593,15 +1631,15 @@ :param n_jobs: number of parallel workers. """ - def __init__(self, classifier: BaseEstimator = None, fit_classifier=True, val_split=5, n_bins=8, + def __init__(self, classifier: BaseEstimator = None, fit_classifier=True, val_split=5, n_bins=8, divergence: Union[str, Callable] = 'HD', tol=1e-05, n_jobs=None): super().__init__(classifier, fit_classifier, val_split) self.tol = tol self.divergence = divergence self.n_bins = n_bins - self.n_jobs = n_jobs + self.n_jobs = qp._get_njobs(n_jobs) - def _ternary_search(self, f, left, right, tol): + def _ternary_search(self, f, left, right, tol): """ Find maximum of unimodal function f() within [left, right] """ @@ -1619,7 +1657,7 @@ [docs] - def aggregation_fit(self, classif_predictions, labels): + def aggregation_fit(self, classif_predictions, labels): """ Trains the aggregation function of DyS. @@ -1637,13 +1675,13 @@ [docs] - def aggregate(self, classif_posteriors): + def aggregate(self, classif_posteriors): Px = classif_posteriors[:, self.pos_label] # takes only the P(y=+1|x) Px_test = np.histogram(Px, bins=self.n_bins, range=(0, 1), density=True)[0] divergence = get_divergence(self.divergence) - def distribution_distance(prev): + def distribution_distance(prev): Px_train = prev * self.Pxy1_density + (1 - prev) * self.Pxy0_density return divergence(Px_train, Px_test) @@ -1655,7 +1693,7 @@ [docs] -class SMM(AggregativeSoftQuantifier, BinaryAggregativeQuantifier): +class SMM(AggregativeSoftQuantifier, BinaryAggregativeQuantifier): """ `SMM method <https://ieeexplore.ieee.org/document/9260028>`_ (SMM). SMM is a simplification of matching distribution methods where the representation of the examples @@ -1674,12 +1712,12 @@ for `k`); or as a tuple (X,y) defining the specific set of data to use for validation. """ - def __init__(self, classifier: BaseEstimator = None, fit_classifier=True, val_split=5): + def __init__(self, classifier: BaseEstimator = None, fit_classifier=True, val_split=5): super().__init__(classifier, fit_classifier, val_split) [docs] - def aggregation_fit(self, classif_predictions, labels): + def aggregation_fit(self, classif_predictions, labels): """ Trains the aggregation function of SMM. @@ -1697,7 +1735,7 @@ [docs] - def aggregate(self, classif_posteriors): + def aggregate(self, classif_posteriors): Px = classif_posteriors[:, self.pos_label] # takes only the P(y=+1|x) Px_mean = np.mean(Px) @@ -1709,7 +1747,7 @@ [docs] -class DMy(AggregativeSoftQuantifier): +class DMy(AggregativeSoftQuantifier): """ Generic Distribution Matching quantifier for binary or multiclass quantification based on the space of posterior probabilities. This implementation takes the number of bins, the divergence, and the possibility to work on CDF @@ -1742,19 +1780,19 @@ :param n_jobs: number of parallel workers (default None) """ - def __init__(self, classifier: BaseEstimator = None, fit_classifier=True, val_split=5, nbins=8, + def __init__(self, classifier: BaseEstimator = None, fit_classifier=True, val_split=5, nbins=8, divergence: Union[str, Callable] = 'HD', cdf=False, search='optim_minimize', n_jobs=None): super().__init__(classifier, fit_classifier, val_split) self.nbins = nbins self.divergence = divergence self.cdf = cdf self.search = search - self.n_jobs = n_jobs + self.n_jobs = qp._get_njobs(n_jobs) [docs] @classmethod - def HDy(cls, classifier: BaseEstimator = None, fit_classifier=True, val_split=5, n_jobs=None): + def HDy(cls, classifier: BaseEstimator = None, fit_classifier=True, val_split=5, n_jobs=None): """ Historical HDy preset expressed as a configuration of :class:`DMy`. @@ -1783,7 +1821,7 @@ return AggregativeMedianEstimator(base_quantifier=base, param_grid=param_grid, n_jobs=n_jobs) - def _get_distributions(self, posteriors): + def _get_distributions(self, posteriors): histograms = [] post_dims = posteriors.shape[1] if post_dims == 2: @@ -1801,7 +1839,7 @@ [docs] - def aggregation_fit(self, classif_predictions, labels): + def aggregation_fit(self, classif_predictions, labels): """ Trains the aggregation function of a distribution matching method. This comes down to generating the validation distributions out of the training data. @@ -1829,7 +1867,7 @@ [docs] - def aggregate(self, posteriors: np.ndarray): + def aggregate(self, posteriors: np.ndarray): """ Searches for the mixture model parameter (the sought prevalence values) that yields a validation distribution (the mixture) that best matches the test distribution, in terms of the divergence measure of choice. @@ -1844,7 +1882,7 @@ divergence = get_divergence(self.divergence) n_classes, n_channels, nbins = self.validation_distribution.shape - def loss(prev): + def loss(prev): prev = np.expand_dims(prev, axis=0) mixture_distribution = (prev @ self.validation_distribution.reshape(n_classes, -1)).reshape(n_channels, -1) divs = [divergence(test_distribution[ch], mixture_distribution[ch]) for ch in range(n_channels)] @@ -1857,7 +1895,7 @@ [docs] -def newELM(svmperf_base=None, loss='01', C=1): +def newELM(svmperf_base=None, loss='01', C=1): """ Explicit Loss Minimization (ELM) quantifiers. Quantifiers based on ELM represent a family of methods based on structured output learning; @@ -1887,7 +1925,7 @@ [docs] -def newSVMQ(svmperf_base=None, C=1): +def newSVMQ(svmperf_base=None, C=1): """ SVM(Q) is an Explicit Loss Minimization (ELM) quantifier set to optimize for the `Q` loss combining a classification-oriented loss and a quantification-oriented loss, as proposed by @@ -1914,7 +1952,9 @@ -def newSVMKLD(svmperf_base=None, C=1): + +[docs] +def newSVMKLD(svmperf_base=None, C=1): """ SVM(KLD) is an Explicit Loss Minimization (ELM) quantifier set to optimize for the Kullback-Leibler Divergence as proposed by `Esuli et al. 2015 <https://dl.acm.org/doi/abs/10.1145/2700406>`_. @@ -1936,14 +1976,15 @@ :return: returns an instance of CC set to work with SVMperf (with loss and C set properly) as the underlying classifier """ - return newELM(svmperf_base, loss='kld', C=C) + return newELM(svmperf_base, loss='kld', C=C) - -[docs] -def newSVMKLD(svmperf_base=None, C=1): + + +[docs] +def newSVMNKLD(svmperf_base=None, C=1): """ - SVM(KLD) is an Explicit Loss Minimization (ELM) quantifier set to optimize for the Kullback-Leibler Divergence + SVM(NKLD) is an Explicit Loss Minimization (ELM) quantifier set to optimize for the Kullback-Leibler Divergence normalized via the logistic function, as proposed by `Esuli et al. 2015 <https://dl.acm.org/doi/abs/10.1145/2700406>`_. Equivalent to: @@ -1970,7 +2011,7 @@ [docs] -def newSVMAE(svmperf_base=None, C=1): +def newSVMAE(svmperf_base=None, C=1): """ SVM(KLD) is an Explicit Loss Minimization (ELM) quantifier set to optimize for the Absolute Error as first used by `Moreo and Sebastiani, 2021 <https://arxiv.org/abs/2011.02552>`_. @@ -1998,7 +2039,7 @@ [docs] -def newSVMRAE(svmperf_base=None, C=1): +def newSVMRAE(svmperf_base=None, C=1): """ SVM(KLD) is an Explicit Loss Minimization (ELM) quantifier set to optimize for the Relative Absolute Error as first used by `Moreo and Sebastiani, 2021 <https://arxiv.org/abs/2011.02552>`_. @@ -2026,7 +2067,7 @@ [docs] -class OneVsAllAggregative(OneVsAllGeneric, AggregativeQuantifier): +class OneVsAllAggregative(OneVsAllGeneric, AggregativeQuantifier): """ Allows any binary quantifier to perform quantification on single-label datasets. The method maintains one binary quantifier for each class, and then l1-normalizes the outputs so that the @@ -2042,7 +2083,7 @@ is removed and no longer available at predict time. """ - def __init__(self, binary_quantifier=None, n_jobs=None, parallel_backend='multiprocessing'): + def __init__(self, binary_quantifier=None, n_jobs=None, parallel_backend='multiprocessing'): if binary_quantifier is None: binary_quantifier = PACC() assert isinstance(binary_quantifier, BaseQuantifier), \ @@ -2055,7 +2096,7 @@ [docs] - def classify(self, X): + def classify(self, X): """ If the base quantifier is not probabilistic, returns a matrix of shape `(n,m,)` with `n` the number of instances and `m` the number of classes. The entry `(i,j)` is a binary value indicating whether instance @@ -2079,34 +2120,34 @@ [docs] - def aggregate(self, classif_predictions): + def aggregate(self, classif_predictions): prevalences = self._parallel(self._delayed_binary_aggregate, classif_predictions) return F.normalize_prevalence(prevalences) [docs] - def aggregation_fit(self, classif_predictions, labels): - self._parallel(self._delayed_binary_aggregate_fit(c, classif_predictions, labels)) + def aggregation_fit(self, classif_predictions, labels): + self._parallel(self._delayed_binary_aggregate_fit, classif_predictions, labels) return self - def _delayed_binary_classification(self, c, X): + def _delayed_binary_classification(self, c, X): return self.dict_binary_quantifiers[c].classify(X) - def _delayed_binary_aggregate(self, c, classif_predictions): + def _delayed_binary_aggregate(self, c, classif_predictions): # the estimation for the positive class prevalence return self.dict_binary_quantifiers[c].aggregate(classif_predictions[:, c])[1] - def _delayed_binary_aggregate_fit(self, c, classif_predictions, labels): + def _delayed_binary_aggregate_fit(self, c, classif_predictions, labels): # trains the aggregation function of the cth quantifier - return self.dict_binary_quantifiers[c].aggregate_fit(classif_predictions[:, c], labels) + return self.dict_binary_quantifiers[c].aggregation_fit(classif_predictions[:, c], labels == c) [docs] -class AggregativeMedianEstimator(BinaryQuantifier): +class AggregativeMedianEstimator(BinaryQuantifier): """ This method is a meta-quantifier that returns, as the estimated class prevalence values, the median of the estimation returned by differently (hyper)parameterized base quantifiers. @@ -2119,7 +2160,7 @@ :param n_jobs: number of parallel workers """ - def __init__(self, base_quantifier: AggregativeQuantifier, param_grid: dict, random_state=None, n_jobs=None): + def __init__(self, base_quantifier: AggregativeQuantifier, param_grid: dict, random_state=None, n_jobs=None): self.base_quantifier = base_quantifier self.param_grid = param_grid self.random_state = random_state @@ -2127,17 +2168,17 @@ [docs] - def get_params(self, deep=True): + def get_params(self, deep=True): return self.base_quantifier.get_params(deep) [docs] - def set_params(self, **params): + def set_params(self, **params): self.base_quantifier.set_params(**params) - def _delayed_fit(self, args): + def _delayed_fit(self, args): with qp.util.temp_seed(self.random_state): params, X, y = args model = deepcopy(self.base_quantifier) @@ -2145,7 +2186,7 @@ model.fit(X, y) return model - def _delayed_fit_classifier(self, args): + def _delayed_fit_classifier(self, args): with qp.util.temp_seed(self.random_state): cls_params, X, y = args model = deepcopy(self.base_quantifier) @@ -2153,7 +2194,7 @@ predictions, labels = model.classifier_fit_predict(X, y) return (model, predictions, labels) - def _delayed_fit_aggregation(self, args): + def _delayed_fit_aggregation(self, args): with qp.util.temp_seed(self.random_state): ((model, predictions, y), q_params) = args model = deepcopy(model) @@ -2163,8 +2204,8 @@ [docs] - def fit(self, X, y): - import itertools + def fit(self, X, y): + import itertools self._check_binary(y, self.__class__.__name__) @@ -2177,8 +2218,7 @@ ((params, X, y) for params in cls_configs), seed=qp.environ.get('_R_SEED', None), n_jobs=self.n_jobs, - asarray=False, - backend='threading' + asarray=False ) else: model = self.base_quantifier @@ -2190,8 +2230,7 @@ self._delayed_fit_aggregation, itertools.product(models_preds, q_configs), seed=qp.environ.get('_R_SEED', None), - n_jobs=self.n_jobs, - backend='threading' + n_jobs=self.n_jobs ) else: configs = qp.model_selection.expand_grid(self.param_grid) @@ -2199,25 +2238,23 @@ self._delayed_fit, ((params, X, y) for params in configs), seed=qp.environ.get('_R_SEED', None), - n_jobs=self.n_jobs, - backend='threading' + n_jobs=self.n_jobs ) return self - def _delayed_predict(self, args): + def _delayed_predict(self, args): model, instances = args return model.predict(instances) [docs] - def predict(self, instances): + def predict(self, instances): prev_preds = qp.util.parallel( self._delayed_predict, ((model, instances) for model in self.models), seed=qp.environ.get('_R_SEED', None), - n_jobs=self.n_jobs, - backend='threading' + n_jobs=self.n_jobs ) return np.median(prev_preds, axis=0) @@ -2228,7 +2265,7 @@ # imports # --------------------------------------------------------------- -from . import _threshold_optim +from . import _threshold_optim T50 = _threshold_optim.T50 MAX = _threshold_optim.MAX @@ -2236,7 +2273,7 @@ MS = _threshold_optim.MS MS2 = _threshold_optim.MS2 -from . import _kdey +from . import _kdey KDEyML = _kdey.KDEyML KDEyHD = _kdey.KDEyHD diff --git a/docs/build/html/_modules/quapy/method/base.html b/docs/build/html/_modules/quapy/method/base.html index e649c5b..5519615 100644 --- a/docs/build/html/_modules/quapy/method/base.html +++ b/docs/build/html/_modules/quapy/method/base.html @@ -1,95 +1,392 @@ - - - - - - quapy.method.base — QuaPy: A Python-based open-source framework for quantification 0.2.1 documentation - - + + + + + + + + quapy.method.base — QuaPy: A Python-based open-source framework for quantification 0.2.1 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Skip to main content - - - - - - - - - + + + + Back to top + + + + + + + + + + Ctrl+K + + - - - - - - - - - - QuaPy: A Python-based open-source framework for quantification - - - - - - - + + - - -
Source code for quapy.method._kdey -import numpy as np -from numbers import Real -from sklearn.base import BaseEstimator -from sklearn.neighbors import KernelDensity +import numpy as np +from numbers import Real +from sklearn.base import BaseEstimator +from sklearn.neighbors import KernelDensity -import quapy as qp -from quapy.method._helper import _labels_to_indices -from quapy.method.aggregative import AggregativeSoftQuantifier -import quapy.functional as F -from scipy.special import logsumexp -from sklearn.metrics.pairwise import rbf_kernel +import quapy as qp +from quapy.method._helper import _labels_to_indices +from quapy.method.aggregative import AggregativeSoftQuantifier +import quapy.functional as F +from scipy.special import logsumexp +from sklearn.metrics.pairwise import rbf_kernel [docs] -class KDEBase: +class KDEBase: """ Common ancestor for KDE-based methods. Implements some common routines. """ @@ -356,7 +394,7 @@ KERNELS = ['gaussian', 'aitchison', 'ilr'] @classmethod - def _check_bandwidth(cls, bandwidth, kernel): + def _check_bandwidth(cls, bandwidth, kernel): """ Checks that the bandwidth parameter is correct @@ -370,13 +408,13 @@ return bandwidth @classmethod - def _check_kernel(cls, kernel): + def _check_kernel(cls, kernel): assert kernel in KDEBase.KERNELS, f'unknown {kernel=}' return kernel [docs] - def get_kde_function(self, X, bandwidth, kernel): + def get_kde_function(self, X, bandwidth, kernel): """ Wraps the KDE function from scikit-learn. @@ -392,7 +430,7 @@ [docs] - def pdf(self, kde, X, kernel, log_densities=False): + def pdf(self, kde, X, kernel, log_densities=False): """ Wraps the density evalution of scikit-learn's KDE. Scikit-learn returns log-scores (s), so this function returns :math:`e^{s}` @@ -411,7 +449,7 @@ [docs] - def get_mixture_components(self, X, y, classes, bandwidth, kernel): + def get_mixture_components(self, X, y, classes, bandwidth, kernel): """ Returns an array containing the mixture components, i.e., the KDE functions for each class. @@ -433,7 +471,7 @@ [docs] - def transform_posteriors(self, X, kernel): + def transform_posteriors(self, X, kernel): if kernel in {'aitchison', 'ilr'}: X = self.shrink_posteriors(X) if kernel == 'aitchison': @@ -445,7 +483,7 @@ [docs] - def shrink_posteriors(self, X): + def shrink_posteriors(self, X): shrinkage = getattr(self, 'shrinkage', 0.0) if shrinkage <= 0: return X @@ -457,7 +495,7 @@ [docs] - def effective_bandwidth(self, bandwidth, kernel): + def effective_bandwidth(self, bandwidth, kernel): shrinkage = getattr(self, 'shrinkage', 0.0) if shrinkage > 0 and kernel in {'aitchison', 'ilr'} and isinstance(bandwidth, Real): return (1.0 - shrinkage) * float(bandwidth) @@ -466,7 +504,7 @@ [docs] - def clr_transform(self, X): + def clr_transform(self, X): if not hasattr(self, 'clr'): self.clr = F.CLRtransformation() return self.clr(X) @@ -474,7 +512,7 @@ [docs] - def ilr_transform(self, X): + def ilr_transform(self, X): if not hasattr(self, 'ilr'): self.ilr = F.ILRtransformation() return self.ilr(X) @@ -484,7 +522,7 @@ [docs] -class KDEyML(AggregativeSoftQuantifier, KDEBase): +class KDEyML(AggregativeSoftQuantifier, KDEBase): """ Kernel Density Estimation model for quantification (KDEy) relying on the Kullback-Leibler divergence (KLD) as the divergence measure to be minimized. This method was first proposed in the paper @@ -526,7 +564,7 @@ :param random_state: a seed to be set before fitting any base quantifier (default None) """ - def __init__(self, classifier: BaseEstimator=None, fit_classifier=True, val_split=5, bandwidth=0.1, + def __init__(self, classifier: BaseEstimator=None, fit_classifier=True, val_split=5, bandwidth=0.1, kernel='gaussian', shrinkage=0.0, random_state=None): super().__init__(classifier, fit_classifier, val_split) self.bandwidth = KDEBase._check_bandwidth(bandwidth, kernel) @@ -539,7 +577,7 @@ [docs] - def aggregation_fit(self, classif_predictions, labels): + def aggregation_fit(self, classif_predictions, labels): self.mix_densities = self.get_mixture_components( classif_predictions, labels, @@ -552,7 +590,7 @@ [docs] - def aggregate(self, posteriors: np.ndarray): + def aggregate(self, posteriors: np.ndarray): """ Searches for the mixture model parameter (the sought prevalence values) that maximizes the likelihood of the data (i.e., that minimizes the negative log-likelihood) @@ -569,14 +607,14 @@ for kde_i in self.mix_densities ] - def neg_loglikelihood(prev): + def neg_loglikelihood(prev): prev = qp.error.smooth(prev, eps=epsilon) test_loglikelihood = logsumexp(np.log(prev)[:, None] + test_log_densities, axis=0) return -np.sum(test_loglikelihood) else: test_densities = [self.pdf(kde_i, posteriors, self.kernel) for kde_i in self.mix_densities] - def neg_loglikelihood(prev): + def neg_loglikelihood(prev): test_mixture_likelihood = prev @ test_densities test_loglikelihood = np.log(test_mixture_likelihood + epsilon) return -np.sum(test_loglikelihood) @@ -588,7 +626,7 @@ [docs] -class KDEyHD(AggregativeSoftQuantifier, KDEBase): +class KDEyHD(AggregativeSoftQuantifier, KDEBase): """ Kernel Density Estimation model for quantification (KDEy) relying on the squared Hellinger Disntace (HD) as the divergence measure to be minimized. This method was first proposed in the paper @@ -633,7 +671,7 @@ :param montecarlo_trials: number of Monte Carlo trials (default 10000) """ - def __init__(self, classifier: BaseEstimator=None, fit_classifier=True, val_split=5, divergence: str='HD', + def __init__(self, classifier: BaseEstimator=None, fit_classifier=True, val_split=5, divergence: str='HD', bandwidth=0.1, random_state=None, montecarlo_trials=10000): super().__init__(classifier, fit_classifier, val_split) @@ -644,7 +682,7 @@ [docs] - def aggregation_fit(self, classif_predictions, labels): + def aggregation_fit(self, classif_predictions, labels): self.mix_densities = self.get_mixture_components( classif_predictions, labels, self.classes_, self.bandwidth, 'gaussian' ) @@ -663,7 +701,7 @@ [docs] - def aggregate(self, posteriors: np.ndarray): + def aggregate(self, posteriors: np.ndarray): # we retain all n*N examples (sampled from a mixture with uniform parameter), and then # apply importance sampling (IS). In this version we compute D(p_alpha||q) with IS n_classes = len(self.mix_densities) @@ -671,7 +709,7 @@ test_kde = self.get_kde_function(posteriors, self.bandwidth, 'gaussian') test_densities = self.pdf(test_kde, self.reference_samples, 'gaussian') - def f_squared_hellinger(u): + def f_squared_hellinger(u): return (np.sqrt(u)-1)**2 # todo: this will fail when self.divergence is a callable, and is not the right place to do it anyway @@ -687,7 +725,7 @@ p_class = self.reference_classwise_densities + epsilon fracs = p_class/qs - def divergence(prev): + def divergence(prev): # ps / qs = (prev @ p_class) / qs = prev @ (p_class / qs) = prev @ fracs ps_div_qs = prev @ fracs return np.mean( f(ps_div_qs) * iw ) @@ -699,7 +737,7 @@ [docs] -class KDEyCS(AggregativeSoftQuantifier): +class KDEyCS(AggregativeSoftQuantifier): """ Kernel Density Estimation model for quantification (KDEy) relying on the Cauchy-Schwarz divergence (CS) as the divergence measure to be minimized. This method was first proposed in the paper @@ -736,13 +774,13 @@ :param bandwidth: float, the bandwidth of the Kernel """ - def __init__(self, classifier: BaseEstimator=None, fit_classifier=True, val_split=5, bandwidth=0.1): + def __init__(self, classifier: BaseEstimator=None, fit_classifier=True, val_split=5, bandwidth=0.1): super().__init__(classifier, fit_classifier, val_split) self.bandwidth = KDEBase._check_bandwidth(bandwidth, kernel='gaussian') [docs] - def gram_matrix_mix_sum(self, X, Y=None): + def gram_matrix_mix_sum(self, X, Y=None): # this adapts the output of the rbf_kernel function (pairwise evaluations of Gaussian kernels k(x,y)) # to contain pairwise evaluations of N(x|mu,Sigma1+Sigma2) with mu=y and Sigma1 and Sigma2 are # two "scalar matrices" (h^2)*I each, so Sigma1+Sigma2 has scalar 2(h^2) (h is the bandwidth) @@ -757,7 +795,7 @@ [docs] - def aggregation_fit(self, classif_predictions, labels): + def aggregation_fit(self, classif_predictions, labels): P, y = classif_predictions, labels n = len(self.classes_) @@ -789,7 +827,7 @@ [docs] - def aggregate(self, posteriors: np.ndarray): + def aggregate(self, posteriors: np.ndarray): Ptr = self.Ptr Pte = posteriors y = self.ytr @@ -809,7 +847,7 @@ for i in range(n): tr_te_sums[i] = self.gram_matrix_mix_sum(Ptr[y==i], Pte) - def divergence(alpha): + def divergence(alpha): # called \overline{r} in the paper alpha_ratio = alpha * self.counts_inv diff --git a/docs/build/html/_modules/quapy/method/_neural.html b/docs/build/html/_modules/quapy/method/_neural.html index 3e6c7c3..3c8215e 100644 --- a/docs/build/html/_modules/quapy/method/_neural.html +++ b/docs/build/html/_modules/quapy/method/_neural.html @@ -29,7 +29,7 @@ - + @@ -370,23 +370,24 @@ Source code for quapy.method._neural -import os -from pathlib import Path -import random +import logging +import os +from pathlib import Path +import random -import torch -from torch.nn import MSELoss -from torch.nn.functional import relu +import torch +from torch.nn import MSELoss +from torch.nn.functional import relu -from quapy.protocol import UPP -from quapy.method.aggregative import * -from quapy.util import EarlyStop -from tqdm import tqdm +from quapy.protocol import UPP +from quapy.method.aggregative import * +from quapy.util import EarlyStop +from tqdm import tqdm [docs] -class QuaNetTrainer(BaseQuantifier): +class QuaNetTrainer(BaseQuantifier): """ Implementation of `QuaNet <https://dl.acm.org/doi/abs/10.1145/3269206.3269287>`_, a neural network for quantification. This implementation uses `PyTorch <https://pytorch.org/>`_ and can take advantage of GPU @@ -438,7 +439,7 @@ :param device: string, indicate "cpu" or "cuda" """ - def __init__(self, + def __init__(self, classifier, fit_classifier=True, sample_size=None, @@ -491,7 +492,7 @@ [docs] - def fit(self, X, y): + def fit(self, X, y): """ Trains QuaNet. @@ -549,7 +550,7 @@ order_by=0 if data.binary else None, **self.quanet_params ).to(self.device) - print(self.quanet) + logging.getLogger(__name__).debug(self.quanet) self.optim = torch.optim.Adam(self.quanet.parameters(), lr=self.lr) early_stop = EarlyStop(self.patience, lower_is_better=True) @@ -564,15 +565,16 @@ if early_stop.IMPROVED: torch.save(self.quanet.state_dict(), checkpoint) elif early_stop.STOP: - print(f'training ended by patience exhausted; loading best model parameters in {checkpoint} ' - f'for epoch {early_stop.best_epoch}') + logging.getLogger(__name__).info( + f'training ended by patience exhausted; loading best model parameters in {checkpoint} ' + f'for epoch {early_stop.best_epoch}') self.quanet.load_state_dict(torch.load(checkpoint)) break return self - def _get_aggregative_estims(self, posteriors): + def _get_aggregative_estims(self, posteriors): label_predictions = np.argmax(posteriors, axis=-1) prevs_estim = [] for quantifier in self.quantifiers.values(): @@ -585,7 +587,7 @@ [docs] - def predict(self, X): + def predict(self, X): posteriors = self.classifier.predict_proba(X) embeddings = self.classifier.transform(X) quant_estims = self._get_aggregative_estims(posteriors) @@ -598,7 +600,7 @@ return prevalence - def _epoch(self, data: LabelledCollection, posteriors, iterations, epoch, early_stop, train): + def _epoch(self, data: LabelledCollection, posteriors, iterations, epoch, early_stop, train): mse_loss = MSELoss() self.quanet.train(mode=train) @@ -650,7 +652,7 @@ [docs] - def get_params(self, deep=True): + def get_params(self, deep=True): classifier_params = self.classifier.get_params() classifier_params = {'classifier__'+k:v for k,v in classifier_params.items()} return {**classifier_params, **self.quanet_params} @@ -658,7 +660,7 @@ [docs] - def set_params(self, **parameters): + def set_params(self, **parameters): learner_params = {} for key, val in parameters.items(): if key in self.quanet_params: @@ -670,7 +672,7 @@ self.classifier.set_params(**learner_params) - def __check_params_colision(self, quanet_params, learner_params): + def __check_params_colision(self, quanet_params, learner_params): quanet_keys = set(quanet_params.keys()) learner_keys = set(learner_params.keys()) intersection = quanet_keys.intersection(learner_keys) @@ -680,7 +682,7 @@ [docs] - def clean_checkpoint(self): + def clean_checkpoint(self): """ Removes the checkpoint """ @@ -689,23 +691,23 @@ [docs] - def clean_checkpoint_dir(self): + def clean_checkpoint_dir(self): """ Removes anything contained in the checkpoint directory """ - import shutil + import shutil shutil.rmtree(self.checkpointdir, ignore_errors=True) @property - def classes_(self): + def classes_(self): return self._classes_ [docs] -def mae_loss(output, target): +def mae_loss(output, target): """ Torch-like wrapper for the Mean Absolute Error @@ -719,7 +721,7 @@ [docs] -class QuaNetModule(torch.nn.Module): +class QuaNetModule(torch.nn.Module): """ Implements the `QuaNet <https://dl.acm.org/doi/abs/10.1145/3269206.3269287>`_ forward pass. See :class:`QuaNetTrainer` for training QuaNet. @@ -736,7 +738,7 @@ :param order_by: integer, class for which the document embeddings are to be sorted """ - def __init__(self, + def __init__(self, doc_embedding_size, n_classes, stats_size, @@ -771,10 +773,10 @@ self.output = torch.nn.Linear(prev_size, n_classes) @property - def device(self): + def device(self): return torch.device('cuda') if next(self.parameters()).is_cuda else torch.device('cpu') - def _init_hidden(self): + def _init_hidden(self): directions = 2 if self.bidirectional else 1 var_hidden = torch.zeros(self.nlayers * directions, 1, self.hidden_size) var_cell = torch.zeros(self.nlayers * directions, 1, self.hidden_size) @@ -784,7 +786,7 @@ [docs] - def forward(self, doc_embeddings, doc_posteriors, statistics): + def forward(self, doc_embeddings, doc_posteriors, statistics): device = self.device doc_embeddings = torch.as_tensor(doc_embeddings, dtype=torch.float, device=device) doc_posteriors = torch.as_tensor(doc_posteriors, dtype=torch.float, device=device) diff --git a/docs/build/html/_modules/quapy/method/_threshold_optim.html b/docs/build/html/_modules/quapy/method/_threshold_optim.html index 6bb8003..304e087 100644 --- a/docs/build/html/_modules/quapy/method/_threshold_optim.html +++ b/docs/build/html/_modules/quapy/method/_threshold_optim.html @@ -1,92 +1,388 @@ - - - - - - quapy.method._threshold_optim — QuaPy: A Python-based open-source framework for quantification 0.2.1 documentation - - + + + + + + + + quapy.method._threshold_optim — QuaPy: A Python-based open-source framework for quantification 0.2.1 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Skip to main content - - - - - - - - - + + + + Back to top + + + + + + + + + + Ctrl+K + + - - - - - - - - - - QuaPy: A Python-based open-source framework for quantification - - - - - - - + + - - -Quickstart - - -Manuals - - -API - - - - + + + + + + + + + + + - - - QuaPy: A Python-based open-source framework for quantification - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Home + + + + + + + Manuals + + + + + + + API + + - - - - - - Module code - quapy.method._threshold_optim - - - - - - - - Source code for quapy.method._threshold_optim -from abc import abstractmethod + + + + + + + + + -import numpy as np -from sklearn.base import BaseEstimator -import quapy as qp -import quapy.functional as F -from quapy.data import LabelledCollection -from quapy.method.aggregative import BinaryAggregativeQuantifier + + + Search + Ctrl+K + + + + + + + + + + + + + + System Settings + Light + Dark + + + + + + + + + + + + + + GitHub + + + + + + + + + + + + + Search + Ctrl+K + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Home + + + + + + + Manuals + + + + + + + API + + + + + + + + + + + + + + + + + + + + + + + System Settings + Light + Dark + + + + + + + + + + + + + + GitHub + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Module code + + quapy.method._threshold_optim + + + + + + + + + + + + + + + + + Source code for quapy.method._threshold_optim +from abc import abstractmethod + +import numpy as np +from sklearn.base import BaseEstimator +import quapy as qp +import quapy.functional as F +from quapy.data import LabelledCollection +from quapy.method.aggregative import BinaryAggregativeQuantifier [docs] -class ThresholdOptimization(BinaryAggregativeQuantifier): +class ThresholdOptimization(BinaryAggregativeQuantifier): """ Abstract class of Threshold Optimization variants for :class:`ACC` as proposed by `Forman 2006 <https://dl.acm.org/doi/abs/10.1145/1150402.1150423>`_ and @@ -111,14 +407,14 @@ :param n_jobs: number of parallel workers """ - def __init__(self, classifier: BaseEstimator=None, fit_classifier=True, val_split=None, n_jobs=None): + def __init__(self, classifier: BaseEstimator=None, fit_classifier=True, val_split=None, n_jobs=None): super().__init__(classifier, fit_classifier, val_split) self.n_jobs = qp._get_njobs(n_jobs) [docs] @abstractmethod - def condition(self, tpr, fpr) -> float: + def condition(self, tpr, fpr) -> float: """ Implements the criterion according to which the threshold should be selected. This function should return the (float) score to be minimized. @@ -132,7 +428,7 @@ [docs] - def discard(self, tpr, fpr) -> bool: + def discard(self, tpr, fpr) -> bool: """ Indicates whether a combination of tpr and fpr should be discarded @@ -144,7 +440,7 @@ - def _eval_candidate_thresholds(self, decision_scores, y): + def _eval_candidate_thresholds(self, decision_scores, y): """ Seeks for the best `tpr` and `fpr` according to the score obtained at different decision thresholds. The scoring function is implemented in function `_condition`. @@ -181,7 +477,7 @@ [docs] - def aggregate_with_threshold(self, classif_predictions, tprs, fprs, thresholds): + def aggregate_with_threshold(self, classif_predictions, tprs, fprs, thresholds): # This function performs the adjusted count for given tpr, fpr, and threshold. # Note that, due to broadcasting, tprs, fprs, and thresholds could be arrays of length > 1 prevs_estims = np.mean(classif_predictions[:, None] >= thresholds, axis=0) @@ -190,26 +486,26 @@ return prevs_estims.squeeze() - def _compute_table(self, y, y_): + def _compute_table(self, y, y_): TP = np.logical_and(y == y_, y == self.pos_label).sum() FP = np.logical_and(y != y_, y == self.neg_label).sum() FN = np.logical_and(y != y_, y == self.pos_label).sum() TN = np.logical_and(y == y_, y == self.neg_label).sum() return TP, FP, FN, TN - def _compute_tpr(self, TP, FP): - if TP + FP == 0: + def _compute_tpr(self, TP, FN): + if TP + FN == 0: return 1 - return TP / (TP + FP) + return TP / (TP + FN) - def _compute_fpr(self, FP, TN): + def _compute_fpr(self, FP, TN): if FP + TN == 0: return 0 return FP / (FP + TN) [docs] - def aggregation_fit(self, classif_predictions, labels): + def aggregation_fit(self, classif_predictions, labels): decision_scores, y = classif_predictions, labels # the standard behavior is to keep the best threshold only self.tpr, self.fpr, self.threshold = self._eval_candidate_thresholds(decision_scores, y)[0] @@ -218,7 +514,7 @@ [docs] - def aggregate(self, classif_predictions: np.ndarray): + def aggregate(self, classif_predictions: np.ndarray): # the standard behavior is to compute the adjusted count using the best threshold found return self.aggregate_with_threshold(classif_predictions, self.tpr, self.fpr, self.threshold) @@ -227,7 +523,7 @@ [docs] -class T50(ThresholdOptimization): +class T50(ThresholdOptimization): """ Threshold Optimization variant for :class:`ACC` as proposed by `Forman 2006 <https://dl.acm.org/doi/abs/10.1145/1150402.1150423>`_ and @@ -249,12 +545,12 @@ """ - def __init__(self, classifier: BaseEstimator=None, fit_classifier=True, val_split=5): + def __init__(self, classifier: BaseEstimator=None, fit_classifier=True, val_split=5): super().__init__(classifier, fit_classifier, val_split) [docs] - def condition(self, tpr, fpr) -> float: + def condition(self, tpr, fpr) -> float: return abs(tpr - 0.5) @@ -262,7 +558,7 @@ [docs] -class MAX(ThresholdOptimization): +class MAX(ThresholdOptimization): """ Threshold Optimization variant for :class:`ACC` as proposed by `Forman 2006 <https://dl.acm.org/doi/abs/10.1145/1150402.1150423>`_ and @@ -282,12 +578,12 @@ """ - def __init__(self, classifier: BaseEstimator=None, fit_classifier=True, val_split=5): + def __init__(self, classifier: BaseEstimator=None, fit_classifier=True, val_split=5): super().__init__(classifier, fit_classifier, val_split) [docs] - def condition(self, tpr, fpr) -> float: + def condition(self, tpr, fpr) -> float: # MAX strives to maximize (tpr - fpr), which is equivalent to minimize (fpr - tpr) return (fpr - tpr) @@ -296,7 +592,7 @@ [docs] -class X(ThresholdOptimization): +class X(ThresholdOptimization): """ Threshold Optimization variant for :class:`ACC` as proposed by `Forman 2006 <https://dl.acm.org/doi/abs/10.1145/1150402.1150423>`_ and @@ -316,12 +612,12 @@ """ - def __init__(self, classifier: BaseEstimator=None, fit_classifier=True, val_split=5): + def __init__(self, classifier: BaseEstimator=None, fit_classifier=True, val_split=5): super().__init__(classifier, fit_classifier, val_split) [docs] - def condition(self, tpr, fpr) -> float: + def condition(self, tpr, fpr) -> float: return abs(1 - (tpr + fpr)) @@ -329,7 +625,7 @@ [docs] -class MS(ThresholdOptimization): +class MS(ThresholdOptimization): """ Median Sweep. Threshold Optimization variant for :class:`ACC` as proposed by `Forman 2006 <https://dl.acm.org/doi/abs/10.1145/1150402.1150423>`_ and @@ -348,18 +644,18 @@ for `k`); or as a tuple (X,y) defining the specific set of data to use for validation. """ - def __init__(self, classifier: BaseEstimator=None, fit_classifier=True, val_split=5): + def __init__(self, classifier: BaseEstimator=None, fit_classifier=True, val_split=5): super().__init__(classifier, fit_classifier, val_split) [docs] - def condition(self, tpr, fpr) -> float: + def condition(self, tpr, fpr) -> float: return 1 [docs] - def aggregation_fit(self, classif_predictions, labels): + def aggregation_fit(self, classif_predictions, labels): decision_scores, y = classif_predictions, labels # keeps all candidates tprs_fprs_thresholds = self._eval_candidate_thresholds(decision_scores, y) @@ -371,7 +667,7 @@ [docs] - def aggregate(self, classif_predictions: np.ndarray): + def aggregate(self, classif_predictions: np.ndarray): prevalences = self.aggregate_with_threshold(classif_predictions, self.tprs, self.fprs, self.thresholds) if prevalences.ndim==2: prevalences = np.median(prevalences, axis=0) @@ -382,7 +678,7 @@ [docs] -class MS2(MS): +class MS2(MS): """ Median Sweep 2. Threshold Optimization variant for :class:`ACC` as proposed by `Forman 2006 <https://dl.acm.org/doi/abs/10.1145/1150402.1150423>`_ and @@ -402,42 +698,86 @@ for `k`); or as a tuple (X,y) defining the specific set of data to use for validation. """ - def __init__(self, classifier: BaseEstimator=None, fit_classifier=True, val_split=5): + def __init__(self, classifier: BaseEstimator=None, fit_classifier=True, val_split=5): super().__init__(classifier, fit_classifier, val_split) [docs] - def discard(self, tpr, fpr) -> bool: + def discard(self, tpr, fpr) -> bool: return (tpr-fpr) <= 0.25 - + + + + + + + + + + + + + - + + + + - Built with Sphinx using a - theme - provided by Read the Docs. - + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/build/html/_modules/quapy/method/aggregative.html b/docs/build/html/_modules/quapy/method/aggregative.html index a6b98d4..0680ac3 100644 --- a/docs/build/html/_modules/quapy/method/aggregative.html +++ b/docs/build/html/_modules/quapy/method/aggregative.html @@ -29,8 +29,9 @@ - - + + + @@ -41,6 +42,7 @@ + @@ -114,8 +116,14 @@ + + + + + + + - QuaPy @@ -131,7 +139,7 @@ - Quickstart + Home @@ -183,6 +191,21 @@ + + + + + + + + + + + GitHub + + + @@ -229,7 +252,7 @@ - Quickstart + Home @@ -272,6 +295,21 @@ + + + + + + + + + + + GitHub + + + @@ -332,25 +370,25 @@ Source code for quapy.method.aggregative -from abc import ABC, abstractmethod -from argparse import ArgumentError -from copy import deepcopy -from typing import Callable, Literal, Union -import numpy as np -from sklearn.base import BaseEstimator -from sklearn.calibration import CalibratedClassifierCV -from sklearn.exceptions import NotFittedError -from sklearn.metrics import confusion_matrix -from sklearn.model_selection import cross_val_predict, train_test_split -from sklearn.utils.validation import check_is_fitted +import warnings +from abc import ABC, abstractmethod +from copy import deepcopy +from typing import Callable, Literal, Union +import numpy as np +from sklearn.base import BaseEstimator +from sklearn.calibration import CalibratedClassifierCV +from sklearn.exceptions import NotFittedError +from sklearn.metrics import confusion_matrix +from sklearn.model_selection import cross_val_predict, train_test_split +from sklearn.utils.validation import check_is_fitted -import quapy as qp -import quapy.functional as F -from quapy.functional import get_divergence -from quapy.classification.svmperf import SVMperf -from quapy.data import LabelledCollection -from quapy.method.base import BaseQuantifier, BinaryQuantifier, OneVsAllGeneric -from quapy.method._helper import ( +import quapy as qp +import quapy.functional as F +from quapy.functional import get_divergence +from quapy.classification.svmperf import SVMperf +from quapy.data import LabelledCollection +from quapy.method.base import BaseQuantifier, BinaryQuantifier, OneVsAllGeneric +from quapy.method._helper import ( _get_abstention_calibrators, _get_cvxpy, _rlls_check_mode, @@ -368,7 +406,7 @@ [docs] -class AggregativeQuantifier(BaseQuantifier, ABC): +class AggregativeQuantifier(BaseQuantifier, ABC): """ Abstract class for quantification methods that base their estimations on the aggregation of classification results. Aggregative quantifiers implement a pipeline that consists of generating classification predictions @@ -396,7 +434,7 @@ the training data be wasted. """ - def __init__(self, + def __init__(self, classifier: Union[None,BaseEstimator], fit_classifier:bool=True, val_split:Union[int,float,tuple,None]=5): @@ -418,9 +456,9 @@ (f'when {val_split=} is indicated as an integer, it represents the number of folds in a kFCV ' f'and must thus be >1') if val_split==5 and not fit_classifier: - print(f'Warning: {val_split=} will be ignored when the classifier is already trained ' - f'({fit_classifier=}). Parameter {self.val_split=} will be set to None. Set {val_split=} ' - f'to None to avoid this warning.') + warnings.warn(f'{val_split=} will be ignored when the classifier is already trained ' + f'({fit_classifier=}). Parameter {self.val_split=} will be set to None. Set {val_split=} ' + f'to None to avoid this warning.') self.val_split=None if val_split!=5: assert fit_classifier, (f'Parameter {val_split=} has been modified, but {fit_classifier=} ' @@ -457,7 +495,7 @@ assert fitted, (f'{fit_classifier=} requires the classifier to be already trained, ' f'but this does not seem to be') - def _check_init_parameters(self): + def _check_init_parameters(self): """ Implements any check to be performed in the parameters of the init method before undertaking the training of the quantifier. This is made as to allow for a quick execution stop when the @@ -467,7 +505,7 @@ """ pass - def _check_non_empty_classes(self, y): + def _check_non_empty_classes(self, y): """ Asserts all classes have positive instances. @@ -484,7 +522,7 @@ [docs] - def fit(self, X, y): + def fit(self, X, y): """ Trains the aggregative quantifier. This comes down to training a classifier (if requested) and an aggregation function. @@ -501,7 +539,7 @@ [docs] - def classifier_fit_predict(self, X, y): + def classifier_fit_predict(self, X, y): """ Trains the classifier if requested (`fit_classifier=True`) and generate the necessary predictions to train the aggregation function. @@ -551,7 +589,7 @@ [docs] @abstractmethod - def aggregation_fit(self, classif_predictions, labels): + def aggregation_fit(self, classif_predictions, labels): """ Trains the aggregation function. @@ -563,7 +601,7 @@ @property - def classifier(self): + def classifier(self): """ Gives access to the classifier @@ -572,7 +610,7 @@ return self.classifier_ @classifier.setter - def classifier(self, classifier): + def classifier(self, classifier): """ Setter for the classifier @@ -582,7 +620,7 @@ [docs] - def classify(self, X): + def classify(self, X): """ Provides the label predictions for the given instances. The predictions should respect the format expected by :meth:`aggregate`, e.g., posterior probabilities for probabilistic quantifiers, or crisp predictions for @@ -594,7 +632,7 @@ return getattr(self.classifier, self._classifier_method())(X) - def _classifier_method(self): + def _classifier_method(self): """ Name of the method that must be used for issuing label predictions. The default one is "decision_function". @@ -602,7 +640,7 @@ """ return 'decision_function' - def _check_classifier(self, adapt_if_necessary=False): + def _check_classifier(self, adapt_if_necessary=False): """ Guarantees that the underlying classifier implements the method required for issuing predictions, i.e., the method indicated by the :meth:`_classifier_method` @@ -614,7 +652,7 @@ [docs] - def predict(self, X): + def predict(self, X): """ Generate class prevalence estimates for the sample's instances by aggregating the label predictions generated by the classifier. @@ -629,7 +667,7 @@ [docs] @abstractmethod - def aggregate(self, classif_predictions: np.ndarray): + def aggregate(self, classif_predictions: np.ndarray): """ Implements the aggregation of the classifier predictions. @@ -640,7 +678,7 @@ @property - def classes_(self): + def classes_(self): """ Class labels, in the same order in which class prevalence values are to be computed. This default implementation actually returns the class labels of the learner. @@ -653,14 +691,14 @@ [docs] -class AggregativeCrispQuantifier(AggregativeQuantifier, ABC): +class AggregativeCrispQuantifier(AggregativeQuantifier, ABC): """ Abstract class for quantification methods that base their estimations on the aggregation of crisp decisions as returned by a hard classifier. Aggregative crisp quantifiers thus extend Aggregative Quantifiers by implementing specifications about crisp predictions. """ - def _classifier_method(self): + def _classifier_method(self): """ Name of the method that must be used for issuing label predictions. For crisp quantifiers, the method is 'predict', that returns an array of shape `(n_instances,)` of label predictions. @@ -673,7 +711,7 @@ [docs] -class AggregativeSoftQuantifier(AggregativeQuantifier, ABC): +class AggregativeSoftQuantifier(AggregativeQuantifier, ABC): """ Abstract class for quantification methods that base their estimations on the aggregation of posterior probabilities as returned by a probabilistic classifier. @@ -681,7 +719,7 @@ about soft predictions. """ - def _classifier_method(self): + def _classifier_method(self): """ Name of the method that must be used for issuing label predictions. For probabilistic quantifiers, the method is 'predict_proba', that returns an array of shape `(n_instances, n_dimensions,)` with posterior @@ -691,7 +729,7 @@ """ return 'predict_proba' - def _check_classifier(self, adapt_if_necessary=False): + def _check_classifier(self, adapt_if_necessary=False): """ Guarantees that the underlying classifier implements the method indicated by the :meth:`_classifier_method`. In case it does not, the classifier is calibrated (by means of the Platt's calibration method implemented by @@ -703,8 +741,8 @@ """ if not hasattr(self.classifier, self._classifier_method()): if adapt_if_necessary: - print(f'warning: The learner {self.classifier.__class__.__name__} does not seem to be ' - f'probabilistic. The learner will be calibrated (using CalibratedClassifierCV).') + warnings.warn(f'The learner {self.classifier.__class__.__name__} does not seem to be ' + f'probabilistic. The learner will be calibrated (using CalibratedClassifierCV).') self.classifier = CalibratedClassifierCV(self.classifier, cv=5) else: raise AssertionError(f'error: The learner {self.classifier.__class__.__name__} does not ' @@ -715,19 +753,19 @@ [docs] -class BinaryAggregativeQuantifier(AggregativeQuantifier, BinaryQuantifier): +class BinaryAggregativeQuantifier(AggregativeQuantifier, BinaryQuantifier): @property - def pos_label(self): + def pos_label(self): return self.classifier.classes_[1] @property - def neg_label(self): + def neg_label(self): return self.classifier.classes_[0] [docs] - def fit(self, X, y): + def fit(self, X, y): self._check_binary(y, self.__class__.__name__) return super().fit(X, y) @@ -738,19 +776,19 @@ # ------------------------------------ [docs] -class CC(AggregativeCrispQuantifier): +class CC(AggregativeCrispQuantifier): """ The most basic Quantification method. One that simply classifies all instances and counts how many have been attributed to each of the classes in order to compute class prevalence estimates. :param classifier: a sklearn's Estimator that generates a classifier """ - def __init__(self, classifier: BaseEstimator = None, fit_classifier: bool = True): + def __init__(self, classifier: BaseEstimator = None, fit_classifier: bool = True): super().__init__(classifier, fit_classifier, val_split=None) [docs] - def aggregation_fit(self, classif_predictions, labels): + def aggregation_fit(self, classif_predictions, labels): """ Nothing to do here! @@ -762,7 +800,7 @@ [docs] - def aggregate(self, classif_predictions: np.ndarray): + def aggregate(self, classif_predictions: np.ndarray): """ Computes class prevalence estimates by counting the prevalence of each of the predicted labels. @@ -776,7 +814,7 @@ [docs] -class PCC(AggregativeSoftQuantifier): +class PCC(AggregativeSoftQuantifier): """ `Probabilistic Classify & Count <https://ieeexplore.ieee.org/abstract/document/5694031>`_, the probabilistic variant of CC that relies on the posterior probabilities returned by a probabilistic classifier. @@ -784,12 +822,12 @@ :param classifier: a sklearn's Estimator that generates a classifier """ - def __init__(self, classifier: BaseEstimator = None, fit_classifier: bool = True, val_split=None): + def __init__(self, classifier: BaseEstimator = None, fit_classifier: bool = True, val_split=None): super().__init__(classifier, fit_classifier, val_split=val_split) [docs] - def aggregation_fit(self, classif_predictions, labels): + def aggregation_fit(self, classif_predictions, labels): """ Nothing to do here! @@ -801,7 +839,7 @@ [docs] - def aggregate(self, classif_posteriors): + def aggregate(self, classif_posteriors): return F.prevalence_from_probabilities(classif_posteriors, binarize=False) @@ -809,7 +847,7 @@ [docs] -class ACC(AggregativeCrispQuantifier): +class ACC(AggregativeCrispQuantifier): """ `Adjusted Classify & Count <https://link.springer.com/article/10.1007/s10618-008-0097-y>`_, the "adjusted" variant of :class:`CC`, that corrects the predictions of CC @@ -857,7 +895,7 @@ :param n_jobs: number of parallel workers """ - def __init__( + def __init__( self, classifier: BaseEstimator = None, fit_classifier = True, @@ -880,7 +918,7 @@ [docs] @classmethod - def newInvariantRatioEstimation(cls, classifier: BaseEstimator, fit_classifier=True, val_split=5, n_jobs=None): + def newInvariantRatioEstimation(cls, classifier: BaseEstimator, fit_classifier=True, val_split=5, n_jobs=None): """ Constructs a quantifier that implements the Invariant Ratio Estimator of `Vaz et al. 2018 <https://jmlr.org/papers/v20/18-456.html>`_. This amounts @@ -905,7 +943,7 @@ return ACC(classifier, fit_classifier=fit_classifier, val_split=val_split, method='invariant-ratio', norm='mapsimplex', n_jobs=n_jobs) - def _check_init_parameters(self): + def _check_init_parameters(self): if self.solver not in ACC.SOLVERS: raise ValueError(f"unknown solver; valid ones are {ACC.SOLVERS}") if self.method not in ACC.METHODS: @@ -915,7 +953,7 @@ [docs] - def aggregation_fit(self, classif_predictions, labels): + def aggregation_fit(self, classif_predictions, labels): """ Estimates the misclassification rates. :param classif_predictions: array-like with the predicted labels @@ -930,7 +968,7 @@ [docs] @classmethod - def getPteCondEstim(cls, classes, y, y_): + def getPteCondEstim(cls, classes, y, y_): """ Estimate the matrix with entry (i,j) being the estimate of P(hat_yi|yj), that is, the probability that a document that belongs to yj ends up being classified as belonging to yi @@ -953,7 +991,7 @@ [docs] - def aggregate(self, classif_predictions): + def aggregate(self, classif_predictions): prevs_estim = self.cc.aggregate(classif_predictions) estimate = F.solve_adjustment( class_conditional_rates=self.Pte_cond_estim_, @@ -968,7 +1006,7 @@ [docs] -class PACC(AggregativeSoftQuantifier): +class PACC(AggregativeSoftQuantifier): """ `Probabilistic Adjusted Classify & Count <https://ieeexplore.ieee.org/abstract/document/5694031>`_, the probabilistic variant of ACC that relies on the posterior probabilities returned by a probabilistic classifier. @@ -1015,7 +1053,7 @@ :param n_jobs: number of parallel workers """ - def __init__( + def __init__( self, classifier: BaseEstimator = None, fit_classifier=True, @@ -1031,7 +1069,7 @@ self.method = method self.norm = norm - def _check_init_parameters(self): + def _check_init_parameters(self): if self.solver not in ACC.SOLVERS: raise ValueError(f"unknown solver; valid ones are {ACC.SOLVERS}") if self.method not in ACC.METHODS: @@ -1041,7 +1079,7 @@ [docs] - def aggregation_fit(self, classif_predictions, labels): + def aggregation_fit(self, classif_predictions, labels): """ Estimates the misclassification rates @@ -1056,7 +1094,7 @@ [docs] - def aggregate(self, classif_posteriors): + def aggregate(self, classif_posteriors): prevs_estim = self.pcc.aggregate(classif_posteriors) estimate = F.solve_adjustment( @@ -1071,7 +1109,7 @@ [docs] @classmethod - def getPteCondEstim(cls, classes, y, y_): + def getPteCondEstim(cls, classes, y, y_): # estimate the matrix with entry (i,j) being the estimate of P(hat_yi|yj), that is, the probability that a # document that belongs to yj ends up being classified as belonging to yi n_classes = len(classes) @@ -1088,7 +1126,7 @@ [docs] -class RLLS(AggregativeSoftQuantifier): +class RLLS(AggregativeSoftQuantifier): """ `Regularized Learning for Domain Adaptation under Label Shifts <https://arxiv.org/abs/1903.09734>`_, used here as an aggregative @@ -1128,7 +1166,7 @@ :func:`quapy.functional.normalize_prevalence` """ - def __init__( + def __init__( self, classifier: BaseEstimator = None, fit_classifier=True, @@ -1147,7 +1185,7 @@ self.norm = norm self.last_w_ = None - def _check_init_parameters(self): + def _check_init_parameters(self): _get_cvxpy() _rlls_check_mode(self.mode) if not isinstance(self.alpha, (int, float)) or self.alpha < 0: @@ -1164,7 +1202,7 @@ [docs] - def aggregation_fit(self, classif_predictions, labels): + def aggregation_fit(self, classif_predictions, labels): if classif_predictions is None or labels is None: raise ValueError('RLLS requires source posterior predictions and source labels') @@ -1181,7 +1219,7 @@ [docs] - def aggregate(self, classif_posteriors): + def aggregate(self, classif_posteriors): qz = _rlls_predicted_marginal(classif_posteriors, mode=self.mode) w = _rlls_compute_weights( self.C_zy_, @@ -1199,7 +1237,7 @@ [docs] -class EMQ(AggregativeSoftQuantifier): +class EMQ(AggregativeSoftQuantifier): """ `Expectation Maximization for Quantification <https://ieeexplore.ieee.org/abstract/document/6789744>`_ (EMQ), aka `Saerens-Latinne-Decaestecker` (SLD) algorithm. @@ -1249,7 +1287,7 @@ ON_CALIB_ERROR_VALUES = ['raise', 'backup'] CALIB_OPTIONS = [None, 'nbvs', 'bcts', 'ts', 'vs'] - def __init__(self, classifier: BaseEstimator = None, fit_classifier=True, val_split=None, exact_train_prev=True, + def __init__(self, classifier: BaseEstimator = None, fit_classifier=True, val_split=None, exact_train_prev=True, calib=None, on_calib_error='raise', n_jobs=None): assert calib in EMQ.CALIB_OPTIONS, \ @@ -1261,12 +1299,12 @@ self.exact_train_prev = exact_train_prev self.calib = calib self.on_calib_error = on_calib_error - self.n_jobs = n_jobs + self.n_jobs = qp._get_njobs(n_jobs) [docs] @classmethod - def EMQ_BCTS(cls, classifier: BaseEstimator, fit_classifier=True, val_split=5, on_calib_error="raise", n_jobs=None): + def EMQ_BCTS(cls, classifier: BaseEstimator, fit_classifier=True, val_split=5, on_calib_error="raise", n_jobs=None): """ Constructs an instance of EMQ using the best configuration found in the `Alexandari et al. paper <http://proceedings.mlr.press/v119/alexandari20a.html>`_, i.e., one that relies on Bias-Corrected Temperature @@ -1298,23 +1336,23 @@ calib='bcts', on_calib_error=on_calib_error, n_jobs=n_jobs) - def _check_init_parameters(self): + def _check_init_parameters(self): if self.val_split is not None: if self.exact_train_prev and self.calib is None: - raise RuntimeWarning(f'The parameter {self.val_split=} was specified for EMQ, while the parameters ' - f'{self.exact_train_prev=} and {self.calib=}. This has no effect and causes an ' - f'unnecessary overload.') + warnings.warn(f'The parameter {self.val_split=} was specified for EMQ, while the parameters ' + f'{self.exact_train_prev=} and {self.calib=}. This has no effect and causes an ' + f'unnecessary overload.', RuntimeWarning) else: if self.calib is not None: - print(f'[warning] The parameter {self.calib=} requires the val_split be different from None. ' - f'This parameter will be set to 5. To avoid this warning, set this value to a float value ' - f'indicating the proportion of training data to be used as validation, or to an integer ' - f'indicating the number of folds for kFCV.') + warnings.warn(f'The parameter {self.calib=} requires the val_split be different from None. ' + f'This parameter will be set to 5. To avoid this warning, set this value to a float value ' + f'indicating the proportion of training data to be used as validation, or to an integer ' + f'indicating the number of folds for kFCV.') self.val_split = 5 [docs] - def classify(self, X): + def classify(self, X): """ Provides the posterior probabilities for the given instances. The calibration function, if required, has no effect in this step, and is only involved in the aggregate method. @@ -1327,13 +1365,13 @@ [docs] - def classifier_fit_predict(self, X, y): + def classifier_fit_predict(self, X, y): classif_predictions = super().classifier_fit_predict(X, y) self.train_prevalence = F.prevalence_from_labels(y, classes=self.classes_) return classif_predictions - def _fit_calibration(self, calibrator, P, y): + def _fit_calibration(self, calibrator, P, y): n_classes = len(self.classes_) if not np.issubdtype(y.dtype, np.number): @@ -1347,7 +1385,7 @@ elif self.on_calib_error == 'backup': self.calibration_function = lambda P: P - def _calibrate_if_requested(self, uncalib_posteriors): + def _calibrate_if_requested(self, uncalib_posteriors): if hasattr(self, 'calibration_function') and self.calibration_function is not None: try: calib_posteriors = self.calibration_function(uncalib_posteriors) @@ -1364,7 +1402,7 @@ [docs] - def aggregation_fit(self, classif_predictions, labels): + def aggregation_fit(self, classif_predictions, labels): """ Trains the aggregation function of EMQ. This comes down to recalibrating the posterior probabilities ir requested. @@ -1379,14 +1417,14 @@ requires_predictions = (self.calib is not None) or (not self.exact_train_prev) if P is None and requires_predictions: # classifier predictions were not generated because val_split=None - raise ArgumentError(self.val_split, self.__class__.__name__ + - ": Classifier predictions for the aggregative fit were not generated because " - "val_split=None. This usually happens when you enable calibrations or heuristics " - "during model selection but left val_split set to its default value (None). " - "Please provide one of the following values for val_split: (i) an integer >1 " - "(e.g. val_split=5) for k-fold cross-validation; (ii) a float in (0,1) (e.g. " - "val_split=0.3) for a proportion split; or (iii) a tuple (X, y) with explicit " - "validation data") + raise ValueError(self.__class__.__name__ + + ": Classifier predictions for the aggregative fit were not generated because " + "val_split=None. This usually happens when you enable calibrations or heuristics " + "during model selection but left val_split set to its default value (None). " + "Please provide one of the following values for val_split: (i) an integer >1 " + "(e.g. val_split=5) for k-fold cross-validation; (ii) a float in (0,1) (e.g. " + "val_split=0.3) for a proportion split; or (iii) a tuple (X, y) with explicit " + "validation data") if self.calib is not None: calibrator = _get_abstention_calibrators().get(self.calib, None) @@ -1403,7 +1441,7 @@ [docs] - def aggregate(self, classif_posteriors, epsilon=EPSILON): + def aggregate(self, classif_posteriors, epsilon=EPSILON): classif_posteriors = self._calibrate_if_requested(classif_posteriors) priors, posteriors = self.EM(self.train_prevalence, classif_posteriors, epsilon) return priors @@ -1411,7 +1449,7 @@ [docs] - def predict_proba(self, instances, epsilon=EPSILON): + def predict_proba(self, instances, epsilon=EPSILON): """ Returns the posterior probabilities updated by the EM algorithm. @@ -1428,7 +1466,7 @@ [docs] @classmethod - def EM(cls, tr_prev, posterior_probabilities, epsilon=EPSILON): + def EM(cls, tr_prev, posterior_probabilities, epsilon=EPSILON): """ Computes the `Expectation Maximization` routine. @@ -1466,7 +1504,7 @@ s += 1 if not converged: - print('[warning] the method has reached the maximum number of iterations; it might have not converged') + warnings.warn('the method has reached the maximum number of iterations; it might have not converged') return qs, ps @@ -1475,7 +1513,7 @@ [docs] -class HDy(AggregativeSoftQuantifier, BinaryAggregativeQuantifier): +class HDy(AggregativeSoftQuantifier, BinaryAggregativeQuantifier): """ `Hellinger Distance y <https://www.sciencedirect.com/science/article/pii/S0020025512004069>`_ (HDy). HDy is a probabilistic method for training binary quantifiers, that models quantification as the problem of @@ -1502,12 +1540,12 @@ for `k`); or as a tuple (X,y) defining the specific set of data to use for validation. """ - def __init__(self, classifier: BaseEstimator = None, fit_classifier=True, val_split=5): + def __init__(self, classifier: BaseEstimator = None, fit_classifier=True, val_split=5): super().__init__(classifier, fit_classifier, val_split) [docs] - def aggregation_fit(self, classif_predictions, labels): + def aggregation_fit(self, classif_predictions, labels): """ Trains the aggregation function of HDy. @@ -1522,7 +1560,7 @@ # pre-compute the histogram for positive and negative examples self.bins = np.linspace(10, 110, 11, dtype=int) # [10, 20, 30, ..., 100, 110] - def hist(P, bins): + def hist(P, bins): h = np.histogram(P, bins=bins, range=(0, 1), density=True)[0] return h / h.sum() @@ -1532,7 +1570,7 @@ [docs] - def aggregate(self, classif_posteriors): + def aggregate(self, classif_posteriors): # "In this work, the number of bins b used in HDx and HDy was chosen from 10 to 110 in steps of 10, # and the final estimated a priori probability was taken as the median of these 11 estimates." # (González-Castro, et al., 2013). @@ -1549,7 +1587,7 @@ # the authors proposed to search for the prevalence yielding the best matching as a linear search # at small steps (modern implementations resort to an optimization procedure, # see class DistributionMatching) - def loss(prev): + def loss(prev): class1_prev = prev[1] Px_train = class1_prev * Pxy1_density + (1 - class1_prev) * Pxy0_density return F.HellingerDistance(Px_train, Px_test) @@ -1564,7 +1602,7 @@ [docs] -class DyS(AggregativeSoftQuantifier, BinaryAggregativeQuantifier): +class DyS(AggregativeSoftQuantifier, BinaryAggregativeQuantifier): """ `DyS framework <https://ojs.aaai.org/index.php/AAAI/article/view/4376>`_ (DyS). DyS is a generalization of HDy method, using a Ternary Search in order to find the prevalence that @@ -1593,15 +1631,15 @@ :param n_jobs: number of parallel workers. """ - def __init__(self, classifier: BaseEstimator = None, fit_classifier=True, val_split=5, n_bins=8, + def __init__(self, classifier: BaseEstimator = None, fit_classifier=True, val_split=5, n_bins=8, divergence: Union[str, Callable] = 'HD', tol=1e-05, n_jobs=None): super().__init__(classifier, fit_classifier, val_split) self.tol = tol self.divergence = divergence self.n_bins = n_bins - self.n_jobs = n_jobs + self.n_jobs = qp._get_njobs(n_jobs) - def _ternary_search(self, f, left, right, tol): + def _ternary_search(self, f, left, right, tol): """ Find maximum of unimodal function f() within [left, right] """ @@ -1619,7 +1657,7 @@ [docs] - def aggregation_fit(self, classif_predictions, labels): + def aggregation_fit(self, classif_predictions, labels): """ Trains the aggregation function of DyS. @@ -1637,13 +1675,13 @@ [docs] - def aggregate(self, classif_posteriors): + def aggregate(self, classif_posteriors): Px = classif_posteriors[:, self.pos_label] # takes only the P(y=+1|x) Px_test = np.histogram(Px, bins=self.n_bins, range=(0, 1), density=True)[0] divergence = get_divergence(self.divergence) - def distribution_distance(prev): + def distribution_distance(prev): Px_train = prev * self.Pxy1_density + (1 - prev) * self.Pxy0_density return divergence(Px_train, Px_test) @@ -1655,7 +1693,7 @@ [docs] -class SMM(AggregativeSoftQuantifier, BinaryAggregativeQuantifier): +class SMM(AggregativeSoftQuantifier, BinaryAggregativeQuantifier): """ `SMM method <https://ieeexplore.ieee.org/document/9260028>`_ (SMM). SMM is a simplification of matching distribution methods where the representation of the examples @@ -1674,12 +1712,12 @@ for `k`); or as a tuple (X,y) defining the specific set of data to use for validation. """ - def __init__(self, classifier: BaseEstimator = None, fit_classifier=True, val_split=5): + def __init__(self, classifier: BaseEstimator = None, fit_classifier=True, val_split=5): super().__init__(classifier, fit_classifier, val_split) [docs] - def aggregation_fit(self, classif_predictions, labels): + def aggregation_fit(self, classif_predictions, labels): """ Trains the aggregation function of SMM. @@ -1697,7 +1735,7 @@ [docs] - def aggregate(self, classif_posteriors): + def aggregate(self, classif_posteriors): Px = classif_posteriors[:, self.pos_label] # takes only the P(y=+1|x) Px_mean = np.mean(Px) @@ -1709,7 +1747,7 @@ [docs] -class DMy(AggregativeSoftQuantifier): +class DMy(AggregativeSoftQuantifier): """ Generic Distribution Matching quantifier for binary or multiclass quantification based on the space of posterior probabilities. This implementation takes the number of bins, the divergence, and the possibility to work on CDF @@ -1742,19 +1780,19 @@ :param n_jobs: number of parallel workers (default None) """ - def __init__(self, classifier: BaseEstimator = None, fit_classifier=True, val_split=5, nbins=8, + def __init__(self, classifier: BaseEstimator = None, fit_classifier=True, val_split=5, nbins=8, divergence: Union[str, Callable] = 'HD', cdf=False, search='optim_minimize', n_jobs=None): super().__init__(classifier, fit_classifier, val_split) self.nbins = nbins self.divergence = divergence self.cdf = cdf self.search = search - self.n_jobs = n_jobs + self.n_jobs = qp._get_njobs(n_jobs) [docs] @classmethod - def HDy(cls, classifier: BaseEstimator = None, fit_classifier=True, val_split=5, n_jobs=None): + def HDy(cls, classifier: BaseEstimator = None, fit_classifier=True, val_split=5, n_jobs=None): """ Historical HDy preset expressed as a configuration of :class:`DMy`. @@ -1783,7 +1821,7 @@ return AggregativeMedianEstimator(base_quantifier=base, param_grid=param_grid, n_jobs=n_jobs) - def _get_distributions(self, posteriors): + def _get_distributions(self, posteriors): histograms = [] post_dims = posteriors.shape[1] if post_dims == 2: @@ -1801,7 +1839,7 @@ [docs] - def aggregation_fit(self, classif_predictions, labels): + def aggregation_fit(self, classif_predictions, labels): """ Trains the aggregation function of a distribution matching method. This comes down to generating the validation distributions out of the training data. @@ -1829,7 +1867,7 @@ [docs] - def aggregate(self, posteriors: np.ndarray): + def aggregate(self, posteriors: np.ndarray): """ Searches for the mixture model parameter (the sought prevalence values) that yields a validation distribution (the mixture) that best matches the test distribution, in terms of the divergence measure of choice. @@ -1844,7 +1882,7 @@ divergence = get_divergence(self.divergence) n_classes, n_channels, nbins = self.validation_distribution.shape - def loss(prev): + def loss(prev): prev = np.expand_dims(prev, axis=0) mixture_distribution = (prev @ self.validation_distribution.reshape(n_classes, -1)).reshape(n_channels, -1) divs = [divergence(test_distribution[ch], mixture_distribution[ch]) for ch in range(n_channels)] @@ -1857,7 +1895,7 @@ [docs] -def newELM(svmperf_base=None, loss='01', C=1): +def newELM(svmperf_base=None, loss='01', C=1): """ Explicit Loss Minimization (ELM) quantifiers. Quantifiers based on ELM represent a family of methods based on structured output learning; @@ -1887,7 +1925,7 @@ [docs] -def newSVMQ(svmperf_base=None, C=1): +def newSVMQ(svmperf_base=None, C=1): """ SVM(Q) is an Explicit Loss Minimization (ELM) quantifier set to optimize for the `Q` loss combining a classification-oriented loss and a quantification-oriented loss, as proposed by @@ -1914,7 +1952,9 @@ -def newSVMKLD(svmperf_base=None, C=1): + +[docs] +def newSVMKLD(svmperf_base=None, C=1): """ SVM(KLD) is an Explicit Loss Minimization (ELM) quantifier set to optimize for the Kullback-Leibler Divergence as proposed by `Esuli et al. 2015 <https://dl.acm.org/doi/abs/10.1145/2700406>`_. @@ -1936,14 +1976,15 @@ :return: returns an instance of CC set to work with SVMperf (with loss and C set properly) as the underlying classifier """ - return newELM(svmperf_base, loss='kld', C=C) + return newELM(svmperf_base, loss='kld', C=C) - -[docs] -def newSVMKLD(svmperf_base=None, C=1): + + +[docs] +def newSVMNKLD(svmperf_base=None, C=1): """ - SVM(KLD) is an Explicit Loss Minimization (ELM) quantifier set to optimize for the Kullback-Leibler Divergence + SVM(NKLD) is an Explicit Loss Minimization (ELM) quantifier set to optimize for the Kullback-Leibler Divergence normalized via the logistic function, as proposed by `Esuli et al. 2015 <https://dl.acm.org/doi/abs/10.1145/2700406>`_. Equivalent to: @@ -1970,7 +2011,7 @@ [docs] -def newSVMAE(svmperf_base=None, C=1): +def newSVMAE(svmperf_base=None, C=1): """ SVM(KLD) is an Explicit Loss Minimization (ELM) quantifier set to optimize for the Absolute Error as first used by `Moreo and Sebastiani, 2021 <https://arxiv.org/abs/2011.02552>`_. @@ -1998,7 +2039,7 @@ [docs] -def newSVMRAE(svmperf_base=None, C=1): +def newSVMRAE(svmperf_base=None, C=1): """ SVM(KLD) is an Explicit Loss Minimization (ELM) quantifier set to optimize for the Relative Absolute Error as first used by `Moreo and Sebastiani, 2021 <https://arxiv.org/abs/2011.02552>`_. @@ -2026,7 +2067,7 @@ [docs] -class OneVsAllAggregative(OneVsAllGeneric, AggregativeQuantifier): +class OneVsAllAggregative(OneVsAllGeneric, AggregativeQuantifier): """ Allows any binary quantifier to perform quantification on single-label datasets. The method maintains one binary quantifier for each class, and then l1-normalizes the outputs so that the @@ -2042,7 +2083,7 @@ is removed and no longer available at predict time. """ - def __init__(self, binary_quantifier=None, n_jobs=None, parallel_backend='multiprocessing'): + def __init__(self, binary_quantifier=None, n_jobs=None, parallel_backend='multiprocessing'): if binary_quantifier is None: binary_quantifier = PACC() assert isinstance(binary_quantifier, BaseQuantifier), \ @@ -2055,7 +2096,7 @@ [docs] - def classify(self, X): + def classify(self, X): """ If the base quantifier is not probabilistic, returns a matrix of shape `(n,m,)` with `n` the number of instances and `m` the number of classes. The entry `(i,j)` is a binary value indicating whether instance @@ -2079,34 +2120,34 @@ [docs] - def aggregate(self, classif_predictions): + def aggregate(self, classif_predictions): prevalences = self._parallel(self._delayed_binary_aggregate, classif_predictions) return F.normalize_prevalence(prevalences) [docs] - def aggregation_fit(self, classif_predictions, labels): - self._parallel(self._delayed_binary_aggregate_fit(c, classif_predictions, labels)) + def aggregation_fit(self, classif_predictions, labels): + self._parallel(self._delayed_binary_aggregate_fit, classif_predictions, labels) return self - def _delayed_binary_classification(self, c, X): + def _delayed_binary_classification(self, c, X): return self.dict_binary_quantifiers[c].classify(X) - def _delayed_binary_aggregate(self, c, classif_predictions): + def _delayed_binary_aggregate(self, c, classif_predictions): # the estimation for the positive class prevalence return self.dict_binary_quantifiers[c].aggregate(classif_predictions[:, c])[1] - def _delayed_binary_aggregate_fit(self, c, classif_predictions, labels): + def _delayed_binary_aggregate_fit(self, c, classif_predictions, labels): # trains the aggregation function of the cth quantifier - return self.dict_binary_quantifiers[c].aggregate_fit(classif_predictions[:, c], labels) + return self.dict_binary_quantifiers[c].aggregation_fit(classif_predictions[:, c], labels == c) [docs] -class AggregativeMedianEstimator(BinaryQuantifier): +class AggregativeMedianEstimator(BinaryQuantifier): """ This method is a meta-quantifier that returns, as the estimated class prevalence values, the median of the estimation returned by differently (hyper)parameterized base quantifiers. @@ -2119,7 +2160,7 @@ :param n_jobs: number of parallel workers """ - def __init__(self, base_quantifier: AggregativeQuantifier, param_grid: dict, random_state=None, n_jobs=None): + def __init__(self, base_quantifier: AggregativeQuantifier, param_grid: dict, random_state=None, n_jobs=None): self.base_quantifier = base_quantifier self.param_grid = param_grid self.random_state = random_state @@ -2127,17 +2168,17 @@ [docs] - def get_params(self, deep=True): + def get_params(self, deep=True): return self.base_quantifier.get_params(deep) [docs] - def set_params(self, **params): + def set_params(self, **params): self.base_quantifier.set_params(**params) - def _delayed_fit(self, args): + def _delayed_fit(self, args): with qp.util.temp_seed(self.random_state): params, X, y = args model = deepcopy(self.base_quantifier) @@ -2145,7 +2186,7 @@ model.fit(X, y) return model - def _delayed_fit_classifier(self, args): + def _delayed_fit_classifier(self, args): with qp.util.temp_seed(self.random_state): cls_params, X, y = args model = deepcopy(self.base_quantifier) @@ -2153,7 +2194,7 @@ predictions, labels = model.classifier_fit_predict(X, y) return (model, predictions, labels) - def _delayed_fit_aggregation(self, args): + def _delayed_fit_aggregation(self, args): with qp.util.temp_seed(self.random_state): ((model, predictions, y), q_params) = args model = deepcopy(model) @@ -2163,8 +2204,8 @@ [docs] - def fit(self, X, y): - import itertools + def fit(self, X, y): + import itertools self._check_binary(y, self.__class__.__name__) @@ -2177,8 +2218,7 @@ ((params, X, y) for params in cls_configs), seed=qp.environ.get('_R_SEED', None), n_jobs=self.n_jobs, - asarray=False, - backend='threading' + asarray=False ) else: model = self.base_quantifier @@ -2190,8 +2230,7 @@ self._delayed_fit_aggregation, itertools.product(models_preds, q_configs), seed=qp.environ.get('_R_SEED', None), - n_jobs=self.n_jobs, - backend='threading' + n_jobs=self.n_jobs ) else: configs = qp.model_selection.expand_grid(self.param_grid) @@ -2199,25 +2238,23 @@ self._delayed_fit, ((params, X, y) for params in configs), seed=qp.environ.get('_R_SEED', None), - n_jobs=self.n_jobs, - backend='threading' + n_jobs=self.n_jobs ) return self - def _delayed_predict(self, args): + def _delayed_predict(self, args): model, instances = args return model.predict(instances) [docs] - def predict(self, instances): + def predict(self, instances): prev_preds = qp.util.parallel( self._delayed_predict, ((model, instances) for model in self.models), seed=qp.environ.get('_R_SEED', None), - n_jobs=self.n_jobs, - backend='threading' + n_jobs=self.n_jobs ) return np.median(prev_preds, axis=0) @@ -2228,7 +2265,7 @@ # imports # --------------------------------------------------------------- -from . import _threshold_optim +from . import _threshold_optim T50 = _threshold_optim.T50 MAX = _threshold_optim.MAX @@ -2236,7 +2273,7 @@ MS = _threshold_optim.MS MS2 = _threshold_optim.MS2 -from . import _kdey +from . import _kdey KDEyML = _kdey.KDEyML KDEyHD = _kdey.KDEyHD diff --git a/docs/build/html/_modules/quapy/method/base.html b/docs/build/html/_modules/quapy/method/base.html index e649c5b..5519615 100644 --- a/docs/build/html/_modules/quapy/method/base.html +++ b/docs/build/html/_modules/quapy/method/base.html @@ -1,95 +1,392 @@ - - - - - - quapy.method.base — QuaPy: A Python-based open-source framework for quantification 0.2.1 documentation - - + + + + + + + + quapy.method.base — QuaPy: A Python-based open-source framework for quantification 0.2.1 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Skip to main content - - - - - - - - - + + + + Back to top + + + + + + + + + + Ctrl+K + + - - - - - - - - - - QuaPy: A Python-based open-source framework for quantification - - - - - - - + + - - -
Source code for quapy.method._neural -import os -from pathlib import Path -import random +import logging +import os +from pathlib import Path +import random -import torch -from torch.nn import MSELoss -from torch.nn.functional import relu +import torch +from torch.nn import MSELoss +from torch.nn.functional import relu -from quapy.protocol import UPP -from quapy.method.aggregative import * -from quapy.util import EarlyStop -from tqdm import tqdm +from quapy.protocol import UPP +from quapy.method.aggregative import * +from quapy.util import EarlyStop +from tqdm import tqdm [docs] -class QuaNetTrainer(BaseQuantifier): +class QuaNetTrainer(BaseQuantifier): """ Implementation of `QuaNet <https://dl.acm.org/doi/abs/10.1145/3269206.3269287>`_, a neural network for quantification. This implementation uses `PyTorch <https://pytorch.org/>`_ and can take advantage of GPU @@ -438,7 +439,7 @@ :param device: string, indicate "cpu" or "cuda" """ - def __init__(self, + def __init__(self, classifier, fit_classifier=True, sample_size=None, @@ -491,7 +492,7 @@ [docs] - def fit(self, X, y): + def fit(self, X, y): """ Trains QuaNet. @@ -549,7 +550,7 @@ order_by=0 if data.binary else None, **self.quanet_params ).to(self.device) - print(self.quanet) + logging.getLogger(__name__).debug(self.quanet) self.optim = torch.optim.Adam(self.quanet.parameters(), lr=self.lr) early_stop = EarlyStop(self.patience, lower_is_better=True) @@ -564,15 +565,16 @@ if early_stop.IMPROVED: torch.save(self.quanet.state_dict(), checkpoint) elif early_stop.STOP: - print(f'training ended by patience exhausted; loading best model parameters in {checkpoint} ' - f'for epoch {early_stop.best_epoch}') + logging.getLogger(__name__).info( + f'training ended by patience exhausted; loading best model parameters in {checkpoint} ' + f'for epoch {early_stop.best_epoch}') self.quanet.load_state_dict(torch.load(checkpoint)) break return self - def _get_aggregative_estims(self, posteriors): + def _get_aggregative_estims(self, posteriors): label_predictions = np.argmax(posteriors, axis=-1) prevs_estim = [] for quantifier in self.quantifiers.values(): @@ -585,7 +587,7 @@ [docs] - def predict(self, X): + def predict(self, X): posteriors = self.classifier.predict_proba(X) embeddings = self.classifier.transform(X) quant_estims = self._get_aggregative_estims(posteriors) @@ -598,7 +600,7 @@ return prevalence - def _epoch(self, data: LabelledCollection, posteriors, iterations, epoch, early_stop, train): + def _epoch(self, data: LabelledCollection, posteriors, iterations, epoch, early_stop, train): mse_loss = MSELoss() self.quanet.train(mode=train) @@ -650,7 +652,7 @@ [docs] - def get_params(self, deep=True): + def get_params(self, deep=True): classifier_params = self.classifier.get_params() classifier_params = {'classifier__'+k:v for k,v in classifier_params.items()} return {**classifier_params, **self.quanet_params} @@ -658,7 +660,7 @@ [docs] - def set_params(self, **parameters): + def set_params(self, **parameters): learner_params = {} for key, val in parameters.items(): if key in self.quanet_params: @@ -670,7 +672,7 @@ self.classifier.set_params(**learner_params) - def __check_params_colision(self, quanet_params, learner_params): + def __check_params_colision(self, quanet_params, learner_params): quanet_keys = set(quanet_params.keys()) learner_keys = set(learner_params.keys()) intersection = quanet_keys.intersection(learner_keys) @@ -680,7 +682,7 @@ [docs] - def clean_checkpoint(self): + def clean_checkpoint(self): """ Removes the checkpoint """ @@ -689,23 +691,23 @@ [docs] - def clean_checkpoint_dir(self): + def clean_checkpoint_dir(self): """ Removes anything contained in the checkpoint directory """ - import shutil + import shutil shutil.rmtree(self.checkpointdir, ignore_errors=True) @property - def classes_(self): + def classes_(self): return self._classes_ [docs] -def mae_loss(output, target): +def mae_loss(output, target): """ Torch-like wrapper for the Mean Absolute Error @@ -719,7 +721,7 @@ [docs] -class QuaNetModule(torch.nn.Module): +class QuaNetModule(torch.nn.Module): """ Implements the `QuaNet <https://dl.acm.org/doi/abs/10.1145/3269206.3269287>`_ forward pass. See :class:`QuaNetTrainer` for training QuaNet. @@ -736,7 +738,7 @@ :param order_by: integer, class for which the document embeddings are to be sorted """ - def __init__(self, + def __init__(self, doc_embedding_size, n_classes, stats_size, @@ -771,10 +773,10 @@ self.output = torch.nn.Linear(prev_size, n_classes) @property - def device(self): + def device(self): return torch.device('cuda') if next(self.parameters()).is_cuda else torch.device('cpu') - def _init_hidden(self): + def _init_hidden(self): directions = 2 if self.bidirectional else 1 var_hidden = torch.zeros(self.nlayers * directions, 1, self.hidden_size) var_cell = torch.zeros(self.nlayers * directions, 1, self.hidden_size) @@ -784,7 +786,7 @@ [docs] - def forward(self, doc_embeddings, doc_posteriors, statistics): + def forward(self, doc_embeddings, doc_posteriors, statistics): device = self.device doc_embeddings = torch.as_tensor(doc_embeddings, dtype=torch.float, device=device) doc_posteriors = torch.as_tensor(doc_posteriors, dtype=torch.float, device=device) diff --git a/docs/build/html/_modules/quapy/method/_threshold_optim.html b/docs/build/html/_modules/quapy/method/_threshold_optim.html index 6bb8003..304e087 100644 --- a/docs/build/html/_modules/quapy/method/_threshold_optim.html +++ b/docs/build/html/_modules/quapy/method/_threshold_optim.html @@ -1,92 +1,388 @@ - - - - - - quapy.method._threshold_optim — QuaPy: A Python-based open-source framework for quantification 0.2.1 documentation - - + + + + + + + + quapy.method._threshold_optim — QuaPy: A Python-based open-source framework for quantification 0.2.1 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Skip to main content - - - - - - - - - + + + + Back to top + + + + + + + + + + Ctrl+K + + - - - - - - - - - - QuaPy: A Python-based open-source framework for quantification - - - - - - - + + - - -Quickstart - - -Manuals - - -API - - - - + + + + + + + + + + + - - - QuaPy: A Python-based open-source framework for quantification - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Home + + + + + + + Manuals + + + + + + + API + + - - - - - - Module code - quapy.method._threshold_optim - - - - - - - - Source code for quapy.method._threshold_optim -from abc import abstractmethod + + + + + + + + + -import numpy as np -from sklearn.base import BaseEstimator -import quapy as qp -import quapy.functional as F -from quapy.data import LabelledCollection -from quapy.method.aggregative import BinaryAggregativeQuantifier + + + Search + Ctrl+K + + + + + + + + + + + + + + System Settings + Light + Dark + + + + + + + + + + + + + + GitHub + + + + + + + + + + + + + Search + Ctrl+K + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Home + + + + + + + Manuals + + + + + + + API + + + + + + + + + + + + + + + + + + + + + + + System Settings + Light + Dark + + + + + + + + + + + + + + GitHub + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Module code + + quapy.method._threshold_optim + + + + + + + + + + + + + + + + + Source code for quapy.method._threshold_optim +from abc import abstractmethod + +import numpy as np +from sklearn.base import BaseEstimator +import quapy as qp +import quapy.functional as F +from quapy.data import LabelledCollection +from quapy.method.aggregative import BinaryAggregativeQuantifier [docs] -class ThresholdOptimization(BinaryAggregativeQuantifier): +class ThresholdOptimization(BinaryAggregativeQuantifier): """ Abstract class of Threshold Optimization variants for :class:`ACC` as proposed by `Forman 2006 <https://dl.acm.org/doi/abs/10.1145/1150402.1150423>`_ and @@ -111,14 +407,14 @@ :param n_jobs: number of parallel workers """ - def __init__(self, classifier: BaseEstimator=None, fit_classifier=True, val_split=None, n_jobs=None): + def __init__(self, classifier: BaseEstimator=None, fit_classifier=True, val_split=None, n_jobs=None): super().__init__(classifier, fit_classifier, val_split) self.n_jobs = qp._get_njobs(n_jobs) [docs] @abstractmethod - def condition(self, tpr, fpr) -> float: + def condition(self, tpr, fpr) -> float: """ Implements the criterion according to which the threshold should be selected. This function should return the (float) score to be minimized. @@ -132,7 +428,7 @@ [docs] - def discard(self, tpr, fpr) -> bool: + def discard(self, tpr, fpr) -> bool: """ Indicates whether a combination of tpr and fpr should be discarded @@ -144,7 +440,7 @@ - def _eval_candidate_thresholds(self, decision_scores, y): + def _eval_candidate_thresholds(self, decision_scores, y): """ Seeks for the best `tpr` and `fpr` according to the score obtained at different decision thresholds. The scoring function is implemented in function `_condition`. @@ -181,7 +477,7 @@ [docs] - def aggregate_with_threshold(self, classif_predictions, tprs, fprs, thresholds): + def aggregate_with_threshold(self, classif_predictions, tprs, fprs, thresholds): # This function performs the adjusted count for given tpr, fpr, and threshold. # Note that, due to broadcasting, tprs, fprs, and thresholds could be arrays of length > 1 prevs_estims = np.mean(classif_predictions[:, None] >= thresholds, axis=0) @@ -190,26 +486,26 @@ return prevs_estims.squeeze() - def _compute_table(self, y, y_): + def _compute_table(self, y, y_): TP = np.logical_and(y == y_, y == self.pos_label).sum() FP = np.logical_and(y != y_, y == self.neg_label).sum() FN = np.logical_and(y != y_, y == self.pos_label).sum() TN = np.logical_and(y == y_, y == self.neg_label).sum() return TP, FP, FN, TN - def _compute_tpr(self, TP, FP): - if TP + FP == 0: + def _compute_tpr(self, TP, FN): + if TP + FN == 0: return 1 - return TP / (TP + FP) + return TP / (TP + FN) - def _compute_fpr(self, FP, TN): + def _compute_fpr(self, FP, TN): if FP + TN == 0: return 0 return FP / (FP + TN) [docs] - def aggregation_fit(self, classif_predictions, labels): + def aggregation_fit(self, classif_predictions, labels): decision_scores, y = classif_predictions, labels # the standard behavior is to keep the best threshold only self.tpr, self.fpr, self.threshold = self._eval_candidate_thresholds(decision_scores, y)[0] @@ -218,7 +514,7 @@ [docs] - def aggregate(self, classif_predictions: np.ndarray): + def aggregate(self, classif_predictions: np.ndarray): # the standard behavior is to compute the adjusted count using the best threshold found return self.aggregate_with_threshold(classif_predictions, self.tpr, self.fpr, self.threshold) @@ -227,7 +523,7 @@ [docs] -class T50(ThresholdOptimization): +class T50(ThresholdOptimization): """ Threshold Optimization variant for :class:`ACC` as proposed by `Forman 2006 <https://dl.acm.org/doi/abs/10.1145/1150402.1150423>`_ and @@ -249,12 +545,12 @@ """ - def __init__(self, classifier: BaseEstimator=None, fit_classifier=True, val_split=5): + def __init__(self, classifier: BaseEstimator=None, fit_classifier=True, val_split=5): super().__init__(classifier, fit_classifier, val_split) [docs] - def condition(self, tpr, fpr) -> float: + def condition(self, tpr, fpr) -> float: return abs(tpr - 0.5) @@ -262,7 +558,7 @@ [docs] -class MAX(ThresholdOptimization): +class MAX(ThresholdOptimization): """ Threshold Optimization variant for :class:`ACC` as proposed by `Forman 2006 <https://dl.acm.org/doi/abs/10.1145/1150402.1150423>`_ and @@ -282,12 +578,12 @@ """ - def __init__(self, classifier: BaseEstimator=None, fit_classifier=True, val_split=5): + def __init__(self, classifier: BaseEstimator=None, fit_classifier=True, val_split=5): super().__init__(classifier, fit_classifier, val_split) [docs] - def condition(self, tpr, fpr) -> float: + def condition(self, tpr, fpr) -> float: # MAX strives to maximize (tpr - fpr), which is equivalent to minimize (fpr - tpr) return (fpr - tpr) @@ -296,7 +592,7 @@ [docs] -class X(ThresholdOptimization): +class X(ThresholdOptimization): """ Threshold Optimization variant for :class:`ACC` as proposed by `Forman 2006 <https://dl.acm.org/doi/abs/10.1145/1150402.1150423>`_ and @@ -316,12 +612,12 @@ """ - def __init__(self, classifier: BaseEstimator=None, fit_classifier=True, val_split=5): + def __init__(self, classifier: BaseEstimator=None, fit_classifier=True, val_split=5): super().__init__(classifier, fit_classifier, val_split) [docs] - def condition(self, tpr, fpr) -> float: + def condition(self, tpr, fpr) -> float: return abs(1 - (tpr + fpr)) @@ -329,7 +625,7 @@ [docs] -class MS(ThresholdOptimization): +class MS(ThresholdOptimization): """ Median Sweep. Threshold Optimization variant for :class:`ACC` as proposed by `Forman 2006 <https://dl.acm.org/doi/abs/10.1145/1150402.1150423>`_ and @@ -348,18 +644,18 @@ for `k`); or as a tuple (X,y) defining the specific set of data to use for validation. """ - def __init__(self, classifier: BaseEstimator=None, fit_classifier=True, val_split=5): + def __init__(self, classifier: BaseEstimator=None, fit_classifier=True, val_split=5): super().__init__(classifier, fit_classifier, val_split) [docs] - def condition(self, tpr, fpr) -> float: + def condition(self, tpr, fpr) -> float: return 1 [docs] - def aggregation_fit(self, classif_predictions, labels): + def aggregation_fit(self, classif_predictions, labels): decision_scores, y = classif_predictions, labels # keeps all candidates tprs_fprs_thresholds = self._eval_candidate_thresholds(decision_scores, y) @@ -371,7 +667,7 @@ [docs] - def aggregate(self, classif_predictions: np.ndarray): + def aggregate(self, classif_predictions: np.ndarray): prevalences = self.aggregate_with_threshold(classif_predictions, self.tprs, self.fprs, self.thresholds) if prevalences.ndim==2: prevalences = np.median(prevalences, axis=0) @@ -382,7 +678,7 @@ [docs] -class MS2(MS): +class MS2(MS): """ Median Sweep 2. Threshold Optimization variant for :class:`ACC` as proposed by `Forman 2006 <https://dl.acm.org/doi/abs/10.1145/1150402.1150423>`_ and @@ -402,42 +698,86 @@ for `k`); or as a tuple (X,y) defining the specific set of data to use for validation. """ - def __init__(self, classifier: BaseEstimator=None, fit_classifier=True, val_split=5): + def __init__(self, classifier: BaseEstimator=None, fit_classifier=True, val_split=5): super().__init__(classifier, fit_classifier, val_split) [docs] - def discard(self, tpr, fpr) -> bool: + def discard(self, tpr, fpr) -> bool: return (tpr-fpr) <= 0.25 - + + + + + + + + + + + + + - + + + + - Built with Sphinx using a - theme - provided by Read the Docs. - + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/build/html/_modules/quapy/method/aggregative.html b/docs/build/html/_modules/quapy/method/aggregative.html index a6b98d4..0680ac3 100644 --- a/docs/build/html/_modules/quapy/method/aggregative.html +++ b/docs/build/html/_modules/quapy/method/aggregative.html @@ -29,8 +29,9 @@ - - + + + @@ -41,6 +42,7 @@ + @@ -114,8 +116,14 @@ + + + + + + + - QuaPy @@ -131,7 +139,7 @@ - Quickstart + Home @@ -183,6 +191,21 @@ + + + + + + + + + + + GitHub + + + @@ -229,7 +252,7 @@ - Quickstart + Home @@ -272,6 +295,21 @@ + + + + + + + + + + + GitHub + + + @@ -332,25 +370,25 @@ Source code for quapy.method.aggregative -from abc import ABC, abstractmethod -from argparse import ArgumentError -from copy import deepcopy -from typing import Callable, Literal, Union -import numpy as np -from sklearn.base import BaseEstimator -from sklearn.calibration import CalibratedClassifierCV -from sklearn.exceptions import NotFittedError -from sklearn.metrics import confusion_matrix -from sklearn.model_selection import cross_val_predict, train_test_split -from sklearn.utils.validation import check_is_fitted +import warnings +from abc import ABC, abstractmethod +from copy import deepcopy +from typing import Callable, Literal, Union +import numpy as np +from sklearn.base import BaseEstimator +from sklearn.calibration import CalibratedClassifierCV +from sklearn.exceptions import NotFittedError +from sklearn.metrics import confusion_matrix +from sklearn.model_selection import cross_val_predict, train_test_split +from sklearn.utils.validation import check_is_fitted -import quapy as qp -import quapy.functional as F -from quapy.functional import get_divergence -from quapy.classification.svmperf import SVMperf -from quapy.data import LabelledCollection -from quapy.method.base import BaseQuantifier, BinaryQuantifier, OneVsAllGeneric -from quapy.method._helper import ( +import quapy as qp +import quapy.functional as F +from quapy.functional import get_divergence +from quapy.classification.svmperf import SVMperf +from quapy.data import LabelledCollection +from quapy.method.base import BaseQuantifier, BinaryQuantifier, OneVsAllGeneric +from quapy.method._helper import ( _get_abstention_calibrators, _get_cvxpy, _rlls_check_mode, @@ -368,7 +406,7 @@ [docs] -class AggregativeQuantifier(BaseQuantifier, ABC): +class AggregativeQuantifier(BaseQuantifier, ABC): """ Abstract class for quantification methods that base their estimations on the aggregation of classification results. Aggregative quantifiers implement a pipeline that consists of generating classification predictions @@ -396,7 +434,7 @@ the training data be wasted. """ - def __init__(self, + def __init__(self, classifier: Union[None,BaseEstimator], fit_classifier:bool=True, val_split:Union[int,float,tuple,None]=5): @@ -418,9 +456,9 @@ (f'when {val_split=} is indicated as an integer, it represents the number of folds in a kFCV ' f'and must thus be >1') if val_split==5 and not fit_classifier: - print(f'Warning: {val_split=} will be ignored when the classifier is already trained ' - f'({fit_classifier=}). Parameter {self.val_split=} will be set to None. Set {val_split=} ' - f'to None to avoid this warning.') + warnings.warn(f'{val_split=} will be ignored when the classifier is already trained ' + f'({fit_classifier=}). Parameter {self.val_split=} will be set to None. Set {val_split=} ' + f'to None to avoid this warning.') self.val_split=None if val_split!=5: assert fit_classifier, (f'Parameter {val_split=} has been modified, but {fit_classifier=} ' @@ -457,7 +495,7 @@ assert fitted, (f'{fit_classifier=} requires the classifier to be already trained, ' f'but this does not seem to be') - def _check_init_parameters(self): + def _check_init_parameters(self): """ Implements any check to be performed in the parameters of the init method before undertaking the training of the quantifier. This is made as to allow for a quick execution stop when the @@ -467,7 +505,7 @@ """ pass - def _check_non_empty_classes(self, y): + def _check_non_empty_classes(self, y): """ Asserts all classes have positive instances. @@ -484,7 +522,7 @@ [docs] - def fit(self, X, y): + def fit(self, X, y): """ Trains the aggregative quantifier. This comes down to training a classifier (if requested) and an aggregation function. @@ -501,7 +539,7 @@ [docs] - def classifier_fit_predict(self, X, y): + def classifier_fit_predict(self, X, y): """ Trains the classifier if requested (`fit_classifier=True`) and generate the necessary predictions to train the aggregation function. @@ -551,7 +589,7 @@ [docs] @abstractmethod - def aggregation_fit(self, classif_predictions, labels): + def aggregation_fit(self, classif_predictions, labels): """ Trains the aggregation function. @@ -563,7 +601,7 @@ @property - def classifier(self): + def classifier(self): """ Gives access to the classifier @@ -572,7 +610,7 @@ return self.classifier_ @classifier.setter - def classifier(self, classifier): + def classifier(self, classifier): """ Setter for the classifier @@ -582,7 +620,7 @@ [docs] - def classify(self, X): + def classify(self, X): """ Provides the label predictions for the given instances. The predictions should respect the format expected by :meth:`aggregate`, e.g., posterior probabilities for probabilistic quantifiers, or crisp predictions for @@ -594,7 +632,7 @@ return getattr(self.classifier, self._classifier_method())(X) - def _classifier_method(self): + def _classifier_method(self): """ Name of the method that must be used for issuing label predictions. The default one is "decision_function". @@ -602,7 +640,7 @@ """ return 'decision_function' - def _check_classifier(self, adapt_if_necessary=False): + def _check_classifier(self, adapt_if_necessary=False): """ Guarantees that the underlying classifier implements the method required for issuing predictions, i.e., the method indicated by the :meth:`_classifier_method` @@ -614,7 +652,7 @@ [docs] - def predict(self, X): + def predict(self, X): """ Generate class prevalence estimates for the sample's instances by aggregating the label predictions generated by the classifier. @@ -629,7 +667,7 @@ [docs] @abstractmethod - def aggregate(self, classif_predictions: np.ndarray): + def aggregate(self, classif_predictions: np.ndarray): """ Implements the aggregation of the classifier predictions. @@ -640,7 +678,7 @@ @property - def classes_(self): + def classes_(self): """ Class labels, in the same order in which class prevalence values are to be computed. This default implementation actually returns the class labels of the learner. @@ -653,14 +691,14 @@ [docs] -class AggregativeCrispQuantifier(AggregativeQuantifier, ABC): +class AggregativeCrispQuantifier(AggregativeQuantifier, ABC): """ Abstract class for quantification methods that base their estimations on the aggregation of crisp decisions as returned by a hard classifier. Aggregative crisp quantifiers thus extend Aggregative Quantifiers by implementing specifications about crisp predictions. """ - def _classifier_method(self): + def _classifier_method(self): """ Name of the method that must be used for issuing label predictions. For crisp quantifiers, the method is 'predict', that returns an array of shape `(n_instances,)` of label predictions. @@ -673,7 +711,7 @@ [docs] -class AggregativeSoftQuantifier(AggregativeQuantifier, ABC): +class AggregativeSoftQuantifier(AggregativeQuantifier, ABC): """ Abstract class for quantification methods that base their estimations on the aggregation of posterior probabilities as returned by a probabilistic classifier. @@ -681,7 +719,7 @@ about soft predictions. """ - def _classifier_method(self): + def _classifier_method(self): """ Name of the method that must be used for issuing label predictions. For probabilistic quantifiers, the method is 'predict_proba', that returns an array of shape `(n_instances, n_dimensions,)` with posterior @@ -691,7 +729,7 @@ """ return 'predict_proba' - def _check_classifier(self, adapt_if_necessary=False): + def _check_classifier(self, adapt_if_necessary=False): """ Guarantees that the underlying classifier implements the method indicated by the :meth:`_classifier_method`. In case it does not, the classifier is calibrated (by means of the Platt's calibration method implemented by @@ -703,8 +741,8 @@ """ if not hasattr(self.classifier, self._classifier_method()): if adapt_if_necessary: - print(f'warning: The learner {self.classifier.__class__.__name__} does not seem to be ' - f'probabilistic. The learner will be calibrated (using CalibratedClassifierCV).') + warnings.warn(f'The learner {self.classifier.__class__.__name__} does not seem to be ' + f'probabilistic. The learner will be calibrated (using CalibratedClassifierCV).') self.classifier = CalibratedClassifierCV(self.classifier, cv=5) else: raise AssertionError(f'error: The learner {self.classifier.__class__.__name__} does not ' @@ -715,19 +753,19 @@ [docs] -class BinaryAggregativeQuantifier(AggregativeQuantifier, BinaryQuantifier): +class BinaryAggregativeQuantifier(AggregativeQuantifier, BinaryQuantifier): @property - def pos_label(self): + def pos_label(self): return self.classifier.classes_[1] @property - def neg_label(self): + def neg_label(self): return self.classifier.classes_[0] [docs] - def fit(self, X, y): + def fit(self, X, y): self._check_binary(y, self.__class__.__name__) return super().fit(X, y) @@ -738,19 +776,19 @@ # ------------------------------------ [docs] -class CC(AggregativeCrispQuantifier): +class CC(AggregativeCrispQuantifier): """ The most basic Quantification method. One that simply classifies all instances and counts how many have been attributed to each of the classes in order to compute class prevalence estimates. :param classifier: a sklearn's Estimator that generates a classifier """ - def __init__(self, classifier: BaseEstimator = None, fit_classifier: bool = True): + def __init__(self, classifier: BaseEstimator = None, fit_classifier: bool = True): super().__init__(classifier, fit_classifier, val_split=None) [docs] - def aggregation_fit(self, classif_predictions, labels): + def aggregation_fit(self, classif_predictions, labels): """ Nothing to do here! @@ -762,7 +800,7 @@ [docs] - def aggregate(self, classif_predictions: np.ndarray): + def aggregate(self, classif_predictions: np.ndarray): """ Computes class prevalence estimates by counting the prevalence of each of the predicted labels. @@ -776,7 +814,7 @@ [docs] -class PCC(AggregativeSoftQuantifier): +class PCC(AggregativeSoftQuantifier): """ `Probabilistic Classify & Count <https://ieeexplore.ieee.org/abstract/document/5694031>`_, the probabilistic variant of CC that relies on the posterior probabilities returned by a probabilistic classifier. @@ -784,12 +822,12 @@ :param classifier: a sklearn's Estimator that generates a classifier """ - def __init__(self, classifier: BaseEstimator = None, fit_classifier: bool = True, val_split=None): + def __init__(self, classifier: BaseEstimator = None, fit_classifier: bool = True, val_split=None): super().__init__(classifier, fit_classifier, val_split=val_split) [docs] - def aggregation_fit(self, classif_predictions, labels): + def aggregation_fit(self, classif_predictions, labels): """ Nothing to do here! @@ -801,7 +839,7 @@ [docs] - def aggregate(self, classif_posteriors): + def aggregate(self, classif_posteriors): return F.prevalence_from_probabilities(classif_posteriors, binarize=False) @@ -809,7 +847,7 @@ [docs] -class ACC(AggregativeCrispQuantifier): +class ACC(AggregativeCrispQuantifier): """ `Adjusted Classify & Count <https://link.springer.com/article/10.1007/s10618-008-0097-y>`_, the "adjusted" variant of :class:`CC`, that corrects the predictions of CC @@ -857,7 +895,7 @@ :param n_jobs: number of parallel workers """ - def __init__( + def __init__( self, classifier: BaseEstimator = None, fit_classifier = True, @@ -880,7 +918,7 @@ [docs] @classmethod - def newInvariantRatioEstimation(cls, classifier: BaseEstimator, fit_classifier=True, val_split=5, n_jobs=None): + def newInvariantRatioEstimation(cls, classifier: BaseEstimator, fit_classifier=True, val_split=5, n_jobs=None): """ Constructs a quantifier that implements the Invariant Ratio Estimator of `Vaz et al. 2018 <https://jmlr.org/papers/v20/18-456.html>`_. This amounts @@ -905,7 +943,7 @@ return ACC(classifier, fit_classifier=fit_classifier, val_split=val_split, method='invariant-ratio', norm='mapsimplex', n_jobs=n_jobs) - def _check_init_parameters(self): + def _check_init_parameters(self): if self.solver not in ACC.SOLVERS: raise ValueError(f"unknown solver; valid ones are {ACC.SOLVERS}") if self.method not in ACC.METHODS: @@ -915,7 +953,7 @@ [docs] - def aggregation_fit(self, classif_predictions, labels): + def aggregation_fit(self, classif_predictions, labels): """ Estimates the misclassification rates. :param classif_predictions: array-like with the predicted labels @@ -930,7 +968,7 @@ [docs] @classmethod - def getPteCondEstim(cls, classes, y, y_): + def getPteCondEstim(cls, classes, y, y_): """ Estimate the matrix with entry (i,j) being the estimate of P(hat_yi|yj), that is, the probability that a document that belongs to yj ends up being classified as belonging to yi @@ -953,7 +991,7 @@ [docs] - def aggregate(self, classif_predictions): + def aggregate(self, classif_predictions): prevs_estim = self.cc.aggregate(classif_predictions) estimate = F.solve_adjustment( class_conditional_rates=self.Pte_cond_estim_, @@ -968,7 +1006,7 @@ [docs] -class PACC(AggregativeSoftQuantifier): +class PACC(AggregativeSoftQuantifier): """ `Probabilistic Adjusted Classify & Count <https://ieeexplore.ieee.org/abstract/document/5694031>`_, the probabilistic variant of ACC that relies on the posterior probabilities returned by a probabilistic classifier. @@ -1015,7 +1053,7 @@ :param n_jobs: number of parallel workers """ - def __init__( + def __init__( self, classifier: BaseEstimator = None, fit_classifier=True, @@ -1031,7 +1069,7 @@ self.method = method self.norm = norm - def _check_init_parameters(self): + def _check_init_parameters(self): if self.solver not in ACC.SOLVERS: raise ValueError(f"unknown solver; valid ones are {ACC.SOLVERS}") if self.method not in ACC.METHODS: @@ -1041,7 +1079,7 @@ [docs] - def aggregation_fit(self, classif_predictions, labels): + def aggregation_fit(self, classif_predictions, labels): """ Estimates the misclassification rates @@ -1056,7 +1094,7 @@ [docs] - def aggregate(self, classif_posteriors): + def aggregate(self, classif_posteriors): prevs_estim = self.pcc.aggregate(classif_posteriors) estimate = F.solve_adjustment( @@ -1071,7 +1109,7 @@ [docs] @classmethod - def getPteCondEstim(cls, classes, y, y_): + def getPteCondEstim(cls, classes, y, y_): # estimate the matrix with entry (i,j) being the estimate of P(hat_yi|yj), that is, the probability that a # document that belongs to yj ends up being classified as belonging to yi n_classes = len(classes) @@ -1088,7 +1126,7 @@ [docs] -class RLLS(AggregativeSoftQuantifier): +class RLLS(AggregativeSoftQuantifier): """ `Regularized Learning for Domain Adaptation under Label Shifts <https://arxiv.org/abs/1903.09734>`_, used here as an aggregative @@ -1128,7 +1166,7 @@ :func:`quapy.functional.normalize_prevalence` """ - def __init__( + def __init__( self, classifier: BaseEstimator = None, fit_classifier=True, @@ -1147,7 +1185,7 @@ self.norm = norm self.last_w_ = None - def _check_init_parameters(self): + def _check_init_parameters(self): _get_cvxpy() _rlls_check_mode(self.mode) if not isinstance(self.alpha, (int, float)) or self.alpha < 0: @@ -1164,7 +1202,7 @@ [docs] - def aggregation_fit(self, classif_predictions, labels): + def aggregation_fit(self, classif_predictions, labels): if classif_predictions is None or labels is None: raise ValueError('RLLS requires source posterior predictions and source labels') @@ -1181,7 +1219,7 @@ [docs] - def aggregate(self, classif_posteriors): + def aggregate(self, classif_posteriors): qz = _rlls_predicted_marginal(classif_posteriors, mode=self.mode) w = _rlls_compute_weights( self.C_zy_, @@ -1199,7 +1237,7 @@ [docs] -class EMQ(AggregativeSoftQuantifier): +class EMQ(AggregativeSoftQuantifier): """ `Expectation Maximization for Quantification <https://ieeexplore.ieee.org/abstract/document/6789744>`_ (EMQ), aka `Saerens-Latinne-Decaestecker` (SLD) algorithm. @@ -1249,7 +1287,7 @@ ON_CALIB_ERROR_VALUES = ['raise', 'backup'] CALIB_OPTIONS = [None, 'nbvs', 'bcts', 'ts', 'vs'] - def __init__(self, classifier: BaseEstimator = None, fit_classifier=True, val_split=None, exact_train_prev=True, + def __init__(self, classifier: BaseEstimator = None, fit_classifier=True, val_split=None, exact_train_prev=True, calib=None, on_calib_error='raise', n_jobs=None): assert calib in EMQ.CALIB_OPTIONS, \ @@ -1261,12 +1299,12 @@ self.exact_train_prev = exact_train_prev self.calib = calib self.on_calib_error = on_calib_error - self.n_jobs = n_jobs + self.n_jobs = qp._get_njobs(n_jobs) [docs] @classmethod - def EMQ_BCTS(cls, classifier: BaseEstimator, fit_classifier=True, val_split=5, on_calib_error="raise", n_jobs=None): + def EMQ_BCTS(cls, classifier: BaseEstimator, fit_classifier=True, val_split=5, on_calib_error="raise", n_jobs=None): """ Constructs an instance of EMQ using the best configuration found in the `Alexandari et al. paper <http://proceedings.mlr.press/v119/alexandari20a.html>`_, i.e., one that relies on Bias-Corrected Temperature @@ -1298,23 +1336,23 @@ calib='bcts', on_calib_error=on_calib_error, n_jobs=n_jobs) - def _check_init_parameters(self): + def _check_init_parameters(self): if self.val_split is not None: if self.exact_train_prev and self.calib is None: - raise RuntimeWarning(f'The parameter {self.val_split=} was specified for EMQ, while the parameters ' - f'{self.exact_train_prev=} and {self.calib=}. This has no effect and causes an ' - f'unnecessary overload.') + warnings.warn(f'The parameter {self.val_split=} was specified for EMQ, while the parameters ' + f'{self.exact_train_prev=} and {self.calib=}. This has no effect and causes an ' + f'unnecessary overload.', RuntimeWarning) else: if self.calib is not None: - print(f'[warning] The parameter {self.calib=} requires the val_split be different from None. ' - f'This parameter will be set to 5. To avoid this warning, set this value to a float value ' - f'indicating the proportion of training data to be used as validation, or to an integer ' - f'indicating the number of folds for kFCV.') + warnings.warn(f'The parameter {self.calib=} requires the val_split be different from None. ' + f'This parameter will be set to 5. To avoid this warning, set this value to a float value ' + f'indicating the proportion of training data to be used as validation, or to an integer ' + f'indicating the number of folds for kFCV.') self.val_split = 5 [docs] - def classify(self, X): + def classify(self, X): """ Provides the posterior probabilities for the given instances. The calibration function, if required, has no effect in this step, and is only involved in the aggregate method. @@ -1327,13 +1365,13 @@ [docs] - def classifier_fit_predict(self, X, y): + def classifier_fit_predict(self, X, y): classif_predictions = super().classifier_fit_predict(X, y) self.train_prevalence = F.prevalence_from_labels(y, classes=self.classes_) return classif_predictions - def _fit_calibration(self, calibrator, P, y): + def _fit_calibration(self, calibrator, P, y): n_classes = len(self.classes_) if not np.issubdtype(y.dtype, np.number): @@ -1347,7 +1385,7 @@ elif self.on_calib_error == 'backup': self.calibration_function = lambda P: P - def _calibrate_if_requested(self, uncalib_posteriors): + def _calibrate_if_requested(self, uncalib_posteriors): if hasattr(self, 'calibration_function') and self.calibration_function is not None: try: calib_posteriors = self.calibration_function(uncalib_posteriors) @@ -1364,7 +1402,7 @@ [docs] - def aggregation_fit(self, classif_predictions, labels): + def aggregation_fit(self, classif_predictions, labels): """ Trains the aggregation function of EMQ. This comes down to recalibrating the posterior probabilities ir requested. @@ -1379,14 +1417,14 @@ requires_predictions = (self.calib is not None) or (not self.exact_train_prev) if P is None and requires_predictions: # classifier predictions were not generated because val_split=None - raise ArgumentError(self.val_split, self.__class__.__name__ + - ": Classifier predictions for the aggregative fit were not generated because " - "val_split=None. This usually happens when you enable calibrations or heuristics " - "during model selection but left val_split set to its default value (None). " - "Please provide one of the following values for val_split: (i) an integer >1 " - "(e.g. val_split=5) for k-fold cross-validation; (ii) a float in (0,1) (e.g. " - "val_split=0.3) for a proportion split; or (iii) a tuple (X, y) with explicit " - "validation data") + raise ValueError(self.__class__.__name__ + + ": Classifier predictions for the aggregative fit were not generated because " + "val_split=None. This usually happens when you enable calibrations or heuristics " + "during model selection but left val_split set to its default value (None). " + "Please provide one of the following values for val_split: (i) an integer >1 " + "(e.g. val_split=5) for k-fold cross-validation; (ii) a float in (0,1) (e.g. " + "val_split=0.3) for a proportion split; or (iii) a tuple (X, y) with explicit " + "validation data") if self.calib is not None: calibrator = _get_abstention_calibrators().get(self.calib, None) @@ -1403,7 +1441,7 @@ [docs] - def aggregate(self, classif_posteriors, epsilon=EPSILON): + def aggregate(self, classif_posteriors, epsilon=EPSILON): classif_posteriors = self._calibrate_if_requested(classif_posteriors) priors, posteriors = self.EM(self.train_prevalence, classif_posteriors, epsilon) return priors @@ -1411,7 +1449,7 @@ [docs] - def predict_proba(self, instances, epsilon=EPSILON): + def predict_proba(self, instances, epsilon=EPSILON): """ Returns the posterior probabilities updated by the EM algorithm. @@ -1428,7 +1466,7 @@ [docs] @classmethod - def EM(cls, tr_prev, posterior_probabilities, epsilon=EPSILON): + def EM(cls, tr_prev, posterior_probabilities, epsilon=EPSILON): """ Computes the `Expectation Maximization` routine. @@ -1466,7 +1504,7 @@ s += 1 if not converged: - print('[warning] the method has reached the maximum number of iterations; it might have not converged') + warnings.warn('the method has reached the maximum number of iterations; it might have not converged') return qs, ps @@ -1475,7 +1513,7 @@ [docs] -class HDy(AggregativeSoftQuantifier, BinaryAggregativeQuantifier): +class HDy(AggregativeSoftQuantifier, BinaryAggregativeQuantifier): """ `Hellinger Distance y <https://www.sciencedirect.com/science/article/pii/S0020025512004069>`_ (HDy). HDy is a probabilistic method for training binary quantifiers, that models quantification as the problem of @@ -1502,12 +1540,12 @@ for `k`); or as a tuple (X,y) defining the specific set of data to use for validation. """ - def __init__(self, classifier: BaseEstimator = None, fit_classifier=True, val_split=5): + def __init__(self, classifier: BaseEstimator = None, fit_classifier=True, val_split=5): super().__init__(classifier, fit_classifier, val_split) [docs] - def aggregation_fit(self, classif_predictions, labels): + def aggregation_fit(self, classif_predictions, labels): """ Trains the aggregation function of HDy. @@ -1522,7 +1560,7 @@ # pre-compute the histogram for positive and negative examples self.bins = np.linspace(10, 110, 11, dtype=int) # [10, 20, 30, ..., 100, 110] - def hist(P, bins): + def hist(P, bins): h = np.histogram(P, bins=bins, range=(0, 1), density=True)[0] return h / h.sum() @@ -1532,7 +1570,7 @@ [docs] - def aggregate(self, classif_posteriors): + def aggregate(self, classif_posteriors): # "In this work, the number of bins b used in HDx and HDy was chosen from 10 to 110 in steps of 10, # and the final estimated a priori probability was taken as the median of these 11 estimates." # (González-Castro, et al., 2013). @@ -1549,7 +1587,7 @@ # the authors proposed to search for the prevalence yielding the best matching as a linear search # at small steps (modern implementations resort to an optimization procedure, # see class DistributionMatching) - def loss(prev): + def loss(prev): class1_prev = prev[1] Px_train = class1_prev * Pxy1_density + (1 - class1_prev) * Pxy0_density return F.HellingerDistance(Px_train, Px_test) @@ -1564,7 +1602,7 @@ [docs] -class DyS(AggregativeSoftQuantifier, BinaryAggregativeQuantifier): +class DyS(AggregativeSoftQuantifier, BinaryAggregativeQuantifier): """ `DyS framework <https://ojs.aaai.org/index.php/AAAI/article/view/4376>`_ (DyS). DyS is a generalization of HDy method, using a Ternary Search in order to find the prevalence that @@ -1593,15 +1631,15 @@ :param n_jobs: number of parallel workers. """ - def __init__(self, classifier: BaseEstimator = None, fit_classifier=True, val_split=5, n_bins=8, + def __init__(self, classifier: BaseEstimator = None, fit_classifier=True, val_split=5, n_bins=8, divergence: Union[str, Callable] = 'HD', tol=1e-05, n_jobs=None): super().__init__(classifier, fit_classifier, val_split) self.tol = tol self.divergence = divergence self.n_bins = n_bins - self.n_jobs = n_jobs + self.n_jobs = qp._get_njobs(n_jobs) - def _ternary_search(self, f, left, right, tol): + def _ternary_search(self, f, left, right, tol): """ Find maximum of unimodal function f() within [left, right] """ @@ -1619,7 +1657,7 @@ [docs] - def aggregation_fit(self, classif_predictions, labels): + def aggregation_fit(self, classif_predictions, labels): """ Trains the aggregation function of DyS. @@ -1637,13 +1675,13 @@ [docs] - def aggregate(self, classif_posteriors): + def aggregate(self, classif_posteriors): Px = classif_posteriors[:, self.pos_label] # takes only the P(y=+1|x) Px_test = np.histogram(Px, bins=self.n_bins, range=(0, 1), density=True)[0] divergence = get_divergence(self.divergence) - def distribution_distance(prev): + def distribution_distance(prev): Px_train = prev * self.Pxy1_density + (1 - prev) * self.Pxy0_density return divergence(Px_train, Px_test) @@ -1655,7 +1693,7 @@ [docs] -class SMM(AggregativeSoftQuantifier, BinaryAggregativeQuantifier): +class SMM(AggregativeSoftQuantifier, BinaryAggregativeQuantifier): """ `SMM method <https://ieeexplore.ieee.org/document/9260028>`_ (SMM). SMM is a simplification of matching distribution methods where the representation of the examples @@ -1674,12 +1712,12 @@ for `k`); or as a tuple (X,y) defining the specific set of data to use for validation. """ - def __init__(self, classifier: BaseEstimator = None, fit_classifier=True, val_split=5): + def __init__(self, classifier: BaseEstimator = None, fit_classifier=True, val_split=5): super().__init__(classifier, fit_classifier, val_split) [docs] - def aggregation_fit(self, classif_predictions, labels): + def aggregation_fit(self, classif_predictions, labels): """ Trains the aggregation function of SMM. @@ -1697,7 +1735,7 @@ [docs] - def aggregate(self, classif_posteriors): + def aggregate(self, classif_posteriors): Px = classif_posteriors[:, self.pos_label] # takes only the P(y=+1|x) Px_mean = np.mean(Px) @@ -1709,7 +1747,7 @@ [docs] -class DMy(AggregativeSoftQuantifier): +class DMy(AggregativeSoftQuantifier): """ Generic Distribution Matching quantifier for binary or multiclass quantification based on the space of posterior probabilities. This implementation takes the number of bins, the divergence, and the possibility to work on CDF @@ -1742,19 +1780,19 @@ :param n_jobs: number of parallel workers (default None) """ - def __init__(self, classifier: BaseEstimator = None, fit_classifier=True, val_split=5, nbins=8, + def __init__(self, classifier: BaseEstimator = None, fit_classifier=True, val_split=5, nbins=8, divergence: Union[str, Callable] = 'HD', cdf=False, search='optim_minimize', n_jobs=None): super().__init__(classifier, fit_classifier, val_split) self.nbins = nbins self.divergence = divergence self.cdf = cdf self.search = search - self.n_jobs = n_jobs + self.n_jobs = qp._get_njobs(n_jobs) [docs] @classmethod - def HDy(cls, classifier: BaseEstimator = None, fit_classifier=True, val_split=5, n_jobs=None): + def HDy(cls, classifier: BaseEstimator = None, fit_classifier=True, val_split=5, n_jobs=None): """ Historical HDy preset expressed as a configuration of :class:`DMy`. @@ -1783,7 +1821,7 @@ return AggregativeMedianEstimator(base_quantifier=base, param_grid=param_grid, n_jobs=n_jobs) - def _get_distributions(self, posteriors): + def _get_distributions(self, posteriors): histograms = [] post_dims = posteriors.shape[1] if post_dims == 2: @@ -1801,7 +1839,7 @@ [docs] - def aggregation_fit(self, classif_predictions, labels): + def aggregation_fit(self, classif_predictions, labels): """ Trains the aggregation function of a distribution matching method. This comes down to generating the validation distributions out of the training data. @@ -1829,7 +1867,7 @@ [docs] - def aggregate(self, posteriors: np.ndarray): + def aggregate(self, posteriors: np.ndarray): """ Searches for the mixture model parameter (the sought prevalence values) that yields a validation distribution (the mixture) that best matches the test distribution, in terms of the divergence measure of choice. @@ -1844,7 +1882,7 @@ divergence = get_divergence(self.divergence) n_classes, n_channels, nbins = self.validation_distribution.shape - def loss(prev): + def loss(prev): prev = np.expand_dims(prev, axis=0) mixture_distribution = (prev @ self.validation_distribution.reshape(n_classes, -1)).reshape(n_channels, -1) divs = [divergence(test_distribution[ch], mixture_distribution[ch]) for ch in range(n_channels)] @@ -1857,7 +1895,7 @@ [docs] -def newELM(svmperf_base=None, loss='01', C=1): +def newELM(svmperf_base=None, loss='01', C=1): """ Explicit Loss Minimization (ELM) quantifiers. Quantifiers based on ELM represent a family of methods based on structured output learning; @@ -1887,7 +1925,7 @@ [docs] -def newSVMQ(svmperf_base=None, C=1): +def newSVMQ(svmperf_base=None, C=1): """ SVM(Q) is an Explicit Loss Minimization (ELM) quantifier set to optimize for the `Q` loss combining a classification-oriented loss and a quantification-oriented loss, as proposed by @@ -1914,7 +1952,9 @@ -def newSVMKLD(svmperf_base=None, C=1): + +[docs] +def newSVMKLD(svmperf_base=None, C=1): """ SVM(KLD) is an Explicit Loss Minimization (ELM) quantifier set to optimize for the Kullback-Leibler Divergence as proposed by `Esuli et al. 2015 <https://dl.acm.org/doi/abs/10.1145/2700406>`_. @@ -1936,14 +1976,15 @@ :return: returns an instance of CC set to work with SVMperf (with loss and C set properly) as the underlying classifier """ - return newELM(svmperf_base, loss='kld', C=C) + return newELM(svmperf_base, loss='kld', C=C) - -[docs] -def newSVMKLD(svmperf_base=None, C=1): + + +[docs] +def newSVMNKLD(svmperf_base=None, C=1): """ - SVM(KLD) is an Explicit Loss Minimization (ELM) quantifier set to optimize for the Kullback-Leibler Divergence + SVM(NKLD) is an Explicit Loss Minimization (ELM) quantifier set to optimize for the Kullback-Leibler Divergence normalized via the logistic function, as proposed by `Esuli et al. 2015 <https://dl.acm.org/doi/abs/10.1145/2700406>`_. Equivalent to: @@ -1970,7 +2011,7 @@ [docs] -def newSVMAE(svmperf_base=None, C=1): +def newSVMAE(svmperf_base=None, C=1): """ SVM(KLD) is an Explicit Loss Minimization (ELM) quantifier set to optimize for the Absolute Error as first used by `Moreo and Sebastiani, 2021 <https://arxiv.org/abs/2011.02552>`_. @@ -1998,7 +2039,7 @@ [docs] -def newSVMRAE(svmperf_base=None, C=1): +def newSVMRAE(svmperf_base=None, C=1): """ SVM(KLD) is an Explicit Loss Minimization (ELM) quantifier set to optimize for the Relative Absolute Error as first used by `Moreo and Sebastiani, 2021 <https://arxiv.org/abs/2011.02552>`_. @@ -2026,7 +2067,7 @@ [docs] -class OneVsAllAggregative(OneVsAllGeneric, AggregativeQuantifier): +class OneVsAllAggregative(OneVsAllGeneric, AggregativeQuantifier): """ Allows any binary quantifier to perform quantification on single-label datasets. The method maintains one binary quantifier for each class, and then l1-normalizes the outputs so that the @@ -2042,7 +2083,7 @@ is removed and no longer available at predict time. """ - def __init__(self, binary_quantifier=None, n_jobs=None, parallel_backend='multiprocessing'): + def __init__(self, binary_quantifier=None, n_jobs=None, parallel_backend='multiprocessing'): if binary_quantifier is None: binary_quantifier = PACC() assert isinstance(binary_quantifier, BaseQuantifier), \ @@ -2055,7 +2096,7 @@ [docs] - def classify(self, X): + def classify(self, X): """ If the base quantifier is not probabilistic, returns a matrix of shape `(n,m,)` with `n` the number of instances and `m` the number of classes. The entry `(i,j)` is a binary value indicating whether instance @@ -2079,34 +2120,34 @@ [docs] - def aggregate(self, classif_predictions): + def aggregate(self, classif_predictions): prevalences = self._parallel(self._delayed_binary_aggregate, classif_predictions) return F.normalize_prevalence(prevalences) [docs] - def aggregation_fit(self, classif_predictions, labels): - self._parallel(self._delayed_binary_aggregate_fit(c, classif_predictions, labels)) + def aggregation_fit(self, classif_predictions, labels): + self._parallel(self._delayed_binary_aggregate_fit, classif_predictions, labels) return self - def _delayed_binary_classification(self, c, X): + def _delayed_binary_classification(self, c, X): return self.dict_binary_quantifiers[c].classify(X) - def _delayed_binary_aggregate(self, c, classif_predictions): + def _delayed_binary_aggregate(self, c, classif_predictions): # the estimation for the positive class prevalence return self.dict_binary_quantifiers[c].aggregate(classif_predictions[:, c])[1] - def _delayed_binary_aggregate_fit(self, c, classif_predictions, labels): + def _delayed_binary_aggregate_fit(self, c, classif_predictions, labels): # trains the aggregation function of the cth quantifier - return self.dict_binary_quantifiers[c].aggregate_fit(classif_predictions[:, c], labels) + return self.dict_binary_quantifiers[c].aggregation_fit(classif_predictions[:, c], labels == c) [docs] -class AggregativeMedianEstimator(BinaryQuantifier): +class AggregativeMedianEstimator(BinaryQuantifier): """ This method is a meta-quantifier that returns, as the estimated class prevalence values, the median of the estimation returned by differently (hyper)parameterized base quantifiers. @@ -2119,7 +2160,7 @@ :param n_jobs: number of parallel workers """ - def __init__(self, base_quantifier: AggregativeQuantifier, param_grid: dict, random_state=None, n_jobs=None): + def __init__(self, base_quantifier: AggregativeQuantifier, param_grid: dict, random_state=None, n_jobs=None): self.base_quantifier = base_quantifier self.param_grid = param_grid self.random_state = random_state @@ -2127,17 +2168,17 @@ [docs] - def get_params(self, deep=True): + def get_params(self, deep=True): return self.base_quantifier.get_params(deep) [docs] - def set_params(self, **params): + def set_params(self, **params): self.base_quantifier.set_params(**params) - def _delayed_fit(self, args): + def _delayed_fit(self, args): with qp.util.temp_seed(self.random_state): params, X, y = args model = deepcopy(self.base_quantifier) @@ -2145,7 +2186,7 @@ model.fit(X, y) return model - def _delayed_fit_classifier(self, args): + def _delayed_fit_classifier(self, args): with qp.util.temp_seed(self.random_state): cls_params, X, y = args model = deepcopy(self.base_quantifier) @@ -2153,7 +2194,7 @@ predictions, labels = model.classifier_fit_predict(X, y) return (model, predictions, labels) - def _delayed_fit_aggregation(self, args): + def _delayed_fit_aggregation(self, args): with qp.util.temp_seed(self.random_state): ((model, predictions, y), q_params) = args model = deepcopy(model) @@ -2163,8 +2204,8 @@ [docs] - def fit(self, X, y): - import itertools + def fit(self, X, y): + import itertools self._check_binary(y, self.__class__.__name__) @@ -2177,8 +2218,7 @@ ((params, X, y) for params in cls_configs), seed=qp.environ.get('_R_SEED', None), n_jobs=self.n_jobs, - asarray=False, - backend='threading' + asarray=False ) else: model = self.base_quantifier @@ -2190,8 +2230,7 @@ self._delayed_fit_aggregation, itertools.product(models_preds, q_configs), seed=qp.environ.get('_R_SEED', None), - n_jobs=self.n_jobs, - backend='threading' + n_jobs=self.n_jobs ) else: configs = qp.model_selection.expand_grid(self.param_grid) @@ -2199,25 +2238,23 @@ self._delayed_fit, ((params, X, y) for params in configs), seed=qp.environ.get('_R_SEED', None), - n_jobs=self.n_jobs, - backend='threading' + n_jobs=self.n_jobs ) return self - def _delayed_predict(self, args): + def _delayed_predict(self, args): model, instances = args return model.predict(instances) [docs] - def predict(self, instances): + def predict(self, instances): prev_preds = qp.util.parallel( self._delayed_predict, ((model, instances) for model in self.models), seed=qp.environ.get('_R_SEED', None), - n_jobs=self.n_jobs, - backend='threading' + n_jobs=self.n_jobs ) return np.median(prev_preds, axis=0) @@ -2228,7 +2265,7 @@ # imports # --------------------------------------------------------------- -from . import _threshold_optim +from . import _threshold_optim T50 = _threshold_optim.T50 MAX = _threshold_optim.MAX @@ -2236,7 +2273,7 @@ MS = _threshold_optim.MS MS2 = _threshold_optim.MS2 -from . import _kdey +from . import _kdey KDEyML = _kdey.KDEyML KDEyHD = _kdey.KDEyHD diff --git a/docs/build/html/_modules/quapy/method/base.html b/docs/build/html/_modules/quapy/method/base.html index e649c5b..5519615 100644 --- a/docs/build/html/_modules/quapy/method/base.html +++ b/docs/build/html/_modules/quapy/method/base.html @@ -1,95 +1,392 @@ - - - - - - quapy.method.base — QuaPy: A Python-based open-source framework for quantification 0.2.1 documentation - - + + + + + + + + quapy.method.base — QuaPy: A Python-based open-source framework for quantification 0.2.1 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Skip to main content - - - - - - - - - + + + + Back to top + + + + + + + + + + Ctrl+K + + - - - - - - - - - - QuaPy: A Python-based open-source framework for quantification - - - - - - - + + - - -
+ + Source code for quapy.method._threshold_optim +from abc import abstractmethod + +import numpy as np +from sklearn.base import BaseEstimator +import quapy as qp +import quapy.functional as F +from quapy.data import LabelledCollection +from quapy.method.aggregative import BinaryAggregativeQuantifier [docs] -class ThresholdOptimization(BinaryAggregativeQuantifier): +class ThresholdOptimization(BinaryAggregativeQuantifier): """ Abstract class of Threshold Optimization variants for :class:`ACC` as proposed by `Forman 2006 <https://dl.acm.org/doi/abs/10.1145/1150402.1150423>`_ and @@ -111,14 +407,14 @@ :param n_jobs: number of parallel workers """ - def __init__(self, classifier: BaseEstimator=None, fit_classifier=True, val_split=None, n_jobs=None): + def __init__(self, classifier: BaseEstimator=None, fit_classifier=True, val_split=None, n_jobs=None): super().__init__(classifier, fit_classifier, val_split) self.n_jobs = qp._get_njobs(n_jobs) [docs] @abstractmethod - def condition(self, tpr, fpr) -> float: + def condition(self, tpr, fpr) -> float: """ Implements the criterion according to which the threshold should be selected. This function should return the (float) score to be minimized. @@ -132,7 +428,7 @@ [docs] - def discard(self, tpr, fpr) -> bool: + def discard(self, tpr, fpr) -> bool: """ Indicates whether a combination of tpr and fpr should be discarded @@ -144,7 +440,7 @@ - def _eval_candidate_thresholds(self, decision_scores, y): + def _eval_candidate_thresholds(self, decision_scores, y): """ Seeks for the best `tpr` and `fpr` according to the score obtained at different decision thresholds. The scoring function is implemented in function `_condition`. @@ -181,7 +477,7 @@ [docs] - def aggregate_with_threshold(self, classif_predictions, tprs, fprs, thresholds): + def aggregate_with_threshold(self, classif_predictions, tprs, fprs, thresholds): # This function performs the adjusted count for given tpr, fpr, and threshold. # Note that, due to broadcasting, tprs, fprs, and thresholds could be arrays of length > 1 prevs_estims = np.mean(classif_predictions[:, None] >= thresholds, axis=0) @@ -190,26 +486,26 @@ return prevs_estims.squeeze() - def _compute_table(self, y, y_): + def _compute_table(self, y, y_): TP = np.logical_and(y == y_, y == self.pos_label).sum() FP = np.logical_and(y != y_, y == self.neg_label).sum() FN = np.logical_and(y != y_, y == self.pos_label).sum() TN = np.logical_and(y == y_, y == self.neg_label).sum() return TP, FP, FN, TN - def _compute_tpr(self, TP, FP): - if TP + FP == 0: + def _compute_tpr(self, TP, FN): + if TP + FN == 0: return 1 - return TP / (TP + FP) + return TP / (TP + FN) - def _compute_fpr(self, FP, TN): + def _compute_fpr(self, FP, TN): if FP + TN == 0: return 0 return FP / (FP + TN) [docs] - def aggregation_fit(self, classif_predictions, labels): + def aggregation_fit(self, classif_predictions, labels): decision_scores, y = classif_predictions, labels # the standard behavior is to keep the best threshold only self.tpr, self.fpr, self.threshold = self._eval_candidate_thresholds(decision_scores, y)[0] @@ -218,7 +514,7 @@ [docs] - def aggregate(self, classif_predictions: np.ndarray): + def aggregate(self, classif_predictions: np.ndarray): # the standard behavior is to compute the adjusted count using the best threshold found return self.aggregate_with_threshold(classif_predictions, self.tpr, self.fpr, self.threshold) @@ -227,7 +523,7 @@ [docs] -class T50(ThresholdOptimization): +class T50(ThresholdOptimization): """ Threshold Optimization variant for :class:`ACC` as proposed by `Forman 2006 <https://dl.acm.org/doi/abs/10.1145/1150402.1150423>`_ and @@ -249,12 +545,12 @@ """ - def __init__(self, classifier: BaseEstimator=None, fit_classifier=True, val_split=5): + def __init__(self, classifier: BaseEstimator=None, fit_classifier=True, val_split=5): super().__init__(classifier, fit_classifier, val_split) [docs] - def condition(self, tpr, fpr) -> float: + def condition(self, tpr, fpr) -> float: return abs(tpr - 0.5) @@ -262,7 +558,7 @@ [docs] -class MAX(ThresholdOptimization): +class MAX(ThresholdOptimization): """ Threshold Optimization variant for :class:`ACC` as proposed by `Forman 2006 <https://dl.acm.org/doi/abs/10.1145/1150402.1150423>`_ and @@ -282,12 +578,12 @@ """ - def __init__(self, classifier: BaseEstimator=None, fit_classifier=True, val_split=5): + def __init__(self, classifier: BaseEstimator=None, fit_classifier=True, val_split=5): super().__init__(classifier, fit_classifier, val_split) [docs] - def condition(self, tpr, fpr) -> float: + def condition(self, tpr, fpr) -> float: # MAX strives to maximize (tpr - fpr), which is equivalent to minimize (fpr - tpr) return (fpr - tpr) @@ -296,7 +592,7 @@ [docs] -class X(ThresholdOptimization): +class X(ThresholdOptimization): """ Threshold Optimization variant for :class:`ACC` as proposed by `Forman 2006 <https://dl.acm.org/doi/abs/10.1145/1150402.1150423>`_ and @@ -316,12 +612,12 @@ """ - def __init__(self, classifier: BaseEstimator=None, fit_classifier=True, val_split=5): + def __init__(self, classifier: BaseEstimator=None, fit_classifier=True, val_split=5): super().__init__(classifier, fit_classifier, val_split) [docs] - def condition(self, tpr, fpr) -> float: + def condition(self, tpr, fpr) -> float: return abs(1 - (tpr + fpr)) @@ -329,7 +625,7 @@ [docs] -class MS(ThresholdOptimization): +class MS(ThresholdOptimization): """ Median Sweep. Threshold Optimization variant for :class:`ACC` as proposed by `Forman 2006 <https://dl.acm.org/doi/abs/10.1145/1150402.1150423>`_ and @@ -348,18 +644,18 @@ for `k`); or as a tuple (X,y) defining the specific set of data to use for validation. """ - def __init__(self, classifier: BaseEstimator=None, fit_classifier=True, val_split=5): + def __init__(self, classifier: BaseEstimator=None, fit_classifier=True, val_split=5): super().__init__(classifier, fit_classifier, val_split) [docs] - def condition(self, tpr, fpr) -> float: + def condition(self, tpr, fpr) -> float: return 1 [docs] - def aggregation_fit(self, classif_predictions, labels): + def aggregation_fit(self, classif_predictions, labels): decision_scores, y = classif_predictions, labels # keeps all candidates tprs_fprs_thresholds = self._eval_candidate_thresholds(decision_scores, y) @@ -371,7 +667,7 @@ [docs] - def aggregate(self, classif_predictions: np.ndarray): + def aggregate(self, classif_predictions: np.ndarray): prevalences = self.aggregate_with_threshold(classif_predictions, self.tprs, self.fprs, self.thresholds) if prevalences.ndim==2: prevalences = np.median(prevalences, axis=0) @@ -382,7 +678,7 @@ [docs] -class MS2(MS): +class MS2(MS): """ Median Sweep 2. Threshold Optimization variant for :class:`ACC` as proposed by `Forman 2006 <https://dl.acm.org/doi/abs/10.1145/1150402.1150423>`_ and @@ -402,42 +698,86 @@ for `k`); or as a tuple (X,y) defining the specific set of data to use for validation. """ - def __init__(self, classifier: BaseEstimator=None, fit_classifier=True, val_split=5): + def __init__(self, classifier: BaseEstimator=None, fit_classifier=True, val_split=5): super().__init__(classifier, fit_classifier, val_split) [docs] - def discard(self, tpr, fpr) -> bool: + def discard(self, tpr, fpr) -> bool: return (tpr-fpr) <= 0.25 - + + + + + + + + + + + + + - + + + + - Built with Sphinx using a - theme - provided by Read the Docs. - + + + + + + + + + + +
Source code for quapy.method.aggregative -from abc import ABC, abstractmethod -from argparse import ArgumentError -from copy import deepcopy -from typing import Callable, Literal, Union -import numpy as np -from sklearn.base import BaseEstimator -from sklearn.calibration import CalibratedClassifierCV -from sklearn.exceptions import NotFittedError -from sklearn.metrics import confusion_matrix -from sklearn.model_selection import cross_val_predict, train_test_split -from sklearn.utils.validation import check_is_fitted +import warnings +from abc import ABC, abstractmethod +from copy import deepcopy +from typing import Callable, Literal, Union +import numpy as np +from sklearn.base import BaseEstimator +from sklearn.calibration import CalibratedClassifierCV +from sklearn.exceptions import NotFittedError +from sklearn.metrics import confusion_matrix +from sklearn.model_selection import cross_val_predict, train_test_split +from sklearn.utils.validation import check_is_fitted -import quapy as qp -import quapy.functional as F -from quapy.functional import get_divergence -from quapy.classification.svmperf import SVMperf -from quapy.data import LabelledCollection -from quapy.method.base import BaseQuantifier, BinaryQuantifier, OneVsAllGeneric -from quapy.method._helper import ( +import quapy as qp +import quapy.functional as F +from quapy.functional import get_divergence +from quapy.classification.svmperf import SVMperf +from quapy.data import LabelledCollection +from quapy.method.base import BaseQuantifier, BinaryQuantifier, OneVsAllGeneric +from quapy.method._helper import ( _get_abstention_calibrators, _get_cvxpy, _rlls_check_mode, @@ -368,7 +406,7 @@ [docs] -class AggregativeQuantifier(BaseQuantifier, ABC): +class AggregativeQuantifier(BaseQuantifier, ABC): """ Abstract class for quantification methods that base their estimations on the aggregation of classification results. Aggregative quantifiers implement a pipeline that consists of generating classification predictions @@ -396,7 +434,7 @@ the training data be wasted. """ - def __init__(self, + def __init__(self, classifier: Union[None,BaseEstimator], fit_classifier:bool=True, val_split:Union[int,float,tuple,None]=5): @@ -418,9 +456,9 @@ (f'when {val_split=} is indicated as an integer, it represents the number of folds in a kFCV ' f'and must thus be >1') if val_split==5 and not fit_classifier: - print(f'Warning: {val_split=} will be ignored when the classifier is already trained ' - f'({fit_classifier=}). Parameter {self.val_split=} will be set to None. Set {val_split=} ' - f'to None to avoid this warning.') + warnings.warn(f'{val_split=} will be ignored when the classifier is already trained ' + f'({fit_classifier=}). Parameter {self.val_split=} will be set to None. Set {val_split=} ' + f'to None to avoid this warning.') self.val_split=None if val_split!=5: assert fit_classifier, (f'Parameter {val_split=} has been modified, but {fit_classifier=} ' @@ -457,7 +495,7 @@ assert fitted, (f'{fit_classifier=} requires the classifier to be already trained, ' f'but this does not seem to be') - def _check_init_parameters(self): + def _check_init_parameters(self): """ Implements any check to be performed in the parameters of the init method before undertaking the training of the quantifier. This is made as to allow for a quick execution stop when the @@ -467,7 +505,7 @@ """ pass - def _check_non_empty_classes(self, y): + def _check_non_empty_classes(self, y): """ Asserts all classes have positive instances. @@ -484,7 +522,7 @@ [docs] - def fit(self, X, y): + def fit(self, X, y): """ Trains the aggregative quantifier. This comes down to training a classifier (if requested) and an aggregation function. @@ -501,7 +539,7 @@ [docs] - def classifier_fit_predict(self, X, y): + def classifier_fit_predict(self, X, y): """ Trains the classifier if requested (`fit_classifier=True`) and generate the necessary predictions to train the aggregation function. @@ -551,7 +589,7 @@ [docs] @abstractmethod - def aggregation_fit(self, classif_predictions, labels): + def aggregation_fit(self, classif_predictions, labels): """ Trains the aggregation function. @@ -563,7 +601,7 @@ @property - def classifier(self): + def classifier(self): """ Gives access to the classifier @@ -572,7 +610,7 @@ return self.classifier_ @classifier.setter - def classifier(self, classifier): + def classifier(self, classifier): """ Setter for the classifier @@ -582,7 +620,7 @@ [docs] - def classify(self, X): + def classify(self, X): """ Provides the label predictions for the given instances. The predictions should respect the format expected by :meth:`aggregate`, e.g., posterior probabilities for probabilistic quantifiers, or crisp predictions for @@ -594,7 +632,7 @@ return getattr(self.classifier, self._classifier_method())(X) - def _classifier_method(self): + def _classifier_method(self): """ Name of the method that must be used for issuing label predictions. The default one is "decision_function". @@ -602,7 +640,7 @@ """ return 'decision_function' - def _check_classifier(self, adapt_if_necessary=False): + def _check_classifier(self, adapt_if_necessary=False): """ Guarantees that the underlying classifier implements the method required for issuing predictions, i.e., the method indicated by the :meth:`_classifier_method` @@ -614,7 +652,7 @@ [docs] - def predict(self, X): + def predict(self, X): """ Generate class prevalence estimates for the sample's instances by aggregating the label predictions generated by the classifier. @@ -629,7 +667,7 @@ [docs] @abstractmethod - def aggregate(self, classif_predictions: np.ndarray): + def aggregate(self, classif_predictions: np.ndarray): """ Implements the aggregation of the classifier predictions. @@ -640,7 +678,7 @@ @property - def classes_(self): + def classes_(self): """ Class labels, in the same order in which class prevalence values are to be computed. This default implementation actually returns the class labels of the learner. @@ -653,14 +691,14 @@ [docs] -class AggregativeCrispQuantifier(AggregativeQuantifier, ABC): +class AggregativeCrispQuantifier(AggregativeQuantifier, ABC): """ Abstract class for quantification methods that base their estimations on the aggregation of crisp decisions as returned by a hard classifier. Aggregative crisp quantifiers thus extend Aggregative Quantifiers by implementing specifications about crisp predictions. """ - def _classifier_method(self): + def _classifier_method(self): """ Name of the method that must be used for issuing label predictions. For crisp quantifiers, the method is 'predict', that returns an array of shape `(n_instances,)` of label predictions. @@ -673,7 +711,7 @@ [docs] -class AggregativeSoftQuantifier(AggregativeQuantifier, ABC): +class AggregativeSoftQuantifier(AggregativeQuantifier, ABC): """ Abstract class for quantification methods that base their estimations on the aggregation of posterior probabilities as returned by a probabilistic classifier. @@ -681,7 +719,7 @@ about soft predictions. """ - def _classifier_method(self): + def _classifier_method(self): """ Name of the method that must be used for issuing label predictions. For probabilistic quantifiers, the method is 'predict_proba', that returns an array of shape `(n_instances, n_dimensions,)` with posterior @@ -691,7 +729,7 @@ """ return 'predict_proba' - def _check_classifier(self, adapt_if_necessary=False): + def _check_classifier(self, adapt_if_necessary=False): """ Guarantees that the underlying classifier implements the method indicated by the :meth:`_classifier_method`. In case it does not, the classifier is calibrated (by means of the Platt's calibration method implemented by @@ -703,8 +741,8 @@ """ if not hasattr(self.classifier, self._classifier_method()): if adapt_if_necessary: - print(f'warning: The learner {self.classifier.__class__.__name__} does not seem to be ' - f'probabilistic. The learner will be calibrated (using CalibratedClassifierCV).') + warnings.warn(f'The learner {self.classifier.__class__.__name__} does not seem to be ' + f'probabilistic. The learner will be calibrated (using CalibratedClassifierCV).') self.classifier = CalibratedClassifierCV(self.classifier, cv=5) else: raise AssertionError(f'error: The learner {self.classifier.__class__.__name__} does not ' @@ -715,19 +753,19 @@ [docs] -class BinaryAggregativeQuantifier(AggregativeQuantifier, BinaryQuantifier): +class BinaryAggregativeQuantifier(AggregativeQuantifier, BinaryQuantifier): @property - def pos_label(self): + def pos_label(self): return self.classifier.classes_[1] @property - def neg_label(self): + def neg_label(self): return self.classifier.classes_[0] [docs] - def fit(self, X, y): + def fit(self, X, y): self._check_binary(y, self.__class__.__name__) return super().fit(X, y) @@ -738,19 +776,19 @@ # ------------------------------------ [docs] -class CC(AggregativeCrispQuantifier): +class CC(AggregativeCrispQuantifier): """ The most basic Quantification method. One that simply classifies all instances and counts how many have been attributed to each of the classes in order to compute class prevalence estimates. :param classifier: a sklearn's Estimator that generates a classifier """ - def __init__(self, classifier: BaseEstimator = None, fit_classifier: bool = True): + def __init__(self, classifier: BaseEstimator = None, fit_classifier: bool = True): super().__init__(classifier, fit_classifier, val_split=None) [docs] - def aggregation_fit(self, classif_predictions, labels): + def aggregation_fit(self, classif_predictions, labels): """ Nothing to do here! @@ -762,7 +800,7 @@ [docs] - def aggregate(self, classif_predictions: np.ndarray): + def aggregate(self, classif_predictions: np.ndarray): """ Computes class prevalence estimates by counting the prevalence of each of the predicted labels. @@ -776,7 +814,7 @@ [docs] -class PCC(AggregativeSoftQuantifier): +class PCC(AggregativeSoftQuantifier): """ `Probabilistic Classify & Count <https://ieeexplore.ieee.org/abstract/document/5694031>`_, the probabilistic variant of CC that relies on the posterior probabilities returned by a probabilistic classifier. @@ -784,12 +822,12 @@ :param classifier: a sklearn's Estimator that generates a classifier """ - def __init__(self, classifier: BaseEstimator = None, fit_classifier: bool = True, val_split=None): + def __init__(self, classifier: BaseEstimator = None, fit_classifier: bool = True, val_split=None): super().__init__(classifier, fit_classifier, val_split=val_split) [docs] - def aggregation_fit(self, classif_predictions, labels): + def aggregation_fit(self, classif_predictions, labels): """ Nothing to do here! @@ -801,7 +839,7 @@ [docs] - def aggregate(self, classif_posteriors): + def aggregate(self, classif_posteriors): return F.prevalence_from_probabilities(classif_posteriors, binarize=False) @@ -809,7 +847,7 @@ [docs] -class ACC(AggregativeCrispQuantifier): +class ACC(AggregativeCrispQuantifier): """ `Adjusted Classify & Count <https://link.springer.com/article/10.1007/s10618-008-0097-y>`_, the "adjusted" variant of :class:`CC`, that corrects the predictions of CC @@ -857,7 +895,7 @@ :param n_jobs: number of parallel workers """ - def __init__( + def __init__( self, classifier: BaseEstimator = None, fit_classifier = True, @@ -880,7 +918,7 @@ [docs] @classmethod - def newInvariantRatioEstimation(cls, classifier: BaseEstimator, fit_classifier=True, val_split=5, n_jobs=None): + def newInvariantRatioEstimation(cls, classifier: BaseEstimator, fit_classifier=True, val_split=5, n_jobs=None): """ Constructs a quantifier that implements the Invariant Ratio Estimator of `Vaz et al. 2018 <https://jmlr.org/papers/v20/18-456.html>`_. This amounts @@ -905,7 +943,7 @@ return ACC(classifier, fit_classifier=fit_classifier, val_split=val_split, method='invariant-ratio', norm='mapsimplex', n_jobs=n_jobs) - def _check_init_parameters(self): + def _check_init_parameters(self): if self.solver not in ACC.SOLVERS: raise ValueError(f"unknown solver; valid ones are {ACC.SOLVERS}") if self.method not in ACC.METHODS: @@ -915,7 +953,7 @@ [docs] - def aggregation_fit(self, classif_predictions, labels): + def aggregation_fit(self, classif_predictions, labels): """ Estimates the misclassification rates. :param classif_predictions: array-like with the predicted labels @@ -930,7 +968,7 @@ [docs] @classmethod - def getPteCondEstim(cls, classes, y, y_): + def getPteCondEstim(cls, classes, y, y_): """ Estimate the matrix with entry (i,j) being the estimate of P(hat_yi|yj), that is, the probability that a document that belongs to yj ends up being classified as belonging to yi @@ -953,7 +991,7 @@ [docs] - def aggregate(self, classif_predictions): + def aggregate(self, classif_predictions): prevs_estim = self.cc.aggregate(classif_predictions) estimate = F.solve_adjustment( class_conditional_rates=self.Pte_cond_estim_, @@ -968,7 +1006,7 @@ [docs] -class PACC(AggregativeSoftQuantifier): +class PACC(AggregativeSoftQuantifier): """ `Probabilistic Adjusted Classify & Count <https://ieeexplore.ieee.org/abstract/document/5694031>`_, the probabilistic variant of ACC that relies on the posterior probabilities returned by a probabilistic classifier. @@ -1015,7 +1053,7 @@ :param n_jobs: number of parallel workers """ - def __init__( + def __init__( self, classifier: BaseEstimator = None, fit_classifier=True, @@ -1031,7 +1069,7 @@ self.method = method self.norm = norm - def _check_init_parameters(self): + def _check_init_parameters(self): if self.solver not in ACC.SOLVERS: raise ValueError(f"unknown solver; valid ones are {ACC.SOLVERS}") if self.method not in ACC.METHODS: @@ -1041,7 +1079,7 @@ [docs] - def aggregation_fit(self, classif_predictions, labels): + def aggregation_fit(self, classif_predictions, labels): """ Estimates the misclassification rates @@ -1056,7 +1094,7 @@ [docs] - def aggregate(self, classif_posteriors): + def aggregate(self, classif_posteriors): prevs_estim = self.pcc.aggregate(classif_posteriors) estimate = F.solve_adjustment( @@ -1071,7 +1109,7 @@ [docs] @classmethod - def getPteCondEstim(cls, classes, y, y_): + def getPteCondEstim(cls, classes, y, y_): # estimate the matrix with entry (i,j) being the estimate of P(hat_yi|yj), that is, the probability that a # document that belongs to yj ends up being classified as belonging to yi n_classes = len(classes) @@ -1088,7 +1126,7 @@ [docs] -class RLLS(AggregativeSoftQuantifier): +class RLLS(AggregativeSoftQuantifier): """ `Regularized Learning for Domain Adaptation under Label Shifts <https://arxiv.org/abs/1903.09734>`_, used here as an aggregative @@ -1128,7 +1166,7 @@ :func:`quapy.functional.normalize_prevalence` """ - def __init__( + def __init__( self, classifier: BaseEstimator = None, fit_classifier=True, @@ -1147,7 +1185,7 @@ self.norm = norm self.last_w_ = None - def _check_init_parameters(self): + def _check_init_parameters(self): _get_cvxpy() _rlls_check_mode(self.mode) if not isinstance(self.alpha, (int, float)) or self.alpha < 0: @@ -1164,7 +1202,7 @@ [docs] - def aggregation_fit(self, classif_predictions, labels): + def aggregation_fit(self, classif_predictions, labels): if classif_predictions is None or labels is None: raise ValueError('RLLS requires source posterior predictions and source labels') @@ -1181,7 +1219,7 @@ [docs] - def aggregate(self, classif_posteriors): + def aggregate(self, classif_posteriors): qz = _rlls_predicted_marginal(classif_posteriors, mode=self.mode) w = _rlls_compute_weights( self.C_zy_, @@ -1199,7 +1237,7 @@ [docs] -class EMQ(AggregativeSoftQuantifier): +class EMQ(AggregativeSoftQuantifier): """ `Expectation Maximization for Quantification <https://ieeexplore.ieee.org/abstract/document/6789744>`_ (EMQ), aka `Saerens-Latinne-Decaestecker` (SLD) algorithm. @@ -1249,7 +1287,7 @@ ON_CALIB_ERROR_VALUES = ['raise', 'backup'] CALIB_OPTIONS = [None, 'nbvs', 'bcts', 'ts', 'vs'] - def __init__(self, classifier: BaseEstimator = None, fit_classifier=True, val_split=None, exact_train_prev=True, + def __init__(self, classifier: BaseEstimator = None, fit_classifier=True, val_split=None, exact_train_prev=True, calib=None, on_calib_error='raise', n_jobs=None): assert calib in EMQ.CALIB_OPTIONS, \ @@ -1261,12 +1299,12 @@ self.exact_train_prev = exact_train_prev self.calib = calib self.on_calib_error = on_calib_error - self.n_jobs = n_jobs + self.n_jobs = qp._get_njobs(n_jobs) [docs] @classmethod - def EMQ_BCTS(cls, classifier: BaseEstimator, fit_classifier=True, val_split=5, on_calib_error="raise", n_jobs=None): + def EMQ_BCTS(cls, classifier: BaseEstimator, fit_classifier=True, val_split=5, on_calib_error="raise", n_jobs=None): """ Constructs an instance of EMQ using the best configuration found in the `Alexandari et al. paper <http://proceedings.mlr.press/v119/alexandari20a.html>`_, i.e., one that relies on Bias-Corrected Temperature @@ -1298,23 +1336,23 @@ calib='bcts', on_calib_error=on_calib_error, n_jobs=n_jobs) - def _check_init_parameters(self): + def _check_init_parameters(self): if self.val_split is not None: if self.exact_train_prev and self.calib is None: - raise RuntimeWarning(f'The parameter {self.val_split=} was specified for EMQ, while the parameters ' - f'{self.exact_train_prev=} and {self.calib=}. This has no effect and causes an ' - f'unnecessary overload.') + warnings.warn(f'The parameter {self.val_split=} was specified for EMQ, while the parameters ' + f'{self.exact_train_prev=} and {self.calib=}. This has no effect and causes an ' + f'unnecessary overload.', RuntimeWarning) else: if self.calib is not None: - print(f'[warning] The parameter {self.calib=} requires the val_split be different from None. ' - f'This parameter will be set to 5. To avoid this warning, set this value to a float value ' - f'indicating the proportion of training data to be used as validation, or to an integer ' - f'indicating the number of folds for kFCV.') + warnings.warn(f'The parameter {self.calib=} requires the val_split be different from None. ' + f'This parameter will be set to 5. To avoid this warning, set this value to a float value ' + f'indicating the proportion of training data to be used as validation, or to an integer ' + f'indicating the number of folds for kFCV.') self.val_split = 5 [docs] - def classify(self, X): + def classify(self, X): """ Provides the posterior probabilities for the given instances. The calibration function, if required, has no effect in this step, and is only involved in the aggregate method. @@ -1327,13 +1365,13 @@ [docs] - def classifier_fit_predict(self, X, y): + def classifier_fit_predict(self, X, y): classif_predictions = super().classifier_fit_predict(X, y) self.train_prevalence = F.prevalence_from_labels(y, classes=self.classes_) return classif_predictions - def _fit_calibration(self, calibrator, P, y): + def _fit_calibration(self, calibrator, P, y): n_classes = len(self.classes_) if not np.issubdtype(y.dtype, np.number): @@ -1347,7 +1385,7 @@ elif self.on_calib_error == 'backup': self.calibration_function = lambda P: P - def _calibrate_if_requested(self, uncalib_posteriors): + def _calibrate_if_requested(self, uncalib_posteriors): if hasattr(self, 'calibration_function') and self.calibration_function is not None: try: calib_posteriors = self.calibration_function(uncalib_posteriors) @@ -1364,7 +1402,7 @@ [docs] - def aggregation_fit(self, classif_predictions, labels): + def aggregation_fit(self, classif_predictions, labels): """ Trains the aggregation function of EMQ. This comes down to recalibrating the posterior probabilities ir requested. @@ -1379,14 +1417,14 @@ requires_predictions = (self.calib is not None) or (not self.exact_train_prev) if P is None and requires_predictions: # classifier predictions were not generated because val_split=None - raise ArgumentError(self.val_split, self.__class__.__name__ + - ": Classifier predictions for the aggregative fit were not generated because " - "val_split=None. This usually happens when you enable calibrations or heuristics " - "during model selection but left val_split set to its default value (None). " - "Please provide one of the following values for val_split: (i) an integer >1 " - "(e.g. val_split=5) for k-fold cross-validation; (ii) a float in (0,1) (e.g. " - "val_split=0.3) for a proportion split; or (iii) a tuple (X, y) with explicit " - "validation data") + raise ValueError(self.__class__.__name__ + + ": Classifier predictions for the aggregative fit were not generated because " + "val_split=None. This usually happens when you enable calibrations or heuristics " + "during model selection but left val_split set to its default value (None). " + "Please provide one of the following values for val_split: (i) an integer >1 " + "(e.g. val_split=5) for k-fold cross-validation; (ii) a float in (0,1) (e.g. " + "val_split=0.3) for a proportion split; or (iii) a tuple (X, y) with explicit " + "validation data") if self.calib is not None: calibrator = _get_abstention_calibrators().get(self.calib, None) @@ -1403,7 +1441,7 @@ [docs] - def aggregate(self, classif_posteriors, epsilon=EPSILON): + def aggregate(self, classif_posteriors, epsilon=EPSILON): classif_posteriors = self._calibrate_if_requested(classif_posteriors) priors, posteriors = self.EM(self.train_prevalence, classif_posteriors, epsilon) return priors @@ -1411,7 +1449,7 @@ [docs] - def predict_proba(self, instances, epsilon=EPSILON): + def predict_proba(self, instances, epsilon=EPSILON): """ Returns the posterior probabilities updated by the EM algorithm. @@ -1428,7 +1466,7 @@ [docs] @classmethod - def EM(cls, tr_prev, posterior_probabilities, epsilon=EPSILON): + def EM(cls, tr_prev, posterior_probabilities, epsilon=EPSILON): """ Computes the `Expectation Maximization` routine. @@ -1466,7 +1504,7 @@ s += 1 if not converged: - print('[warning] the method has reached the maximum number of iterations; it might have not converged') + warnings.warn('the method has reached the maximum number of iterations; it might have not converged') return qs, ps @@ -1475,7 +1513,7 @@ [docs] -class HDy(AggregativeSoftQuantifier, BinaryAggregativeQuantifier): +class HDy(AggregativeSoftQuantifier, BinaryAggregativeQuantifier): """ `Hellinger Distance y <https://www.sciencedirect.com/science/article/pii/S0020025512004069>`_ (HDy). HDy is a probabilistic method for training binary quantifiers, that models quantification as the problem of @@ -1502,12 +1540,12 @@ for `k`); or as a tuple (X,y) defining the specific set of data to use for validation. """ - def __init__(self, classifier: BaseEstimator = None, fit_classifier=True, val_split=5): + def __init__(self, classifier: BaseEstimator = None, fit_classifier=True, val_split=5): super().__init__(classifier, fit_classifier, val_split) [docs] - def aggregation_fit(self, classif_predictions, labels): + def aggregation_fit(self, classif_predictions, labels): """ Trains the aggregation function of HDy. @@ -1522,7 +1560,7 @@ # pre-compute the histogram for positive and negative examples self.bins = np.linspace(10, 110, 11, dtype=int) # [10, 20, 30, ..., 100, 110] - def hist(P, bins): + def hist(P, bins): h = np.histogram(P, bins=bins, range=(0, 1), density=True)[0] return h / h.sum() @@ -1532,7 +1570,7 @@ [docs] - def aggregate(self, classif_posteriors): + def aggregate(self, classif_posteriors): # "In this work, the number of bins b used in HDx and HDy was chosen from 10 to 110 in steps of 10, # and the final estimated a priori probability was taken as the median of these 11 estimates." # (González-Castro, et al., 2013). @@ -1549,7 +1587,7 @@ # the authors proposed to search for the prevalence yielding the best matching as a linear search # at small steps (modern implementations resort to an optimization procedure, # see class DistributionMatching) - def loss(prev): + def loss(prev): class1_prev = prev[1] Px_train = class1_prev * Pxy1_density + (1 - class1_prev) * Pxy0_density return F.HellingerDistance(Px_train, Px_test) @@ -1564,7 +1602,7 @@ [docs] -class DyS(AggregativeSoftQuantifier, BinaryAggregativeQuantifier): +class DyS(AggregativeSoftQuantifier, BinaryAggregativeQuantifier): """ `DyS framework <https://ojs.aaai.org/index.php/AAAI/article/view/4376>`_ (DyS). DyS is a generalization of HDy method, using a Ternary Search in order to find the prevalence that @@ -1593,15 +1631,15 @@ :param n_jobs: number of parallel workers. """ - def __init__(self, classifier: BaseEstimator = None, fit_classifier=True, val_split=5, n_bins=8, + def __init__(self, classifier: BaseEstimator = None, fit_classifier=True, val_split=5, n_bins=8, divergence: Union[str, Callable] = 'HD', tol=1e-05, n_jobs=None): super().__init__(classifier, fit_classifier, val_split) self.tol = tol self.divergence = divergence self.n_bins = n_bins - self.n_jobs = n_jobs + self.n_jobs = qp._get_njobs(n_jobs) - def _ternary_search(self, f, left, right, tol): + def _ternary_search(self, f, left, right, tol): """ Find maximum of unimodal function f() within [left, right] """ @@ -1619,7 +1657,7 @@ [docs] - def aggregation_fit(self, classif_predictions, labels): + def aggregation_fit(self, classif_predictions, labels): """ Trains the aggregation function of DyS. @@ -1637,13 +1675,13 @@ [docs] - def aggregate(self, classif_posteriors): + def aggregate(self, classif_posteriors): Px = classif_posteriors[:, self.pos_label] # takes only the P(y=+1|x) Px_test = np.histogram(Px, bins=self.n_bins, range=(0, 1), density=True)[0] divergence = get_divergence(self.divergence) - def distribution_distance(prev): + def distribution_distance(prev): Px_train = prev * self.Pxy1_density + (1 - prev) * self.Pxy0_density return divergence(Px_train, Px_test) @@ -1655,7 +1693,7 @@ [docs] -class SMM(AggregativeSoftQuantifier, BinaryAggregativeQuantifier): +class SMM(AggregativeSoftQuantifier, BinaryAggregativeQuantifier): """ `SMM method <https://ieeexplore.ieee.org/document/9260028>`_ (SMM). SMM is a simplification of matching distribution methods where the representation of the examples @@ -1674,12 +1712,12 @@ for `k`); or as a tuple (X,y) defining the specific set of data to use for validation. """ - def __init__(self, classifier: BaseEstimator = None, fit_classifier=True, val_split=5): + def __init__(self, classifier: BaseEstimator = None, fit_classifier=True, val_split=5): super().__init__(classifier, fit_classifier, val_split) [docs] - def aggregation_fit(self, classif_predictions, labels): + def aggregation_fit(self, classif_predictions, labels): """ Trains the aggregation function of SMM. @@ -1697,7 +1735,7 @@ [docs] - def aggregate(self, classif_posteriors): + def aggregate(self, classif_posteriors): Px = classif_posteriors[:, self.pos_label] # takes only the P(y=+1|x) Px_mean = np.mean(Px) @@ -1709,7 +1747,7 @@ [docs] -class DMy(AggregativeSoftQuantifier): +class DMy(AggregativeSoftQuantifier): """ Generic Distribution Matching quantifier for binary or multiclass quantification based on the space of posterior probabilities. This implementation takes the number of bins, the divergence, and the possibility to work on CDF @@ -1742,19 +1780,19 @@ :param n_jobs: number of parallel workers (default None) """ - def __init__(self, classifier: BaseEstimator = None, fit_classifier=True, val_split=5, nbins=8, + def __init__(self, classifier: BaseEstimator = None, fit_classifier=True, val_split=5, nbins=8, divergence: Union[str, Callable] = 'HD', cdf=False, search='optim_minimize', n_jobs=None): super().__init__(classifier, fit_classifier, val_split) self.nbins = nbins self.divergence = divergence self.cdf = cdf self.search = search - self.n_jobs = n_jobs + self.n_jobs = qp._get_njobs(n_jobs) [docs] @classmethod - def HDy(cls, classifier: BaseEstimator = None, fit_classifier=True, val_split=5, n_jobs=None): + def HDy(cls, classifier: BaseEstimator = None, fit_classifier=True, val_split=5, n_jobs=None): """ Historical HDy preset expressed as a configuration of :class:`DMy`. @@ -1783,7 +1821,7 @@ return AggregativeMedianEstimator(base_quantifier=base, param_grid=param_grid, n_jobs=n_jobs) - def _get_distributions(self, posteriors): + def _get_distributions(self, posteriors): histograms = [] post_dims = posteriors.shape[1] if post_dims == 2: @@ -1801,7 +1839,7 @@ [docs] - def aggregation_fit(self, classif_predictions, labels): + def aggregation_fit(self, classif_predictions, labels): """ Trains the aggregation function of a distribution matching method. This comes down to generating the validation distributions out of the training data. @@ -1829,7 +1867,7 @@ [docs] - def aggregate(self, posteriors: np.ndarray): + def aggregate(self, posteriors: np.ndarray): """ Searches for the mixture model parameter (the sought prevalence values) that yields a validation distribution (the mixture) that best matches the test distribution, in terms of the divergence measure of choice. @@ -1844,7 +1882,7 @@ divergence = get_divergence(self.divergence) n_classes, n_channels, nbins = self.validation_distribution.shape - def loss(prev): + def loss(prev): prev = np.expand_dims(prev, axis=0) mixture_distribution = (prev @ self.validation_distribution.reshape(n_classes, -1)).reshape(n_channels, -1) divs = [divergence(test_distribution[ch], mixture_distribution[ch]) for ch in range(n_channels)] @@ -1857,7 +1895,7 @@ [docs] -def newELM(svmperf_base=None, loss='01', C=1): +def newELM(svmperf_base=None, loss='01', C=1): """ Explicit Loss Minimization (ELM) quantifiers. Quantifiers based on ELM represent a family of methods based on structured output learning; @@ -1887,7 +1925,7 @@ [docs] -def newSVMQ(svmperf_base=None, C=1): +def newSVMQ(svmperf_base=None, C=1): """ SVM(Q) is an Explicit Loss Minimization (ELM) quantifier set to optimize for the `Q` loss combining a classification-oriented loss and a quantification-oriented loss, as proposed by @@ -1914,7 +1952,9 @@ -def newSVMKLD(svmperf_base=None, C=1): + +[docs] +def newSVMKLD(svmperf_base=None, C=1): """ SVM(KLD) is an Explicit Loss Minimization (ELM) quantifier set to optimize for the Kullback-Leibler Divergence as proposed by `Esuli et al. 2015 <https://dl.acm.org/doi/abs/10.1145/2700406>`_. @@ -1936,14 +1976,15 @@ :return: returns an instance of CC set to work with SVMperf (with loss and C set properly) as the underlying classifier """ - return newELM(svmperf_base, loss='kld', C=C) + return newELM(svmperf_base, loss='kld', C=C) - -[docs] -def newSVMKLD(svmperf_base=None, C=1): + + +[docs] +def newSVMNKLD(svmperf_base=None, C=1): """ - SVM(KLD) is an Explicit Loss Minimization (ELM) quantifier set to optimize for the Kullback-Leibler Divergence + SVM(NKLD) is an Explicit Loss Minimization (ELM) quantifier set to optimize for the Kullback-Leibler Divergence normalized via the logistic function, as proposed by `Esuli et al. 2015 <https://dl.acm.org/doi/abs/10.1145/2700406>`_. Equivalent to: @@ -1970,7 +2011,7 @@ [docs] -def newSVMAE(svmperf_base=None, C=1): +def newSVMAE(svmperf_base=None, C=1): """ SVM(KLD) is an Explicit Loss Minimization (ELM) quantifier set to optimize for the Absolute Error as first used by `Moreo and Sebastiani, 2021 <https://arxiv.org/abs/2011.02552>`_. @@ -1998,7 +2039,7 @@ [docs] -def newSVMRAE(svmperf_base=None, C=1): +def newSVMRAE(svmperf_base=None, C=1): """ SVM(KLD) is an Explicit Loss Minimization (ELM) quantifier set to optimize for the Relative Absolute Error as first used by `Moreo and Sebastiani, 2021 <https://arxiv.org/abs/2011.02552>`_. @@ -2026,7 +2067,7 @@ [docs] -class OneVsAllAggregative(OneVsAllGeneric, AggregativeQuantifier): +class OneVsAllAggregative(OneVsAllGeneric, AggregativeQuantifier): """ Allows any binary quantifier to perform quantification on single-label datasets. The method maintains one binary quantifier for each class, and then l1-normalizes the outputs so that the @@ -2042,7 +2083,7 @@ is removed and no longer available at predict time. """ - def __init__(self, binary_quantifier=None, n_jobs=None, parallel_backend='multiprocessing'): + def __init__(self, binary_quantifier=None, n_jobs=None, parallel_backend='multiprocessing'): if binary_quantifier is None: binary_quantifier = PACC() assert isinstance(binary_quantifier, BaseQuantifier), \ @@ -2055,7 +2096,7 @@ [docs] - def classify(self, X): + def classify(self, X): """ If the base quantifier is not probabilistic, returns a matrix of shape `(n,m,)` with `n` the number of instances and `m` the number of classes. The entry `(i,j)` is a binary value indicating whether instance @@ -2079,34 +2120,34 @@ [docs] - def aggregate(self, classif_predictions): + def aggregate(self, classif_predictions): prevalences = self._parallel(self._delayed_binary_aggregate, classif_predictions) return F.normalize_prevalence(prevalences) [docs] - def aggregation_fit(self, classif_predictions, labels): - self._parallel(self._delayed_binary_aggregate_fit(c, classif_predictions, labels)) + def aggregation_fit(self, classif_predictions, labels): + self._parallel(self._delayed_binary_aggregate_fit, classif_predictions, labels) return self - def _delayed_binary_classification(self, c, X): + def _delayed_binary_classification(self, c, X): return self.dict_binary_quantifiers[c].classify(X) - def _delayed_binary_aggregate(self, c, classif_predictions): + def _delayed_binary_aggregate(self, c, classif_predictions): # the estimation for the positive class prevalence return self.dict_binary_quantifiers[c].aggregate(classif_predictions[:, c])[1] - def _delayed_binary_aggregate_fit(self, c, classif_predictions, labels): + def _delayed_binary_aggregate_fit(self, c, classif_predictions, labels): # trains the aggregation function of the cth quantifier - return self.dict_binary_quantifiers[c].aggregate_fit(classif_predictions[:, c], labels) + return self.dict_binary_quantifiers[c].aggregation_fit(classif_predictions[:, c], labels == c) [docs] -class AggregativeMedianEstimator(BinaryQuantifier): +class AggregativeMedianEstimator(BinaryQuantifier): """ This method is a meta-quantifier that returns, as the estimated class prevalence values, the median of the estimation returned by differently (hyper)parameterized base quantifiers. @@ -2119,7 +2160,7 @@ :param n_jobs: number of parallel workers """ - def __init__(self, base_quantifier: AggregativeQuantifier, param_grid: dict, random_state=None, n_jobs=None): + def __init__(self, base_quantifier: AggregativeQuantifier, param_grid: dict, random_state=None, n_jobs=None): self.base_quantifier = base_quantifier self.param_grid = param_grid self.random_state = random_state @@ -2127,17 +2168,17 @@ [docs] - def get_params(self, deep=True): + def get_params(self, deep=True): return self.base_quantifier.get_params(deep) [docs] - def set_params(self, **params): + def set_params(self, **params): self.base_quantifier.set_params(**params) - def _delayed_fit(self, args): + def _delayed_fit(self, args): with qp.util.temp_seed(self.random_state): params, X, y = args model = deepcopy(self.base_quantifier) @@ -2145,7 +2186,7 @@ model.fit(X, y) return model - def _delayed_fit_classifier(self, args): + def _delayed_fit_classifier(self, args): with qp.util.temp_seed(self.random_state): cls_params, X, y = args model = deepcopy(self.base_quantifier) @@ -2153,7 +2194,7 @@ predictions, labels = model.classifier_fit_predict(X, y) return (model, predictions, labels) - def _delayed_fit_aggregation(self, args): + def _delayed_fit_aggregation(self, args): with qp.util.temp_seed(self.random_state): ((model, predictions, y), q_params) = args model = deepcopy(model) @@ -2163,8 +2204,8 @@ [docs] - def fit(self, X, y): - import itertools + def fit(self, X, y): + import itertools self._check_binary(y, self.__class__.__name__) @@ -2177,8 +2218,7 @@ ((params, X, y) for params in cls_configs), seed=qp.environ.get('_R_SEED', None), n_jobs=self.n_jobs, - asarray=False, - backend='threading' + asarray=False ) else: model = self.base_quantifier @@ -2190,8 +2230,7 @@ self._delayed_fit_aggregation, itertools.product(models_preds, q_configs), seed=qp.environ.get('_R_SEED', None), - n_jobs=self.n_jobs, - backend='threading' + n_jobs=self.n_jobs ) else: configs = qp.model_selection.expand_grid(self.param_grid) @@ -2199,25 +2238,23 @@ self._delayed_fit, ((params, X, y) for params in configs), seed=qp.environ.get('_R_SEED', None), - n_jobs=self.n_jobs, - backend='threading' + n_jobs=self.n_jobs ) return self - def _delayed_predict(self, args): + def _delayed_predict(self, args): model, instances = args return model.predict(instances) [docs] - def predict(self, instances): + def predict(self, instances): prev_preds = qp.util.parallel( self._delayed_predict, ((model, instances) for model in self.models), seed=qp.environ.get('_R_SEED', None), - n_jobs=self.n_jobs, - backend='threading' + n_jobs=self.n_jobs ) return np.median(prev_preds, axis=0) @@ -2228,7 +2265,7 @@ # imports # --------------------------------------------------------------- -from . import _threshold_optim +from . import _threshold_optim T50 = _threshold_optim.T50 MAX = _threshold_optim.MAX @@ -2236,7 +2273,7 @@ MS = _threshold_optim.MS MS2 = _threshold_optim.MS2 -from . import _kdey +from . import _kdey KDEyML = _kdey.KDEyML KDEyHD = _kdey.KDEyHD diff --git a/docs/build/html/_modules/quapy/method/base.html b/docs/build/html/_modules/quapy/method/base.html index e649c5b..5519615 100644 --- a/docs/build/html/_modules/quapy/method/base.html +++ b/docs/build/html/_modules/quapy/method/base.html @@ -1,95 +1,392 @@ - - - - - - quapy.method.base — QuaPy: A Python-based open-source framework for quantification 0.2.1 documentation - - + + + + + + + + quapy.method.base — QuaPy: A Python-based open-source framework for quantification 0.2.1 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Skip to main content - - - - - - - - - + + + + Back to top + + + + + + + + + + Ctrl+K + + - - - - - - - - - - QuaPy: A Python-based open-source framework for quantification - - - - - - - + + - - -