Repository: Lightning-AI/torchmetrics

Issue:
Add a wrapper adapted to multi-task learning
## 🚀 Feature

Add a wrapper adapted to multi-task learning. Given a list of model outputs and a list of labels, it should be able to compute a different metric (or several different metrics) on each output.

### Motivation

When doing multi-task learning, we have several outputs, and the metric that we want to compute on each output is generally different. 

For instance, in image detection with K classes, the output could consist of 4 neurons for bounding box location (pos_x, pos_y, height, width), and K neurons for the class of the detected object. We would like to have a way to compute some regression metrics (like MSE, MAE, etc...) on the outputs corresponding to the bounding box locations, and to compute some classification metrics (like Accuracy) on the outputs corresponding to the class. 

An idea could be to simply generalize `MultioutputWrapper` to use a list of `base_metric` rather than a single, duplicated, `base_metric`. But in the previous example, the output and the label are `tuple[Tensor, Tensor]` (the first element is a `Tensor` of shape [4], the second element is a `Tensor` of shape [K]), and `MultioutputWrapper` works with `Tensor` only.

### Pitch

Add a `MultitaskWrapper` class that would take a list of `Metric`s at initialization (or maybe a `MetricCollection`, or an `Iterable[Metric]`), and whose `update` signature would look like:
```python
def update(self, preds: Iterable[Tensor], targets: Iterable[Tensor]) -> None:
```


Discussion:
Hi @ValerianRey, that sounds like an excellent addition to torchmetrics.
However, I am still a bit in doubt what it should look like. Maybe you could give a concreate coding example how you would like the metric to work?

What I understand you are looking for is something like this?
```python
from torchmetrics import Accuracy, MeanSquaredError, MultitaskWrapper
metrics = MultiTaskWrapper(
   Accuracy(), MeanSquaredError()
)

class_probs, bounding_box_pred = model(some_input)
metrics.update([class_probs, bounding_box_pred], [class_targets, bounding_box_target])
val = metrics.compute()  # return dict with both accuracy + mse
```





Yes, this is more or less what I was thinking about.
I'm not sure what kind of dict you're thinking about for the output of `metrics.compute()`, but I don't think that a dict of the form:
```python
{
    "Accuracy": Tensor(0.5),
    "MeanSquaredError": Tensor(10.)
}
```
would work. If we imagine a problem with 3 tasks: 2 regressions for which we want to compute the MSE and 1 classification for which we want to compute the accuracy, we would need the keys of the dict to differentiate between the first and the second "MeanSquaredError". Therefore,  I think that we should either have the output of `metrics.compute()` be a simple list, or a dict with keys corresponding to the task names. The latter would give something like:
```python
from torchmetrics import Accuracy, MeanSquaredError, MultitaskWrapper
metrics = MultiTaskWrapper(
   [Accuracy(), MeanSquaredError()], names=["image classification", "bounding box regression"]
)

class_probs, bounding_box_pred = model(some_input)
metrics.update([class_probs, bounding_box_pred], [class_targets, bounding_box_target])
val = metrics.compute()
val
>> {"image classification": Tensor(0.5), "bounding box regression": Tensor(10.)}
```

If we want to have the information about which kind of metric was actually computed for each task, we can be more specific when naming the tasks. Here for instance, we could chose the names "image classification accuracy" and "bounding box regression MSE".
I'll work on a draft implementation later this week!
> I'll work on a draft implementation later this week!

Alright, I assign the issue to you and look forward to your draft :]

Resolving patch:
diff --git a/src/torchmetrics/__init__.py b/src/torchmetrics/__init__.py
--- a/src/torchmetrics/__init__.py
+++ b/src/torchmetrics/__init__.py
@@ -129,6 +129,7 @@
     MetricTracker,
     MinMaxMetric,
     MultioutputWrapper,
