Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Fix] fix a bug in multi-label classification #2425

Merged
merged 4 commits into from
Apr 24, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 18 additions & 33 deletions mmaction/evaluation/metrics/acc_metric.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,16 +31,13 @@ class AccMetric(BaseMetric):
"""Accuracy evaluation metric."""
default_prefix: Optional[str] = 'acc'

def __init__(
self,
metric_list: Optional[Union[str,
Tuple[str]]] = ('top_k_accuracy',
'mean_class_accuracy'),
collect_device: str = 'cpu',
metric_options: Optional[Dict] = dict(
top_k_accuracy=dict(topk=(1, 5))),
prefix: Optional[str] = None,
num_classes: Optional[int] = None):
def __init__(self,
metric_list: Optional[Union[str, Tuple[str]]] = (
'top_k_accuracy', 'mean_class_accuracy'),
collect_device: str = 'cpu',
metric_options: Optional[Dict] = dict(
top_k_accuracy=dict(topk=(1, 5))),
prefix: Optional[str] = None) -> None:

# TODO: fix the metric_list argument with a better one.
# `metrics` is not a safe argument here with mmengine.
Expand All @@ -62,14 +59,8 @@ def __init__(
'mmit_mean_average_precision', 'mean_average_precision'
]

if metric in [
'mmit_mean_average_precision', 'mean_average_precision'
]:
assert type(num_classes) == int

self.metrics = metrics
self.metric_options = metric_options
self.num_classes = num_classes

