From 4a90004447d4bcfb480159668c57e7d641b24001 Mon Sep 17 00:00:00 2001 From: Nikita Date: Fri, 23 Oct 2020 19:45:22 +0300 Subject: [PATCH 01/28] add multiclass preprocessing function to functional.py --- catalyst/metrics/functional.py | 57 ++++++++++++++++++++++++++++++++-- 1 file changed, 55 insertions(+), 2 deletions(-) diff --git a/catalyst/metrics/functional.py b/catalyst/metrics/functional.py index 41283e9838..7ba4bbe37b 100644 --- a/catalyst/metrics/functional.py +++ b/catalyst/metrics/functional.py @@ -1,6 +1,8 @@ from typing import Callable, Dict, Optional, Sequence, Tuple from functools import partial +import numpy as np + import torch from torch import Tensor from torch.nn import functional as F @@ -11,10 +13,61 @@ # as a baseline -def process_multilabel_components( +def map_labels_to_classes( + outputs: torch.Tensor, targets: torch.Tensor, +) -> Tuple[torch.Tensor, torch.Tensor]: + label_to_class = {} + keys = torch.sort(torch.unique(targets)) + values = torch.arange(len(torch.unique(targets))) + for k, v in zip(keys, values): + label_to_class[k] = v + + list_outputs = [label_to_class[label] for label in outputs.flatten()] + list_targets = [label_to_class[label] for label in targets.flatten()] + tensor_outputs = torch.from_numpy(np.array(list_outputs)).view(-1, 1) + tensor_targets = torch.from_numpy(np.array(list_targets)).view(-1, 1) + + return tensor_outputs, tensor_targets + + +def process_multiclass_components( outputs: torch.Tensor, targets: torch.Tensor, - weights: Optional[torch.Tensor] = None, + raise_class_labels_mismatch: Optional[bool] = False, +) -> Tuple[torch.Tensor, torch.Tensor]: + if not torch.is_tensor(outputs): + outputs = torch.from_numpy(outputs) + if not torch.is_tensor(targets): + targets = torch.from_numpy(targets) + + if outputs.dim() == 1: + outputs = outputs.view(-1, 1) + else: + assert outputs.size(1) == 1 and outputs.dim() == 2, ( + "Wrong `outputs` shape, " + "expected 1D or 2D with size 1 in the second dim" + ) + + if targets.dim() == 1: + targets = targets.view(-1, 1) + else: + assert targets.size(1) == 1 and targets.dim() == 2, ( + "Wrong `outputs` shape, " + "expected 1D or 2D with size 1 in the second dim" + ) + + if targets.max() != len(torch.unique(targets)) - 1: + if raise_class_labels_mismatch: + raise Exception( + "`targets` maximum does not represent number of classes" + ) + # mapping classes + outputs, targets = map_labels_to_classes(outputs, targets) + return outputs, targets + + +def process_multilabel_components( + outputs: torch.Tensor, targets: torch.Tensor, weights: torch.Tensor, ) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor]: """General preprocessing for multi-label-based metrics. From e839626c28d8dc6030ce71f3619e934b36fb6627 Mon Sep 17 00:00:00 2001 From: Nikita Date: Fri, 23 Oct 2020 20:22:51 +0300 Subject: [PATCH 02/28] remove preprocesssing --- catalyst/metrics/functional.py | 88 +++++++++++++++++----------------- 1 file changed, 45 insertions(+), 43 deletions(-) diff --git a/catalyst/metrics/functional.py b/catalyst/metrics/functional.py index 7ba4bbe37b..ad48b524d1 100644 --- a/catalyst/metrics/functional.py +++ b/catalyst/metrics/functional.py @@ -13,33 +13,51 @@ # as a baseline -def map_labels_to_classes( - outputs: torch.Tensor, targets: torch.Tensor, -) -> Tuple[torch.Tensor, torch.Tensor]: - label_to_class = {} - keys = torch.sort(torch.unique(targets)) - values = torch.arange(len(torch.unique(targets))) - for k, v in zip(keys, values): - label_to_class[k] = v +def process_multiclass_components( + outputs: torch.Tensor, + targets: torch.Tensor, + argmax_dim: int = -1, + num_classes: Optional[int] = None, +) -> Tuple[torch.Tensor, torch.Tensor, int]: + """ + Preprocess input in case multiclass classification task. - list_outputs = [label_to_class[label] for label in outputs.flatten()] - list_targets = [label_to_class[label] for label in targets.flatten()] - tensor_outputs = torch.from_numpy(np.array(list_outputs)).view(-1, 1) - tensor_targets = torch.from_numpy(np.array(list_targets)).view(-1, 1) + Args: + outputs: estimated targets as predicted by a model + with shape [bs; ..., (num_classes or 1)] + targets: ground truth (correct) target values + with shape [bs; ..., 1] + argmax_dim: int, that specifies dimension for argmax transformation + in case of scores/probabilities in ``outputs`` + num_classes: int, that specifies number of classes if it known - return tensor_outputs, tensor_targets + Returns: + preprocessed outputs, targets and num_classes + """ -def process_multiclass_components( - outputs: torch.Tensor, - targets: torch.Tensor, - raise_class_labels_mismatch: Optional[bool] = False, -) -> Tuple[torch.Tensor, torch.Tensor]: + # @TODO: better multiclass preprocessing, label -> class_id mapping if not torch.is_tensor(outputs): outputs = torch.from_numpy(outputs) if not torch.is_tensor(targets): targets = torch.from_numpy(targets) + # @TODO: move to process_multiclass_components ? + if outputs.dim() == targets.dim() + 1: + # looks like we have scores/probabilities in our outputs + # let's convert them to final model predictions + num_classes = max( + outputs.shape[argmax_dim], int(targets.max().detach().item() + 1) + ) + outputs = torch.argmax(outputs, dim=argmax_dim) + if num_classes is None: + # as far as we expect the outputs/targets tensors to be int64 + # we could find number of classes as max available number + num_classes = max( + int(outputs.max().detach().item() + 1), + int(targets.max().detach().item() + 1), + ) + if outputs.dim() == 1: outputs = outputs.view(-1, 1) else: @@ -56,14 +74,7 @@ def process_multiclass_components( "expected 1D or 2D with size 1 in the second dim" ) - if targets.max() != len(torch.unique(targets)) - 1: - if raise_class_labels_mismatch: - raise Exception( - "`targets` maximum does not represent number of classes" - ) - # mapping classes - outputs, targets = map_labels_to_classes(outputs, targets) - return outputs, targets + return outputs, targets, num_classes def process_multilabel_components( @@ -180,8 +191,8 @@ def get_multiclass_statistics( with shape [bs; ..., (num_classes or 1)] targets: ground truth (correct) target values with shape [bs; ..., 1] - argmax_dim: int, that specifies dimention for argmax transformation - in case of scores/probabilites in ``outputs`` + argmax_dim: int, that specifies dimension for argmax transformation + in case of scores/probabilities in ``outputs`` num_classes: int, that specifies number of classes if it known Returns: @@ -196,21 +207,12 @@ def get_multiclass_statistics( tensor([0., 0., 0., 1., 1.]), tensor([1., 1., 0., 0., 0.]), tensor([1., 1., 0., 1., 1.]) """ - # @TODO: move to process_multiclass_components ? - if outputs.dim() == targets.dim() + 1: - # looks like we have scores/probabilities in our outputs - # let's convert them to final model predictions - num_classes = max( - outputs.shape[argmax_dim], int(targets.max().detach().item() + 1) - ) - outputs = torch.argmax(outputs, dim=argmax_dim) - if num_classes is None: - # as far as we expect the outputs/targets tensors to be int64 - # we could find number of classes as max available number - num_classes = max( - int(outputs.max().detach().item() + 1), - int(targets.max().detach().item() + 1), - ) + outputs, targets, num_classes = process_multiclass_components( + outputs=outputs, + targets=targets, + argmax_dim=argmax_dim, + num_classes=num_classes + ) tn = torch.zeros((num_classes,), device=outputs.device) fp = torch.zeros((num_classes,), device=outputs.device) From ce9f20fbefeb20dda52e98a3d2bf7a535047ff8d Mon Sep 17 00:00:00 2001 From: Nikita Date: Fri, 23 Oct 2020 20:27:55 +0300 Subject: [PATCH 03/28] remove np --- catalyst/metrics/functional.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/catalyst/metrics/functional.py b/catalyst/metrics/functional.py index ad48b524d1..7a61836813 100644 --- a/catalyst/metrics/functional.py +++ b/catalyst/metrics/functional.py @@ -1,8 +1,6 @@ from typing import Callable, Dict, Optional, Sequence, Tuple from functools import partial -import numpy as np - import torch from torch import Tensor from torch.nn import functional as F From a36126f41401cb689adf7462769d3f9b4ab51b23 Mon Sep 17 00:00:00 2001 From: Nikita Date: Fri, 23 Oct 2020 21:12:42 +0300 Subject: [PATCH 04/28] add precision / recall --- catalyst/metrics/functional.py | 2 +- catalyst/metrics/precision.py | 35 +++++++++++++++++++++++++++++++++- catalyst/metrics/recall.py | 35 ++++++++++++++++++++++++++++++++++ 3 files changed, 70 insertions(+), 2 deletions(-) create mode 100644 catalyst/metrics/recall.py diff --git a/catalyst/metrics/functional.py b/catalyst/metrics/functional.py index 7a61836813..630bb0f82a 100644 --- a/catalyst/metrics/functional.py +++ b/catalyst/metrics/functional.py @@ -209,7 +209,7 @@ def get_multiclass_statistics( outputs=outputs, targets=targets, argmax_dim=argmax_dim, - num_classes=num_classes + num_classes=num_classes, ) tn = torch.zeros((num_classes,), device=outputs.device) diff --git a/catalyst/metrics/precision.py b/catalyst/metrics/precision.py index 45e47c6a20..7d54b31889 100644 --- a/catalyst/metrics/precision.py +++ b/catalyst/metrics/precision.py @@ -2,7 +2,10 @@ import torch -from catalyst.metrics.functional import process_multilabel_components +from catalyst.metrics.functional import ( + get_multiclass_statistics, + process_multilabel_components, +) def average_precision( @@ -66,4 +69,34 @@ def average_precision( return ap +def precision( + outputs: torch.Tensor, + targets: torch.Tensor, + argmax_dim: int = -1, + num_classes: Optional[int] = None, +) -> torch.Tensor: + """ + Multiclass precision metric. + + Args: + outputs: estimated targets as predicted by a model + with shape [bs; ..., (num_classes or 1)] + targets: ground truth (correct) target values + with shape [bs; ..., 1] + argmax_dim: int, that specifies dimension for argmax transformation + in case of scores/probabilities in ``outputs`` + num_classes: int, that specifies number of classes if it known + + Returns: + Tensor: + """ + _, fp, _, tp, _ = get_multiclass_statistics( + outputs=outputs, + targets=targets, + argmax_dim=argmax_dim, + num_classes=num_classes, + ) + return tp / (fp + tp) + + __all__ = ["average_precision"] diff --git a/catalyst/metrics/recall.py b/catalyst/metrics/recall.py new file mode 100644 index 0000000000..2a7eaef146 --- /dev/null +++ b/catalyst/metrics/recall.py @@ -0,0 +1,35 @@ +from typing import Optional + +import torch + +from catalyst.metrics.functional import get_multiclass_statistics + + +def recall( + outputs: torch.Tensor, + targets: torch.Tensor, + argmax_dim: int = -1, + num_classes: Optional[int] = None, +) -> torch.Tensor: + """ + Multiclass precision metric. + + Args: + outputs: estimated targets as predicted by a model + with shape [bs; ..., (num_classes or 1)] + targets: ground truth (correct) target values + with shape [bs; ..., 1] + argmax_dim: int, that specifies dimension for argmax transformation + in case of scores/probabilities in ``outputs`` + num_classes: int, that specifies number of classes if it known + + Returns: + Tensor: recall for every class + """ + _, _, fn, tp, _ = get_multiclass_statistics( + outputs=outputs, + targets=targets, + argmax_dim=argmax_dim, + num_classes=num_classes, + ) + return tp / (fn + tp) From 5dd74722bda71656f9404a4a71a62d55e0b88d05 Mon Sep 17 00:00:00 2001 From: Nikita Date: Fri, 23 Oct 2020 21:25:20 +0300 Subject: [PATCH 05/28] fix --- catalyst/metrics/functional.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/catalyst/metrics/functional.py b/catalyst/metrics/functional.py index 630bb0f82a..c779c1fea5 100644 --- a/catalyst/metrics/functional.py +++ b/catalyst/metrics/functional.py @@ -31,9 +31,7 @@ def process_multiclass_components( Returns: preprocessed outputs, targets and num_classes - """ - # @TODO: better multiclass preprocessing, label -> class_id mapping if not torch.is_tensor(outputs): outputs = torch.from_numpy(outputs) From 60e1a90602b0607824bad6e051177324bda27aae Mon Sep 17 00:00:00 2001 From: Nikita Date: Sun, 25 Oct 2020 19:12:06 +0300 Subject: [PATCH 06/28] add fbeta and f1 --- catalyst/metrics/f1_score.py | 76 ++++++++++++++++++++++++------------ 1 file changed, 51 insertions(+), 25 deletions(-) diff --git a/catalyst/metrics/f1_score.py b/catalyst/metrics/f1_score.py index 3b22f78767..d3ada2d848 100644 --- a/catalyst/metrics/f1_score.py +++ b/catalyst/metrics/f1_score.py @@ -1,53 +1,79 @@ """ F1 score. """ +from typing import Optional + import torch -from catalyst.utils.torch import get_activation_fn +from catalyst.metrics.functional import get_multiclass_statistics -def f1_score( +def fbeta_score( outputs: torch.Tensor, targets: torch.Tensor, beta: float = 1.0, eps: float = 1e-7, - threshold: float = None, - activation: str = "Sigmoid", -): + argmax_dim: int = -1, + num_classes: Optional[int] = None, +) -> torch.Tensor: """ Args: outputs: A list of predicted elements targets: A list of elements that are to be predicted - eps: epsilon to avoid zero division beta: beta param for f_score - threshold: threshold for outputs binarization - activation: An torch.nn activation applied to the outputs. - Must be one of ["none", "Sigmoid", "Softmax2d"] + eps: epsilon to avoid zero division + argmax_dim: int, that specifies dimension for argmax transformation + in case of scores/probabilities in ``outputs`` + num_classes: int, that specifies number of classes if it known Returns: float: F_1 score - """ - activation_fn = get_activation_fn(activation) + """ + if beta < 0: + raise Exception("beta parameter should be non-negative") - outputs = activation_fn(outputs) + _, fp, fn, tp, _ = get_multiclass_statistics( + outputs=outputs, + targets=targets, + argmax_dim=argmax_dim, + num_classes=num_classes, + ) - if threshold is not None: - outputs = (outputs > threshold).float() + precision_plus_recall = (1 + beta ** 2) * tp + beta ** 2 * fn + fp + eps - true_positive = torch.sum(targets * outputs) - false_positive = torch.sum(outputs) - true_positive - false_negative = torch.sum(targets) - true_positive + score = ((1 + beta ** 2) * tp + eps) / precision_plus_recall + return score - precision_plus_recall = ( - (1 + beta ** 2) * true_positive - + beta ** 2 * false_negative - + false_positive - + eps - ) - score = ((1 + beta ** 2) * true_positive + eps) / precision_plus_recall +def f1_score( + outputs: torch.Tensor, + targets: torch.Tensor, + eps: float = 1e-7, + argmax_dim: int = -1, + num_classes: Optional[int] = None, +) -> float: + """ + Args: + outputs: A list of predicted elements + targets: A list of elements that are to be predicted + eps: epsilon to avoid zero division + argmax_dim: int, that specifies dimension for argmax transformation + in case of scores/probabilities in ``outputs`` + num_classes: int, that specifies number of classes if it known + + Returns: + float: F_1 score + """ + score = fbeta_score( + outputs=outputs, + targets=targets, + beta=1, + eps=eps, + argmax_dim=argmax_dim, + num_classes=num_classes, + ) return score -__all__ = ["f1_score"] +__all__ = ["f1_score", "fbeta_score"] From 73d0babb17ed2afb2f6796726c72e66a748878b6 Mon Sep 17 00:00:00 2001 From: Nikita Date: Sun, 25 Oct 2020 22:11:08 +0300 Subject: [PATCH 07/28] fix --- catalyst/metrics/functional.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/catalyst/metrics/functional.py b/catalyst/metrics/functional.py index c779c1fea5..96e7e24c65 100644 --- a/catalyst/metrics/functional.py +++ b/catalyst/metrics/functional.py @@ -74,7 +74,9 @@ def process_multiclass_components( def process_multilabel_components( - outputs: torch.Tensor, targets: torch.Tensor, weights: torch.Tensor, + outputs: torch.Tensor, + targets: torch.Tensor, + weights: Optional[torch.Tensor] = None, ) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor]: """General preprocessing for multi-label-based metrics. From 8c96a9cf98447cc894051cae2c8807dca986b74a Mon Sep 17 00:00:00 2001 From: Nikita Date: Sun, 25 Oct 2020 22:26:19 +0300 Subject: [PATCH 08/28] docs fix --- catalyst/metrics/f1_score.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/catalyst/metrics/f1_score.py b/catalyst/metrics/f1_score.py index d3ada2d848..7efe1633a5 100644 --- a/catalyst/metrics/f1_score.py +++ b/catalyst/metrics/f1_score.py @@ -17,6 +17,8 @@ def fbeta_score( num_classes: Optional[int] = None, ) -> torch.Tensor: """ + Counts fbeta score for given ``outputs`` and ``targets``. + Args: outputs: A list of predicted elements targets: A list of elements that are to be predicted @@ -26,8 +28,11 @@ def fbeta_score( in case of scores/probabilities in ``outputs`` num_classes: int, that specifies number of classes if it known + Raises: + Exception: If ``beta`` is a negative number. + Returns: - float: F_1 score + float: F_1 score. """ if beta < 0: raise Exception("beta parameter should be non-negative") From 237c446911a9e917f31c048f69d2f579962136cb Mon Sep 17 00:00:00 2001 From: Nikita Date: Sun, 25 Oct 2020 22:41:42 +0300 Subject: [PATCH 09/28] fix --- catalyst/metrics/f1_score.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/catalyst/metrics/f1_score.py b/catalyst/metrics/f1_score.py index 7efe1633a5..11b7bd56cb 100644 --- a/catalyst/metrics/f1_score.py +++ b/catalyst/metrics/f1_score.py @@ -15,7 +15,7 @@ def fbeta_score( eps: float = 1e-7, argmax_dim: int = -1, num_classes: Optional[int] = None, -) -> torch.Tensor: +) -> float: """ Counts fbeta score for given ``outputs`` and ``targets``. From 07d7401aed3420def0d836d06bc3feeab87d953c Mon Sep 17 00:00:00 2001 From: Nikita Date: Sun, 25 Oct 2020 23:33:54 +0300 Subject: [PATCH 10/28] add test for binary class --- catalyst/metrics/__init__.py | 4 +++- catalyst/metrics/functional.py | 21 +++++++++++++++---- catalyst/metrics/precision.py | 6 ++++-- catalyst/metrics/recall.py | 6 +++++- .../tests/test_fbeta_precision_recall.py | 10 +++++++++ 5 files changed, 39 insertions(+), 8 deletions(-) create mode 100644 catalyst/metrics/tests/test_fbeta_precision_recall.py diff --git a/catalyst/metrics/__init__.py b/catalyst/metrics/__init__.py index 37cea48815..5d617e59f6 100644 --- a/catalyst/metrics/__init__.py +++ b/catalyst/metrics/__init__.py @@ -3,7 +3,9 @@ from catalyst.metrics.auc import auc from catalyst.metrics.cmc_score import cmc_score, cmc_score_count from catalyst.metrics.dice import dice, calculate_dice -from catalyst.metrics.f1_score import f1_score +from catalyst.metrics.f1_score import f1_score, fbeta_score +from catalyst.metrics.precision import precision +from catalyst.metrics.recall import recall from catalyst.metrics.focal import sigmoid_focal_loss, reduced_focal_loss from catalyst.metrics.functional import ( process_multilabel_components, diff --git a/catalyst/metrics/functional.py b/catalyst/metrics/functional.py index 96e7e24c65..6c6d5e44b7 100644 --- a/catalyst/metrics/functional.py +++ b/catalyst/metrics/functional.py @@ -1,3 +1,5 @@ +import numpy as np + from typing import Callable, Dict, Optional, Sequence, Tuple from functools import partial @@ -34,11 +36,10 @@ def process_multiclass_components( """ # @TODO: better multiclass preprocessing, label -> class_id mapping if not torch.is_tensor(outputs): - outputs = torch.from_numpy(outputs) + outputs = torch.from_numpy(np.array(outputs)) if not torch.is_tensor(targets): - targets = torch.from_numpy(targets) + targets = torch.from_numpy(np.array(targets)) - # @TODO: move to process_multiclass_components ? if outputs.dim() == targets.dim() + 1: # looks like we have scores/probabilities in our outputs # let's convert them to final model predictions @@ -56,14 +57,21 @@ def process_multiclass_components( if outputs.dim() == 1: outputs = outputs.view(-1, 1) + elif outputs.dim() == 2 and outputs.size(0) == 1: + # transpose case + outputs.permute(1, 0) else: assert outputs.size(1) == 1 and outputs.dim() == 2, ( "Wrong `outputs` shape, " - "expected 1D or 2D with size 1 in the second dim" + "expected 1D or 2D with size 1 in the second dim " + "got {}".format(outputs.shape) ) if targets.dim() == 1: targets = targets.view(-1, 1) + elif targets.dim() == 2 and targets.size(0) == 1: + # transpose case + targets.permute(1, 0) else: assert targets.size(1) == 1 and targets.dim() == 2, ( "Wrong `outputs` shape, " @@ -211,6 +219,11 @@ def get_multiclass_statistics( argmax_dim=argmax_dim, num_classes=num_classes, ) + if num_classes <= 2: + return get_binary_statistics( + outputs=outputs, + targets=targets, + ) tn = torch.zeros((num_classes,), device=outputs.device) fp = torch.zeros((num_classes,), device=outputs.device) diff --git a/catalyst/metrics/precision.py b/catalyst/metrics/precision.py index 7d54b31889..d48dce6cf8 100644 --- a/catalyst/metrics/precision.py +++ b/catalyst/metrics/precision.py @@ -73,6 +73,7 @@ def precision( outputs: torch.Tensor, targets: torch.Tensor, argmax_dim: int = -1, + eps: float = 1e-7, num_classes: Optional[int] = None, ) -> torch.Tensor: """ @@ -85,6 +86,7 @@ def precision( with shape [bs; ..., 1] argmax_dim: int, that specifies dimension for argmax transformation in case of scores/probabilities in ``outputs`` + eps: float. Epsilon to avoid zero division. num_classes: int, that specifies number of classes if it known Returns: @@ -96,7 +98,7 @@ def precision( argmax_dim=argmax_dim, num_classes=num_classes, ) - return tp / (fp + tp) + return (tp + eps) / (fp + tp + eps) -__all__ = ["average_precision"] +__all__ = ["average_precision", "precision"] diff --git a/catalyst/metrics/recall.py b/catalyst/metrics/recall.py index 2a7eaef146..047f8b1fcc 100644 --- a/catalyst/metrics/recall.py +++ b/catalyst/metrics/recall.py @@ -9,7 +9,9 @@ def recall( outputs: torch.Tensor, targets: torch.Tensor, argmax_dim: int = -1, + eps: float = 1e-7, num_classes: Optional[int] = None, + ) -> torch.Tensor: """ Multiclass precision metric. @@ -21,6 +23,7 @@ def recall( with shape [bs; ..., 1] argmax_dim: int, that specifies dimension for argmax transformation in case of scores/probabilities in ``outputs`` + eps: float. Epsilon to avoid zero division. num_classes: int, that specifies number of classes if it known Returns: @@ -32,4 +35,5 @@ def recall( argmax_dim=argmax_dim, num_classes=num_classes, ) - return tp / (fn + tp) + + return (tp + eps)/(fn + tp + eps) diff --git a/catalyst/metrics/tests/test_fbeta_precision_recall.py b/catalyst/metrics/tests/test_fbeta_precision_recall.py new file mode 100644 index 0000000000..588896f53b --- /dev/null +++ b/catalyst/metrics/tests/test_fbeta_precision_recall.py @@ -0,0 +1,10 @@ +import pytest +from catalyst.metrics import f1_score, fbeta_score, precision, recall + + +def test_precision_recall_f_binary_single_class() -> None: + # Test precision, recall and F-scores behave with a single positive + assert 1. == precision([1, 1], [1, 1]) + assert 1. == recall([1, 1], [1, 1]) + assert 1. == f1_score([1, 1], [1, 1]) + assert 1. == fbeta_score([1, 1], [1, 1], 0) \ No newline at end of file From 32ecc293d9abb9abc0ae5ad80af9b74ef63862db Mon Sep 17 00:00:00 2001 From: Nikita Date: Sun, 25 Oct 2020 23:46:09 +0300 Subject: [PATCH 11/28] fix codestyle --- catalyst/metrics/functional.py | 9 +++------ catalyst/metrics/recall.py | 3 +-- catalyst/metrics/tests/test_fbeta_precision_recall.py | 9 +++++---- 3 files changed, 9 insertions(+), 12 deletions(-) diff --git a/catalyst/metrics/functional.py b/catalyst/metrics/functional.py index 6c6d5e44b7..5d1467d30d 100644 --- a/catalyst/metrics/functional.py +++ b/catalyst/metrics/functional.py @@ -1,8 +1,8 @@ -import numpy as np - from typing import Callable, Dict, Optional, Sequence, Tuple from functools import partial +import numpy as np + import torch from torch import Tensor from torch.nn import functional as F @@ -220,10 +220,7 @@ def get_multiclass_statistics( num_classes=num_classes, ) if num_classes <= 2: - return get_binary_statistics( - outputs=outputs, - targets=targets, - ) + return get_binary_statistics(outputs=outputs, targets=targets,) tn = torch.zeros((num_classes,), device=outputs.device) fp = torch.zeros((num_classes,), device=outputs.device) diff --git a/catalyst/metrics/recall.py b/catalyst/metrics/recall.py index 047f8b1fcc..45e5b30ce9 100644 --- a/catalyst/metrics/recall.py +++ b/catalyst/metrics/recall.py @@ -11,7 +11,6 @@ def recall( argmax_dim: int = -1, eps: float = 1e-7, num_classes: Optional[int] = None, - ) -> torch.Tensor: """ Multiclass precision metric. @@ -36,4 +35,4 @@ def recall( num_classes=num_classes, ) - return (tp + eps)/(fn + tp + eps) + return (tp + eps) / (fn + tp + eps) diff --git a/catalyst/metrics/tests/test_fbeta_precision_recall.py b/catalyst/metrics/tests/test_fbeta_precision_recall.py index 588896f53b..4bfd4720e6 100644 --- a/catalyst/metrics/tests/test_fbeta_precision_recall.py +++ b/catalyst/metrics/tests/test_fbeta_precision_recall.py @@ -1,10 +1,11 @@ import pytest + from catalyst.metrics import f1_score, fbeta_score, precision, recall def test_precision_recall_f_binary_single_class() -> None: # Test precision, recall and F-scores behave with a single positive - assert 1. == precision([1, 1], [1, 1]) - assert 1. == recall([1, 1], [1, 1]) - assert 1. == f1_score([1, 1], [1, 1]) - assert 1. == fbeta_score([1, 1], [1, 1], 0) \ No newline at end of file + assert 1.0 == precision([1, 1], [1, 1]) + assert 1.0 == recall([1, 1], [1, 1]) + assert 1.0 == f1_score([1, 1], [1, 1]) + assert 1.0 == fbeta_score([1, 1], [1, 1], 0) From 37628c3badaf4bccec0d044051bcb15483ae4bdc Mon Sep 17 00:00:00 2001 From: Nikita Date: Sun, 1 Nov 2020 18:02:17 +0300 Subject: [PATCH 12/28] add tests --- catalyst/metrics/f1_score.py | 6 +++--- catalyst/metrics/tests/test_fbeta_precision_recall.py | 7 ++++++- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/catalyst/metrics/f1_score.py b/catalyst/metrics/f1_score.py index 11b7bd56cb..9f7b6f84f7 100644 --- a/catalyst/metrics/f1_score.py +++ b/catalyst/metrics/f1_score.py @@ -1,7 +1,7 @@ """ F1 score. """ -from typing import Optional +from typing import Optional, Union import torch @@ -15,7 +15,7 @@ def fbeta_score( eps: float = 1e-7, argmax_dim: int = -1, num_classes: Optional[int] = None, -) -> float: +) -> Union[float, torch.Tensor]: """ Counts fbeta score for given ``outputs`` and ``targets``. @@ -56,7 +56,7 @@ def f1_score( eps: float = 1e-7, argmax_dim: int = -1, num_classes: Optional[int] = None, -) -> float: +) -> Union[float, torch.Tensor]: """ Args: outputs: A list of predicted elements diff --git a/catalyst/metrics/tests/test_fbeta_precision_recall.py b/catalyst/metrics/tests/test_fbeta_precision_recall.py index 4bfd4720e6..e6b05ab23a 100644 --- a/catalyst/metrics/tests/test_fbeta_precision_recall.py +++ b/catalyst/metrics/tests/test_fbeta_precision_recall.py @@ -1,11 +1,16 @@ -import pytest +import pytest # noqa: F401 from catalyst.metrics import f1_score, fbeta_score, precision, recall def test_precision_recall_f_binary_single_class() -> None: + """Metrics test""" # Test precision, recall and F-scores behave with a single positive assert 1.0 == precision([1, 1], [1, 1]) assert 1.0 == recall([1, 1], [1, 1]) assert 1.0 == f1_score([1, 1], [1, 1]) assert 1.0 == fbeta_score([1, 1], [1, 1], 0) + # test with several classes + assert 3. == f1_score([0, 1, 2], [0, 1, 2]).sum().item() + assert 3. == precision([0, 1, 2], [0, 1, 2]).sum().item() + assert 3. == recall([0, 1, 2], [0, 1, 2]).sum().item() From c2c3b1c6b54c51027fcefa8d92d72753d1f87e95 Mon Sep 17 00:00:00 2001 From: Nikita Date: Sun, 1 Nov 2020 18:03:28 +0300 Subject: [PATCH 13/28] add tests --- catalyst/metrics/precision.py | 4 ++-- catalyst/metrics/recall.py | 4 ++-- catalyst/metrics/tests/test_fbeta_precision_recall.py | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/catalyst/metrics/precision.py b/catalyst/metrics/precision.py index d48dce6cf8..a588a4861f 100644 --- a/catalyst/metrics/precision.py +++ b/catalyst/metrics/precision.py @@ -1,4 +1,4 @@ -from typing import Optional +from typing import Optional, Union import torch @@ -75,7 +75,7 @@ def precision( argmax_dim: int = -1, eps: float = 1e-7, num_classes: Optional[int] = None, -) -> torch.Tensor: +) -> Union[float, torch.Tensor]: """ Multiclass precision metric. diff --git a/catalyst/metrics/recall.py b/catalyst/metrics/recall.py index 45e5b30ce9..3a309a05e7 100644 --- a/catalyst/metrics/recall.py +++ b/catalyst/metrics/recall.py @@ -1,4 +1,4 @@ -from typing import Optional +from typing import Optional, Union import torch @@ -11,7 +11,7 @@ def recall( argmax_dim: int = -1, eps: float = 1e-7, num_classes: Optional[int] = None, -) -> torch.Tensor: +) -> Union[float, torch.Tensor]: """ Multiclass precision metric. diff --git a/catalyst/metrics/tests/test_fbeta_precision_recall.py b/catalyst/metrics/tests/test_fbeta_precision_recall.py index e6b05ab23a..2c01ee2ef5 100644 --- a/catalyst/metrics/tests/test_fbeta_precision_recall.py +++ b/catalyst/metrics/tests/test_fbeta_precision_recall.py @@ -11,6 +11,6 @@ def test_precision_recall_f_binary_single_class() -> None: assert 1.0 == f1_score([1, 1], [1, 1]) assert 1.0 == fbeta_score([1, 1], [1, 1], 0) # test with several classes - assert 3. == f1_score([0, 1, 2], [0, 1, 2]).sum().item() - assert 3. == precision([0, 1, 2], [0, 1, 2]).sum().item() - assert 3. == recall([0, 1, 2], [0, 1, 2]).sum().item() + assert 3.0 == f1_score([0, 1, 2], [0, 1, 2]).sum().item() + assert 3.0 == precision([0, 1, 2], [0, 1, 2]).sum().item() + assert 3.0 == recall([0, 1, 2], [0, 1, 2]).sum().item() From 50b75278cafae219a2d29075a814e92d566f3769 Mon Sep 17 00:00:00 2001 From: Nikita Date: Sun, 1 Nov 2020 19:21:09 +0300 Subject: [PATCH 14/28] fix codestyle --- catalyst/metrics/functional.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/catalyst/metrics/functional.py b/catalyst/metrics/functional.py index 5d1467d30d..eeb6b1f7fa 100644 --- a/catalyst/metrics/functional.py +++ b/catalyst/metrics/functional.py @@ -220,7 +220,7 @@ def get_multiclass_statistics( num_classes=num_classes, ) if num_classes <= 2: - return get_binary_statistics(outputs=outputs, targets=targets,) + return get_binary_statistics(outputs=outputs, targets=targets) tn = torch.zeros((num_classes,), device=outputs.device) fp = torch.zeros((num_classes,), device=outputs.device) From ba6fcaf6b36ce4e7b9453c2092e2088607aa8942 Mon Sep 17 00:00:00 2001 From: Nikita Balagansky Date: Tue, 3 Nov 2020 18:32:38 +0300 Subject: [PATCH 15/28] refactor --- catalyst/metrics/__init__.py | 1 + catalyst/metrics/f1_score.py | 16 +++++------ catalyst/metrics/functional.py | 2 -- catalyst/metrics/precision_recall_fbeta.py | 27 +++++++++++++++++++ .../tests/test_fbeta_precision_recall.py | 8 +++--- 5 files changed, 39 insertions(+), 15 deletions(-) create mode 100644 catalyst/metrics/precision_recall_fbeta.py diff --git a/catalyst/metrics/__init__.py b/catalyst/metrics/__init__.py index 5d617e59f6..f6fcf7d45c 100644 --- a/catalyst/metrics/__init__.py +++ b/catalyst/metrics/__init__.py @@ -4,6 +4,7 @@ from catalyst.metrics.cmc_score import cmc_score, cmc_score_count from catalyst.metrics.dice import dice, calculate_dice from catalyst.metrics.f1_score import f1_score, fbeta_score +from catalyst.metrics.precision_recall_fbeta import precision_recall_fbeta from catalyst.metrics.precision import precision from catalyst.metrics.recall import recall from catalyst.metrics.focal import sigmoid_focal_loss, reduced_focal_loss diff --git a/catalyst/metrics/f1_score.py b/catalyst/metrics/f1_score.py index 9f7b6f84f7..6e9a103463 100644 --- a/catalyst/metrics/f1_score.py +++ b/catalyst/metrics/f1_score.py @@ -5,7 +5,7 @@ import torch -from catalyst.metrics.functional import get_multiclass_statistics +from catalyst.metrics.precision_recall_fbeta import precision_recall_fbeta def fbeta_score( @@ -14,7 +14,7 @@ def fbeta_score( beta: float = 1.0, eps: float = 1e-7, argmax_dim: int = -1, - num_classes: Optional[int] = None, + num_classes: Optional[int] = None ) -> Union[float, torch.Tensor]: """ Counts fbeta score for given ``outputs`` and ``targets``. @@ -37,17 +37,15 @@ def fbeta_score( if beta < 0: raise Exception("beta parameter should be non-negative") - _, fp, fn, tp, _ = get_multiclass_statistics( + _p, _r, fbeta = precision_recall_fbeta( outputs=outputs, targets=targets, + beta=beta, + eps=eps, argmax_dim=argmax_dim, num_classes=num_classes, ) - - precision_plus_recall = (1 + beta ** 2) * tp + beta ** 2 * fn + fp + eps - - score = ((1 + beta ** 2) * tp + eps) / precision_plus_recall - return score + return fbeta def f1_score( @@ -75,7 +73,7 @@ def f1_score( beta=1, eps=eps, argmax_dim=argmax_dim, - num_classes=num_classes, + num_classes=num_classes ) return score diff --git a/catalyst/metrics/functional.py b/catalyst/metrics/functional.py index eeb6b1f7fa..8a7635756b 100644 --- a/catalyst/metrics/functional.py +++ b/catalyst/metrics/functional.py @@ -219,8 +219,6 @@ def get_multiclass_statistics( argmax_dim=argmax_dim, num_classes=num_classes, ) - if num_classes <= 2: - return get_binary_statistics(outputs=outputs, targets=targets) tn = torch.zeros((num_classes,), device=outputs.device) fp = torch.zeros((num_classes,), device=outputs.device) diff --git a/catalyst/metrics/precision_recall_fbeta.py b/catalyst/metrics/precision_recall_fbeta.py new file mode 100644 index 0000000000..ae78290e9c --- /dev/null +++ b/catalyst/metrics/precision_recall_fbeta.py @@ -0,0 +1,27 @@ +from typing import Optional, Tuple + +import torch +from catalyst.metrics.functional import get_multiclass_statistics + + +def precision_recall_fbeta( + outputs: torch.Tensor, + targets: torch.Tensor, + beta: float = 1, + eps: float = 1e-6, + argmax_dim: int = -1, + num_classes: Optional[int] = None +) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor]: + tn, fp, fn, tp, support = get_multiclass_statistics( + outputs=outputs, + targets=targets, + argmax_dim=argmax_dim, + num_classes=num_classes + ) + precision = (tp + eps) / (fp + tp + eps) + recall = (tp + eps) / (fn + tp + eps) + numerator = (1 + beta**2) * precision * recall + denominator = beta ** 2 * precision + recall + fbeta = numerator / denominator + + return precision, recall, fbeta diff --git a/catalyst/metrics/tests/test_fbeta_precision_recall.py b/catalyst/metrics/tests/test_fbeta_precision_recall.py index 2c01ee2ef5..5aad262c29 100644 --- a/catalyst/metrics/tests/test_fbeta_precision_recall.py +++ b/catalyst/metrics/tests/test_fbeta_precision_recall.py @@ -6,10 +6,10 @@ def test_precision_recall_f_binary_single_class() -> None: """Metrics test""" # Test precision, recall and F-scores behave with a single positive - assert 1.0 == precision([1, 1], [1, 1]) - assert 1.0 == recall([1, 1], [1, 1]) - assert 1.0 == f1_score([1, 1], [1, 1]) - assert 1.0 == fbeta_score([1, 1], [1, 1], 0) + assert 1.0 == precision([1, 1], [1, 1])[1] + assert 1.0 == recall([1, 1], [1, 1])[1] + assert 1.0 == f1_score([1, 1], [1, 1])[1] + assert 1.0 == fbeta_score([1, 1], [1, 1], 0)[1] # test with several classes assert 3.0 == f1_score([0, 1, 2], [0, 1, 2]).sum().item() assert 3.0 == precision([0, 1, 2], [0, 1, 2]).sum().item() From 3494ce5c9c890b09b82ef6e42e8bac6e06511da7 Mon Sep 17 00:00:00 2001 From: Nikita Balagansky Date: Tue, 3 Nov 2020 18:44:06 +0300 Subject: [PATCH 16/28] docs --- catalyst/metrics/precision_recall_fbeta.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/catalyst/metrics/precision_recall_fbeta.py b/catalyst/metrics/precision_recall_fbeta.py index ae78290e9c..e97bef1adb 100644 --- a/catalyst/metrics/precision_recall_fbeta.py +++ b/catalyst/metrics/precision_recall_fbeta.py @@ -12,6 +12,20 @@ def precision_recall_fbeta( argmax_dim: int = -1, num_classes: Optional[int] = None ) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor]: + """ + Counts precision, recall, fbeta_score. + + Args: + outputs: A list of predicted elements + targets: A list of elements that are to be predicted + beta: beta param for f_score + eps: epsilon to avoid zero division + argmax_dim: int, that specifies dimension for argmax transformation + in case of scores/probabilities in ``outputs`` + num_classes: int, that specifies number of classes if it known + Returns: + tuple of precision, recall, fbeta_score + """ tn, fp, fn, tp, support = get_multiclass_statistics( outputs=outputs, targets=targets, From cd71d6490f3986c14f3111f7fa031dbeb6615041 Mon Sep 17 00:00:00 2001 From: Nikita Balagansky Date: Tue, 3 Nov 2020 18:52:33 +0300 Subject: [PATCH 17/28] codestyle --- catalyst/metrics/precision_recall_fbeta.py | 1 + 1 file changed, 1 insertion(+) diff --git a/catalyst/metrics/precision_recall_fbeta.py b/catalyst/metrics/precision_recall_fbeta.py index e97bef1adb..3637f61138 100644 --- a/catalyst/metrics/precision_recall_fbeta.py +++ b/catalyst/metrics/precision_recall_fbeta.py @@ -1,6 +1,7 @@ from typing import Optional, Tuple import torch + from catalyst.metrics.functional import get_multiclass_statistics From 8f4a4f038a55f7f9b15d5702c3ad7dda9c8f03a3 Mon Sep 17 00:00:00 2001 From: Nikita Balagansky Date: Tue, 3 Nov 2020 18:56:55 +0300 Subject: [PATCH 18/28] codestyle --- catalyst/metrics/precision_recall_fbeta.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/catalyst/metrics/precision_recall_fbeta.py b/catalyst/metrics/precision_recall_fbeta.py index 3637f61138..1934c1f3d6 100644 --- a/catalyst/metrics/precision_recall_fbeta.py +++ b/catalyst/metrics/precision_recall_fbeta.py @@ -35,7 +35,7 @@ def precision_recall_fbeta( ) precision = (tp + eps) / (fp + tp + eps) recall = (tp + eps) / (fn + tp + eps) - numerator = (1 + beta**2) * precision * recall + numerator = (1 + beta ** 2) * precision * recall denominator = beta ** 2 * precision + recall fbeta = numerator / denominator From 36be75d2fc37470cd4c59cb09ab6e1f06e0b8941 Mon Sep 17 00:00:00 2001 From: Nikita Balagansky Date: Tue, 3 Nov 2020 21:40:04 +0300 Subject: [PATCH 19/28] fix --- catalyst/metrics/__init__.py | 2 +- .../{precision_recall_fbeta.py => classification.py} | 8 ++++---- catalyst/metrics/f1_score.py | 2 +- catalyst/metrics/precision.py | 9 +++++---- catalyst/metrics/recall.py | 7 ++++--- catalyst/metrics/tests/test_fbeta_precision_recall.py | 5 +++++ 6 files changed, 20 insertions(+), 13 deletions(-) rename catalyst/metrics/{precision_recall_fbeta.py => classification.py} (86%) diff --git a/catalyst/metrics/__init__.py b/catalyst/metrics/__init__.py index f6fcf7d45c..d47edb7186 100644 --- a/catalyst/metrics/__init__.py +++ b/catalyst/metrics/__init__.py @@ -4,7 +4,7 @@ from catalyst.metrics.cmc_score import cmc_score, cmc_score_count from catalyst.metrics.dice import dice, calculate_dice from catalyst.metrics.f1_score import f1_score, fbeta_score -from catalyst.metrics.precision_recall_fbeta import precision_recall_fbeta +from catalyst.metrics.classification import precision_recall_fbeta_support from catalyst.metrics.precision import precision from catalyst.metrics.recall import recall from catalyst.metrics.focal import sigmoid_focal_loss, reduced_focal_loss diff --git a/catalyst/metrics/precision_recall_fbeta.py b/catalyst/metrics/classification.py similarity index 86% rename from catalyst/metrics/precision_recall_fbeta.py rename to catalyst/metrics/classification.py index 1934c1f3d6..5aa79706f0 100644 --- a/catalyst/metrics/precision_recall_fbeta.py +++ b/catalyst/metrics/classification.py @@ -5,14 +5,14 @@ from catalyst.metrics.functional import get_multiclass_statistics -def precision_recall_fbeta( +def precision_recall_fbeta_support( outputs: torch.Tensor, targets: torch.Tensor, beta: float = 1, eps: float = 1e-6, argmax_dim: int = -1, num_classes: Optional[int] = None -) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor]: +) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]: """ Counts precision, recall, fbeta_score. @@ -31,7 +31,7 @@ def precision_recall_fbeta( outputs=outputs, targets=targets, argmax_dim=argmax_dim, - num_classes=num_classes + num_classes=num_classes, ) precision = (tp + eps) / (fp + tp + eps) recall = (tp + eps) / (fn + tp + eps) @@ -39,4 +39,4 @@ def precision_recall_fbeta( denominator = beta ** 2 * precision + recall fbeta = numerator / denominator - return precision, recall, fbeta + return precision, recall, fbeta, support diff --git a/catalyst/metrics/f1_score.py b/catalyst/metrics/f1_score.py index 6e9a103463..b4e718664a 100644 --- a/catalyst/metrics/f1_score.py +++ b/catalyst/metrics/f1_score.py @@ -5,7 +5,7 @@ import torch -from catalyst.metrics.precision_recall_fbeta import precision_recall_fbeta +from catalyst.metrics.classification import precision_recall_fbeta def fbeta_score( diff --git a/catalyst/metrics/precision.py b/catalyst/metrics/precision.py index a588a4861f..9c5caef07a 100644 --- a/catalyst/metrics/precision.py +++ b/catalyst/metrics/precision.py @@ -2,8 +2,8 @@ import torch -from catalyst.metrics.functional import ( - get_multiclass_statistics, +from catalyst.metrics import ( + precision_recall_fbeta_support, process_multilabel_components, ) @@ -92,13 +92,14 @@ def precision( Returns: Tensor: """ - _, fp, _, tp, _ = get_multiclass_statistics( + precision_score, _, _, _, = precision_recall_fbeta_support( outputs=outputs, targets=targets, argmax_dim=argmax_dim, + eps=eps, num_classes=num_classes, ) - return (tp + eps) / (fp + tp + eps) + return precision_score __all__ = ["average_precision", "precision"] diff --git a/catalyst/metrics/recall.py b/catalyst/metrics/recall.py index 3a309a05e7..a788877065 100644 --- a/catalyst/metrics/recall.py +++ b/catalyst/metrics/recall.py @@ -2,7 +2,7 @@ import torch -from catalyst.metrics.functional import get_multiclass_statistics +from catalyst.metrics import precision_recall_fbeta_support def recall( @@ -28,11 +28,12 @@ def recall( Returns: Tensor: recall for every class """ - _, _, fn, tp, _ = get_multiclass_statistics( + _, recall_score, _, _ = precision_recall_fbeta_support( outputs=outputs, targets=targets, argmax_dim=argmax_dim, + eps=eps, num_classes=num_classes, ) - return (tp + eps) / (fn + tp + eps) + return recall_score diff --git a/catalyst/metrics/tests/test_fbeta_precision_recall.py b/catalyst/metrics/tests/test_fbeta_precision_recall.py index 5aad262c29..b54c475114 100644 --- a/catalyst/metrics/tests/test_fbeta_precision_recall.py +++ b/catalyst/metrics/tests/test_fbeta_precision_recall.py @@ -1,3 +1,5 @@ +import torch + import pytest # noqa: F401 from catalyst.metrics import f1_score, fbeta_score, precision, recall @@ -14,3 +16,6 @@ def test_precision_recall_f_binary_single_class() -> None: assert 3.0 == f1_score([0, 1, 2], [0, 1, 2]).sum().item() assert 3.0 == precision([0, 1, 2], [0, 1, 2]).sum().item() assert 3.0 == recall([0, 1, 2], [0, 1, 2]).sum().item() + + assert 0.0 == f1_score(torch.arange(10), torch.arange(10)[::-1]).sum() + From d751ebe2e267e04b5fbc702f6776466c900dc58f Mon Sep 17 00:00:00 2001 From: Nikita Date: Tue, 3 Nov 2020 21:50:10 +0300 Subject: [PATCH 20/28] fix codestyle --- catalyst/metrics/classification.py | 2 +- catalyst/metrics/f1_score.py | 4 ++-- catalyst/metrics/tests/test_fbeta_precision_recall.py | 5 ++--- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/catalyst/metrics/classification.py b/catalyst/metrics/classification.py index 5aa79706f0..92935c2303 100644 --- a/catalyst/metrics/classification.py +++ b/catalyst/metrics/classification.py @@ -11,7 +11,7 @@ def precision_recall_fbeta_support( beta: float = 1, eps: float = 1e-6, argmax_dim: int = -1, - num_classes: Optional[int] = None + num_classes: Optional[int] = None, ) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]: """ Counts precision, recall, fbeta_score. diff --git a/catalyst/metrics/f1_score.py b/catalyst/metrics/f1_score.py index b4e718664a..70470da0d3 100644 --- a/catalyst/metrics/f1_score.py +++ b/catalyst/metrics/f1_score.py @@ -14,7 +14,7 @@ def fbeta_score( beta: float = 1.0, eps: float = 1e-7, argmax_dim: int = -1, - num_classes: Optional[int] = None + num_classes: Optional[int] = None, ) -> Union[float, torch.Tensor]: """ Counts fbeta score for given ``outputs`` and ``targets``. @@ -73,7 +73,7 @@ def f1_score( beta=1, eps=eps, argmax_dim=argmax_dim, - num_classes=num_classes + num_classes=num_classes, ) return score diff --git a/catalyst/metrics/tests/test_fbeta_precision_recall.py b/catalyst/metrics/tests/test_fbeta_precision_recall.py index b54c475114..f6c40fd61b 100644 --- a/catalyst/metrics/tests/test_fbeta_precision_recall.py +++ b/catalyst/metrics/tests/test_fbeta_precision_recall.py @@ -1,7 +1,7 @@ -import torch - import pytest # noqa: F401 +import torch + from catalyst.metrics import f1_score, fbeta_score, precision, recall @@ -18,4 +18,3 @@ def test_precision_recall_f_binary_single_class() -> None: assert 3.0 == recall([0, 1, 2], [0, 1, 2]).sum().item() assert 0.0 == f1_score(torch.arange(10), torch.arange(10)[::-1]).sum() - From 4c734ce0f9c635aca991d3dc9a3f35f623768c23 Mon Sep 17 00:00:00 2001 From: Nikita Date: Tue, 3 Nov 2020 22:26:00 +0300 Subject: [PATCH 21/28] fix --- catalyst/metrics/classification.py | 3 ++- catalyst/metrics/f1_score.py | 4 ++-- catalyst/metrics/recall.py | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/catalyst/metrics/classification.py b/catalyst/metrics/classification.py index 92935c2303..c507f85e2f 100644 --- a/catalyst/metrics/classification.py +++ b/catalyst/metrics/classification.py @@ -23,7 +23,8 @@ def precision_recall_fbeta_support( eps: epsilon to avoid zero division argmax_dim: int, that specifies dimension for argmax transformation in case of scores/probabilities in ``outputs`` - num_classes: int, that specifies number of classes if it known + num_classes: int, that specifies number of classes if it known. + Returns: tuple of precision, recall, fbeta_score """ diff --git a/catalyst/metrics/f1_score.py b/catalyst/metrics/f1_score.py index 70470da0d3..286a4cba91 100644 --- a/catalyst/metrics/f1_score.py +++ b/catalyst/metrics/f1_score.py @@ -5,7 +5,7 @@ import torch -from catalyst.metrics.classification import precision_recall_fbeta +from catalyst.metrics.classification import precision_recall_fbeta_support def fbeta_score( @@ -37,7 +37,7 @@ def fbeta_score( if beta < 0: raise Exception("beta parameter should be non-negative") - _p, _r, fbeta = precision_recall_fbeta( + _p, _r, fbeta, _ = precision_recall_fbeta_support( outputs=outputs, targets=targets, beta=beta, diff --git a/catalyst/metrics/recall.py b/catalyst/metrics/recall.py index a788877065..52319a2b70 100644 --- a/catalyst/metrics/recall.py +++ b/catalyst/metrics/recall.py @@ -23,7 +23,7 @@ def recall( argmax_dim: int, that specifies dimension for argmax transformation in case of scores/probabilities in ``outputs`` eps: float. Epsilon to avoid zero division. - num_classes: int, that specifies number of classes if it known + num_classes: int, that specifies number of classes if it known. Returns: Tensor: recall for every class From 74f9914bd9e8bce60001f06acc3e1ae744cdfed4 Mon Sep 17 00:00:00 2001 From: Nikita Date: Tue, 3 Nov 2020 22:38:04 +0300 Subject: [PATCH 22/28] fix --- catalyst/metrics/f1_score.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/catalyst/metrics/f1_score.py b/catalyst/metrics/f1_score.py index 286a4cba91..92fe85a040 100644 --- a/catalyst/metrics/f1_score.py +++ b/catalyst/metrics/f1_score.py @@ -18,7 +18,6 @@ def fbeta_score( ) -> Union[float, torch.Tensor]: """ Counts fbeta score for given ``outputs`` and ``targets``. - Args: outputs: A list of predicted elements targets: A list of elements that are to be predicted @@ -33,7 +32,7 @@ def fbeta_score( Returns: float: F_1 score. - """ + """ if beta < 0: raise Exception("beta parameter should be non-negative") @@ -56,6 +55,8 @@ def f1_score( num_classes: Optional[int] = None, ) -> Union[float, torch.Tensor]: """ + Fbeta_score with beta=1. + Args: outputs: A list of predicted elements targets: A list of elements that are to be predicted From 5a017580bc41a9f03a51fd18f68db8699b734e10 Mon Sep 17 00:00:00 2001 From: Nikita Date: Tue, 3 Nov 2020 23:30:32 +0300 Subject: [PATCH 23/28] add tests --- catalyst/metrics/precision.py | 3 +- .../tests/test_fbeta_precision_recall.py | 44 ++++++++++++++++++- 2 files changed, 44 insertions(+), 3 deletions(-) diff --git a/catalyst/metrics/precision.py b/catalyst/metrics/precision.py index 9c5caef07a..416f5108ea 100644 --- a/catalyst/metrics/precision.py +++ b/catalyst/metrics/precision.py @@ -3,8 +3,7 @@ import torch from catalyst.metrics import ( - precision_recall_fbeta_support, - process_multilabel_components, + precision_recall_fbeta_support, process_multilabel_components, ) diff --git a/catalyst/metrics/tests/test_fbeta_precision_recall.py b/catalyst/metrics/tests/test_fbeta_precision_recall.py index f6c40fd61b..db25ce568e 100644 --- a/catalyst/metrics/tests/test_fbeta_precision_recall.py +++ b/catalyst/metrics/tests/test_fbeta_precision_recall.py @@ -2,7 +2,13 @@ import torch -from catalyst.metrics import f1_score, fbeta_score, precision, recall +from catalyst.metrics import ( + f1_score, + fbeta_score, + precision, + precision_recall_fbeta_support, + recall, +) def test_precision_recall_f_binary_single_class() -> None: @@ -18,3 +24,39 @@ def test_precision_recall_f_binary_single_class() -> None: assert 3.0 == recall([0, 1, 2], [0, 1, 2]).sum().item() assert 0.0 == f1_score(torch.arange(10), torch.arange(10)[::-1]).sum() + + +@pytest.mark.parametrize( + [ + "outputs", + "targets", + "precision_true", + "recall_true", + "fbeta_true", + "support_true", + ], + [ + pytest.param( + torch.tensor([[0, 0, 1, 1, 0, 1, 0, 1]]), + torch.tensor([[0, 1, 0, 1, 0, 0, 1, 1]]), + 0.5, + 0.5, + 0.5, + 4, + ), + ], +) +def test_precision_recall_fbeta_support_binary( + outputs, targets, precision_true, recall_true, fbeta_true, support_true, +): + ( + precision_score, + recall_score, + fbeta_score, + support, + ) = precision_recall_fbeta_support(outputs=outputs, targets=targets) + + assert torch.isclose(precision_score[1], precision_true) + assert torch.isclose(precision_score[1], precision_true) + assert torch.isclose(precision_score[1], precision_true) + assert support[1] == support_true From 85f87f58353ecc1192ae68d1003a3ae48aa60481 Mon Sep 17 00:00:00 2001 From: Nikita Date: Tue, 3 Nov 2020 23:37:29 +0300 Subject: [PATCH 24/28] fix --- catalyst/metrics/precision.py | 5 ++--- catalyst/metrics/tests/test_fbeta_precision_recall.py | 8 +++----- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/catalyst/metrics/precision.py b/catalyst/metrics/precision.py index 416f5108ea..39a9c2589f 100644 --- a/catalyst/metrics/precision.py +++ b/catalyst/metrics/precision.py @@ -2,9 +2,8 @@ import torch -from catalyst.metrics import ( - precision_recall_fbeta_support, process_multilabel_components, -) +from catalyst.metrics import precision_recall_fbeta_support +from catalyst.metrics.functional import process_multilabel_components def average_precision( diff --git a/catalyst/metrics/tests/test_fbeta_precision_recall.py b/catalyst/metrics/tests/test_fbeta_precision_recall.py index db25ce568e..563ac7ff91 100644 --- a/catalyst/metrics/tests/test_fbeta_precision_recall.py +++ b/catalyst/metrics/tests/test_fbeta_precision_recall.py @@ -23,8 +23,6 @@ def test_precision_recall_f_binary_single_class() -> None: assert 3.0 == precision([0, 1, 2], [0, 1, 2]).sum().item() assert 3.0 == recall([0, 1, 2], [0, 1, 2]).sum().item() - assert 0.0 == f1_score(torch.arange(10), torch.arange(10)[::-1]).sum() - @pytest.mark.parametrize( [ @@ -56,7 +54,7 @@ def test_precision_recall_fbeta_support_binary( support, ) = precision_recall_fbeta_support(outputs=outputs, targets=targets) - assert torch.isclose(precision_score[1], precision_true) - assert torch.isclose(precision_score[1], precision_true) - assert torch.isclose(precision_score[1], precision_true) + assert torch.isclose(precision_score[1], torch.tensor(precision_true)) + assert torch.isclose(recall_score[1], torch.tensor(recall_true)) + assert torch.isclose(fbeta_score[1], torch.tensor(fbeta_true)) assert support[1] == support_true From da910a2b38e34739e372fdedf1c8eab39d63d4f7 Mon Sep 17 00:00:00 2001 From: Nikita Date: Tue, 3 Nov 2020 23:45:30 +0300 Subject: [PATCH 25/28] CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7c83f7bc80..904073a5a1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/). - extra functions for classification metrics ([#966](https://github.com/catalyst-team/catalyst/pull/966)) - `OneOf` and `OneOfV2` batch transforms ([#951](https://github.com/catalyst-team/catalyst/pull/951)) +- ``precision_recall_fbeta_support`` metric ([#971](https://github.com/catalyst-team/catalyst/pull/971)) ### Changed From 3d43d789a0b019dfee1cd607e7c21ba0d6f15b44 Mon Sep 17 00:00:00 2001 From: Nikita Date: Tue, 3 Nov 2020 23:46:17 +0300 Subject: [PATCH 26/28] docs --- catalyst/metrics/f1_score.py | 1 + 1 file changed, 1 insertion(+) diff --git a/catalyst/metrics/f1_score.py b/catalyst/metrics/f1_score.py index 92fe85a040..360869dcf6 100644 --- a/catalyst/metrics/f1_score.py +++ b/catalyst/metrics/f1_score.py @@ -18,6 +18,7 @@ def fbeta_score( ) -> Union[float, torch.Tensor]: """ Counts fbeta score for given ``outputs`` and ``targets``. + Args: outputs: A list of predicted elements targets: A list of elements that are to be predicted From b32b355fb77cf97469cf0fb0c2f3fff23d4ef27e Mon Sep 17 00:00:00 2001 From: Nikita Date: Tue, 3 Nov 2020 23:57:19 +0300 Subject: [PATCH 27/28] docs --- .../tests/test_fbeta_precision_recall.py | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/catalyst/metrics/tests/test_fbeta_precision_recall.py b/catalyst/metrics/tests/test_fbeta_precision_recall.py index 563ac7ff91..1256b73207 100644 --- a/catalyst/metrics/tests/test_fbeta_precision_recall.py +++ b/catalyst/metrics/tests/test_fbeta_precision_recall.py @@ -46,15 +46,26 @@ def test_precision_recall_f_binary_single_class() -> None: ) def test_precision_recall_fbeta_support_binary( outputs, targets, precision_true, recall_true, fbeta_true, support_true, -): +) -> None: + """ + Test for precision_recall_fbeta_support. + + Args: + outputs: + targets: + precision_true: + recall_true: + fbeta_true: + support_true: + """ ( precision_score, recall_score, - fbeta_score, + fbeta_score_, support, ) = precision_recall_fbeta_support(outputs=outputs, targets=targets) assert torch.isclose(precision_score[1], torch.tensor(precision_true)) assert torch.isclose(recall_score[1], torch.tensor(recall_true)) - assert torch.isclose(fbeta_score[1], torch.tensor(fbeta_true)) + assert torch.isclose(fbeta_score_[1], torch.tensor(fbeta_true)) assert support[1] == support_true From c2c9feedb3499a112bfddd96163c906f86a9545a Mon Sep 17 00:00:00 2001 From: Nikita Date: Wed, 4 Nov 2020 00:12:03 +0300 Subject: [PATCH 28/28] docs --- .../metrics/tests/test_fbeta_precision_recall.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/catalyst/metrics/tests/test_fbeta_precision_recall.py b/catalyst/metrics/tests/test_fbeta_precision_recall.py index 1256b73207..2804d56168 100644 --- a/catalyst/metrics/tests/test_fbeta_precision_recall.py +++ b/catalyst/metrics/tests/test_fbeta_precision_recall.py @@ -51,21 +51,21 @@ def test_precision_recall_fbeta_support_binary( Test for precision_recall_fbeta_support. Args: - outputs: - targets: - precision_true: - recall_true: - fbeta_true: - support_true: + outputs: test arg + targets: test arg + precision_true: test arg + recall_true: test arg + fbeta_true: test arg + support_true: test arg """ ( precision_score, recall_score, - fbeta_score_, + fbeta_score_ev, support, ) = precision_recall_fbeta_support(outputs=outputs, targets=targets) assert torch.isclose(precision_score[1], torch.tensor(precision_true)) assert torch.isclose(recall_score[1], torch.tensor(recall_true)) - assert torch.isclose(fbeta_score_[1], torch.tensor(fbeta_true)) + assert torch.isclose(fbeta_score_ev[1], torch.tensor(fbeta_true)) assert support[1] == support_true