+    MultitaskWrapper,
 )
 
 __all__ = [
@@ -177,6 +178,7 @@
     "MinMetric",
     "ModifiedPanopticQuality",
     "MultioutputWrapper",
+    "MultitaskWrapper",
     "MultiScaleStructuralSimilarityIndexMeasure",
     "PanopticQuality",
     "PearsonCorrCoef",
diff --git a/src/torchmetrics/wrappers/__init__.py b/src/torchmetrics/wrappers/__init__.py
--- a/src/torchmetrics/wrappers/__init__.py
+++ b/src/torchmetrics/wrappers/__init__.py
@@ -15,6 +15,7 @@
 from torchmetrics.wrappers.classwise import ClasswiseWrapper
 from torchmetrics.wrappers.minmax import MinMaxMetric
 from torchmetrics.wrappers.multioutput import MultioutputWrapper
+from torchmetrics.wrappers.multitask import MultitaskWrapper
 from torchmetrics.wrappers.running import Running
 from torchmetrics.wrappers.tracker import MetricTracker
 
@@ -23,5 +24,7 @@
     "ClasswiseWrapper",
     "MinMaxMetric",
     "MultioutputWrapper",
+    "MultitaskWrapper",
     "MetricTracker",
+    "Running",
 ]
diff --git a/src/torchmetrics/wrappers/multitask.py b/src/torchmetrics/wrappers/multitask.py
new file mode 100644
--- /dev/null
+++ b/src/torchmetrics/wrappers/multitask.py
@@ -0,0 +1,257 @@
+# Copyright The Lightning team.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+# this is just a bypass for this module name collision with build-in one
+from typing import Any, Callable, Dict, Optional, Sequence, Union
+
+from torch import Tensor, nn
+
+from torchmetrics.collections import MetricCollection
+from torchmetrics.metric import Metric
+from torchmetrics.utilities.imports import _MATPLOTLIB_AVAILABLE
+from torchmetrics.utilities.plot import _AX_TYPE, _PLOT_OUT_TYPE
+
+if not _MATPLOTLIB_AVAILABLE:
+    __doctest_skip__ = ["MultitaskWrapper.plot"]
+
+
+class MultitaskWrapper(Metric):
+    """Wrapper class for computing different metrics on different tasks in the context of multitask learning.
+
+    In multitask learning the different tasks requires different metrics to be evaluated. This wrapper allows
+    for easy evaluation in such cases by supporting multiple predictions and targets through a dictionary.
+    Note that only metrics where the signature of `update` follows the stardard `preds, target` is supported.
+
+    Args:
+        task_metrics:
+            Dictionary associating each task to a Metric or a MetricCollection. The keys of the dictionary represent the
+            names of the tasks, and the values represent the metrics to use for each task.
+
+    Raises:
+        TypeError:
+            If argument `task_metrics` is not an dictionary
+        TypeError:
+            If not all values in the `task_metrics` dictionary is instances of `Metric` or `MetricCollection`
+
+    Example (with a single metric per class):
+         >>> import torch
+         >>> from torchmetrics.wrappers import MultitaskWrapper
+         >>> from torchmetrics.regression import MeanSquaredError
+         >>> from torchmetrics.classification import BinaryAccuracy
+         >>>
+         >>> classification_target = torch.tensor([0, 1, 0])
+         >>> regression_target = torch.tensor([2.5, 5.0, 4.0])
+         >>> targets = {"Classification": classification_target, "Regression": regression_target}
+         >>>
+         >>> classification_preds = torch.tensor([0, 0, 1])
+         >>> regression_preds = torch.tensor([3.0, 5.0, 2.5])
+         >>> preds = {"Classification": classification_preds, "Regression": regression_preds}
+         >>>
+         >>> metrics = MultitaskWrapper({
+         ...     "Classification": BinaryAccuracy(),
+         ...     "Regression": MeanSquaredError()
+         ... })
+         >>> metrics.update(preds, targets)
+         >>> metrics.compute()
+         {'Classification': tensor(0.3333), 'Regression': tensor(0.8333)}
+
+    Example (with several metrics per task):
+         >>> import torch
+         >>> from torchmetrics import MetricCollection
+         >>> from torchmetrics.wrappers import MultitaskWrapper
+         >>> from torchmetrics.regression import MeanSquaredError, MeanAbsoluteError
+         >>> from torchmetrics.classification import BinaryAccuracy, BinaryF1Score
+         >>>
+         >>> classification_target = torch.tensor([0, 1, 0])
+         >>> regression_target = torch.tensor([2.5, 5.0, 4.0])
+         >>> targets = {"Classification": classification_target, "Regression": regression_target}
+         >>>
+         >>> classification_preds = torch.tensor([0, 0, 1])
+         >>> regression_preds = torch.tensor([3.0, 5.0, 2.5])
+         >>> preds = {"Classification": classification_preds, "Regression": regression_preds}
+         >>>
+         >>> metrics = MultitaskWrapper({
+         ...     "Classification": MetricCollection(BinaryAccuracy(), BinaryF1Score()),
+         ...     "Regression": MetricCollection(MeanSquaredError(), MeanAbsoluteError())
+         ... })
+         >>> metrics.update(preds, targets)
+         >>> metrics.compute()
+         {'Classification': {'BinaryAccuracy': tensor(0.3333), 'BinaryF1Score': tensor(0.)},
+          'Regression': {'MeanSquaredError': tensor(0.8333), 'MeanAbsoluteError': tensor(0.6667)}}
+    """
+
+    is_differentiable = False
+
+    def __init__(
+        self,
+        task_metrics: Dict[str, Union[Metric, MetricCollection]],
+    ) -> None:
+        self._check_task_metrics_type(task_metrics)
+        super().__init__()
+        self.task_metrics = nn.ModuleDict(task_metrics)
+
+    @staticmethod
+    def _check_task_metrics_type(task_metrics: Dict[str, Union[Metric, MetricCollection]]) -> None:
+        if not isinstance(task_metrics, dict):
+            raise TypeError(f"Expected argument `task_metrics` to be a dict. Found task_metrics = {task_metrics}")
+
+        for metric in task_metrics.values():
+            if not (isinstance(metric, (Metric, MetricCollection))):
+                raise TypeError(
+                    "Expected each task's metric to be a Metric or a MetricCollection. "
+                    f"Found a metric of type {type(metric)}"
+                )
+
+    def update(self, task_preds: Dict[str, Tensor], task_targets: Dict[str, Tensor]) -> None:
+        """Update each task's metric with its corresponding pred and target.
+
+        Args:
+            task_preds: Dictionary associating each task to a Tensor of pred.
+            task_targets: Dictionary associating each task to a Tensor of target.
+        """
+        if not self.task_metrics.keys() == task_preds.keys() == task_targets.keys():
+            raise ValueError(
+                "Expected arguments `task_preds` and `task_targets` to have the same keys as the wrapped `task_metrics`"
+                f". Found task_preds.keys() = {task_preds.keys()}, task_targets.keys() = {task_targets.keys()} "
+                f"and self.task_metrics.keys() = {self.task_metrics.keys()}"
+            )
+
+        for task_name, metric in self.task_metrics.items():
+            pred = task_preds[task_name]
+            target = task_targets[task_name]
+            metric.update(pred, target)
+
+    def compute(self) -> Dict[str, Any]:
+        """Compute metrics for all tasks."""
+        return {task_name: metric.compute() for task_name, metric in self.task_metrics.items()}
+
+    def forward(self, task_preds: Dict[str, Tensor], task_targets: Dict[str, Tensor]) -> Dict[str, Any]:
+        """Call underlying forward methods for all tasks and return the result as a dictionary."""
+        # This method is overriden because we do not need the complex version defined in Metric, that relies on the
+        # value of full_state_update, and that also accumulates the results. Here, all computations are handled by the
+        # underlying metrics, which all have their own value of full_state_update, and which all accumulate the results
+        # by themselves.
+        return {
+            task_name: metric(task_preds[task_name], task_targets[task_name])
+            for task_name, metric in self.task_metrics.items()
+        }
+
+    def reset(self) -> None:
+        """Reset all underlying metrics."""
+        for metric in self.task_metrics.values():
+            metric.reset()
+        super().reset()
+
+    def _wrap_update(self, update: Callable) -> Callable:
+        """Overwrite to do nothing."""
+        return update
+
+    def _wrap_compute(self, compute: Callable) -> Callable:
+        """Overwrite to do nothing."""
+        return compute
+
+    def plot(
+        self, val: Optional[Union[Dict, Sequence[Dict]]] = None, axes: Optional[Sequence[_AX_TYPE]] = None
+    ) -> Sequence[_PLOT_OUT_TYPE]:
+        """Plot a single or multiple values from the metric.
+
+        All tasks' results are plotted on individual axes.
+
+        Args:
+            val: Either a single result from calling `metric.forward` or `metric.compute` or a list of these results.
+                If no value is provided, will automatically call `metric.compute` and plot that result.
+            axes: Sequence of matplotlib axis objects. If provided, will add the plots to the provided axis objects.
+                If not provided, will create them.
+
+        Returns:
+            Sequence of tuples with Figure and Axes object for each task.
+
+        .. plot::
+            :scale: 75
+
+            >>> # Example plotting a single value
+            >>> import torch
+            >>> from torchmetrics.wrappers import MultitaskWrapper
+            >>> from torchmetrics.regression import MeanSquaredError
+            >>> from torchmetrics.classification import BinaryAccuracy
+            >>>
+            >>> classification_target = torch.tensor([0, 1, 0])
+            >>> regression_target = torch.tensor([2.5, 5.0, 4.0])
+            >>> targets = {"Classification": classification_target, "Regression": regression_target}
+            >>>
+            >>> classification_preds = torch.tensor([0, 0, 1])
+            >>> regression_preds = torch.tensor([3.0, 5.0, 2.5])
+            >>> preds = {"Classification": classification_preds, "Regression": regression_preds}
+            >>>
+            >>> metrics = MultitaskWrapper({
+            ...     "Classification": BinaryAccuracy(),
+            ...     "Regression": MeanSquaredError()
+            ... })
+            >>> metrics.update(preds, targets)
+            >>> value = metrics.compute()
+            >>> fig_, ax_ = metrics.plot(value)
+
+        .. plot::
+            :scale: 75
+
+            >>> # Example plotting multiple values
+            >>> import torch
+            >>> from torchmetrics.wrappers import MultitaskWrapper
+            >>> from torchmetrics.regression import MeanSquaredError
+            >>> from torchmetrics.classification import BinaryAccuracy
+            >>>
+            >>> classification_target = torch.tensor([0, 1, 0])
+            >>> regression_target = torch.tensor([2.5, 5.0, 4.0])
+            >>> targets = {"Classification": classification_target, "Regression": regression_target}
+            >>>
+            >>> classification_preds = torch.tensor([0, 0, 1])
+            >>> regression_preds = torch.tensor([3.0, 5.0, 2.5])
+            >>> preds = {"Classification": classification_preds, "Regression": regression_preds}
+            >>>
+            >>> metrics = MultitaskWrapper({
+            ...     "Classification": BinaryAccuracy(),
+            ...     "Regression": MeanSquaredError()
+            ... })
+            >>> values = []
+            >>> for _ in range(10):
+            ...     values.append(metrics(preds, targets))
+            >>> fig_, ax_ = metrics.plot(values)
+        """
+        if axes is not None:
+            if not isinstance(axes, Sequence):
+                raise TypeError(f"Expected argument `axes` to be a Sequence. Found type(axes) = {type(axes)}")
+
+            if not all(isinstance(ax, _AX_TYPE) for ax in axes):
+                raise TypeError("Expected each ax in argument `axes` to be a matplotlib axis object")
+
+            if len(axes) != len(self.task_metrics):
+                raise ValueError(
+                    "Expected argument `axes` to be a Sequence of the same length as the number of tasks."
+                    f"Found len(axes) = {len(axes)} and {len(self.task_metrics)} tasks"
+                )
+
+        val = val if val is not None else self.compute()
+        fig_axs = []
+        for i, (task_name, task_metric) in enumerate(self.task_metrics.items()):
+            ax = axes[i] if axes is not None else None
+            if isinstance(val, Dict):
+                f, a = task_metric.plot(val[task_name], ax=ax)
+            elif isinstance(val, Sequence):
+                f, a = task_metric.plot([v[task_name] for v in val], ax=ax)
+            else:
+                raise TypeError(
+                    "Expected argument `val` to be None or of type Dict or Sequence[Dict]. "
+                    f"Found type(val)= {type(val)}"
+                )
+            fig_axs.append((f, a))
+        return fig_axs