def process(self, data_batch: Sequence[Tuple[Any, Dict]],
data_samples: Sequence[Dict]) -> None:
Expand All @@ -89,7 +80,12 @@ def process(self, data_batch: Sequence[Tuple[Any, Dict]],
for item_name, score in pred.items():
pred[item_name] = score.cpu().numpy()
result['pred'] = pred
result['label'] = label['item'].item()
if label['item'].size(0) == 1:
# single-label
result['label'] = label['item'].item()
else:
# multi-label
result['label'] = label['item'].cpu().numpy()
self.results.append(result)

def compute_metrics(self, results: List) -> Dict:
Expand Down Expand Up @@ -138,12 +134,13 @@ def compute_metrics(self, results: List) -> Dict:

return eval_results

def calculate(self, preds: List[np.ndarray], labels: List[int]) -> Dict:
def calculate(self, preds: List[np.ndarray],
labels: List[Union[int, np.ndarray]]) -> Dict:
"""Compute the metrics from processed results.

Args:
preds (list[np.ndarray]): List of the prediction scores.
labels (list[int]): List of the labels.
labels (list[int | np.ndarray]): List of the labels.

Returns:
dict: The computed metrics. The keys are the names of the metrics,
Expand Down Expand Up @@ -176,28 +173,16 @@ def calculate(self, preds: List[np.ndarray], labels: List[int]) -> Dict:
'mean_average_precision',
'mmit_mean_average_precision',
]:
gt_labels_arrays = [
self.label2array(self.num_classes, label)
for label in labels
]

if metric == 'mean_average_precision':
mAP = mean_average_precision(preds, gt_labels_arrays)
mAP = mean_average_precision(preds, labels)
eval_results['mean_average_precision'] = mAP

elif metric == 'mmit_mean_average_precision':
mAP = mmit_mean_average_precision(preds, gt_labels_arrays)
mAP = mmit_mean_average_precision(preds, labels)
eval_results['mmit_mean_average_precision'] = mAP

return eval_results

@staticmethod
def label2array(num, label):
"""Convert multi-label to array."""
arr = np.zeros(num, dtype=np.float32)
arr[label] = 1.
return arr


@METRICS.register_module()
class ConfusionMatrix(BaseMetric):
Expand Down
2 changes: 1 addition & 1 deletion mmaction/models/heads/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ def loss_by_feat(self, cls_scores: torch.Tensor,
elif labels.dim() == 1 and labels.size()[0] == self.num_classes \
and cls_scores.size()[0] == 1:
# Fix a bug when training with soft labels and batch size is 1.
# When using soft labels, `labels` and `cls_socre` share the same
# When using soft labels, `labels` and `cls_score` share the same
# shape.
labels = labels.unsqueeze(0)

Expand Down
45 changes: 27 additions & 18 deletions mmaction/models/losses/cross_entropy_loss.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
# Copyright (c) OpenMMLab. All rights reserved.
from typing import List, Optional

import numpy as np
import torch
import torch.nn.functional as F
Expand All @@ -17,27 +19,30 @@ class CrossEntropyLoss(BaseWeightedLoss):
1) Hard label: This label is an integer array and all of the elements are
in the range [0, num_classes - 1]. This label's shape should be
``cls_score``'s shape with the `num_classes` dimension removed.
2) Soft label(probablity distribution over classes): This label is a
2) Soft label(probability distribution over classes): This label is a
probability distribution and all of the elements are in the range
[0, 1]. This label's shape must be the same as ``cls_score``. For now,
only 2-dim soft label is supported.

Args:
loss_weight (float): Factor scalar multiplied on the loss.
Default: 1.0.
Defaults to 1.0.
class_weight (list[float] | None): Loss weight for each class. If set
as None, use the same weight 1 for all classes. Only applies
to CrossEntropyLoss and BCELossWithLogits (should not be set when
using other losses). Default: None.
using other losses). Defaults to None.
"""

def __init__(self, loss_weight=1.0, class_weight=None):
def __init__(self,
loss_weight: float = 1.0,
class_weight: Optional[List[float]] = None) -> None:
super().__init__(loss_weight=loss_weight)
self.class_weight = None
if class_weight is not None:
self.class_weight = torch.Tensor(class_weight)

def _forward(self, cls_score, label, **kwargs):
def _forward(self, cls_score: torch.Tensor, label: torch.Tensor,
**kwargs) -> torch.Tensor:
"""Forward function.

Args:
Expand Down Expand Up @@ -89,20 +94,23 @@ class BCELossWithLogits(BaseWeightedLoss):

Args:
loss_weight (float): Factor scalar multiplied on the loss.
Default: 1.0.
Defaults to 1.0.
class_weight (list[float] | None): Loss weight for each class. If set
as None, use the same weight 1 for all classes. Only applies
to CrossEntropyLoss and BCELossWithLogits (should not be set when
using other losses). Default: None.
using other losses). Defaults to None.
"""

def __init__(self, loss_weight=1.0, class_weight=None):
def __init__(self,
loss_weight: float = 1.0,
class_weight: Optional[List[float]] = None) -> None:
super().__init__(loss_weight=loss_weight)
self.class_weight = None
if class_weight is not None:
self.class_weight = torch.Tensor(class_weight)

def _forward(self, cls_score, label, **kwargs):
def _forward(self, cls_score: torch.Tensor, label: torch.Tensor,
**kwargs) -> torch.Tensor:
"""Forward function.

Args:
Expand Down Expand Up @@ -130,19 +138,19 @@ class CBFocalLoss(BaseWeightedLoss):

Args:
loss_weight (float): Factor scalar multiplied on the loss.
Default: 1.0.
Defaults to 1.0.
samples_per_cls (list[int]): The number of samples per class.
Default: [].
Defaults to [].
beta (float): Hyperparameter that controls the per class loss weight.
Default: 0.9999.
gamma (float): Hyperparameter of the focal loss. Default: 2.0.
Defaults to 0.9999.
gamma (float): Hyperparameter of the focal loss. Defaults to 2.0.
"""

def __init__(self,
loss_weight=1.0,
samples_per_cls=[],
beta=0.9999,
gamma=2.):
loss_weight: float = 1.0,
samples_per_cls: List[int] = [],
beta: float = 0.9999,
gamma: float = 2.) -> None:
super().__init__(loss_weight=loss_weight)
self.samples_per_cls = samples_per_cls
self.beta = beta
Expand All @@ -153,7 +161,8 @@ def __init__(self,
self.weights = weights
self.num_classes = len(weights)

def _forward(self, cls_score, label, **kwargs):
def _forward(self, cls_score: torch.Tensor, label: torch.Tensor,
**kwargs) -> torch.Tensor:
"""Forward function.

Args:
Expand Down
30 changes: 18 additions & 12 deletions tests/evaluation/metrics/test_acc_metric.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,27 +9,26 @@
from mmaction.structures import ActionDataSample


def generate_data(num_classes=5, random_label=False):
def generate_data(num_classes=5, random_label=False, multi_label=False):
data_batch = []
data_samples = []
for i in range(num_classes * 10):
logit = torch.randn(num_classes)
if random_label:
label = torch.randint(num_classes, size=[])
scores = torch.randn(num_classes)
if multi_label:
label = torch.ones_like(scores)
elif random_label:
label = torch.randint(num_classes, size=[1])
else:
label = torch.tensor(logit.argmax().item())
label = torch.LongTensor([scores.argmax().item()])
data_sample = dict(
pred_scores=dict(item=logit), gt_labels=dict(item=label))
pred_scores=dict(item=scores), gt_labels=dict(item=label))
data_samples.append(data_sample)
return data_batch, data_samples


def test_accmetric():
def test_acc_metric():
num_classes = 32
metric = AccMetric(
metric_list=('top_k_accuracy', 'mean_class_accuracy',
'mmit_mean_average_precision', 'mean_average_precision'),
num_classes=num_classes)
metric = AccMetric(metric_list=('top_k_accuracy', 'mean_class_accuracy'))
data_batch, predictions = generate_data(
num_classes=num_classes, random_label=True)
metric.process(data_batch, predictions)
Expand All @@ -44,8 +43,15 @@ def test_accmetric():
eval_results = metric.compute_metrics(metric.results)
assert eval_results['top1'] == eval_results['top5'] == 1.0
assert eval_results['mean1'] == 1.0

metric = AccMetric(
metric_list=('mean_average_precision', 'mmit_mean_average_precision'))
data_batch, predictions = generate_data(
num_classes=num_classes, multi_label=True)
metric.process(data_batch, predictions)
eval_results = metric.compute_metrics(metric.results)
assert eval_results['mean_average_precision'] == 1.0
assert eval_results['mmit_mean_average_precision'] == 1.0
return


class TestConfusionMatrix(TestCase):
Expand Down