adding documentation
This commit is contained in:
parent
5deb92b457
commit
3835f89e9d
4
TODO.txt
4
TODO.txt
|
@ -24,6 +24,10 @@ Do we want to cover cross-lingual quantification natively in QuaPy, or does it m
|
||||||
|
|
||||||
Current issues:
|
Current issues:
|
||||||
==========================================
|
==========================================
|
||||||
|
Revise the class structure of quantification methods and the methods they inherit... There is some confusion regarding
|
||||||
|
methods isbinary, isprobabilistic, and the like. The attribute "learner_" in aggregative quantifiers is also
|
||||||
|
confusing, since there is a getter and a setter.
|
||||||
|
Remove the "deep" in get_params. There is no real compatibility with scikit-learn as for now.
|
||||||
SVMperf-based learners do not remove temp files in __del__?
|
SVMperf-based learners do not remove temp files in __del__?
|
||||||
In binary quantification (hp, kindle, imdb) we used F1 in the minority class (which in kindle and hp happens to be the
|
In binary quantification (hp, kindle, imdb) we used F1 in the minority class (which in kindle and hp happens to be the
|
||||||
negative class). This is not covered in this new implementation, in which the binary case is not treated as such, but as
|
negative class). This is not covered in this new implementation, in which the binary case is not treated as such, but as
|
||||||
|
|
|
@ -80,8 +80,6 @@
|
||||||
<li><a href="quapy.html#quapy.error.acc_error">acc_error() (in module quapy.error)</a>
|
<li><a href="quapy.html#quapy.error.acc_error">acc_error() (in module quapy.error)</a>
|
||||||
</li>
|
</li>
|
||||||
<li><a href="quapy.html#quapy.error.acce">acce() (in module quapy.error)</a>
|
<li><a href="quapy.html#quapy.error.acce">acce() (in module quapy.error)</a>
|
||||||
</li>
|
|
||||||
<li><a href="quapy.method.html#quapy.method.meta.Ensemble.accuracy_policy">accuracy_policy() (quapy.method.meta.Ensemble method)</a>
|
|
||||||
</li>
|
</li>
|
||||||
<li><a href="quapy.data.html#quapy.data.preprocessing.IndexTransformer.add_word">add_word() (quapy.data.preprocessing.IndexTransformer method)</a>
|
<li><a href="quapy.data.html#quapy.data.preprocessing.IndexTransformer.add_word">add_word() (quapy.data.preprocessing.IndexTransformer method)</a>
|
||||||
</li>
|
</li>
|
||||||
|
@ -226,12 +224,6 @@
|
||||||
<li><a href="quapy.method.html#quapy.method.neural.QuaNetTrainer.clean_checkpoint_dir">clean_checkpoint_dir() (quapy.method.neural.QuaNetTrainer method)</a>
|
<li><a href="quapy.method.html#quapy.method.neural.QuaNetTrainer.clean_checkpoint_dir">clean_checkpoint_dir() (quapy.method.neural.QuaNetTrainer method)</a>
|
||||||
</li>
|
</li>
|
||||||
<li><a href="quapy.classification.html#quapy.classification.neural.CNNnet">CNNnet (class in quapy.classification.neural)</a>
|
<li><a href="quapy.classification.html#quapy.classification.neural.CNNnet">CNNnet (class in quapy.classification.neural)</a>
|
||||||
</li>
|
|
||||||
<li><a href="quapy.method.html#quapy.method.aggregative.ThresholdOptimization.compute_fpr">compute_fpr() (quapy.method.aggregative.ThresholdOptimization method)</a>
|
|
||||||
</li>
|
|
||||||
<li><a href="quapy.method.html#quapy.method.aggregative.ThresholdOptimization.compute_table">compute_table() (quapy.method.aggregative.ThresholdOptimization method)</a>
|
|
||||||
</li>
|
|
||||||
<li><a href="quapy.method.html#quapy.method.aggregative.ThresholdOptimization.compute_tpr">compute_tpr() (quapy.method.aggregative.ThresholdOptimization method)</a>
|
|
||||||
</li>
|
</li>
|
||||||
<li><a href="quapy.data.html#quapy.data.base.LabelledCollection.counts">counts() (quapy.data.base.LabelledCollection method)</a>
|
<li><a href="quapy.data.html#quapy.data.base.LabelledCollection.counts">counts() (quapy.data.base.LabelledCollection method)</a>
|
||||||
</li>
|
</li>
|
||||||
|
@ -257,6 +249,8 @@
|
||||||
</ul></li>
|
</ul></li>
|
||||||
<li><a href="quapy.classification.html#quapy.classification.neural.TextClassifierNet.dimensions">dimensions() (quapy.classification.neural.TextClassifierNet method)</a>
|
<li><a href="quapy.classification.html#quapy.classification.neural.TextClassifierNet.dimensions">dimensions() (quapy.classification.neural.TextClassifierNet method)</a>
|
||||||
</li>
|
</li>
|
||||||
|
</ul></td>
|
||||||
|
<td style="width: 33%; vertical-align: top;"><ul>
|
||||||
<li><a href="quapy.classification.html#quapy.classification.neural.CNNnet.document_embedding">document_embedding() (quapy.classification.neural.CNNnet method)</a>
|
<li><a href="quapy.classification.html#quapy.classification.neural.CNNnet.document_embedding">document_embedding() (quapy.classification.neural.CNNnet method)</a>
|
||||||
|
|
||||||
<ul>
|
<ul>
|
||||||
|
@ -265,15 +259,9 @@
|
||||||
<li><a href="quapy.classification.html#quapy.classification.neural.TextClassifierNet.document_embedding">(quapy.classification.neural.TextClassifierNet method)</a>
|
<li><a href="quapy.classification.html#quapy.classification.neural.TextClassifierNet.document_embedding">(quapy.classification.neural.TextClassifierNet method)</a>
|
||||||
</li>
|
</li>
|
||||||
</ul></li>
|
</ul></li>
|
||||||
</ul></td>
|
|
||||||
<td style="width: 33%; vertical-align: top;"><ul>
|
|
||||||
<li><a href="quapy.html#quapy.util.download_file">download_file() (in module quapy.util)</a>
|
<li><a href="quapy.html#quapy.util.download_file">download_file() (in module quapy.util)</a>
|
||||||
</li>
|
</li>
|
||||||
<li><a href="quapy.html#quapy.util.download_file_if_not_exists">download_file_if_not_exists() (in module quapy.util)</a>
|
<li><a href="quapy.html#quapy.util.download_file_if_not_exists">download_file_if_not_exists() (in module quapy.util)</a>
|
||||||
</li>
|
|
||||||
<li><a href="quapy.method.html#quapy.method.meta.Ensemble.ds_policy">ds_policy() (quapy.method.meta.Ensemble method)</a>
|
|
||||||
</li>
|
|
||||||
<li><a href="quapy.method.html#quapy.method.meta.Ensemble.ds_policy_get_posteriors">ds_policy_get_posteriors() (quapy.method.meta.Ensemble method)</a>
|
|
||||||
</li>
|
</li>
|
||||||
</ul></td>
|
</ul></td>
|
||||||
</tr></table>
|
</tr></table>
|
||||||
|
@ -619,7 +607,7 @@
|
||||||
<ul>
|
<ul>
|
||||||
<li><a href="quapy.data.html#quapy.data.base.LabelledCollection.n_classes">(quapy.data.base.LabelledCollection property)</a>
|
<li><a href="quapy.data.html#quapy.data.base.LabelledCollection.n_classes">(quapy.data.base.LabelledCollection property)</a>
|
||||||
</li>
|
</li>
|
||||||
<li><a href="quapy.method.html#quapy.method.aggregative.AggregativeQuantifier.n_classes">(quapy.method.aggregative.AggregativeQuantifier property)</a>
|
<li><a href="quapy.method.html#quapy.method.base.BaseQuantifier.n_classes">(quapy.method.base.BaseQuantifier property)</a>
|
||||||
</li>
|
</li>
|
||||||
</ul></li>
|
</ul></li>
|
||||||
<li><a href="quapy.html#quapy.evaluation.natural_prevalence_prediction">natural_prevalence_prediction() (in module quapy.evaluation)</a>
|
<li><a href="quapy.html#quapy.evaluation.natural_prevalence_prediction">natural_prevalence_prediction() (in module quapy.evaluation)</a>
|
||||||
|
@ -650,14 +638,6 @@
|
||||||
<td style="width: 33%; vertical-align: top;"><ul>
|
<td style="width: 33%; vertical-align: top;"><ul>
|
||||||
<li><a href="quapy.method.html#quapy.method.aggregative.OneVsAll">OneVsAll (class in quapy.method.aggregative)</a>
|
<li><a href="quapy.method.html#quapy.method.aggregative.OneVsAll">OneVsAll (class in quapy.method.aggregative)</a>
|
||||||
</li>
|
</li>
|
||||||
<li><a href="quapy.method.html#quapy.method.aggregative.MS.optimize_threshold">optimize_threshold() (quapy.method.aggregative.MS method)</a>
|
|
||||||
|
|
||||||
<ul>
|
|
||||||
<li><a href="quapy.method.html#quapy.method.aggregative.MS2.optimize_threshold">(quapy.method.aggregative.MS2 method)</a>
|
|
||||||
</li>
|
|
||||||
<li><a href="quapy.method.html#quapy.method.aggregative.ThresholdOptimization.optimize_threshold">(quapy.method.aggregative.ThresholdOptimization method)</a>
|
|
||||||
</li>
|
|
||||||
</ul></li>
|
|
||||||
</ul></td>
|
</ul></td>
|
||||||
</tr></table>
|
</tr></table>
|
||||||
|
|
||||||
|
@ -721,8 +701,6 @@
|
||||||
<li><a href="quapy.method.html#quapy.method.aggregative.ProbabilisticAdjustedClassifyAndCount">ProbabilisticAdjustedClassifyAndCount (in module quapy.method.aggregative)</a>
|
<li><a href="quapy.method.html#quapy.method.aggregative.ProbabilisticAdjustedClassifyAndCount">ProbabilisticAdjustedClassifyAndCount (in module quapy.method.aggregative)</a>
|
||||||
</li>
|
</li>
|
||||||
<li><a href="quapy.method.html#quapy.method.aggregative.ProbabilisticClassifyAndCount">ProbabilisticClassifyAndCount (in module quapy.method.aggregative)</a>
|
<li><a href="quapy.method.html#quapy.method.aggregative.ProbabilisticClassifyAndCount">ProbabilisticClassifyAndCount (in module quapy.method.aggregative)</a>
|
||||||
</li>
|
|
||||||
<li><a href="quapy.method.html#quapy.method.meta.Ensemble.ptr_policy">ptr_policy() (quapy.method.meta.Ensemble method)</a>
|
|
||||||
</li>
|
</li>
|
||||||
</ul></td>
|
</ul></td>
|
||||||
</tr></table>
|
</tr></table>
|
||||||
|
@ -968,11 +946,11 @@
|
||||||
</ul></li>
|
</ul></li>
|
||||||
</ul></td>
|
</ul></td>
|
||||||
<td style="width: 33%; vertical-align: top;"><ul>
|
<td style="width: 33%; vertical-align: top;"><ul>
|
||||||
|
<li><a href="quapy.method.html#quapy.method.aggregative.SLD">SLD (in module quapy.method.aggregative)</a>
|
||||||
|
</li>
|
||||||
<li><a href="quapy.html#quapy.error.smooth">smooth() (in module quapy.error)</a>
|
<li><a href="quapy.html#quapy.error.smooth">smooth() (in module quapy.error)</a>
|
||||||
</li>
|
</li>
|
||||||
<li><a href="quapy.method.html#quapy.method.aggregative.ACC.solve_adjustment">solve_adjustment() (quapy.method.aggregative.ACC class method)</a>
|
<li><a href="quapy.method.html#quapy.method.aggregative.ACC.solve_adjustment">solve_adjustment() (quapy.method.aggregative.ACC class method)</a>
|
||||||
</li>
|
|
||||||
<li><a href="quapy.method.html#quapy.method.meta.Ensemble.sout">sout() (quapy.method.meta.Ensemble method)</a>
|
|
||||||
</li>
|
</li>
|
||||||
<li><a href="quapy.data.html#quapy.data.base.LabelledCollection.split_stratified">split_stratified() (quapy.data.base.LabelledCollection method)</a>
|
<li><a href="quapy.data.html#quapy.data.base.LabelledCollection.split_stratified">split_stratified() (quapy.data.base.LabelledCollection method)</a>
|
||||||
</li>
|
</li>
|
||||||
|
@ -1013,14 +991,12 @@
|
||||||
<li><a href="quapy.data.html#quapy.data.preprocessing.text2tfidf">text2tfidf() (in module quapy.data.preprocessing)</a>
|
<li><a href="quapy.data.html#quapy.data.preprocessing.text2tfidf">text2tfidf() (in module quapy.data.preprocessing)</a>
|
||||||
</li>
|
</li>
|
||||||
<li><a href="quapy.classification.html#quapy.classification.neural.TextClassifierNet">TextClassifierNet (class in quapy.classification.neural)</a>
|
<li><a href="quapy.classification.html#quapy.classification.neural.TextClassifierNet">TextClassifierNet (class in quapy.classification.neural)</a>
|
||||||
</li>
|
|
||||||
<li><a href="quapy.method.html#quapy.method.aggregative.ThresholdOptimization">ThresholdOptimization (class in quapy.method.aggregative)</a>
|
|
||||||
</li>
|
</li>
|
||||||
</ul></td>
|
</ul></td>
|
||||||
<td style="width: 33%; vertical-align: top;"><ul>
|
<td style="width: 33%; vertical-align: top;"><ul>
|
||||||
<li><a href="quapy.classification.html#quapy.classification.neural.TorchDataset">TorchDataset (class in quapy.classification.neural)</a>
|
<li><a href="quapy.method.html#quapy.method.aggregative.ThresholdOptimization">ThresholdOptimization (class in quapy.method.aggregative)</a>
|
||||||
</li>
|
</li>
|
||||||
<li><a href="quapy.method.html#quapy.method.aggregative.training_helper">training_helper() (in module quapy.method.aggregative)</a>
|
<li><a href="quapy.classification.html#quapy.classification.neural.TorchDataset">TorchDataset (class in quapy.classification.neural)</a>
|
||||||
</li>
|
</li>
|
||||||
<li><a href="quapy.classification.html#quapy.classification.methods.LowRankLogisticRegression.transform">transform() (quapy.classification.methods.LowRankLogisticRegression method)</a>
|
<li><a href="quapy.classification.html#quapy.classification.methods.LowRankLogisticRegression.transform">transform() (quapy.classification.methods.LowRankLogisticRegression method)</a>
|
||||||
|
|
||||||
|
|
Binary file not shown.
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
|
@ -23,46 +23,109 @@ from quapy.method.base import BaseQuantifier, BinaryQuantifier
|
||||||
class AggregativeQuantifier(BaseQuantifier):
|
class AggregativeQuantifier(BaseQuantifier):
|
||||||
"""
|
"""
|
||||||
Abstract class for quantification methods that base their estimations on the aggregation of classification
|
Abstract class for quantification methods that base their estimations on the aggregation of classification
|
||||||
results. Aggregative Quantifiers thus implement a _classify_ method and maintain a _learner_ attribute.
|
results. Aggregative Quantifiers thus implement a :meth:`classify` method and maintain a :attr:`learner` attribute.
|
||||||
|
Subclasses of this abstract class must implement the method :meth:`aggregate` which computes the aggregation
|
||||||
|
of label predictions. The method :meth:`quantify` comes with a default implementation based on
|
||||||
|
:meth:`classify` and :meth:`aggregate`.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def fit(self, data: LabelledCollection, fit_learner=True): ...
|
def fit(self, data: LabelledCollection, fit_learner=True):
|
||||||
|
"""
|
||||||
|
Trains the aggregative quantifier
|
||||||
|
|
||||||
|
:param data: a :class:`quapy.data.base.LabelledCollection` consisting of the training data
|
||||||
|
:param fit_learner: whether or not to train the learner (default is True). Set to False if the
|
||||||
|
learner has been trained outside the quantifier.
|
||||||
|
:return: self
|
||||||
|
"""
|
||||||
|
...
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def learner(self):
|
def learner(self):
|
||||||
|
"""
|
||||||
|
Gives access to the classifier
|
||||||
|
|
||||||
|
:return: the classifier (typically an sklearn's Estimator)
|
||||||
|
"""
|
||||||
return self.learner_
|
return self.learner_
|
||||||
|
|
||||||
@learner.setter
|
@learner.setter
|
||||||
def learner(self, value):
|
def learner(self, classifier):
|
||||||
self.learner_ = value
|
"""
|
||||||
|
Setter for the classifier
|
||||||
|
|
||||||
|
:param classifier: the classifier
|
||||||
|
"""
|
||||||
|
self.learner_ = classifier
|
||||||
|
|
||||||
def classify(self, instances):
|
def classify(self, instances):
|
||||||
|
"""
|
||||||
|
Provides the label predictions for the given instances.
|
||||||
|
|
||||||
|
:param instances: array-like
|
||||||
|
:return: np.ndarray of shape `(n_instances,)` with label predictions
|
||||||
|
"""
|
||||||
return self.learner.predict(instances)
|
return self.learner.predict(instances)
|
||||||
|
|
||||||
def quantify(self, instances):
|
def quantify(self, instances):
|
||||||
|
"""
|
||||||
|
Generate class prevalence estimates for the sample's instances by aggregating the label predictions generated
|
||||||
|
by the classifier.
|
||||||
|
|
||||||
|
:param instances: array-like
|
||||||
|
:return: `np.ndarray` of shape `(self.n_classes_,)` with class prevalence estimates.
|
||||||
|
"""
|
||||||
classif_predictions = self.classify(instances)
|
classif_predictions = self.classify(instances)
|
||||||
return self.aggregate(classif_predictions)
|
return self.aggregate(classif_predictions)
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def aggregate(self, classif_predictions: np.ndarray): ...
|
def aggregate(self, classif_predictions: np.ndarray):
|
||||||
|
"""
|
||||||
|
Implements the aggregation of label predictions.
|
||||||
|
|
||||||
|
:param classif_predictions: `np.ndarray` of label predictions
|
||||||
|
:return: `np.ndarray` of shape `(self.n_classes_,)` with class prevalence estimates.
|
||||||
|
"""
|
||||||
|
...
|
||||||
|
|
||||||
def get_params(self, deep=True):
|
def get_params(self, deep=True):
|
||||||
|
"""
|
||||||
|
Return the current parameters of the quantifier.
|
||||||
|
|
||||||
|
:param deep: for compatibility with sklearn
|
||||||
|
:return: a dictionary of param-value pairs
|
||||||
|
"""
|
||||||
|
|
||||||
return self.learner.get_params()
|
return self.learner.get_params()
|
||||||
|
|
||||||
def set_params(self, **parameters):
|
def set_params(self, **parameters):
|
||||||
|
"""
|
||||||
|
Set the parameters of the quantifier.
|
||||||
|
|
||||||
|
:param parameters: dictionary of param-value pairs
|
||||||
|
"""
|
||||||
|
|
||||||
self.learner.set_params(**parameters)
|
self.learner.set_params(**parameters)
|
||||||
|
|
||||||
@property
|
|
||||||
def n_classes(self):
|
|
||||||
return len(self.classes_)
|
|
||||||
|
|
||||||
@property
|
@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.
|
||||||
|
|
||||||
|
:return: array-like
|
||||||
|
"""
|
||||||
return self.learner.classes_
|
return self.learner.classes_
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def aggregative(self):
|
def aggregative(self):
|
||||||
|
"""
|
||||||
|
Returns True, indicating the quantifier is of type aggregative.
|
||||||
|
|
||||||
|
:return: True
|
||||||
|
"""
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
@ -96,23 +159,24 @@ class AggregativeProbabilisticQuantifier(AggregativeQuantifier):
|
||||||
|
|
||||||
# Helper
|
# Helper
|
||||||
# ------------------------------------
|
# ------------------------------------
|
||||||
def training_helper(learner,
|
def _training_helper(learner,
|
||||||
data: LabelledCollection,
|
data: LabelledCollection,
|
||||||
fit_learner: bool = True,
|
fit_learner: bool = True,
|
||||||
ensure_probabilistic=False,
|
ensure_probabilistic=False,
|
||||||
val_split: Union[LabelledCollection, float] = None):
|
val_split: Union[LabelledCollection, float] = None):
|
||||||
"""
|
"""
|
||||||
Training procedure common to all Aggregative Quantifiers.
|
Training procedure common to all Aggregative Quantifiers.
|
||||||
|
|
||||||
:param learner: the learner to be fit
|
:param learner: the learner to be fit
|
||||||
:param data: the data on which to fit the learner. If requested, the data will be split before fitting the learner.
|
:param data: the data on which to fit the learner. If requested, the data will be split before fitting the learner.
|
||||||
:param fit_learner: whether or not to fit the learner (if False, then bypasses any action)
|
:param fit_learner: whether or not to fit the learner (if False, then bypasses any action)
|
||||||
:param ensure_probabilistic: if True, guarantees that the resulting classifier implements predict_proba (if the
|
:param ensure_probabilistic: if True, guarantees that the resulting classifier implements predict_proba (if the
|
||||||
learner is not probabilistic, then a CalibratedCV instance of it is trained)
|
learner is not probabilistic, then a CalibratedCV instance of it is trained)
|
||||||
:param val_split: if specified as a float, indicates the proportion of training instances that will define the
|
:param val_split: if specified as a float, indicates the proportion of training instances that will define the
|
||||||
validation split (e.g., 0.3 for using 30% of the training set as validation data); if specified as a
|
validation split (e.g., 0.3 for using 30% of the training set as validation data); if specified as a
|
||||||
LabelledCollection, represents the validation split itself
|
LabelledCollection, represents the validation split itself
|
||||||
:return: the learner trained on the training set, and the unused data (a _LabelledCollection_ if train_val_split>0
|
:return: the learner trained on the training set, and the unused data (a _LabelledCollection_ if train_val_split>0
|
||||||
or None otherwise) to be used as a validation set for any subsequent parameter fitting
|
or None otherwise) to be used as a validation set for any subsequent parameter fitting
|
||||||
"""
|
"""
|
||||||
if fit_learner:
|
if fit_learner:
|
||||||
if ensure_probabilistic:
|
if ensure_probabilistic:
|
||||||
|
@ -154,8 +218,10 @@ def training_helper(learner,
|
||||||
# ------------------------------------
|
# ------------------------------------
|
||||||
class CC(AggregativeQuantifier):
|
class CC(AggregativeQuantifier):
|
||||||
"""
|
"""
|
||||||
The most basic Quantification method. One that simply classifies all instances and countes how many have been
|
The most basic Quantification method. One that simply classifies all instances and counts how many have been
|
||||||
attributed each of the classes in order to compute class prevalence estimates.
|
attributed to each of the classes in order to compute class prevalence estimates.
|
||||||
|
|
||||||
|
:param learner: a sklearn's Estimator that generates a classifier
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, learner: BaseEstimator):
|
def __init__(self, learner: BaseEstimator):
|
||||||
|
@ -163,19 +229,40 @@ class CC(AggregativeQuantifier):
|
||||||
|
|
||||||
def fit(self, data: LabelledCollection, fit_learner=True):
|
def fit(self, data: LabelledCollection, fit_learner=True):
|
||||||
"""
|
"""
|
||||||
Trains the Classify & Count method unless _fit_learner_ is False, in which case it is assumed to be already fit.
|
Trains the Classify & Count method unless `fit_learner` is False, in which case, the classifier is assumed to
|
||||||
:param data: training data
|
be already fit and there is nothing else to do.
|
||||||
|
|
||||||
|
:param data: a :class:`quapy.data.base.LabelledCollection` consisting of the training data
|
||||||
:param fit_learner: if False, the classifier is assumed to be fit
|
:param fit_learner: if False, the classifier is assumed to be fit
|
||||||
:return: self
|
:return: self
|
||||||
"""
|
"""
|
||||||
self.learner, _ = training_helper(self.learner, data, fit_learner)
|
self.learner, _ = _training_helper(self.learner, data, fit_learner)
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def aggregate(self, classif_predictions):
|
def aggregate(self, classif_predictions: np.ndarray):
|
||||||
|
"""
|
||||||
|
Computes class prevalence estimates by counting the prevalence of each of the predicted labels.
|
||||||
|
|
||||||
|
:param classif_predictions: array-like with label predictions
|
||||||
|
:return: `np.ndarray` of shape `(self.n_classes_,)` with class prevalence estimates.
|
||||||
|
"""
|
||||||
return F.prevalence_from_labels(classif_predictions, self.classes_)
|
return F.prevalence_from_labels(classif_predictions, self.classes_)
|
||||||
|
|
||||||
|
|
||||||
class ACC(AggregativeQuantifier):
|
class ACC(AggregativeQuantifier):
|
||||||
|
"""
|
||||||
|
`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
|
||||||
|
according to the `misclassification rates`.
|
||||||
|
|
||||||
|
:param learner: a sklearn's Estimator that generates a classifier
|
||||||
|
:param val_split: indicates the proportion of data to be used as a stratified held-out validation set in which the
|
||||||
|
misclassification rates are to be estimated.
|
||||||
|
This parameter can be indicated as a real value (between 0 and 1, default 0.4), representing a proportion of
|
||||||
|
validation data, or as an integer, indicating that the misclassification rates should be estimated via
|
||||||
|
`k`-fold cross validation (this integer stands for the number of folds `k`), or as a
|
||||||
|
:class:`quapy.data.base.LabelledCollection` (the split itself).
|
||||||
|
"""
|
||||||
|
|
||||||
def __init__(self, learner: BaseEstimator, val_split=0.4):
|
def __init__(self, learner: BaseEstimator, val_split=0.4):
|
||||||
self.learner = learner
|
self.learner = learner
|
||||||
|
@ -183,13 +270,14 @@ class ACC(AggregativeQuantifier):
|
||||||
|
|
||||||
def fit(self, data: LabelledCollection, fit_learner=True, val_split: Union[float, int, LabelledCollection] = None):
|
def fit(self, data: LabelledCollection, fit_learner=True, val_split: Union[float, int, LabelledCollection] = None):
|
||||||
"""
|
"""
|
||||||
Trains a ACC quantifier
|
Trains a ACC quantifier.
|
||||||
|
|
||||||
:param data: the training set
|
:param data: the training set
|
||||||
:param fit_learner: set to False to bypass the training (the learner is assumed to be already fit)
|
:param fit_learner: set to False to bypass the training (the learner is assumed to be already fit)
|
||||||
:param val_split: either a float in (0,1) indicating the proportion of training instances to use for
|
:param val_split: either a float in (0,1) indicating the proportion of training instances to use for
|
||||||
validation (e.g., 0.3 for using 30% of the training set as validation data), or a LabelledCollection
|
validation (e.g., 0.3 for using 30% of the training set as validation data), or a LabelledCollection
|
||||||
indicating the validation set itself, or an int indicating the number k of folds to be used in kFCV
|
indicating the validation set itself, or an int indicating the number `k` of folds to be used in `k`-fold
|
||||||
to estimate the parameters
|
cross validation to estimate the parameters
|
||||||
:return: self
|
:return: self
|
||||||
"""
|
"""
|
||||||
if val_split is None:
|
if val_split is None:
|
||||||
|
@ -205,7 +293,7 @@ class ACC(AggregativeQuantifier):
|
||||||
pbar.set_description(f'{self.__class__.__name__} fitting fold {k}')
|
pbar.set_description(f'{self.__class__.__name__} fitting fold {k}')
|
||||||
training = data.sampling_from_index(training_idx)
|
training = data.sampling_from_index(training_idx)
|
||||||
validation = data.sampling_from_index(validation_idx)
|
validation = data.sampling_from_index(validation_idx)
|
||||||
learner, val_data = training_helper(self.learner, training, fit_learner, val_split=validation)
|
learner, val_data = _training_helper(self.learner, training, fit_learner, val_split=validation)
|
||||||
y_.append(learner.predict(val_data.instances))
|
y_.append(learner.predict(val_data.instances))
|
||||||
y.append(val_data.labels)
|
y.append(val_data.labels)
|
||||||
|
|
||||||
|
@ -214,10 +302,10 @@ class ACC(AggregativeQuantifier):
|
||||||
class_count = data.counts()
|
class_count = data.counts()
|
||||||
|
|
||||||
# fit the learner on all data
|
# fit the learner on all data
|
||||||
self.learner, _ = training_helper(self.learner, data, fit_learner, val_split=None)
|
self.learner, _ = _training_helper(self.learner, data, fit_learner, val_split=None)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
self.learner, val_data = training_helper(self.learner, data, fit_learner, val_split=val_split)
|
self.learner, val_data = _training_helper(self.learner, data, fit_learner, val_split=val_split)
|
||||||
y_ = self.learner.predict(val_data.instances)
|
y_ = self.learner.predict(val_data.instances)
|
||||||
y = val_data.labels
|
y = val_data.labels
|
||||||
class_count = val_data.counts()
|
class_count = val_data.counts()
|
||||||
|
@ -239,7 +327,15 @@ class ACC(AggregativeQuantifier):
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def solve_adjustment(cls, PteCondEstim, prevs_estim):
|
def solve_adjustment(cls, PteCondEstim, prevs_estim):
|
||||||
# solve for the linear system Ax = B with A=PteCondEstim and B = prevs_estim
|
"""
|
||||||
|
Solves the system linear system :math:`Ax = B` with :math:`A` = `PteCondEstim` and :math:`B` = `prevs_estim`
|
||||||
|
|
||||||
|
:param PteCondEstim: a `np.ndarray` of shape `(n_classes,n_classes,)` with entry `(i,j)` being the estimate
|
||||||
|
of :math:`P(y_i|y_j)`, that is, the probability that an instance that belongs to :math:`y_j` ends up being
|
||||||
|
classified as belonging to :math:`y_i`
|
||||||
|
:param prevs_estim: a `np.ndarray` of shape `(n_classes,)` with the class prevalence estimates
|
||||||
|
:return: an adjusted `np.ndarray` of shape `(n_classes,)` with the corrected class prevalence estimates
|
||||||
|
"""
|
||||||
A = PteCondEstim
|
A = PteCondEstim
|
||||||
B = prevs_estim
|
B = prevs_estim
|
||||||
try:
|
try:
|
||||||
|
@ -252,11 +348,18 @@ class ACC(AggregativeQuantifier):
|
||||||
|
|
||||||
|
|
||||||
class PCC(AggregativeProbabilisticQuantifier):
|
class PCC(AggregativeProbabilisticQuantifier):
|
||||||
|
"""
|
||||||
|
`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.
|
||||||
|
|
||||||
|
:param learner: a sklearn's Estimator that generates a classifier
|
||||||
|
"""
|
||||||
|
|
||||||
def __init__(self, learner: BaseEstimator):
|
def __init__(self, learner: BaseEstimator):
|
||||||
self.learner = learner
|
self.learner = learner
|
||||||
|
|
||||||
def fit(self, data: LabelledCollection, fit_learner=True):
|
def fit(self, data: LabelledCollection, fit_learner=True):
|
||||||
self.learner, _ = training_helper(self.learner, data, fit_learner, ensure_probabilistic=True)
|
self.learner, _ = _training_helper(self.learner, data, fit_learner, ensure_probabilistic=True)
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def aggregate(self, classif_posteriors):
|
def aggregate(self, classif_posteriors):
|
||||||
|
@ -264,6 +367,18 @@ class PCC(AggregativeProbabilisticQuantifier):
|
||||||
|
|
||||||
|
|
||||||
class PACC(AggregativeProbabilisticQuantifier):
|
class PACC(AggregativeProbabilisticQuantifier):
|
||||||
|
"""
|
||||||
|
`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.
|
||||||
|
|
||||||
|
:param learner: a sklearn's Estimator that generates a classifier
|
||||||
|
:param val_split: indicates the proportion of data to be used as a stratified held-out validation set in which the
|
||||||
|
misclassification rates are to be estimated.
|
||||||
|
This parameter can be indicated as a real value (between 0 and 1, default 0.4), representing a proportion of
|
||||||
|
validation data, or as an integer, indicating that the misclassification rates should be estimated via
|
||||||
|
`k`-fold cross validation (this integer stands for the number of folds `k`), or as a
|
||||||
|
:class:`quapy.data.base.LabelledCollection` (the split itself).
|
||||||
|
"""
|
||||||
|
|
||||||
def __init__(self, learner: BaseEstimator, val_split=0.4):
|
def __init__(self, learner: BaseEstimator, val_split=0.4):
|
||||||
self.learner = learner
|
self.learner = learner
|
||||||
|
@ -271,7 +386,8 @@ class PACC(AggregativeProbabilisticQuantifier):
|
||||||
|
|
||||||
def fit(self, data: LabelledCollection, fit_learner=True, val_split: Union[float, int, LabelledCollection] = None):
|
def fit(self, data: LabelledCollection, fit_learner=True, val_split: Union[float, int, LabelledCollection] = None):
|
||||||
"""
|
"""
|
||||||
Trains a PACC quantifier
|
Trains a PACC quantifier.
|
||||||
|
|
||||||
:param data: the training set
|
:param data: the training set
|
||||||
:param fit_learner: set to False to bypass the training (the learner is assumed to be already fit)
|
:param fit_learner: set to False to bypass the training (the learner is assumed to be already fit)
|
||||||
:param val_split: either a float in (0,1) indicating the proportion of training instances to use for
|
:param val_split: either a float in (0,1) indicating the proportion of training instances to use for
|
||||||
|
@ -294,7 +410,7 @@ class PACC(AggregativeProbabilisticQuantifier):
|
||||||
pbar.set_description(f'{self.__class__.__name__} fitting fold {k}')
|
pbar.set_description(f'{self.__class__.__name__} fitting fold {k}')
|
||||||
training = data.sampling_from_index(training_idx)
|
training = data.sampling_from_index(training_idx)
|
||||||
validation = data.sampling_from_index(validation_idx)
|
validation = data.sampling_from_index(validation_idx)
|
||||||
learner, val_data = training_helper(
|
learner, val_data = _training_helper(
|
||||||
self.learner, training, fit_learner, ensure_probabilistic=True, val_split=validation)
|
self.learner, training, fit_learner, ensure_probabilistic=True, val_split=validation)
|
||||||
y_.append(learner.predict_proba(val_data.instances))
|
y_.append(learner.predict_proba(val_data.instances))
|
||||||
y.append(val_data.labels)
|
y.append(val_data.labels)
|
||||||
|
@ -303,12 +419,12 @@ class PACC(AggregativeProbabilisticQuantifier):
|
||||||
y_ = np.vstack(y_)
|
y_ = np.vstack(y_)
|
||||||
|
|
||||||
# fit the learner on all data
|
# fit the learner on all data
|
||||||
self.learner, _ = training_helper(self.learner, data, fit_learner, ensure_probabilistic=True,
|
self.learner, _ = _training_helper(self.learner, data, fit_learner, ensure_probabilistic=True,
|
||||||
val_split=None)
|
val_split=None)
|
||||||
classes = data.classes_
|
classes = data.classes_
|
||||||
|
|
||||||
else:
|
else:
|
||||||
self.learner, val_data = training_helper(
|
self.learner, val_data = _training_helper(
|
||||||
self.learner, data, fit_learner, ensure_probabilistic=True, val_split=val_split)
|
self.learner, data, fit_learner, ensure_probabilistic=True, val_split=val_split)
|
||||||
y_ = self.learner.predict_proba(val_data.instances)
|
y_ = self.learner.predict_proba(val_data.instances)
|
||||||
y = val_data.labels
|
y = val_data.labels
|
||||||
|
@ -337,10 +453,13 @@ class PACC(AggregativeProbabilisticQuantifier):
|
||||||
|
|
||||||
class EMQ(AggregativeProbabilisticQuantifier):
|
class EMQ(AggregativeProbabilisticQuantifier):
|
||||||
"""
|
"""
|
||||||
The method is described in:
|
`Expectation Maximization for Quantification <https://ieeexplore.ieee.org/abstract/document/6789744>`_ (EMQ),
|
||||||
Saerens, M., Latinne, P., and Decaestecker, C. (2002).
|
aka `Saerens-Latinne-Decaestecker` (SLD) algorithm.
|
||||||
Adjusting the outputs of a classifier to new a priori probabilities: A simple procedure.
|
EMQ consists of using the well-known `Expectation Maximization algorithm` to iteratively update the posterior
|
||||||
Neural Computation, 14(1): 21–41.
|
probabilities generated by a probabilistic classifier and the class prevalence estimates obtained via
|
||||||
|
maximum-likelihood estimation, in a mutually recursive way, until convergence.
|
||||||
|
|
||||||
|
:param learner: a sklearn's Estimator that generates a classifier
|
||||||
"""
|
"""
|
||||||
|
|
||||||
MAX_ITER = 1000
|
MAX_ITER = 1000
|
||||||
|
@ -350,7 +469,7 @@ class EMQ(AggregativeProbabilisticQuantifier):
|
||||||
self.learner = learner
|
self.learner = learner
|
||||||
|
|
||||||
def fit(self, data: LabelledCollection, fit_learner=True):
|
def fit(self, data: LabelledCollection, fit_learner=True):
|
||||||
self.learner, _ = training_helper(self.learner, data, fit_learner, ensure_probabilistic=True)
|
self.learner, _ = _training_helper(self.learner, data, fit_learner, ensure_probabilistic=True)
|
||||||
self.train_prevalence = F.prevalence_from_labels(data.labels, self.classes_)
|
self.train_prevalence = F.prevalence_from_labels(data.labels, self.classes_)
|
||||||
return self
|
return self
|
||||||
|
|
||||||
|
@ -365,6 +484,17 @@ class EMQ(AggregativeProbabilisticQuantifier):
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def EM(cls, tr_prev, posterior_probabilities, epsilon=EPSILON):
|
def EM(cls, tr_prev, posterior_probabilities, epsilon=EPSILON):
|
||||||
|
"""
|
||||||
|
Computes the `Expectation Maximization` routine.
|
||||||
|
|
||||||
|
:param tr_prev: array-like, the training prevalence
|
||||||
|
:param posterior_probabilities: `np.ndarray` of shape `(n_instances, n_classes,)` with the
|
||||||
|
posterior probabilities
|
||||||
|
:param epsilon: float, the threshold different between two consecutive iterations
|
||||||
|
to reach before stopping the loop
|
||||||
|
:return: a tuple with the estimated prevalence values (shape `(n_classes,)`) and
|
||||||
|
the corrected posterior probabilities (shape `(n_instances, n_classes,)`)
|
||||||
|
"""
|
||||||
Px = posterior_probabilities
|
Px = posterior_probabilities
|
||||||
Ptr = np.copy(tr_prev)
|
Ptr = np.copy(tr_prev)
|
||||||
qs = np.copy(Ptr) # qs (the running estimate) is initialized as the training prevalence
|
qs = np.copy(Ptr) # qs (the running estimate) is initialized as the training prevalence
|
||||||
|
@ -393,9 +523,17 @@ class EMQ(AggregativeProbabilisticQuantifier):
|
||||||
|
|
||||||
class HDy(AggregativeProbabilisticQuantifier, BinaryQuantifier):
|
class HDy(AggregativeProbabilisticQuantifier, BinaryQuantifier):
|
||||||
"""
|
"""
|
||||||
Implementation of the method based on the Hellinger Distance y (HDy) proposed by
|
`Hellinger Distance y <https://www.sciencedirect.com/science/article/pii/S0020025512004069>`_ (HDy).
|
||||||
González-Castro, V., Alaiz-Rodrı́guez, R., and Alegre, E. (2013). Class distribution
|
HDy is a probabilistic method for training binary quantifiers, that models quantification as the problem of
|
||||||
estimation based on the Hellinger distance. Information Sciences, 218:146–164.
|
minimizing the divergence (in terms of the Hellinger Distance) between two cumulative distributions of posterior
|
||||||
|
probabilities returned by the classifier. One of the distributions is generated from the unlabelled examples and
|
||||||
|
the other is generated from a validation set. This latter distribution is defined as a mixture of the
|
||||||
|
class-conditional distributions of the posterior probabilities returned for the positive and negative validation
|
||||||
|
examples, respectively. The parameters of the mixture thus represent the estimates of the class prevalence values.
|
||||||
|
|
||||||
|
:param learner: a sklearn's Estimator that generates a binary classifier
|
||||||
|
:param val_split: a float in range (0,1) indicating the proportion of data to be used as a stratified held-out
|
||||||
|
validation distribution, or a :class:`quapy.data.base.LabelledCollection` (the split itself).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, learner: BaseEstimator, val_split=0.4):
|
def __init__(self, learner: BaseEstimator, val_split=0.4):
|
||||||
|
@ -404,19 +542,20 @@ class HDy(AggregativeProbabilisticQuantifier, BinaryQuantifier):
|
||||||
|
|
||||||
def fit(self, data: LabelledCollection, fit_learner=True, val_split: Union[float, LabelledCollection] = None):
|
def fit(self, data: LabelledCollection, fit_learner=True, val_split: Union[float, LabelledCollection] = None):
|
||||||
"""
|
"""
|
||||||
Trains a HDy quantifier
|
Trains a HDy quantifier.
|
||||||
|
|
||||||
:param data: the training set
|
:param data: the training set
|
||||||
:param fit_learner: set to False to bypass the training (the learner is assumed to be already fit)
|
:param fit_learner: set to False to bypass the training (the learner is assumed to be already fit)
|
||||||
:param val_split: either a float in (0,1) indicating the proportion of training instances to use for
|
:param val_split: either a float in (0,1) indicating the proportion of training instances to use for
|
||||||
validation (e.g., 0.3 for using 30% of the training set as validation data), or a LabelledCollection
|
validation (e.g., 0.3 for using 30% of the training set as validation data), or a
|
||||||
indicating the validation set itself
|
:class:`quapy.data.base.LabelledCollection` indicating the validation set itself
|
||||||
:return: self
|
:return: self
|
||||||
"""
|
"""
|
||||||
if val_split is None:
|
if val_split is None:
|
||||||
val_split = self.val_split
|
val_split = self.val_split
|
||||||
|
|
||||||
self._check_binary(data, self.__class__.__name__)
|
self._check_binary(data, self.__class__.__name__)
|
||||||
self.learner, validation = training_helper(
|
self.learner, validation = _training_helper(
|
||||||
self.learner, data, fit_learner, ensure_probabilistic=True, val_split=val_split)
|
self.learner, data, fit_learner, ensure_probabilistic=True, val_split=val_split)
|
||||||
Px = self.posterior_probabilities(validation.instances)[:, 1] # takes only the P(y=+1|x)
|
Px = self.posterior_probabilities(validation.instances)[:, 1] # takes only the P(y=+1|x)
|
||||||
self.Pxy1 = Px[validation.labels == self.learner.classes_[1]]
|
self.Pxy1 = Px[validation.labels == self.learner.classes_[1]]
|
||||||
|
@ -459,6 +598,19 @@ class HDy(AggregativeProbabilisticQuantifier, BinaryQuantifier):
|
||||||
|
|
||||||
|
|
||||||
class ELM(AggregativeQuantifier, BinaryQuantifier):
|
class ELM(AggregativeQuantifier, BinaryQuantifier):
|
||||||
|
"""
|
||||||
|
Class of Explicit Loss Minimization (ELM) quantifiers.
|
||||||
|
Quantifiers based on ELM represent a family of methods based on structured output learning;
|
||||||
|
these quantifiers rely on classifiers that have been optimized using a quantification-oriented loss
|
||||||
|
measure. This implementation relies on
|
||||||
|
`Joachims’ SVM perf <https://www.cs.cornell.edu/people/tj/svm_light/svm_perf.html>`_ structured output
|
||||||
|
learning algorithm, which has to be installed and patched for the purpose (see this
|
||||||
|
`script <https://github.com/HLT-ISTI/QuaPy/blob/master/prepare_svmperf.sh>`_).
|
||||||
|
|
||||||
|
:param svmperf_base: path to the folder containing the binary files of `SVM perf`
|
||||||
|
:param loss: the loss to optimize (see :attr:`quapy.classification.svmperf.SVMperf.valid_losses`)
|
||||||
|
:param kwargs: rest of SVM perf's parameters
|
||||||
|
"""
|
||||||
|
|
||||||
def __init__(self, svmperf_base=None, loss='01', **kwargs):
|
def __init__(self, svmperf_base=None, loss='01', **kwargs):
|
||||||
self.svmperf_base = svmperf_base if svmperf_base is not None else qp.environ['SVMPERF_HOME']
|
self.svmperf_base = svmperf_base if svmperf_base is not None else qp.environ['SVMPERF_HOME']
|
||||||
|
@ -481,9 +633,15 @@ class ELM(AggregativeQuantifier, BinaryQuantifier):
|
||||||
|
|
||||||
class SVMQ(ELM):
|
class SVMQ(ELM):
|
||||||
"""
|
"""
|
||||||
Barranquero, J., Díez, J., and del Coz, J. J. (2015).
|
SVM(Q), which attempts to minimize the `Q` loss combining a classification-oriented loss and a
|
||||||
Quantification-oriented learning based on reliable classifiers.
|
quantification-oriented loss, as proposed by
|
||||||
Pattern Recognition, 48(2):591–604.
|
`Barranquero et al. 2015 <https://www.sciencedirect.com/science/article/pii/S003132031400291X>`_.
|
||||||
|
Equivalent to:
|
||||||
|
|
||||||
|
>>> ELM(svmperf_base, loss='q', **kwargs)
|
||||||
|
|
||||||
|
:param svmperf_base: path to the folder containing the binary files of `SVM perf`
|
||||||
|
:param kwargs: rest of SVM perf's parameters
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, svmperf_base=None, **kwargs):
|
def __init__(self, svmperf_base=None, **kwargs):
|
||||||
|
@ -492,9 +650,14 @@ class SVMQ(ELM):
|
||||||
|
|
||||||
class SVMKLD(ELM):
|
class SVMKLD(ELM):
|
||||||
"""
|
"""
|
||||||
Esuli, A. and Sebastiani, F. (2015).
|
SVM(KLD), which attempts to minimize the Kullback-Leibler Divergence as proposed by
|
||||||
Optimizing text quantifiers for multivariate loss functions.
|
`Esuli et al. 2015 <https://dl.acm.org/doi/abs/10.1145/2700406>`_.
|
||||||
ACM Transactions on Knowledge Discovery and Data, 9(4):Article 27.
|
Equivalent to:
|
||||||
|
|
||||||
|
>>> ELM(svmperf_base, loss='kld', **kwargs)
|
||||||
|
|
||||||
|
:param svmperf_base: path to the folder containing the binary files of `SVM perf`
|
||||||
|
:param kwargs: rest of SVM perf's parameters
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, svmperf_base=None, **kwargs):
|
def __init__(self, svmperf_base=None, **kwargs):
|
||||||
|
@ -503,9 +666,15 @@ class SVMKLD(ELM):
|
||||||
|
|
||||||
class SVMNKLD(ELM):
|
class SVMNKLD(ELM):
|
||||||
"""
|
"""
|
||||||
Esuli, A. and Sebastiani, F. (2015).
|
SVM(NKLD), which attempts to minimize a version of the the Kullback-Leibler Divergence normalized
|
||||||
Optimizing text quantifiers for multivariate loss functions.
|
via the logistic function, as proposed by
|
||||||
ACM Transactions on Knowledge Discovery and Data, 9(4):Article 27.
|
`Esuli et al. 2015 <https://dl.acm.org/doi/abs/10.1145/2700406>`_.
|
||||||
|
Equivalent to:
|
||||||
|
|
||||||
|
>>> ELM(svmperf_base, loss='nkld', **kwargs)
|
||||||
|
|
||||||
|
:param svmperf_base: path to the folder containing the binary files of `SVM perf`
|
||||||
|
:param kwargs: rest of SVM perf's parameters
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, svmperf_base=None, **kwargs):
|
def __init__(self, svmperf_base=None, **kwargs):
|
||||||
|
@ -513,25 +682,60 @@ class SVMNKLD(ELM):
|
||||||
|
|
||||||
|
|
||||||
class SVMAE(ELM):
|
class SVMAE(ELM):
|
||||||
|
"""
|
||||||
|
SVM(AE), which attempts to minimize Absolute Error as first used by
|
||||||
|
`Moreo and Sebastiani, 2021 <https://arxiv.org/abs/2011.02552>`_.
|
||||||
|
Equivalent to:
|
||||||
|
|
||||||
|
>>> ELM(svmperf_base, loss='mae', **kwargs)
|
||||||
|
|
||||||
|
:param svmperf_base: path to the folder containing the binary files of `SVM perf`
|
||||||
|
:param kwargs: rest of SVM perf's parameters
|
||||||
|
"""
|
||||||
|
|
||||||
def __init__(self, svmperf_base=None, **kwargs):
|
def __init__(self, svmperf_base=None, **kwargs):
|
||||||
super(SVMAE, self).__init__(svmperf_base, loss='mae', **kwargs)
|
super(SVMAE, self).__init__(svmperf_base, loss='mae', **kwargs)
|
||||||
|
|
||||||
|
|
||||||
class SVMRAE(ELM):
|
class SVMRAE(ELM):
|
||||||
|
"""
|
||||||
|
SVM(RAE), which attempts to minimize Relative Absolute Error as first used by
|
||||||
|
`Moreo and Sebastiani, 2021 <https://arxiv.org/abs/2011.02552>`_.
|
||||||
|
Equivalent to:
|
||||||
|
|
||||||
|
>>> ELM(svmperf_base, loss='mrae', **kwargs)
|
||||||
|
|
||||||
|
:param svmperf_base: path to the folder containing the binary files of `SVM perf`
|
||||||
|
:param kwargs: rest of SVM perf's parameters
|
||||||
|
"""
|
||||||
|
|
||||||
def __init__(self, svmperf_base=None, **kwargs):
|
def __init__(self, svmperf_base=None, **kwargs):
|
||||||
super(SVMRAE, self).__init__(svmperf_base, loss='mrae', **kwargs)
|
super(SVMRAE, self).__init__(svmperf_base, loss='mrae', **kwargs)
|
||||||
|
|
||||||
|
|
||||||
class ThresholdOptimization(AggregativeQuantifier, BinaryQuantifier):
|
class ThresholdOptimization(AggregativeQuantifier, BinaryQuantifier):
|
||||||
|
"""
|
||||||
|
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
|
||||||
|
`Forman 2008 <https://link.springer.com/article/10.1007/s10618-008-0097-y>`_.
|
||||||
|
The goal is to bring improved stability to the denominator of the adjustment.
|
||||||
|
The different variants are based on different heuristics for choosing a decision threshold
|
||||||
|
that would allow for more true positives and many more false positives, on the grounds this
|
||||||
|
would deliver larger denominators.
|
||||||
|
|
||||||
|
:param learner: a sklearn's Estimator that generates a classifier
|
||||||
|
:param val_split: indicates the proportion of data to be used as a stratified held-out validation set in which the
|
||||||
|
misclassification rates are to be estimated.
|
||||||
|
This parameter can be indicated as a real value (between 0 and 1, default 0.4), representing a proportion of
|
||||||
|
validation data, or as an integer, indicating that the misclassification rates should be estimated via
|
||||||
|
`k`-fold cross validation (this integer stands for the number of folds `k`), or as a
|
||||||
|
:class:`quapy.data.base.LabelledCollection` (the split itself).
|
||||||
|
"""
|
||||||
|
|
||||||
def __init__(self, learner: BaseEstimator, val_split=0.4):
|
def __init__(self, learner: BaseEstimator, val_split=0.4):
|
||||||
self.learner = learner
|
self.learner = learner
|
||||||
self.val_split = val_split
|
self.val_split = val_split
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def optimize_threshold(self, y, probabilities):
|
|
||||||
...
|
|
||||||
|
|
||||||
def fit(self, data: LabelledCollection, fit_learner=True, val_split: Union[float, int, LabelledCollection] = None):
|
def fit(self, data: LabelledCollection, fit_learner=True, val_split: Union[float, int, LabelledCollection] = None):
|
||||||
self._check_binary(data, "Threshold Optimization")
|
self._check_binary(data, "Threshold Optimization")
|
||||||
|
|
||||||
|
@ -548,7 +752,7 @@ class ThresholdOptimization(AggregativeQuantifier, BinaryQuantifier):
|
||||||
pbar.set_description(f'{self.__class__.__name__} fitting fold {k}')
|
pbar.set_description(f'{self.__class__.__name__} fitting fold {k}')
|
||||||
training = data.sampling_from_index(training_idx)
|
training = data.sampling_from_index(training_idx)
|
||||||
validation = data.sampling_from_index(validation_idx)
|
validation = data.sampling_from_index(validation_idx)
|
||||||
learner, val_data = training_helper(self.learner, training, fit_learner, val_split=validation)
|
learner, val_data = _training_helper(self.learner, training, fit_learner, val_split=validation)
|
||||||
probabilities.append(learner.predict_proba(val_data.instances))
|
probabilities.append(learner.predict_proba(val_data.instances))
|
||||||
y.append(val_data.labels)
|
y.append(val_data.labels)
|
||||||
|
|
||||||
|
@ -556,16 +760,16 @@ class ThresholdOptimization(AggregativeQuantifier, BinaryQuantifier):
|
||||||
probabilities = np.concatenate(probabilities)
|
probabilities = np.concatenate(probabilities)
|
||||||
|
|
||||||
# fit the learner on all data
|
# fit the learner on all data
|
||||||
self.learner, _ = training_helper(self.learner, data, fit_learner, val_split=None)
|
self.learner, _ = _training_helper(self.learner, data, fit_learner, val_split=None)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
self.learner, val_data = training_helper(self.learner, data, fit_learner, val_split=val_split)
|
self.learner, val_data = _training_helper(self.learner, data, fit_learner, val_split=val_split)
|
||||||
probabilities = self.learner.predict_proba(val_data.instances)
|
probabilities = self.learner.predict_proba(val_data.instances)
|
||||||
y = val_data.labels
|
y = val_data.labels
|
||||||
|
|
||||||
self.cc = CC(self.learner)
|
self.cc = CC(self.learner)
|
||||||
|
|
||||||
self.tpr, self.fpr = self.optimize_threshold(y, probabilities)
|
self.tpr, self.fpr = self._optimize_threshold(y, probabilities)
|
||||||
|
|
||||||
return self
|
return self
|
||||||
|
|
||||||
|
@ -573,20 +777,32 @@ class ThresholdOptimization(AggregativeQuantifier, BinaryQuantifier):
|
||||||
def _condition(self, tpr, fpr) -> float:
|
def _condition(self, tpr, fpr) -> float:
|
||||||
"""
|
"""
|
||||||
Implements the criterion according to which the threshold should be selected.
|
Implements the criterion according to which the threshold should be selected.
|
||||||
This function should return a (float) score to be minimized.
|
This function should return the (float) score to be minimized.
|
||||||
|
|
||||||
|
:param tpr: float, true positive rate
|
||||||
|
:param fpr: float, false positive rate
|
||||||
|
:return: float, a score for the given `tpr` and `fpr`
|
||||||
"""
|
"""
|
||||||
...
|
...
|
||||||
|
|
||||||
def optimize_threshold(self, y, probabilities):
|
def _optimize_threshold(self, y, probabilities):
|
||||||
|
"""
|
||||||
|
Seeks for the best `tpr` and `fpr` according to the score obtained at different
|
||||||
|
decision thresholds. The scoring function is implemented in function `_condition`.
|
||||||
|
|
||||||
|
:param y: predicted labels for the validation set (or for the training set via `k`-fold cross validation)
|
||||||
|
:param probabilities: array-like with the posterior probabilities
|
||||||
|
:return: best `tpr` and `fpr` according to `_condition`
|
||||||
|
"""
|
||||||
best_candidate_threshold_score = None
|
best_candidate_threshold_score = None
|
||||||
best_tpr = 0
|
best_tpr = 0
|
||||||
best_fpr = 0
|
best_fpr = 0
|
||||||
candidate_thresholds = np.unique(probabilities[:, 1])
|
candidate_thresholds = np.unique(probabilities[:, 1])
|
||||||
for candidate_threshold in candidate_thresholds:
|
for candidate_threshold in candidate_thresholds:
|
||||||
y_ = [self.classes_[1] if p > candidate_threshold else self.classes_[0] for p in probabilities[:, 1]]
|
y_ = [self.classes_[1] if p > candidate_threshold else self.classes_[0] for p in probabilities[:, 1]]
|
||||||
TP, FP, FN, TN = self.compute_table(y, y_)
|
TP, FP, FN, TN = self._compute_table(y, y_)
|
||||||
tpr = self.compute_tpr(TP, FP)
|
tpr = self._compute_tpr(TP, FP)
|
||||||
fpr = self.compute_fpr(FP, TN)
|
fpr = self._compute_fpr(FP, TN)
|
||||||
condition_score = self._condition(tpr, fpr)
|
condition_score = self._condition(tpr, fpr)
|
||||||
if best_candidate_threshold_score is None or condition_score < best_candidate_threshold_score:
|
if best_candidate_threshold_score is None or condition_score < best_candidate_threshold_score:
|
||||||
best_candidate_threshold_score = condition_score
|
best_candidate_threshold_score = condition_score
|
||||||
|
@ -603,25 +819,40 @@ class ThresholdOptimization(AggregativeQuantifier, BinaryQuantifier):
|
||||||
adjusted_prevs_estim = np.array((1 - adjusted_prevs_estim, adjusted_prevs_estim))
|
adjusted_prevs_estim = np.array((1 - adjusted_prevs_estim, adjusted_prevs_estim))
|
||||||
return adjusted_prevs_estim
|
return adjusted_prevs_estim
|
||||||
|
|
||||||
def compute_table(self, y, y_):
|
def _compute_table(self, y, y_):
|
||||||
TP = np.logical_and(y == y_, y == self.classes_[1]).sum()
|
TP = np.logical_and(y == y_, y == self.classes_[1]).sum()
|
||||||
FP = np.logical_and(y != y_, y == self.classes_[0]).sum()
|
FP = np.logical_and(y != y_, y == self.classes_[0]).sum()
|
||||||
FN = np.logical_and(y != y_, y == self.classes_[1]).sum()
|
FN = np.logical_and(y != y_, y == self.classes_[1]).sum()
|
||||||
TN = np.logical_and(y == y_, y == self.classes_[0]).sum()
|
TN = np.logical_and(y == y_, y == self.classes_[0]).sum()
|
||||||
return TP, FP, FN, TN
|
return TP, FP, FN, TN
|
||||||
|
|
||||||
def compute_tpr(self, TP, FP):
|
def _compute_tpr(self, TP, FP):
|
||||||
if TP + FP == 0:
|
if TP + FP == 0:
|
||||||
return 0
|
return 0
|
||||||
return TP / (TP + FP)
|
return TP / (TP + FP)
|
||||||
|
|
||||||
def compute_fpr(self, FP, TN):
|
def _compute_fpr(self, FP, TN):
|
||||||
if FP + TN == 0:
|
if FP + TN == 0:
|
||||||
return 0
|
return 0
|
||||||
return FP / (FP + TN)
|
return FP / (FP + TN)
|
||||||
|
|
||||||
|
|
||||||
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
|
||||||
|
`Forman 2008 <https://link.springer.com/article/10.1007/s10618-008-0097-y>`_ that looks
|
||||||
|
for the threshold that makes `tpr` cosest to 0.5.
|
||||||
|
The goal is to bring improved stability to the denominator of the adjustment.
|
||||||
|
|
||||||
|
:param learner: a sklearn's Estimator that generates a classifier
|
||||||
|
:param val_split: indicates the proportion of data to be used as a stratified held-out validation set in which the
|
||||||
|
misclassification rates are to be estimated.
|
||||||
|
This parameter can be indicated as a real value (between 0 and 1, default 0.4), representing a proportion of
|
||||||
|
validation data, or as an integer, indicating that the misclassification rates should be estimated via
|
||||||
|
`k`-fold cross validation (this integer stands for the number of folds `k`), or as a
|
||||||
|
:class:`quapy.data.base.LabelledCollection` (the split itself).
|
||||||
|
"""
|
||||||
|
|
||||||
def __init__(self, learner: BaseEstimator, val_split=0.4):
|
def __init__(self, learner: BaseEstimator, val_split=0.4):
|
||||||
super().__init__(learner, val_split)
|
super().__init__(learner, val_split)
|
||||||
|
@ -631,6 +862,21 @@ class T50(ThresholdOptimization):
|
||||||
|
|
||||||
|
|
||||||
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
|
||||||
|
`Forman 2008 <https://link.springer.com/article/10.1007/s10618-008-0097-y>`_ that looks
|
||||||
|
for the threshold that maximizes `tpr-fpr`.
|
||||||
|
The goal is to bring improved stability to the denominator of the adjustment.
|
||||||
|
|
||||||
|
:param learner: a sklearn's Estimator that generates a classifier
|
||||||
|
:param val_split: indicates the proportion of data to be used as a stratified held-out validation set in which the
|
||||||
|
misclassification rates are to be estimated.
|
||||||
|
This parameter can be indicated as a real value (between 0 and 1, default 0.4), representing a proportion of
|
||||||
|
validation data, or as an integer, indicating that the misclassification rates should be estimated via
|
||||||
|
`k`-fold cross validation (this integer stands for the number of folds `k`), or as a
|
||||||
|
:class:`quapy.data.base.LabelledCollection` (the split itself).
|
||||||
|
"""
|
||||||
|
|
||||||
def __init__(self, learner: BaseEstimator, val_split=0.4):
|
def __init__(self, learner: BaseEstimator, val_split=0.4):
|
||||||
super().__init__(learner, val_split)
|
super().__init__(learner, val_split)
|
||||||
|
@ -641,6 +887,21 @@ class MAX(ThresholdOptimization):
|
||||||
|
|
||||||
|
|
||||||
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
|
||||||
|
`Forman 2008 <https://link.springer.com/article/10.1007/s10618-008-0097-y>`_ that looks
|
||||||
|
for the threshold that yields `tpr=1-fpr`.
|
||||||
|
The goal is to bring improved stability to the denominator of the adjustment.
|
||||||
|
|
||||||
|
:param learner: a sklearn's Estimator that generates a classifier
|
||||||
|
:param val_split: indicates the proportion of data to be used as a stratified held-out validation set in which the
|
||||||
|
misclassification rates are to be estimated.
|
||||||
|
This parameter can be indicated as a real value (between 0 and 1, default 0.4), representing a proportion of
|
||||||
|
validation data, or as an integer, indicating that the misclassification rates should be estimated via
|
||||||
|
`k`-fold cross validation (this integer stands for the number of folds `k`), or as a
|
||||||
|
:class:`quapy.data.base.LabelledCollection` (the split itself).
|
||||||
|
"""
|
||||||
|
|
||||||
def __init__(self, learner: BaseEstimator, val_split=0.4):
|
def __init__(self, learner: BaseEstimator, val_split=0.4):
|
||||||
super().__init__(learner, val_split)
|
super().__init__(learner, val_split)
|
||||||
|
@ -650,41 +911,70 @@ class X(ThresholdOptimization):
|
||||||
|
|
||||||
|
|
||||||
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
|
||||||
|
`Forman 2008 <https://link.springer.com/article/10.1007/s10618-008-0097-y>`_ that generates
|
||||||
|
class prevalence estimates for all decision thresholds and returns the median of them all.
|
||||||
|
The goal is to bring improved stability to the denominator of the adjustment.
|
||||||
|
|
||||||
|
:param learner: a sklearn's Estimator that generates a classifier
|
||||||
|
:param val_split: indicates the proportion of data to be used as a stratified held-out validation set in which the
|
||||||
|
misclassification rates are to be estimated.
|
||||||
|
This parameter can be indicated as a real value (between 0 and 1, default 0.4), representing a proportion of
|
||||||
|
validation data, or as an integer, indicating that the misclassification rates should be estimated via
|
||||||
|
`k`-fold cross validation (this integer stands for the number of folds `k`), or as a
|
||||||
|
:class:`quapy.data.base.LabelledCollection` (the split itself).
|
||||||
|
"""
|
||||||
def __init__(self, learner: BaseEstimator, val_split=0.4):
|
def __init__(self, learner: BaseEstimator, val_split=0.4):
|
||||||
super().__init__(learner, val_split)
|
super().__init__(learner, val_split)
|
||||||
|
|
||||||
def _condition(self, tpr, fpr) -> float:
|
def _condition(self, tpr, fpr) -> float:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def optimize_threshold(self, y, probabilities):
|
def _optimize_threshold(self, y, probabilities):
|
||||||
tprs = []
|
tprs = []
|
||||||
fprs = []
|
fprs = []
|
||||||
candidate_thresholds = np.unique(probabilities[:, 1])
|
candidate_thresholds = np.unique(probabilities[:, 1])
|
||||||
for candidate_threshold in candidate_thresholds:
|
for candidate_threshold in candidate_thresholds:
|
||||||
y_ = [self.classes_[1] if p > candidate_threshold else self.classes_[0] for p in probabilities[:, 1]]
|
y_ = [self.classes_[1] if p > candidate_threshold else self.classes_[0] for p in probabilities[:, 1]]
|
||||||
TP, FP, FN, TN = self.compute_table(y, y_)
|
TP, FP, FN, TN = self._compute_table(y, y_)
|
||||||
tpr = self.compute_tpr(TP, FP)
|
tpr = self._compute_tpr(TP, FP)
|
||||||
fpr = self.compute_fpr(FP, TN)
|
fpr = self._compute_fpr(FP, TN)
|
||||||
tprs.append(tpr)
|
tprs.append(tpr)
|
||||||
fprs.append(fpr)
|
fprs.append(fpr)
|
||||||
return np.median(tprs), np.median(fprs)
|
return np.median(tprs), np.median(fprs)
|
||||||
|
|
||||||
|
|
||||||
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
|
||||||
|
`Forman 2008 <https://link.springer.com/article/10.1007/s10618-008-0097-y>`_ that generates
|
||||||
|
class prevalence estimates for all decision thresholds and returns the median of for cases in
|
||||||
|
which `tpr-fpr>0.25`
|
||||||
|
The goal is to bring improved stability to the denominator of the adjustment.
|
||||||
|
|
||||||
|
:param learner: a sklearn's Estimator that generates a classifier
|
||||||
|
:param val_split: indicates the proportion of data to be used as a stratified held-out validation set in which the
|
||||||
|
misclassification rates are to be estimated.
|
||||||
|
This parameter can be indicated as a real value (between 0 and 1, default 0.4), representing a proportion of
|
||||||
|
validation data, or as an integer, indicating that the misclassification rates should be estimated via
|
||||||
|
`k`-fold cross validation (this integer stands for the number of folds `k`), or as a
|
||||||
|
:class:`quapy.data.base.LabelledCollection` (the split itself).
|
||||||
|
"""
|
||||||
def __init__(self, learner: BaseEstimator, val_split=0.4):
|
def __init__(self, learner: BaseEstimator, val_split=0.4):
|
||||||
super().__init__(learner, val_split)
|
super().__init__(learner, val_split)
|
||||||
|
|
||||||
def optimize_threshold(self, y, probabilities):
|
def _optimize_threshold(self, y, probabilities):
|
||||||
tprs = [0, 1]
|
tprs = [0, 1]
|
||||||
fprs = [0, 1]
|
fprs = [0, 1]
|
||||||
candidate_thresholds = np.unique(probabilities[:, 1])
|
candidate_thresholds = np.unique(probabilities[:, 1])
|
||||||
for candidate_threshold in candidate_thresholds:
|
for candidate_threshold in candidate_thresholds:
|
||||||
y_ = [self.classes_[1] if p > candidate_threshold else self.classes_[0] for p in probabilities[:, 1]]
|
y_ = [self.classes_[1] if p > candidate_threshold else self.classes_[0] for p in probabilities[:, 1]]
|
||||||
TP, FP, FN, TN = self.compute_table(y, y_)
|
TP, FP, FN, TN = self._compute_table(y, y_)
|
||||||
tpr = self.compute_tpr(TP, FP)
|
tpr = self._compute_tpr(TP, FP)
|
||||||
fpr = self.compute_fpr(FP, TN)
|
fpr = self._compute_fpr(FP, TN)
|
||||||
if (tpr - fpr) > 0.25:
|
if (tpr - fpr) > 0.25:
|
||||||
tprs.append(tpr)
|
tprs.append(tpr)
|
||||||
fprs.append(fpr)
|
fprs.append(fpr)
|
||||||
|
@ -696,6 +986,7 @@ AdjustedClassifyAndCount = ACC
|
||||||
ProbabilisticClassifyAndCount = PCC
|
ProbabilisticClassifyAndCount = PCC
|
||||||
ProbabilisticAdjustedClassifyAndCount = PACC
|
ProbabilisticAdjustedClassifyAndCount = PACC
|
||||||
ExpectationMaximizationQuantifier = EMQ
|
ExpectationMaximizationQuantifier = EMQ
|
||||||
|
SLD = EMQ
|
||||||
HellingerDistanceY = HDy
|
HellingerDistanceY = HDy
|
||||||
ExplicitLossMinimisation = ELM
|
ExplicitLossMinimisation = ELM
|
||||||
MedianSweep = MS
|
MedianSweep = MS
|
||||||
|
@ -704,11 +995,14 @@ MedianSweep2 = MS2
|
||||||
|
|
||||||
class OneVsAll(AggregativeQuantifier):
|
class OneVsAll(AggregativeQuantifier):
|
||||||
"""
|
"""
|
||||||
Allows any binary quantifier to perform quantification on single-label datasets. The method maintains one binary
|
Allows any binary quantifier to perform quantification on single-label datasets.
|
||||||
quantifier for each class, and then l1-normalizes the outputs so that the class prevelences sum up to 1.
|
The method maintains one binary quantifier for each class, and then l1-normalizes the outputs so that the
|
||||||
This variant was used, along with the ExplicitLossMinimization quantifier in
|
class prevelences sum up to 1.
|
||||||
Gao, W., Sebastiani, F.: From classification to quantification in tweet sentiment analysis.
|
This variant was used, along with the :class:`EMQ` quantifier, in
|
||||||
Social Network Analysis and Mining 6(19), 1–22 (2016)
|
`Gao and Sebastiani, 2016 <https://link.springer.com/content/pdf/10.1007/s13278-016-0327-z.pdf>`_.
|
||||||
|
|
||||||
|
:param learner: a sklearn's Estimator that generates a binary classifier
|
||||||
|
:param n_jobs: number of parallel workers
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, binary_quantifier, n_jobs=-1):
|
def __init__(self, binary_quantifier, n_jobs=-1):
|
||||||
|
@ -727,18 +1021,30 @@ class OneVsAll(AggregativeQuantifier):
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def classify(self, instances):
|
def classify(self, instances):
|
||||||
# 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 i belongs to class j. The binary classifications are
|
Returns a matrix of shape `(n,m,)` with `n` the number of instances and `m` the number of classes. The entry
|
||||||
# independent of each other, meaning that an instance can end up be attributed to 0, 1, or more classes.
|
`(i,j)` is a binary value indicating whether instance `i `belongs to class `j`. The binary classifications are
|
||||||
|
independent of each other, meaning that an instance can end up be attributed to 0, 1, or more classes.
|
||||||
|
|
||||||
|
:param instances: array-like
|
||||||
|
:return: `np.ndarray`
|
||||||
|
"""
|
||||||
|
|
||||||
classif_predictions_bin = self.__parallel(self._delayed_binary_classification, instances)
|
classif_predictions_bin = self.__parallel(self._delayed_binary_classification, instances)
|
||||||
return classif_predictions_bin.T
|
return classif_predictions_bin.T
|
||||||
|
|
||||||
def posterior_probabilities(self, instances):
|
def posterior_probabilities(self, instances):
|
||||||
# returns a matrix of shape (n,m,2) with n the number of instances and m the number of classes. The entry
|
"""
|
||||||
# (i,j,1) (resp. (i,j,0)) is a value in [0,1] indicating the posterior probability that instance i belongs
|
Returns a matrix of shape `(n,m,2)` with `n` the number of instances and `m` the number of classes. The entry
|
||||||
# (resp. does not belong) to class j.
|
`(i,j,1)` (resp. `(i,j,0)`) is a value in [0,1] indicating the posterior probability that instance `i` belongs
|
||||||
# The posterior probabilities are independent of each other, meaning that, in general, they do not sum
|
(resp. does not belong) to class `j`.
|
||||||
# up to one.
|
The posterior probabilities are independent of each other, meaning that, in general, they do not sum
|
||||||
|
up to one.
|
||||||
|
|
||||||
|
:param instances: array-like
|
||||||
|
:return: `np.ndarray`
|
||||||
|
"""
|
||||||
|
|
||||||
if not self.binary_quantifier.probabilistic:
|
if not self.binary_quantifier.probabilistic:
|
||||||
raise NotImplementedError(f'{self.__class__.__name__} does not implement posterior_probabilities because '
|
raise NotImplementedError(f'{self.__class__.__name__} does not implement posterior_probabilities because '
|
||||||
f'the base quantifier {self.binary_quantifier.__class__.__name__} is not '
|
f'the base quantifier {self.binary_quantifier.__class__.__name__} is not '
|
||||||
|
@ -800,8 +1106,19 @@ class OneVsAll(AggregativeQuantifier):
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def binary(self):
|
def binary(self):
|
||||||
|
"""
|
||||||
|
Informs that the classifier is not binary
|
||||||
|
|
||||||
|
:return: False
|
||||||
|
"""
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def probabilistic(self):
|
def probabilistic(self):
|
||||||
|
"""
|
||||||
|
Indicates if the classifier is probabilistic or not (depending on the nature of the base classifier).
|
||||||
|
|
||||||
|
:return: boolean
|
||||||
|
"""
|
||||||
|
|
||||||
return self.binary_quantifier.probabilistic
|
return self.binary_quantifier.probabilistic
|
||||||
|
|
|
@ -6,39 +6,107 @@ from quapy.data import LabelledCollection
|
||||||
# Base Quantifier abstract class
|
# Base Quantifier abstract class
|
||||||
# ------------------------------------
|
# ------------------------------------
|
||||||
class BaseQuantifier(metaclass=ABCMeta):
|
class BaseQuantifier(metaclass=ABCMeta):
|
||||||
|
"""
|
||||||
|
Abstract Quantifier. A quantifier is defined as an object of a class that implements the method :meth:`fit` on
|
||||||
|
:class:`quapy.data.base.LabelledCollection`, the method :meth:`quantify`, and the :meth:`set_params` and
|
||||||
|
:meth:`get_params` for model selection (see :meth:`quapy.model_selection.GridSearchQ`)
|
||||||
|
"""
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def fit(self, data: LabelledCollection): ...
|
def fit(self, data: LabelledCollection):
|
||||||
|
"""
|
||||||
|
Trains a quantifier.
|
||||||
|
|
||||||
|
:param data: a :class:`quapy.data.base.LabelledCollection` consisting of the training data
|
||||||
|
:return: self
|
||||||
|
"""
|
||||||
|
...
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def quantify(self, instances): ...
|
def quantify(self, instances):
|
||||||
|
"""
|
||||||
|
Generate class prevalence estimates for the sample's instances
|
||||||
|
|
||||||
|
:param instances: array-like
|
||||||
|
:return: `np.ndarray` of shape `(self.n_classes_,)` with class prevalence estimates.
|
||||||
|
"""
|
||||||
|
...
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def set_params(self, **parameters): ...
|
def set_params(self, **parameters):
|
||||||
|
"""
|
||||||
|
Set the parameters of the quantifier.
|
||||||
|
|
||||||
|
:param parameters: dictionary of param-value pairs
|
||||||
|
"""
|
||||||
|
...
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def get_params(self, deep=True): ...
|
def get_params(self, deep=True):
|
||||||
|
"""
|
||||||
|
Return the current parameters of the quantifier.
|
||||||
|
|
||||||
|
:param deep: for compatibility with sklearn
|
||||||
|
:return: a dictionary of param-value pairs
|
||||||
|
"""
|
||||||
|
...
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def classes_(self): ...
|
def classes_(self):
|
||||||
|
"""
|
||||||
|
Class labels, in the same order in which class prevalence values are to be computed.
|
||||||
|
|
||||||
|
:return: array-like
|
||||||
|
"""
|
||||||
|
...
|
||||||
|
|
||||||
|
@property
|
||||||
|
def n_classes(self):
|
||||||
|
"""
|
||||||
|
Returns the number of classes
|
||||||
|
|
||||||
|
:return: integer
|
||||||
|
"""
|
||||||
|
return len(self.classes_)
|
||||||
|
|
||||||
# these methods allows meta-learners to reimplement the decision based on their constituents, and not
|
# these methods allows meta-learners to reimplement the decision based on their constituents, and not
|
||||||
# based on class structure
|
# based on class structure
|
||||||
@property
|
@property
|
||||||
def binary(self):
|
def binary(self):
|
||||||
|
"""
|
||||||
|
Indicates whether the quantifier is binary or not.
|
||||||
|
|
||||||
|
:return: False (to be overridden)
|
||||||
|
"""
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def aggregative(self):
|
def aggregative(self):
|
||||||
|
"""
|
||||||
|
Indicates whether the quantifier is of type aggregative or not
|
||||||
|
|
||||||
|
:return: False (to be overridden)
|
||||||
|
"""
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def probabilistic(self):
|
def probabilistic(self):
|
||||||
|
"""
|
||||||
|
Indicates whether the quantifier is of type probabilistic or not
|
||||||
|
|
||||||
|
:return: False (to be overridden)
|
||||||
|
"""
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
class BinaryQuantifier(BaseQuantifier):
|
class BinaryQuantifier(BaseQuantifier):
|
||||||
|
"""
|
||||||
|
Abstract class of binary quantifiers, i.e., quantifiers estimating class prevalence values for only two classes
|
||||||
|
(typically, to be interpreted as one class and its complement).
|
||||||
|
"""
|
||||||
|
|
||||||
def _check_binary(self, data: LabelledCollection, quantifier_name):
|
def _check_binary(self, data: LabelledCollection, quantifier_name):
|
||||||
assert data.binary, f'{quantifier_name} works only on problems of binary classification. ' \
|
assert data.binary, f'{quantifier_name} works only on problems of binary classification. ' \
|
||||||
|
@ -46,18 +114,43 @@ class BinaryQuantifier(BaseQuantifier):
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def binary(self):
|
def binary(self):
|
||||||
|
"""
|
||||||
|
Informs that the quantifier is binary
|
||||||
|
|
||||||
|
:return: True
|
||||||
|
"""
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
def isbinary(model:BaseQuantifier):
|
def isbinary(model:BaseQuantifier):
|
||||||
|
"""
|
||||||
|
Alias for property `binary`
|
||||||
|
|
||||||
|
:param model: the model
|
||||||
|
:return: True if the model is binary, False otherwise
|
||||||
|
"""
|
||||||
return model.binary
|
return model.binary
|
||||||
|
|
||||||
|
|
||||||
def isaggregative(model:BaseQuantifier):
|
def isaggregative(model:BaseQuantifier):
|
||||||
|
"""
|
||||||
|
Alias for property `aggregative`
|
||||||
|
|
||||||
|
:param model: the model
|
||||||
|
:return: True if the model is aggregative, False otherwise
|
||||||
|
"""
|
||||||
|
|
||||||
return model.aggregative
|
return model.aggregative
|
||||||
|
|
||||||
|
|
||||||
def isprobabilistic(model:BaseQuantifier):
|
def isprobabilistic(model:BaseQuantifier):
|
||||||
|
"""
|
||||||
|
Alias for property `probabilistic`
|
||||||
|
|
||||||
|
:param model: the model
|
||||||
|
:return: True if the model is probabilistic, False otherwise
|
||||||
|
"""
|
||||||
|
|
||||||
return model.probabilistic
|
return model.probabilistic
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
from typing import Union
|
from typing import Union
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from sklearn.linear_model import LogisticRegression
|
from sklearn.linear_model import LogisticRegression
|
||||||
from sklearn.metrics import f1_score, make_scorer, accuracy_score
|
from sklearn.metrics import f1_score, make_scorer, accuracy_score
|
||||||
|
@ -30,14 +29,40 @@ class Ensemble(BaseQuantifier):
|
||||||
VALID_POLICIES = {'ave', 'ptr', 'ds'} | qp.error.QUANTIFICATION_ERROR_NAMES
|
VALID_POLICIES = {'ave', 'ptr', 'ds'} | qp.error.QUANTIFICATION_ERROR_NAMES
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Methods from the articles:
|
Implementation of the Ensemble methods for quantification described by
|
||||||
Pérez-Gállego, P., Quevedo, J. R., & del Coz, J. J. (2017).
|
`Pérez-Gállego et al., 2017 <https://www.sciencedirect.com/science/article/pii/S1566253516300628>`_
|
||||||
Using ensembles for problems with characterizable changes in data distribution: A case study on quantification.
|
|
||||||
Information Fusion, 34, 87-100.
|
|
||||||
and
|
and
|
||||||
Pérez-Gállego, P., Castano, A., Quevedo, J. R., & del Coz, J. J. (2019).
|
`Pérez-Gállego et al., 2019 <https://www.sciencedirect.com/science/article/pii/S1566253517303652>`_.
|
||||||
Dynamic ensemble selection for quantification tasks.
|
The policies implemented include:
|
||||||
Information Fusion, 45, 1-15.
|
|
||||||
|
- Average (`policy='ave'`): computes class prevalence estimates as the average of the estimates
|
||||||
|
returned by the base quantifiers.
|
||||||
|
- Training Prevalence (`policy='ptr'`): applies a dynamic selection to the ensemble’s members by retaining only
|
||||||
|
those members such that the class prevalence values in the samples they use as training set are closest to
|
||||||
|
preliminary class prevalence estimates computed as the average of the estimates of all the members. The final
|
||||||
|
estimate is recomputed by considering only the selected members.
|
||||||
|
- Distribution Similarity (`policy='ds'`): performs a dynamic selection of base members by retaining
|
||||||
|
the members trained on samples whose distribution of posterior probabilities is closest, in terms of the
|
||||||
|
Hellinger Distance, to the distribution of posterior probabilities in the test sample
|
||||||
|
- Accuracy (`policy='<valid error name>'`): performs a static selection of the ensemble members by
|
||||||
|
retaining those that minimize a quantification error measure, which is passed as an argument.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
>>> model = Ensemble(quantifier=ACC(LogisticRegression()), size=30, policy='ave', n_jobs=-1)
|
||||||
|
|
||||||
|
:param quantifier: base quantification member of the ensemble
|
||||||
|
:param size: number of members
|
||||||
|
:param red_size: number of members to retain after selection (depending on the policy)
|
||||||
|
:param min_pos: minimum number of positive instances to consider a sample as valid
|
||||||
|
:param policy: the selection policy; available policies include: `ave` (default), `ptr`, `ds`, and accuracy
|
||||||
|
(which is instantiated via a valid error name, e.g., `mae`)
|
||||||
|
:param max_sample_size: maximum number of instances to consider in the samples (set to None
|
||||||
|
to indicate no limit, default)
|
||||||
|
:param val_split: a float in range (0,1) indicating the proportion of data to be used as a stratified held-out
|
||||||
|
validation split, or a :class:`quapy.data.base.LabelledCollection` (the split itself).
|
||||||
|
:param n_jobs: number of parallel workers (default 1)
|
||||||
|
:param verbose: set to True (default is False) to get some information in standard output
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self,
|
def __init__(self,
|
||||||
|
@ -47,7 +72,7 @@ class Ensemble(BaseQuantifier):
|
||||||
min_pos=5,
|
min_pos=5,
|
||||||
policy='ave',
|
policy='ave',
|
||||||
max_sample_size=None,
|
max_sample_size=None,
|
||||||
val_split=None,
|
val_split:Union[qp.data.LabelledCollection, float]=None,
|
||||||
n_jobs=1,
|
n_jobs=1,
|
||||||
verbose=False):
|
verbose=False):
|
||||||
assert policy in Ensemble.VALID_POLICIES, \
|
assert policy in Ensemble.VALID_POLICIES, \
|
||||||
|
@ -65,12 +90,12 @@ class Ensemble(BaseQuantifier):
|
||||||
self.verbose = verbose
|
self.verbose = verbose
|
||||||
self.max_sample_size = max_sample_size
|
self.max_sample_size = max_sample_size
|
||||||
|
|
||||||
def sout(self, msg):
|
def _sout(self, msg):
|
||||||
if self.verbose:
|
if self.verbose:
|
||||||
print('[Ensemble]' + msg)
|
print('[Ensemble]' + msg)
|
||||||
|
|
||||||
def fit(self, data: qp.data.LabelledCollection, val_split: Union[qp.data.LabelledCollection, float] = None):
|
def fit(self, data: qp.data.LabelledCollection, val_split: Union[qp.data.LabelledCollection, float] = None):
|
||||||
self.sout('Fit')
|
self._sout('Fit')
|
||||||
if self.policy == 'ds' and not data.binary:
|
if self.policy == 'ds' and not data.binary:
|
||||||
raise ValueError(f'ds policy is only defined for binary quantification, but this dataset is not binary')
|
raise ValueError(f'ds policy is only defined for binary quantification, but this dataset is not binary')
|
||||||
if val_split is None:
|
if val_split is None:
|
||||||
|
@ -84,7 +109,7 @@ class Ensemble(BaseQuantifier):
|
||||||
posteriors = None
|
posteriors = None
|
||||||
if self.policy == 'ds':
|
if self.policy == 'ds':
|
||||||
# precompute the training posterior probabilities
|
# precompute the training posterior probabilities
|
||||||
posteriors, self.post_proba_fn = self.ds_policy_get_posteriors(data)
|
posteriors, self.post_proba_fn = self._ds_policy_get_posteriors(data)
|
||||||
|
|
||||||
is_static_policy = (self.policy in qp.error.QUANTIFICATION_ERROR_NAMES)
|
is_static_policy = (self.policy in qp.error.QUANTIFICATION_ERROR_NAMES)
|
||||||
|
|
||||||
|
@ -99,9 +124,9 @@ class Ensemble(BaseQuantifier):
|
||||||
|
|
||||||
# static selection policy (the name of a quantification-oriented error function to minimize)
|
# static selection policy (the name of a quantification-oriented error function to minimize)
|
||||||
if self.policy in qp.error.QUANTIFICATION_ERROR_NAMES:
|
if self.policy in qp.error.QUANTIFICATION_ERROR_NAMES:
|
||||||
self.accuracy_policy(error_name=self.policy)
|
self._accuracy_policy(error_name=self.policy)
|
||||||
|
|
||||||
self.sout('Fit [Done]')
|
self._sout('Fit [Done]')
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def quantify(self, instances):
|
def quantify(self, instances):
|
||||||
|
@ -110,23 +135,42 @@ class Ensemble(BaseQuantifier):
|
||||||
)
|
)
|
||||||
|
|
||||||
if self.policy == 'ptr':
|
if self.policy == 'ptr':
|
||||||
predictions = self.ptr_policy(predictions)
|
predictions = self._ptr_policy(predictions)
|
||||||
elif self.policy == 'ds':
|
elif self.policy == 'ds':
|
||||||
predictions = self.ds_policy(predictions, instances)
|
predictions = self._ds_policy(predictions, instances)
|
||||||
|
|
||||||
predictions = np.mean(predictions, axis=0)
|
predictions = np.mean(predictions, axis=0)
|
||||||
return F.normalize_prevalence(predictions)
|
return F.normalize_prevalence(predictions)
|
||||||
|
|
||||||
def set_params(self, **parameters):
|
def set_params(self, **parameters):
|
||||||
|
"""
|
||||||
|
This function should not be used within :class:`quapy.model_selection.GridSearchQ` (is here for compatibility
|
||||||
|
with the abstract class).
|
||||||
|
Instead, use `Ensemble(GridSearchQ(q),...)`, with `q` a Quantifier (recommended), or
|
||||||
|
`Ensemble(Q(GridSearchCV(l)))` with `Q` a quantifier class that has a learner `l` optimized for
|
||||||
|
classification (not recommended).
|
||||||
|
|
||||||
|
:param parameters: dictionary
|
||||||
|
:return: raises an Exception
|
||||||
|
"""
|
||||||
raise NotImplementedError(f'{self.__class__.__name__} should not be used within GridSearchQ; '
|
raise NotImplementedError(f'{self.__class__.__name__} should not be used within GridSearchQ; '
|
||||||
f'instead, use Ensemble(GridSearchQ(q),...), with q a Quantifier (recommended), '
|
f'instead, use Ensemble(GridSearchQ(q),...), with q a Quantifier (recommended), '
|
||||||
f'or Ensemble(Q(GridSearchCV(l))) with Q a quantifier class that has a learner '
|
f'or Ensemble(Q(GridSearchCV(l))) with Q a quantifier class that has a learner '
|
||||||
f'l optimized for classification (not recommended).')
|
f'l optimized for classification (not recommended).')
|
||||||
|
|
||||||
def get_params(self, deep=True):
|
def get_params(self, deep=True):
|
||||||
|
"""
|
||||||
|
This function should not be used within :class:`quapy.model_selection.GridSearchQ` (is here for compatibility
|
||||||
|
with the abstract class).
|
||||||
|
Instead, use `Ensemble(GridSearchQ(q),...)`, with `q` a Quantifier (recommended), or
|
||||||
|
`Ensemble(Q(GridSearchCV(l)))` with `Q` a quantifier class that has a learner `l` optimized for
|
||||||
|
classification (not recommended).
|
||||||
|
|
||||||
|
:return: raises an Exception
|
||||||
|
"""
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
def accuracy_policy(self, error_name):
|
def _accuracy_policy(self, error_name):
|
||||||
"""
|
"""
|
||||||
Selects the red_size best performant quantifiers in a static way (i.e., dropping all non-selected instances).
|
Selects the red_size best performant quantifiers in a static way (i.e., dropping all non-selected instances).
|
||||||
For each model in the ensemble, the performance is measured in terms of _error_name_ on the quantification of
|
For each model in the ensemble, the performance is measured in terms of _error_name_ on the quantification of
|
||||||
|
@ -141,7 +185,7 @@ class Ensemble(BaseQuantifier):
|
||||||
|
|
||||||
self.ensemble = _select_k(self.ensemble, order, k=self.red_size)
|
self.ensemble = _select_k(self.ensemble, order, k=self.red_size)
|
||||||
|
|
||||||
def ptr_policy(self, predictions):
|
def _ptr_policy(self, predictions):
|
||||||
"""
|
"""
|
||||||
Selects the predictions made by models that have been trained on samples with a prevalence that is most similar
|
Selects the predictions made by models that have been trained on samples with a prevalence that is most similar
|
||||||
to a first approximation of the test prevalence as made by all models in the ensemble.
|
to a first approximation of the test prevalence as made by all models in the ensemble.
|
||||||
|
@ -152,7 +196,7 @@ class Ensemble(BaseQuantifier):
|
||||||
order = np.argsort(ptr_differences)
|
order = np.argsort(ptr_differences)
|
||||||
return _select_k(predictions, order, k=self.red_size)
|
return _select_k(predictions, order, k=self.red_size)
|
||||||
|
|
||||||
def ds_policy_get_posteriors(self, data: LabelledCollection):
|
def _ds_policy_get_posteriors(self, data: LabelledCollection):
|
||||||
"""
|
"""
|
||||||
In the original article, this procedure is not described in a sufficient level of detail. The paper only says
|
In the original article, this procedure is not described in a sufficient level of detail. The paper only says
|
||||||
that the distribution of posterior probabilities from training and test examples is compared by means of the
|
that the distribution of posterior probabilities from training and test examples is compared by means of the
|
||||||
|
@ -182,7 +226,7 @@ class Ensemble(BaseQuantifier):
|
||||||
|
|
||||||
return posteriors, posteriors_generator
|
return posteriors, posteriors_generator
|
||||||
|
|
||||||
def ds_policy(self, predictions, test):
|
def _ds_policy(self, predictions, test):
|
||||||
test_posteriors = self.post_proba_fn(test)
|
test_posteriors = self.post_proba_fn(test)
|
||||||
test_distribution = get_probability_distribution(test_posteriors)
|
test_distribution = get_probability_distribution(test_posteriors)
|
||||||
tr_distributions = [m[2] for m in self.ensemble]
|
tr_distributions = [m[2] for m in self.ensemble]
|
||||||
|
@ -196,18 +240,40 @@ class Ensemble(BaseQuantifier):
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def binary(self):
|
def binary(self):
|
||||||
|
"""
|
||||||
|
Returns a boolean indicating whether the base quantifiers are binary or not
|
||||||
|
|
||||||
|
:return: boolean
|
||||||
|
"""
|
||||||
return self.base_quantifier.binary
|
return self.base_quantifier.binary
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def aggregative(self):
|
def aggregative(self):
|
||||||
|
"""
|
||||||
|
Indicates that the quantifier is not aggregative.
|
||||||
|
|
||||||
|
:return: False
|
||||||
|
"""
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def probabilistic(self):
|
def probabilistic(self):
|
||||||
|
"""
|
||||||
|
Indicates that the quantifier is not probabilistic.
|
||||||
|
|
||||||
|
:return: False
|
||||||
|
"""
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def get_probability_distribution(posterior_probabilities, bins=8):
|
def get_probability_distribution(posterior_probabilities, bins=8):
|
||||||
|
"""
|
||||||
|
Gets a histogram out of the posterior probabilities (only for the binary case).
|
||||||
|
|
||||||
|
:param posterior_probabilities: array-like of shape `(n_instances, 2,)`
|
||||||
|
:param bins: integer
|
||||||
|
:return: `np.ndarray` with the relative frequencies for each bin (for the positive class only)
|
||||||
|
"""
|
||||||
assert posterior_probabilities.shape[1] == 2, 'the posterior probabilities do not seem to be for a binary problem'
|
assert posterior_probabilities.shape[1] == 2, 'the posterior probabilities do not seem to be for a binary problem'
|
||||||
posterior_probabilities = posterior_probabilities[:, 1] # take the positive posteriors only
|
posterior_probabilities = posterior_probabilities[:, 1] # take the positive posteriors only
|
||||||
distribution, _ = np.histogram(posterior_probabilities, bins=bins, range=(0, 1), density=True)
|
distribution, _ = np.histogram(posterior_probabilities, bins=bins, range=(0, 1), density=True)
|
||||||
|
@ -306,6 +372,23 @@ def _check_error(error):
|
||||||
|
|
||||||
def ensembleFactory(learner, base_quantifier_class, param_grid=None, optim=None, param_model_sel: dict = None,
|
def ensembleFactory(learner, base_quantifier_class, param_grid=None, optim=None, param_model_sel: dict = None,
|
||||||
**kwargs):
|
**kwargs):
|
||||||
|
"""
|
||||||
|
Ensemble factory. Provides a unified interface for instantiating ensembles that can be optimized (via model
|
||||||
|
selection for quantification) for a given evaluation metric using :class:`quapy.model_selection.GridSearchQ`.
|
||||||
|
If the evaluation metric is classification-oriented
|
||||||
|
(instead of quantification-oriented), then the optimization will be carried out via sklearn's
|
||||||
|
`GridSearchCV <https://scikit-learn.org/stable/modules/generated/sklearn.model_selection.GridSearchCV.html>`_.
|
||||||
|
|
||||||
|
|
||||||
|
:param learner: sklearn's Estimator that generates a classifier
|
||||||
|
:param base_quantifier_class: a class of quantifiers
|
||||||
|
:param param_grid: a dictionary with the grid of parameters to optimize for
|
||||||
|
:param optim: a valid quantification or classification error, or a string name of it
|
||||||
|
:param param_model_sel: a dictionary containing any keyworded argument to pass to
|
||||||
|
:class:`quapy.model_selection.GridSearchQ`
|
||||||
|
:param kwargs: kwargs for the class :class:`Ensemble`
|
||||||
|
:return: an instance of :class:`Ensemble`
|
||||||
|
"""
|
||||||
if optim is not None:
|
if optim is not None:
|
||||||
if param_grid is None:
|
if param_grid is None:
|
||||||
raise ValueError(f'param_grid is None but optim was requested.')
|
raise ValueError(f'param_grid is None but optim was requested.')
|
||||||
|
@ -316,20 +399,83 @@ def ensembleFactory(learner, base_quantifier_class, param_grid=None, optim=None,
|
||||||
|
|
||||||
|
|
||||||
def ECC(learner, param_grid=None, optim=None, param_mod_sel=None, **kwargs):
|
def ECC(learner, param_grid=None, optim=None, param_mod_sel=None, **kwargs):
|
||||||
|
"""
|
||||||
|
Implements an ensemble of :class:`quapy.method.aggregative.CC` quantifiers, as used by
|
||||||
|
`Pérez-Gállego et al., 2019 <https://www.sciencedirect.com/science/article/pii/S1566253517303652>`_.
|
||||||
|
|
||||||
|
:param learner: sklearn's Estimator that generates a classifier
|
||||||
|
:param param_grid: a dictionary with the grid of parameters to optimize for
|
||||||
|
:param optim: a valid quantification or classification error, or a string name of it
|
||||||
|
:param param_model_sel: a dictionary containing any keyworded argument to pass to
|
||||||
|
:class:`quapy.model_selection.GridSearchQ`
|
||||||
|
:param kwargs: kwargs for the class :class:`Ensemble`
|
||||||
|
:return: an instance of :class:`Ensemble`
|
||||||
|
"""
|
||||||
|
|
||||||
return ensembleFactory(learner, CC, param_grid, optim, param_mod_sel, **kwargs)
|
return ensembleFactory(learner, CC, param_grid, optim, param_mod_sel, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
def EACC(learner, param_grid=None, optim=None, param_mod_sel=None, **kwargs):
|
def EACC(learner, param_grid=None, optim=None, param_mod_sel=None, **kwargs):
|
||||||
|
"""
|
||||||
|
Implements an ensemble of :class:`quapy.method.aggregative.ACC` quantifiers, as used by
|
||||||
|
`Pérez-Gállego et al., 2019 <https://www.sciencedirect.com/science/article/pii/S1566253517303652>`_.
|
||||||
|
|
||||||
|
:param learner: sklearn's Estimator that generates a classifier
|
||||||
|
:param param_grid: a dictionary with the grid of parameters to optimize for
|
||||||
|
:param optim: a valid quantification or classification error, or a string name of it
|
||||||
|
:param param_model_sel: a dictionary containing any keyworded argument to pass to
|
||||||
|
:class:`quapy.model_selection.GridSearchQ`
|
||||||
|
:param kwargs: kwargs for the class :class:`Ensemble`
|
||||||
|
:return: an instance of :class:`Ensemble`
|
||||||
|
"""
|
||||||
|
|
||||||
return ensembleFactory(learner, ACC, param_grid, optim, param_mod_sel, **kwargs)
|
return ensembleFactory(learner, ACC, param_grid, optim, param_mod_sel, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
def EPACC(learner, param_grid=None, optim=None, param_mod_sel=None, **kwargs):
|
def EPACC(learner, param_grid=None, optim=None, param_mod_sel=None, **kwargs):
|
||||||
|
"""
|
||||||
|
Implements an ensemble of :class:`quapy.method.aggregative.PACC` quantifiers.
|
||||||
|
|
||||||
|
:param learner: sklearn's Estimator that generates a classifier
|
||||||
|
:param param_grid: a dictionary with the grid of parameters to optimize for
|
||||||
|
:param optim: a valid quantification or classification error, or a string name of it
|
||||||
|
:param param_model_sel: a dictionary containing any keyworded argument to pass to
|
||||||
|
:class:`quapy.model_selection.GridSearchQ`
|
||||||
|
:param kwargs: kwargs for the class :class:`Ensemble`
|
||||||
|
:return: an instance of :class:`Ensemble`
|
||||||
|
"""
|
||||||
|
|
||||||
return ensembleFactory(learner, PACC, param_grid, optim, param_mod_sel, **kwargs)
|
return ensembleFactory(learner, PACC, param_grid, optim, param_mod_sel, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
def EHDy(learner, param_grid=None, optim=None, param_mod_sel=None, **kwargs):
|
def EHDy(learner, param_grid=None, optim=None, param_mod_sel=None, **kwargs):
|
||||||
|
"""
|
||||||
|
Implements an ensemble of :class:`quapy.method.aggregative.HDy` quantifiers, as used by
|
||||||
|
`Pérez-Gállego et al., 2019 <https://www.sciencedirect.com/science/article/pii/S1566253517303652>`_.
|
||||||
|
|
||||||
|
:param learner: sklearn's Estimator that generates a classifier
|
||||||
|
:param param_grid: a dictionary with the grid of parameters to optimize for
|
||||||
|
:param optim: a valid quantification or classification error, or a string name of it
|
||||||
|
:param param_model_sel: a dictionary containing any keyworded argument to pass to
|
||||||
|
:class:`quapy.model_selection.GridSearchQ`
|
||||||
|
:param kwargs: kwargs for the class :class:`Ensemble`
|
||||||
|
:return: an instance of :class:`Ensemble`
|
||||||
|
"""
|
||||||
|
|
||||||
return ensembleFactory(learner, HDy, param_grid, optim, param_mod_sel, **kwargs)
|
return ensembleFactory(learner, HDy, param_grid, optim, param_mod_sel, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
def EEMQ(learner, param_grid=None, optim=None, param_mod_sel=None, **kwargs):
|
def EEMQ(learner, param_grid=None, optim=None, param_mod_sel=None, **kwargs):
|
||||||
|
"""
|
||||||
|
Implements an ensemble of :class:`quapy.method.aggregative.EMQ` quantifiers.
|
||||||
|
|
||||||
|
:param learner: sklearn's Estimator that generates a classifier
|
||||||
|
:param param_grid: a dictionary with the grid of parameters to optimize for
|
||||||
|
:param optim: a valid quantification or classification error, or a string name of it
|
||||||
|
:param param_model_sel: a dictionary containing any keyworded argument to pass to
|
||||||
|
:class:`quapy.model_selection.GridSearchQ`
|
||||||
|
:param kwargs: kwargs for the class :class:`Ensemble`
|
||||||
|
:return: an instance of :class:`Ensemble`
|
||||||
|
"""
|
||||||
|
|
||||||
return ensembleFactory(learner, EMQ, param_grid, optim, param_mod_sel, **kwargs)
|
return ensembleFactory(learner, EMQ, param_grid, optim, param_mod_sel, **kwargs)
|
||||||
|
|
|
@ -62,9 +62,11 @@ class QuaNetTrainer(BaseQuantifier):
|
||||||
|
|
||||||
def fit(self, data: LabelledCollection, fit_learner=True):
|
def fit(self, data: LabelledCollection, fit_learner=True):
|
||||||
"""
|
"""
|
||||||
|
Trains QuaNet.
|
||||||
|
|
||||||
:param data: the training data on which to train QuaNet. If fit_learner=True, the data will be split in
|
:param data: the training data on which to train QuaNet. If fit_learner=True, the data will be split in
|
||||||
40/40/20 for training the classifier, training QuaNet, and validating QuaNet, respectively. If
|
40/40/20 for training the classifier, training QuaNet, and validating QuaNet, respectively. If
|
||||||
fit_learner=False, the data will be split in 66/34 for training QuaNet and validating it, respectively.
|
fit_learner=False, the data will be split in 66/34 for training QuaNet and validating it, respectively.
|
||||||
:param fit_learner: if true, trains the classifier on a split containing 40% of the data
|
:param fit_learner: if true, trains the classifier on a split containing 40% of the data
|
||||||
:return: self
|
:return: self
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -3,24 +3,60 @@ from .base import BaseQuantifier
|
||||||
|
|
||||||
|
|
||||||
class MaximumLikelihoodPrevalenceEstimation(BaseQuantifier):
|
class MaximumLikelihoodPrevalenceEstimation(BaseQuantifier):
|
||||||
|
"""
|
||||||
|
The `Maximum Likelihood Prevalence Estimation` (MLPE) method is a lazy method that assumes there is no prior
|
||||||
|
probability shift between training and test instances (put it other way, that the i.i.d. assumpion holds).
|
||||||
|
The estimation of class prevalence values for any test sample is always (i.e., irrespective of the test sample
|
||||||
|
itself) the class prevalence seen during training. This method is considered to be a lower-bound quantifier that
|
||||||
|
any quantification method should beat.
|
||||||
|
"""
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
def __init__(self):
|
||||||
self._classes_ = None
|
self._classes_ = None
|
||||||
|
|
||||||
def fit(self, data: LabelledCollection, *args):
|
def fit(self, data: LabelledCollection):
|
||||||
|
"""
|
||||||
|
Computes the training prevalence and stores it.
|
||||||
|
|
||||||
|
:param data: the training sample
|
||||||
|
:return: self
|
||||||
|
"""
|
||||||
self._classes_ = data.classes_
|
self._classes_ = data.classes_
|
||||||
self.estimated_prevalence = data.prevalence()
|
self.estimated_prevalence = data.prevalence()
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def quantify(self, documents, *args):
|
def quantify(self, instances):
|
||||||
|
"""
|
||||||
|
Ignores the input instances and returns, as the class prevalence estimantes, the training prevalence.
|
||||||
|
|
||||||
|
:param instances: array-like (ignored)
|
||||||
|
:return: the class prevalence seen during training
|
||||||
|
"""
|
||||||
return self.estimated_prevalence
|
return self.estimated_prevalence
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def classes_(self):
|
def classes_(self):
|
||||||
|
"""
|
||||||
|
Number of classes
|
||||||
|
|
||||||
|
:return: integer
|
||||||
|
"""
|
||||||
|
|
||||||
return self._classes_
|
return self._classes_
|
||||||
|
|
||||||
def get_params(self):
|
def get_params(self, deep=True):
|
||||||
pass
|
"""
|
||||||
|
Does nothing, since this learner has no parameters.
|
||||||
|
|
||||||
|
:param deep: for compatibility with sklearn
|
||||||
|
:return: `None`
|
||||||
|
"""
|
||||||
|
return None
|
||||||
|
|
||||||
def set_params(self, **parameters):
|
def set_params(self, **parameters):
|
||||||
|
"""
|
||||||
|
Does nothing, since this learner has no parameters.
|
||||||
|
|
||||||
|
:param parameters: dictionary of param-value pairs (ignored)
|
||||||
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
Loading…
Reference in New Issue