Test patch:
diff --git a/tests/unittests/wrappers/test_multitask.py b/tests/unittests/wrappers/test_multitask.py
new file mode 100644
--- /dev/null
+++ b/tests/unittests/wrappers/test_multitask.py
@@ -0,0 +1,208 @@
+# Copyright The Lightning team.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+# this is just a bypass for this module name collision with build-in one
+import re
+
+import pytest
+import torch
+
+from torchmetrics import MetricCollection
+from torchmetrics.classification import BinaryAccuracy, BinaryF1Score
+from torchmetrics.regression import MeanAbsoluteError, MeanSquaredError
+from torchmetrics.wrappers import MultitaskWrapper
+from unittests import BATCH_SIZE, NUM_BATCHES
+from unittests.helpers import seed_all
+
+seed_all(42)
+
+_regression_preds = torch.rand(NUM_BATCHES, BATCH_SIZE)
+_regression_target = torch.rand(NUM_BATCHES, BATCH_SIZE)
+_regression_preds_2 = torch.rand(NUM_BATCHES, BATCH_SIZE)
+_regression_target_2 = torch.rand(NUM_BATCHES, BATCH_SIZE)
+_classification_preds = torch.randint(high=2, size=(NUM_BATCHES, BATCH_SIZE))
+_classification_target = torch.randint(high=2, size=(NUM_BATCHES, BATCH_SIZE))
+_multitask_preds = {"Classification": _classification_preds, "Regression": _regression_preds}
+_multitask_targets = {"Classification": _classification_target, "Regression": _regression_target}
+
+
+def _dict_results_same_as_individual_results(classification_results, regression_results, multitask_results):
+    return (
+        multitask_results["Classification"] == classification_results
+        and multitask_results["Regression"] == regression_results
+    )
+
+
+def _multitask_same_as_individual_tasks(classification_metric, regression_metric, multitask_metrics):
+    """Update classification and regression metrics individually and together using a multitask wrapper.
+
+    Return True if the results are the same.
+    """
+    classification_metric.update(_classification_preds, _classification_target)
+    regression_metric.update(_regression_preds, _regression_target)
+    multitask_metrics.update(_multitask_preds, _multitask_targets)
+
+    classification_results = classification_metric.compute()
+    regression_results = regression_metric.compute()
+    multitask_results = multitask_metrics.compute()
+
+    return _dict_results_same_as_individual_results(classification_results, regression_results, multitask_results)
+
+
+def test_errors_on_wrong_input():
+    """Check that type errors are raised when inputs are of the wrong type."""
+    with pytest.raises(TypeError, match="Expected *"):
+        MultitaskWrapper(1)
+
+    with pytest.raises(TypeError, match="Expected *"):
+        MultitaskWrapper(None)
+
+    with pytest.raises(TypeError, match="Expected *"):
+        MultitaskWrapper({"Classification": 1})
+
+
+def test_error_on_wrong_keys():
+    """Check that ValueError is raised when the sets of keys of the task metrics, preds, and targets do not match."""
+    multitask_metrics = MultitaskWrapper(
+        {
+            "Classification": BinaryAccuracy(),
+            "Regression": MeanSquaredError(),
+        }
+    )
+
+    # Classification preds, but not regression preds
+    wrong_key_preds = {"Classification": _classification_preds}
+
+    # Classification targets, but not regression targets
+    wrong_key_targets = {"Classification": _classification_target}
+
+    # Classification metric, but not regression metric
+    wrong_key_multitask_metrics = MultitaskWrapper(
+        {
+            "Classification": BinaryAccuracy(),
+        }
+    )
+
+    with pytest.raises(
+        ValueError,
+        match=re.escape(
+            "Expected arguments `task_preds` and `task_targets` to have the same keys as the wrapped `task_metrics`. "
+            "Found task_preds.keys() = dict_keys(['Classification']), task_targets.keys() = "
+            "dict_keys(['Classification', 'Regression']) and self.task_metrics.keys() = "
+            "odict_keys(['Classification', 'Regression'])"
+        ),
+    ):
+        multitask_metrics.update(wrong_key_preds, _multitask_targets)
+
+    with pytest.raises(
+        ValueError,
+        match=re.escape(
+            "Expected arguments `task_preds` and `task_targets` to have the same keys as the wrapped `task_metrics`. "
+            "Found task_preds.keys() = dict_keys(['Classification', 'Regression']), task_targets.keys() = "
+            "dict_keys(['Classification']) and self.task_metrics.keys() = odict_keys(['Classification', 'Regression'])"
+        ),
+    ):
+        multitask_metrics.update(_multitask_preds, wrong_key_targets)
+
+    with pytest.raises(
+        ValueError,
+        match=re.escape(
+            "Expected arguments `task_preds` and `task_targets` to have the same keys as the wrapped `task_metrics`. "
+            "Found task_preds.keys() = dict_keys(['Classification', 'Regression']), task_targets.keys() = "
+            "dict_keys(['Classification', 'Regression']) and self.task_metrics.keys() = odict_keys(['Classification'])"
+        ),
+    ):
+        wrong_key_multitask_metrics.update(_multitask_preds, _multitask_targets)
+
+
+def test_basic_multitask():
+    """Check that wrapping some Metrics in a MultitaskWrapper is the same as computing them individually."""
+    classification_metric = BinaryAccuracy()
+    regression_metric = MeanSquaredError()
+    multitask_metrics = MultitaskWrapper({"Classification": BinaryAccuracy(), "Regression": MeanSquaredError()})
+
+    assert _multitask_same_as_individual_tasks(classification_metric, regression_metric, multitask_metrics)
+
+
+def test_metric_collection_multitask():
+    """Check that wrapping some MetricCollections in a MultitaskWrapper is the same as computing them individually."""
+    classification_metric = MetricCollection([BinaryAccuracy(), BinaryF1Score()])
+    regression_metric = MetricCollection([MeanSquaredError(), MeanAbsoluteError()])
+    multitask_metrics = MultitaskWrapper(
+        {
+            "Classification": MetricCollection([BinaryAccuracy(), BinaryF1Score()]),
+            "Regression": MetricCollection([MeanSquaredError(), MeanAbsoluteError()]),
+        }
+    )
+
+    assert _multitask_same_as_individual_tasks(classification_metric, regression_metric, multitask_metrics)
+
+
+def test_forward():
+    """Check that the forward method works as expected."""
+    classification_metric = BinaryAccuracy()
+    regression_metric = MeanSquaredError()
+    multitask_metrics = MultitaskWrapper({"Classification": BinaryAccuracy(), "Regression": MeanSquaredError()})
+
+    classification_results = classification_metric(_classification_preds, _classification_target)
+    regression_results = regression_metric(_regression_preds, _regression_target)
+    multitask_results = multitask_metrics(_multitask_preds, _multitask_targets)
+
+    assert _dict_results_same_as_individual_results(classification_results, regression_results, multitask_results)
+
+
+def test_nested_multitask_wrapper():
+    """Check that nested multitask wrappers work as expected."""
+    classification_metric = BinaryAccuracy()
+    regression_position_metric = MeanSquaredError()
+    regression_size_metric = MeanAbsoluteError()
+    multitask_metrics = MultitaskWrapper(
+        {
+            "Classification": BinaryAccuracy(),
+            "Regression": MultitaskWrapper(
+                {
+                    "Position": MeanSquaredError(),
+                    "Size": MeanAbsoluteError(),
+                }
+            ),
+        }
+    )
+
+    multitask_preds = {
+        "Classification": _classification_preds,
+        "Regression": {
+            "Position": _regression_preds,
+            "Size": _regression_preds_2,
+        },
+    }
+
+    multitask_targets = {
+        "Classification": _classification_target,
+        "Regression": {
+            "Position": _regression_target,
+            "Size": _regression_target_2,
+        },
+    }
+
+    classification_metric.update(_classification_preds, _classification_target)
+    regression_position_metric.update(_regression_preds, _regression_target)
+    regression_size_metric.update(_regression_preds_2, _regression_target_2)
+    multitask_metrics.update(multitask_targets, multitask_preds)
+
+    classification_results = classification_metric.compute()
+    regression_position_results = regression_position_metric.compute()
+    regression_size_results = regression_size_metric.compute()
+    regression_results = {"Position": regression_position_results, "Size": regression_size_results}
+    multitask_results = multitask_metrics.compute()
+
+    assert _dict_results_same_as_individual_results(classification_results, regression_results, multitask_results)