Repository: pytorch/ignite

Issue:
Bug in recursive attach of MetricsLambda
As a result of #385, the recursive attach of MetricsLambda is not working as intended. 

The code below throws this error: `NotComputableError: Accuracy must have at least one example before it can be computed.`

```python
import torch

from ignite.metrics import Accuracy, Precision, Recall, MetricsLambda
from ignite.engine import Engine

accuracy_1 = Accuracy()
accuracy_2 = Accuracy()
mean_accuracy = (accuracy_1 + accuracy_2) / 2


metrics = {
#     "a1": accuracy_1,
#     "a2": accuracy_2,    
    "mean accuracy": mean_accuracy,
}


y_pred = torch.randint(0, 2, size=(15, 10, 4)).float()
y = torch.randint(0, 2, size=(15, 10, 4)).long()

def update_fn(engine, batch):
    y_pred, y = batch
    return y_pred, y

validator = Engine(update_fn)

for name, metric in metrics.items():
    metric.attach(validator, name)

def data(y_pred, y):
    for i in range(y_pred.shape[0]):
        yield (y_pred[i], y[i])

d = data(y_pred, y)
state = validator.run(d, max_epochs=1)

print(state.metrics)
```

cc @vfdev-5 @amitibo 

Discussion:
vfdev-5: Let's wait @amitibo comments before merging this.

Patch:
diff --git a/docs/source/contrib/metrics.rst b/docs/source/contrib/metrics.rst
index f92093a7..74396e6c 100644
--- a/docs/source/contrib/metrics.rst
+++ b/docs/source/contrib/metrics.rst
@@ -19,7 +19,50 @@ metrics useful for regression tasks. Definitions of metrics are based on `Botchk
 .. _`Botchkarev 2018`:
         https://arxiv.org/ftp/arxiv/papers/1809/1809.03006.pdf
 
+Complete list of metrics:
 
-.. automodule:: ignite.contrib.metrics.regression
-   :members:
-   :imported-members:
+    - :class:`~ignite.contrib.metrics.regression.CanberraMetric`
+    - :class:`~ignite.contrib.metrics.regression.FractionalAbsoluteError`
+    - :class:`~ignite.contrib.metrics.regression.FractionalBias`
+    - :class:`~ignite.contrib.metrics.regression.GeometricMeanAbsoluteError`
+    - :class:`~ignite.contrib.metrics.regression.GeometricMeanRelativeAbsoluteError`
+    - :class:`~ignite.contrib.metrics.regression.ManhattanDistance`
+    - :class:`~ignite.contrib.metrics.regression.MaximumAbsoluteError`
+    - :class:`~ignite.contrib.metrics.regression.MeanAbsoluteRelativeError`
+    - :class:`~ignite.contrib.metrics.regression.MeanError`
+    - :class:`~ignite.contrib.metrics.regression.MeanNormalizedBias`
+    - :class:`~ignite.contrib.metrics.regression.MedianAbsoluteError`
+    - :class:`~ignite.contrib.metrics.regression.MedianAbsolutePercentageError`
+    - :class:`~ignite.contrib.metrics.regression.MedianRelativeAbsoluteError`
+    - :class:`~ignite.contrib.metrics.regression.WaveHedgesDistance`
+
+
+.. currentmodule:: ignite.contrib.metrics.regression
+
+.. autoclass:: CanberraMetric
+
+.. autoclass:: FractionalAbsoluteError
+
+.. autoclass:: FractionalBias
+
+.. autoclass:: GeometricMeanAbsoluteError
+
+.. autoclass:: GeometricMeanRelativeAbsoluteError
+
+.. autoclass:: ManhattanDistance
+
+.. autoclass:: MaximumAbsoluteError
+
+.. autoclass:: MeanAbsoluteRelativeError
+
+.. autoclass:: MeanError
+
+.. autoclass:: MeanNormalizedBias
+
+.. autoclass:: MedianAbsoluteError
+
+.. autoclass:: MedianAbsolutePercentageError
+
+.. autoclass:: MedianRelativeAbsoluteError
+
+.. autoclass:: WaveHedgesDistance
diff --git a/ignite/contrib/metrics/average_precision.py b/ignite/contrib/metrics/average_precision.py
index 03148dda..08c568e8 100644
--- a/ignite/contrib/metrics/average_precision.py
+++ b/ignite/contrib/metrics/average_precision.py
@@ -1,16 +1,13 @@
-from functools import partial
 from ignite.metrics import EpochMetric
 
 
-def average_precision_compute_fn(y_preds, y_targets, activation=None):
+def average_precision_compute_fn(y_preds, y_targets):
     try:
         from sklearn.metrics import average_precision_score
     except ImportError:
         raise RuntimeError("This contrib module requires sklearn to be installed.")
 
     y_true = y_targets.numpy()
-    if activation is not None:
-        y_preds = activation(y_preds)
     y_pred = y_preds.numpy()
     return average_precision_score(y_true, y_pred)
 
@@ -21,14 +18,23 @@ class AveragePrecision(EpochMetric):
     sklearn.metrics.average_precision_score.html#sklearn.metrics.average_precision_score>`_ .
 
     Args:
-        activation (callable, optional): optional function to apply on prediction tensors,
-            e.g. `activation=torch.sigmoid` to transform logits.
         output_transform (callable, optional): a callable that is used to transform the
             :class:`~ignite.engine.Engine`'s `process_function`'s output into the
             form expected by the metric. This can be useful if, for example, you have a multi-output model and
             you want to compute the metric with respect to one of the outputs.
 
+    AveragePrecision expects y to be comprised of 0's and 1's. y_pred must either be probability estimates or
+    confidence values. To apply an activation to y_pred, use output_transform as shown below:
+
+    .. code-block:: python
+
+        def activated_output_transform(output):
+            y_pred, y = output
+            y_pred = torch.softmax(y_pred)
+            return y_pred, y
+
+        avg_precision = AveragePrecision(activated_output_transform)
+
     """
     def __init__(self, activation=None, output_transform=lambda x: x):
-        super(AveragePrecision, self).__init__(partial(average_precision_compute_fn, activation=activation),
-                                               output_transform=output_transform)
+        super(AveragePrecision, self).__init__(average_precision_compute_fn, output_transform=output_transform)
diff --git a/ignite/contrib/metrics/regression/__init__.py b/ignite/contrib/metrics/regression/__init__.py
index 1843b096..d486f7ca 100644
--- a/ignite/contrib/metrics/regression/__init__.py
+++ b/ignite/contrib/metrics/regression/__init__.py
@@ -8,3 +8,7 @@ from ignite.contrib.metrics.regression.canberra_metric import CanberraMetric
 from ignite.contrib.metrics.regression.fractional_absolute_error import FractionalAbsoluteError
 from ignite.contrib.metrics.regression.wave_hedges_distance import WaveHedgesDistance
 from ignite.contrib.metrics.regression.geometric_mean_absolute_error import GeometricMeanAbsoluteError
+from ignite.contrib.metrics.regression.median_absolute_error import MedianAbsoluteError
+from ignite.contrib.metrics.regression.median_relative_absolute_error import MedianRelativeAbsoluteError
+from ignite.contrib.metrics.regression.median_absolute_percentage_error import MedianAbsolutePercentageError
+from ignite.contrib.metrics.regression.geometric_mean_relative_absolute_error import GeometricMeanRelativeAbsoluteError
diff --git a/ignite/contrib/metrics/regression/_base.py b/ignite/contrib/metrics/regression/_base.py
index 6686c87e..aa178e31 100644
--- a/ignite/contrib/metrics/regression/_base.py
+++ b/ignite/contrib/metrics/regression/_base.py
@@ -1,12 +1,16 @@
 from abc import abstractmethod
 
-from ignite.metrics import Metric
+import warnings
+
+import torch
+
+from ignite.metrics import Metric, EpochMetric
 
 
 class _BaseRegression(Metric):
     # Base class for all regression metrics
     # `update` method check the shapes and call internal overloaded
-    # method `_update`
+    # method `_update`.
 
     def update(self, output):
         y_pred, y = output
@@ -33,3 +37,32 @@ class _BaseRegression(Metric):
     @abstractmethod
     def _update(self, output):
         pass
+
+
+class _BaseRegressionEpoch(_BaseRegression, EpochMetric):
+    # Base class for all median-based regression metrics
+    # `update` method check the shapes and call internal overloaded method `_update`.
+    # Class internally stores complete history of predictions and targets of type float32.
+
+    def __init__(self, compute_fn, output_transform=lambda x: x):
+        EpochMetric.__init__(self, compute_fn=compute_fn, output_transform=output_transform)
+
+    def reset(self):
+        self._predictions = torch.tensor([], dtype=torch.float32)
+        self._targets = torch.tensor([], dtype=torch.float32)
+
+    def _update(self, output):
+        y_pred, y = output
+        y_pred = y_pred.type_as(self._predictions)
+        y = y.type_as(self._targets)
+
+        self._predictions = torch.cat([self._predictions, y_pred], dim=0)
+        self._targets = torch.cat([self._targets, y], dim=0)
+
+        # Check once the signature and execution of compute_fn
+        if self._predictions.shape == y_pred.shape:
+            try:
+                self.compute_fn(self._predictions, self._targets)
+            except Exception as e:
+                warnings.warn("Probably, there can be a problem with `compute_fn`:\n {}".format(e),
+                              RuntimeWarning)
diff --git a/ignite/contrib/metrics/regression/geometric_mean_relative_absolute_error.py b/ignite/contrib/metrics/regression/geometric_mean_relative_absolute_error.py
new file mode 100644
index 00000000..93940dd2
--- /dev/null
+++ b/ignite/contrib/metrics/regression/geometric_mean_relative_absolute_error.py
@@ -0,0 +1,43 @@
+from __future__ import division
+
+import torch
+
+from ignite.contrib.metrics.regression._base import _BaseRegression
+
+
+class GeometricMeanRelativeAbsoluteError(_BaseRegression):
+    r"""
+    Calculates the Geometric Mean Relative Absolute Error:
+
+    :math:`\text{GMRAE} = \exp(\frac{1}{n}\sum_{j=1}^n \ln\frac{|A_j - P_j|}{|A_j - \bar{A}|})`
+
+    where :math:`A_j` is the ground truth and :math:`P_j` is the predicted value.
+
+    More details can be found in `Botchkarev 2018`__.
+
+    - `update` must receive output of the form `(y_pred, y)`.
+    - `y` and `y_pred` must be of same shape `(N, )` or `(N, 1)`.
+
+
+    __ https://arxiv.org/abs/1809.03006
+
+    """
+    def reset(self):
+        self._sum_y = 0.0
+        self._num_examples = 0
+        self._sum_of_errors = 0.0
+
+    def _update(self, output):
+        y_pred, y = output
+        self._sum_y += y.sum()
+        self._num_examples += y.shape[0]
+        y_mean = self._sum_y / self._num_examples
+        numerator = torch.abs(y.view_as(y_pred) - y_pred)
+        denominator = torch.abs(y.view_as(y_pred) - y_mean)
+        self._sum_of_errors += torch.log(numerator / denominator).sum()
+
+    def compute(self):
+        if self._num_examples == 0:
+            raise NotComputableError('GeometricMeanRelativeAbsoluteError must have at least '
+                                     'one example before it can be computed.')
+        return torch.exp(torch.mean(self._sum_of_errors / self._num_examples)).item()
diff --git a/ignite/contrib/metrics/regression/median_absolute_error.py b/ignite/contrib/metrics/regression/median_absolute_error.py
new file mode 100644
index 00000000..f35f0b3d
--- /dev/null
+++ b/ignite/contrib/metrics/regression/median_absolute_error.py
@@ -0,0 +1,34 @@
+import torch
+
+from ignite.contrib.metrics.regression._base import _BaseRegressionEpoch
+
+
+def median_absolute_error_compute_fn(y_pred, y):
+    e = torch.abs(y.view_as(y_pred) - y_pred)
+    return torch.median(e).item()
+
+
+class MedianAbsoluteError(_BaseRegressionEpoch):
+    r"""
+    Calculates the Median Absolute Error:
+
+    :math:`\text{MdAE} = \text{MD}_{j=1,n} \left( |A_j - P_j| \right)`,
+
+    where :math:`A_j` is the ground truth and :math:`P_j` is the predicted value.
+
+    More details can be found in `Botchkarev 2018`__.
+
+    - `update` must receive output of the form `(y_pred, y)`.
+    - `y` and `y_pred` must be of same shape `(N, )` or `(N, 1)` and of type `float32`.
+
+    .. warning::
+
+        Current implementation stores all input data (output and target) in as tensors before computing a metric.
+        This can potentially lead to a memory error if the input data is larger than available RAM.
+
+
+    __ https://arxiv.org/abs/1809.03006
+
+    """
+    def __init__(self, output_transform=lambda x: x):
+        super(MedianAbsoluteError, self).__init__(median_absolute_error_compute_fn, output_transform)
diff --git a/ignite/contrib/metrics/regression/median_absolute_percentage_error.py b/ignite/contrib/metrics/regression/median_absolute_percentage_error.py
new file mode 100644
index 00000000..d3b9741c
--- /dev/null
+++ b/ignite/contrib/metrics/regression/median_absolute_percentage_error.py
@@ -0,0 +1,37 @@
+from __future__ import division
+
+import torch
+
+from ignite.contrib.metrics.regression._base import _BaseRegressionEpoch
+
+
+def median_absolute_percentage_error_compute_fn(y_pred, y):
+    e = torch.abs(y.view_as(y_pred) - y_pred) / torch.abs(y.view_as(y_pred))
+    return 100.0 * torch.median(e).item()
+
+
+class MedianAbsolutePercentageError(_BaseRegressionEpoch):
+    r"""
+    Calculates the Median Absolute Percentage Error:
+
+    :math:`\text{MdAPE} = 100 \cdot \text{MD}_{j=1,n} \left( \frac{|A_j - P_j|}{|A_j|} \right)`,
+
+    where :math:`A_j` is the ground truth and :math:`P_j` is the predicted value.
+
+    More details can be found in `Botchkarev 2018`__.
+
+    - `update` must receive output of the form `(y_pred, y)`.
+    - `y` and `y_pred` must be of same shape `(N, )` or `(N, 1)` and of type `float32`.
+
+    .. warning::
+
+        Current implementation stores all input data (output and target) in as tensors before computing a metric.
+        This can potentially lead to a memory error if the input data is larger than available RAM.
+
+
+    __ https://arxiv.org/abs/1809.03006
+
+    """
+    def __init__(self, output_transform=lambda x: x):
+        super(MedianAbsolutePercentageError, self).__init__(median_absolute_percentage_error_compute_fn,
+                                                            output_transform)
diff --git a/ignite/contrib/metrics/regression/median_relative_absolute_error.py b/ignite/contrib/metrics/regression/median_relative_absolute_error.py
new file mode 100644
index 00000000..32a18d53
--- /dev/null
+++ b/ignite/contrib/metrics/regression/median_relative_absolute_error.py
@@ -0,0 +1,37 @@
+from __future__ import division
+
+import torch
+
+from ignite.contrib.metrics.regression._base import _BaseRegressionEpoch
+
+
+def median_relative_absolute_error_compute_fn(y_pred, y):
+    e = torch.abs(y.view_as(y_pred) - y_pred) / torch.abs(y.view_as(y_pred) - torch.mean(y))
+    return torch.median(e).item()
+
+
+class MedianRelativeAbsoluteError(_BaseRegressionEpoch):
+    r"""
+    Calculates the Median Relative Absolute Error:
+
+    :math:`\text{MdRAE} = \text{MD}_{j=1,n} \left( \frac{|A_j - P_j|}{|A_j - \bar{A}|} \right)`,
+
+    where :math:`A_j` is the ground truth and :math:`P_j` is the predicted value.
+
+    More details can be found in `Botchkarev 2018`__.
+
+    - `update` must receive output of the form `(y_pred, y)`.
+    - `y` and `y_pred` must be of same shape `(N, )` or `(N, 1)` and of type `float32`.
+
+    .. warning::
+
+        Current implementation stores all input data (output and target) in as tensors before computing a metric.
+        This can potentially lead to a memory error if the input data is larger than available RAM.
+
+
+    __ https://arxiv.org/abs/1809.03006
+
+    """
+    def __init__(self, output_transform=lambda x: x):
+        super(MedianRelativeAbsoluteError, self).__init__(median_relative_absolute_error_compute_fn,
+                                                          output_transform)
diff --git a/ignite/contrib/metrics/roc_auc.py b/ignite/contrib/metrics/roc_auc.py
index ba2acc37..25e39cdd 100644
--- a/ignite/contrib/metrics/roc_auc.py
+++ b/ignite/contrib/metrics/roc_auc.py
@@ -1,16 +1,13 @@
-from functools import partial
 from ignite.metrics import EpochMetric
 
 
-def roc_auc_compute_fn(y_preds, y_targets, activation=None):
+def roc_auc_compute_fn(y_preds, y_targets):
     try:
         from sklearn.metrics import roc_auc_score
     except ImportError:
         raise RuntimeError("This contrib module requires sklearn to be installed.")
 
     y_true = y_targets.numpy()
-    if activation is not None:
-        y_preds = activation(y_preds)
     y_pred = y_preds.numpy()
     return roc_auc_score(y_true, y_pred)
 
@@ -22,14 +19,23 @@ class ROC_AUC(EpochMetric):
     sklearn.metrics.roc_auc_score.html#sklearn.metrics.roc_auc_score>`_ .
 
     Args:
-        activation (callable, optional): optional function to apply on prediction tensors,
-            e.g. `activation=torch.sigmoid` to transform logits.
         output_transform (callable, optional): a callable that is used to transform the
             :class:`~ignite.engine.Engine`'s `process_function`'s output into the
             form expected by the metric. This can be useful if, for example, you have a multi-output model and
             you want to compute the metric with respect to one of the outputs.
 
+    ROC_AUC expects y to be comprised of 0's and 1's. y_pred must either be probability estimates or confidence
+    values. To apply an activation to y_pred, use output_transform as shown below:
+
+    .. code-block:: python
+
+        def activated_output_transform(output):
+            y_pred, y = output
+            y_pred = torch.sigmoid(y_pred)
+            return y_pred, y
+
+        roc_auc = ROC_AUC(activated_output_transform)
+
     """
-    def __init__(self, activation=None, output_transform=lambda x: x):
-        super(ROC_AUC, self).__init__(partial(roc_auc_compute_fn, activation=activation),
-                                      output_transform=output_transform)
+    def __init__(self, output_transform=lambda x: x):
+        super(ROC_AUC, self).__init__(roc_auc_compute_fn, output_transform=output_transform)
diff --git a/ignite/metrics/metrics_lambda.py b/ignite/metrics/metrics_lambda.py
index 22827521..8c9ca090 100644
--- a/ignite/metrics/metrics_lambda.py
+++ b/ignite/metrics/metrics_lambda.py
@@ -53,12 +53,18 @@ class MetricsLambda(Metric):
         materialized = [i.compute() if isinstance(i, Metric) else i for i in self.args]
         return self.function(*materialized)
 
-    def attach(self, engine, name):
-        # recursively attach all its dependencies
+    def _internal_attach(self, engine):
         for index, metric in enumerate(self.args):
-            if isinstance(metric, Metric):
+            if isinstance(metric, MetricsLambda):
+                metric._internal_attach(engine)
+            elif isinstance(metric, Metric):
                 if not engine.has_event_handler(metric.started, Events.EPOCH_STARTED):
                     engine.add_event_handler(Events.EPOCH_STARTED, metric.started)
                 if not engine.has_event_handler(metric.iteration_completed, Events.ITERATION_COMPLETED):
                     engine.add_event_handler(Events.ITERATION_COMPLETED, metric.iteration_completed)
-        super(MetricsLambda, self).attach(engine, name)
+
+    def attach(self, engine, name):
+        # recursively attach all its dependencies
+        self._internal_attach(engine)
+        # attach only handler on EPOCH_COMPLETED
+        engine.add_event_handler(Events.EPOCH_COMPLETED, self.completed, name)


Test patch:
diff --git a/tests/ignite/contrib/metrics/regression/test__base.py b/tests/ignite/contrib/metrics/regression/test__base.py
new file mode 100644
index 00000000..5a65fbca
--- /dev/null
+++ b/tests/ignite/contrib/metrics/regression/test__base.py
@@ -0,0 +1,103 @@
+import torch
+import numpy as np
+import pytest
+from ignite.contrib.metrics.regression._base import _BaseRegression, _BaseRegressionEpoch
+
+
+def test_base_regression_shapes():
+
+    class L1(_BaseRegression):
+        def reset(self):
+            self._sum_of_errors = 0.0
+
+        def _update(self, output):
+            y_pred, y = output
+            errors = torch.abs(y.view_as(y_pred) - y_pred)
+            self._sum_of_errors += torch.sum(errors).item()
+
+        def compute(self):
+            return self._sum_of_errors
+
+    m = L1()
+
+    with pytest.raises(ValueError):
+        m.update((torch.rand(4, 1, 2),
+                  torch.rand(4, 1)))
+
+    with pytest.raises(ValueError):
+        m.update((torch.rand(4, 1),
+                  torch.rand(4, 1, 2)))
+
+    with pytest.raises(ValueError):
+        m.update((torch.rand(4, 1, 2),
+                  torch.rand(4,)))
+
+    with pytest.raises(ValueError):
+        m.update((torch.rand(4,),
+                  torch.rand(4, 1, 2)))
+
+    with pytest.raises(ValueError):
+        m.update((torch.rand(4, 3),
+                  torch.rand(4, 1)))
+
+    with pytest.raises(ValueError):
+        m.update((torch.rand(4, 1),
+                  torch.rand(4, 3)))
+
+    with pytest.raises(ValueError):
+        m.update((torch.rand(4, 7),
+                  torch.rand(4,)))
+
+    with pytest.raises(ValueError):
+        m.update((torch.rand(4,),
+                  torch.rand(4, 7)))
+
+
+def test_base_regression_epoch_shapes():
+
+    def compute_fn(y_pred, y):
+        return 0.0
+
+    class ZeroEpoch(_BaseRegressionEpoch):
+        def __init__(self, output_transform=lambda x: x):
+            super(ZeroEpoch, self).__init__(compute_fn, output_transform)
+
+    m = ZeroEpoch()
+
+    with pytest.raises(ValueError):
+        m.update((torch.rand(4, 1, 2),
+                  torch.rand(4, 1)))
+
+    with pytest.raises(ValueError):
+        m.update((torch.rand(4, 1),
+                  torch.rand(4, 1, 2)))
+
+    with pytest.raises(ValueError):
+        m.update((torch.rand(4, 1, 2),
+                  torch.rand(4,)))
+
+    with pytest.raises(ValueError):
+        m.update((torch.rand(4,),
+                  torch.rand(4, 1, 2)))
+
+    with pytest.raises(ValueError):
+        m.update((torch.rand(4, 3),
+                  torch.rand(4, 1)))
+
+    with pytest.raises(ValueError):
+        m.update((torch.rand(4, 1),
+                  torch.rand(4, 3)))
+
+    with pytest.raises(ValueError):
+        m.update((torch.rand(4, 7),
+                  torch.rand(4,)))
+
+    with pytest.raises(ValueError):
+        m.update((torch.rand(4,),
+                  torch.rand(4, 7)))
+
+
+def test_base_regression_compute_fn():
+    # Wrong compute function
+    with pytest.raises(TypeError):
+        _BaseRegressionEpoch(12345)
diff --git a/tests/ignite/contrib/metrics/regression/test_geometric_mean_relative_absolute_error.py b/tests/ignite/contrib/metrics/regression/test_geometric_mean_relative_absolute_error.py
new file mode 100644
index 00000000..032cfd64
--- /dev/null
+++ b/tests/ignite/contrib/metrics/regression/test_geometric_mean_relative_absolute_error.py
@@ -0,0 +1,119 @@
+import torch
+import numpy as np
+import pytest
+from ignite.engine import Engine
+from ignite.contrib.metrics.regression import GeometricMeanRelativeAbsoluteError
+
+
+def test_wrong_input_shapes():
+    m = GeometricMeanRelativeAbsoluteError()
+
+    with pytest.raises(ValueError):
+        m.update((torch.rand(4, 1, 2),
+                  torch.rand(4, 1)))
+
+    with pytest.raises(ValueError):
+        m.update((torch.rand(4, 1),
+                  torch.rand(4, 1, 2)))
+
+    with pytest.raises(ValueError):
+        m.update((torch.rand(4, 1, 2),
+                  torch.rand(4,)))
+
+    with pytest.raises(ValueError):
+        m.update((torch.rand(4,),
+                  torch.rand(4, 1, 2)))
+
+
+def test_geometric_mean_relative_absolute_error():
+    size = 51
+    np_y_pred = np.random.rand(size,)
+    np_y = np.random.rand(size,)
+    np_gmrae = np.exp(np.log(np.abs(np_y - np_y_pred) / np.abs(np_y - np_y.mean())).mean())
+
+    m = GeometricMeanRelativeAbsoluteError()
+    y_pred = torch.from_numpy(np_y_pred)
+    y = torch.from_numpy(np_y)
+
+    m.reset()
+    m.update((y_pred, y))
+
+    assert np_gmrae == pytest.approx(m.compute())
+
+
+def test_geometric_mean_relative_absolute_error_2():
+
+    np.random.seed(1)
+    size = 105
+    np_y_pred = np.random.rand(size, 1)
+    np_y = np.random.rand(size, 1)
+    np.random.shuffle(np_y)
+
+    np_y_sum = 0
+    num_examples = 0
+    num_sum_of_errors = 0
+    np_gmrae = 0
+
+    m = GeometricMeanRelativeAbsoluteError()
+    y_pred = torch.from_numpy(np_y_pred)
+    y = torch.from_numpy(np_y)
+
+    m.reset()
+    n_iters = 15
+    batch_size = size // n_iters
+    for i in range(n_iters + 1):
+        idx = i * batch_size
+        np_y_i = np_y[idx: idx + batch_size]
+        np_y_pred_i = np_y_pred[idx: idx + batch_size]
+
+        np_y_sum += np_y_i.sum()
+        num_examples += np_y_i.shape[0]
+        np_mean = np_y_sum / num_examples
+
+        np_gmrae += np.log(np.abs(np_y_i - np_y_pred_i) / np.abs(np_y_i - np_mean)).sum()
+        m.update((y_pred[idx: idx + batch_size], y[idx: idx + batch_size]))
+
+    assert np.exp(np_gmrae / num_examples) == pytest.approx(m.compute())
+
+
+def test_integration_geometric_mean_relative_absolute_error_with_output_transform():
+
+    np.random.seed(1)
+    size = 105
+    np_y_pred = np.random.rand(size, 1)
+    np_y = np.random.rand(size, 1)
+    np.random.shuffle(np_y)
+
+    np_y_sum = 0
+    num_examples = 0
+    num_sum_of_errors = 0
+    np_gmrae = 0
+
+    n_iters = 15
+    batch_size = size // n_iters
+    for i in range(n_iters + 1):
+        idx = i * batch_size
+        np_y_i = np_y[idx: idx + batch_size]
+        np_y_pred_i = np_y_pred[idx: idx + batch_size]
+
+        np_y_sum += np_y_i.sum()
+        num_examples += np_y_i.shape[0]
+        np_mean = np_y_sum / num_examples
+
+        np_gmrae += np.log(np.abs(np_y_i - np_y_pred_i) / np.abs(np_y_i - np_mean)).sum()
+
+    def update_fn(engine, batch):
+        idx = (engine.state.iteration - 1) * batch_size
+        y_true_batch = np_y[idx:idx + batch_size]
+        y_pred_batch = np_y_pred[idx:idx + batch_size]
+        return idx, torch.from_numpy(y_pred_batch), torch.from_numpy(y_true_batch)
+
+    engine = Engine(update_fn)
+
+    m = GeometricMeanRelativeAbsoluteError(output_transform=lambda x: (x[1], x[2]))
+    m.attach(engine, 'geometric_mean_relative_absolute_error')
+
+    data = list(range(size // batch_size))
+    gmrae = engine.run(data, max_epochs=1).metrics['geometric_mean_relative_absolute_error']
+
+    assert np.exp(np_gmrae / num_examples) == pytest.approx(m.compute())
diff --git a/tests/ignite/contrib/metrics/regression/test_median_absolute_error.py b/tests/ignite/contrib/metrics/regression/test_median_absolute_error.py
new file mode 100644
index 00000000..1712adf3
--- /dev/null
+++ b/tests/ignite/contrib/metrics/regression/test_median_absolute_error.py
@@ -0,0 +1,98 @@
+import torch
+import numpy as np
+import pytest
+from ignite.engine import Engine
+from ignite.contrib.metrics.regression import MedianAbsoluteError
+
+
+def test_wrong_input_shapes():
+    m = MedianAbsoluteError()
+
+    with pytest.raises(ValueError):
+        m.update((torch.rand(4, 1, 2),
+                  torch.rand(4, 1)))
+
+    with pytest.raises(ValueError):
+        m.update((torch.rand(4, 1),
+                  torch.rand(4, 1, 2)))
+
+    with pytest.raises(ValueError):
+        m.update((torch.rand(4, 1, 2),
+                  torch.rand(4,)))
+
+    with pytest.raises(ValueError):
+        m.update((torch.rand(4,),
+                  torch.rand(4, 1, 2)))
+
+
+def test_median_absolute_error():
+
+    # See https://github.com/torch/torch7/pull/182
+    # For even number of elements, PyTorch returns middle element
+    # NumPy returns average of middle elements
+    # Size of dataset will be odd for these tests
+
+    size = 51
+    np_y_pred = np.random.rand(size,)
+    np_y = np.random.rand(size,)
+    np_median_absolute_error = np.median(np.abs(np_y - np_y_pred))
+
+    m = MedianAbsoluteError()
+    y_pred = torch.from_numpy(np_y_pred)
+    y = torch.from_numpy(np_y)
+
+    m.reset()
+    m.update((y_pred, y))
+
+    assert np_median_absolute_error == pytest.approx(m.compute())
+
+
+def test_median_absolute_error_2():
+
+    np.random.seed(1)
+    size = 105
+    np_y_pred = np.random.rand(size, 1)
+    np_y = np.random.rand(size, 1)
+    np.random.shuffle(np_y)
+    np_median_absolute_error = np.median(np.abs(np_y - np_y_pred))
+
+    m = MedianAbsoluteError()
+    y_pred = torch.from_numpy(np_y_pred)
+    y = torch.from_numpy(np_y)
+
+    m.reset()
+    batch_size = 16
+    n_iters = size // batch_size + 1
+    for i in range(n_iters):
+        idx = i * batch_size
+        m.update((y_pred[idx: idx + batch_size], y[idx: idx + batch_size]))
+
+    assert np_median_absolute_error == pytest.approx(m.compute())
+
+
+def test_integration_median_absolute_error_with_output_transform():
+
+    np.random.seed(1)
+    size = 105
+    np_y_pred = np.random.rand(size, 1)
+    np_y = np.random.rand(size, 1)
+    np.random.shuffle(np_y)
+    np_median_absolute_error = np.median(np.abs(np_y - np_y_pred))
+
+    batch_size = 15
+
+    def update_fn(engine, batch):
+        idx = (engine.state.iteration - 1) * batch_size
+        y_true_batch = np_y[idx:idx + batch_size]
+        y_pred_batch = np_y_pred[idx:idx + batch_size]
+        return idx, torch.from_numpy(y_pred_batch), torch.from_numpy(y_true_batch)
+
+    engine = Engine(update_fn)
+
+    m = MedianAbsoluteError(output_transform=lambda x: (x[1], x[2]))
+    m.attach(engine, 'median_absolute_error')
+
+    data = list(range(size // batch_size))
+    median_absolute_error = engine.run(data, max_epochs=1).metrics['median_absolute_error']
+
+    assert np_median_absolute_error == pytest.approx(median_absolute_error)
diff --git a/tests/ignite/contrib/metrics/regression/test_median_absolute_percentage_error.py b/tests/ignite/contrib/metrics/regression/test_median_absolute_percentage_error.py
new file mode 100644
index 00000000..ab40726d
--- /dev/null
+++ b/tests/ignite/contrib/metrics/regression/test_median_absolute_percentage_error.py
@@ -0,0 +1,98 @@
+import torch
+import numpy as np
+import pytest
+from ignite.engine import Engine
+from ignite.contrib.metrics.regression import MedianAbsolutePercentageError
+
+
+def test_wrong_input_shapes():
+    m = MedianAbsolutePercentageError()
+
+    with pytest.raises(ValueError):
+        m.update((torch.rand(4, 1, 2),
+                  torch.rand(4, 1)))
+
+    with pytest.raises(ValueError):
+        m.update((torch.rand(4, 1),
+                  torch.rand(4, 1, 2)))
+
+    with pytest.raises(ValueError):
+        m.update((torch.rand(4, 1, 2),
+                  torch.rand(4,)))
+
+    with pytest.raises(ValueError):
+        m.update((torch.rand(4,),
+                  torch.rand(4, 1, 2)))
+
+
+def test_median_absolute_percentage_error():
+
+    # See https://github.com/torch/torch7/pull/182
+    # For even number of elements, PyTorch returns middle element
+    # NumPy returns average of middle elements
+    # Size of dataset will be odd for these tests
+
+    size = 51
+    np_y_pred = np.random.rand(size,)
+    np_y = np.random.rand(size,)
+    np_median_absolute_percentage_error = 100.0 * np.median(np.abs(np_y - np_y_pred) / np.abs(np_y))
+
+    m = MedianAbsolutePercentageError()
+    y_pred = torch.from_numpy(np_y_pred)
+    y = torch.from_numpy(np_y)
+
+    m.reset()
+    m.update((y_pred, y))
+
+    assert np_median_absolute_percentage_error == pytest.approx(m.compute())
+
+
+def test_median_absolute_percentage_error_2():
+
+    np.random.seed(1)
+    size = 105
+    np_y_pred = np.random.rand(size, 1)
+    np_y = np.random.rand(size, 1)
+    np.random.shuffle(np_y)
+    np_median_absolute_percentage_error = 100.0 * np.median(np.abs(np_y - np_y_pred) / np.abs(np_y))
+
+    m = MedianAbsolutePercentageError()
+    y_pred = torch.from_numpy(np_y_pred)
+    y = torch.from_numpy(np_y)
+
+    m.reset()
+    batch_size = 16
+    n_iters = size // batch_size + 1
+    for i in range(n_iters):
+        idx = i * batch_size
+        m.update((y_pred[idx: idx + batch_size], y[idx: idx + batch_size]))
+
+    assert np_median_absolute_percentage_error == pytest.approx(m.compute())
+
+
+def test_integration_median_absolute_percentage_error_with_output_transform():
+
+    np.random.seed(1)
+    size = 105
+    np_y_pred = np.random.rand(size, 1)
+    np_y = np.random.rand(size, 1)
+    np.random.shuffle(np_y)
+    np_median_absolute_percentage_error = 100.0 * np.median(np.abs(np_y - np_y_pred) / np.abs(np_y))
+
+    batch_size = 15
+
+    def update_fn(engine, batch):
+        idx = (engine.state.iteration - 1) * batch_size
+        y_true_batch = np_y[idx:idx + batch_size]
+        y_pred_batch = np_y_pred[idx:idx + batch_size]
+        return idx, torch.from_numpy(y_pred_batch), torch.from_numpy(y_true_batch)
+
+    engine = Engine(update_fn)
+
+    m = MedianAbsolutePercentageError(output_transform=lambda x: (x[1], x[2]))
+    m.attach(engine, 'median_absolute_percentage_error')
+
+    data = list(range(size // batch_size))
+    median_absolute_percentage_error = engine.run(data, max_epochs=1).metrics['median_absolute_percentage_error']
+
+    assert np_median_absolute_percentage_error == pytest.approx(median_absolute_percentage_error)
diff --git a/tests/ignite/contrib/metrics/regression/test_median_relative_absolute_error.py b/tests/ignite/contrib/metrics/regression/test_median_relative_absolute_error.py
new file mode 100644
index 00000000..05399934
--- /dev/null
+++ b/tests/ignite/contrib/metrics/regression/test_median_relative_absolute_error.py
@@ -0,0 +1,98 @@
+import torch
+import numpy as np
+import pytest
+from ignite.engine import Engine
+from ignite.contrib.metrics.regression import MedianRelativeAbsoluteError
+
+
+def test_wrong_input_shapes():
+    m = MedianRelativeAbsoluteError()
+
+    with pytest.raises(ValueError):
+        m.update((torch.rand(4, 1, 2),
+                  torch.rand(4, 1)))
+
+    with pytest.raises(ValueError):
+        m.update((torch.rand(4, 1),
+                  torch.rand(4, 1, 2)))
+
+    with pytest.raises(ValueError):
+        m.update((torch.rand(4, 1, 2),
+                  torch.rand(4,)))
+
+    with pytest.raises(ValueError):
+        m.update((torch.rand(4,),
+                  torch.rand(4, 1, 2)))
+
+
+def test_median_relative_absolute_error():
+
+    # See https://github.com/torch/torch7/pull/182
+    # For even number of elements, PyTorch returns middle element
+    # NumPy returns average of middle elements
+    # Size of dataset will be odd for these tests
+
+    size = 51
+    np_y_pred = np.random.rand(size,)
+    np_y = np.random.rand(size,)
+    np_median_absolute_relative_error = np.median(np.abs(np_y - np_y_pred) / np.abs(np_y - np_y.mean()))
+
+    m = MedianRelativeAbsoluteError()
+    y_pred = torch.from_numpy(np_y_pred)
+    y = torch.from_numpy(np_y)
+
+    m.reset()
+    m.update((y_pred, y))
+
+    assert np_median_absolute_relative_error == pytest.approx(m.compute())
+
+
+def test_median_relative_absolute_error_2():
+
+    np.random.seed(1)
+    size = 105
+    np_y_pred = np.random.rand(size, 1)
+    np_y = np.random.rand(size, 1)
+    np.random.shuffle(np_y)
+    np_median_absolute_relative_error = np.median(np.abs(np_y - np_y_pred) / np.abs(np_y - np_y.mean()))
+
+    m = MedianRelativeAbsoluteError()
+    y_pred = torch.from_numpy(np_y_pred)
+    y = torch.from_numpy(np_y)
+
+    m.reset()
+    batch_size = 16
+    n_iters = size // batch_size + 1
+    for i in range(n_iters + 1):
+        idx = i * batch_size
+        m.update((y_pred[idx: idx + batch_size], y[idx: idx + batch_size]))
+
+    assert np_median_absolute_relative_error == pytest.approx(m.compute())
+
+
+def test_integration_median_relative_absolute_error_with_output_transform():
+
+    np.random.seed(1)
+    size = 105
+    np_y_pred = np.random.rand(size, 1)
+    np_y = np.random.rand(size, 1)
+    np.random.shuffle(np_y)
+    np_median_absolute_relative_error = np.median(np.abs(np_y - np_y_pred) / np.abs(np_y - np_y.mean()))
+
+    batch_size = 15
+
+    def update_fn(engine, batch):
+        idx = (engine.state.iteration - 1) * batch_size
+        y_true_batch = np_y[idx:idx + batch_size]
+        y_pred_batch = np_y_pred[idx:idx + batch_size]
+        return idx, torch.from_numpy(y_pred_batch), torch.from_numpy(y_true_batch)
+
+    engine = Engine(update_fn)
+
+    m = MedianRelativeAbsoluteError(output_transform=lambda x: (x[1], x[2]))
+    m.attach(engine, 'median_absolute_relative_error')
+
+    data = list(range(size // batch_size))
+    median_absolute_relative_error = engine.run(data, max_epochs=1).metrics['median_absolute_relative_error']
+
+    assert np_median_absolute_relative_error == pytest.approx(median_absolute_relative_error)
diff --git a/tests/ignite/contrib/metrics/test_average_precision.py b/tests/ignite/contrib/metrics/test_average_precision.py
index 71ca4af2..0e8034be 100644
--- a/tests/ignite/contrib/metrics/test_average_precision.py
+++ b/tests/ignite/contrib/metrics/test_average_precision.py
@@ -51,35 +51,47 @@ def test_ap_score_2():
     assert ap == np_ap
 
 
-def test_ap_score_with_activation():
+def test_integration_ap_score_with_output_transform():
 
+    np.random.seed(1)
     size = 100
-    np_y_pred = np.random.rand(size, 5)
-    np_y_pred_softmax = torch.softmax(torch.from_numpy(np_y_pred), dim=1).numpy()
-    np_y = np.random.randint(0, 2, size=(size, 5), dtype=np.long)
-    np_ap = average_precision_score(np_y, np_y_pred_softmax)
+    np_y_pred = np.random.rand(size, 1)
+    np_y = np.zeros((size,), dtype=np.long)
+    np_y[size // 2:] = 1
+    np.random.shuffle(np_y)
 
-    ap_metric = AveragePrecision(activation=torch.nn.Softmax(dim=1))
-    y_pred = torch.from_numpy(np_y_pred)
-    y = torch.from_numpy(np_y)
+    np_ap = average_precision_score(np_y, np_y_pred)
 
-    ap_metric.reset()
-    ap_metric.update((y_pred, y))
-    ap = ap_metric.compute()
+    batch_size = 10
+
+    def update_fn(engine, batch):
+        idx = (engine.state.iteration - 1) * batch_size
+        y_true_batch = np_y[idx:idx + batch_size]
+        y_pred_batch = np_y_pred[idx:idx + batch_size]
+        return idx, torch.from_numpy(y_pred_batch), torch.from_numpy(y_true_batch)
+
+    engine = Engine(update_fn)
+
+    ap_metric = AveragePrecision(output_transform=lambda x: (x[1], x[2]))
+    ap_metric.attach(engine, 'ap')
+
+    data = list(range(size // batch_size))
+    ap = engine.run(data, max_epochs=1).metrics['ap']
 
     assert ap == np_ap
 
 
-def test_integration_ap_score_with_output_transform():
+def test_integration_ap_score_with_activated_output_transform():
 
     np.random.seed(1)
     size = 100
     np_y_pred = np.random.rand(size, 1)
+    np_y_pred_softmax = torch.softmax(torch.from_numpy(np_y_pred), dim=1).numpy()
     np_y = np.zeros((size,), dtype=np.long)
     np_y[size // 2:] = 1
     np.random.shuffle(np_y)
 
-    np_ap = average_precision_score(np_y, np_y_pred)
+    np_ap = average_precision_score(np_y, np_y_pred_softmax)
 
     batch_size = 10
 
@@ -91,8 +103,8 @@ def test_integration_ap_score_with_output_transform():
 
     engine = Engine(update_fn)
 
-    roc_auc_metric = AveragePrecision(output_transform=lambda x: (x[1], x[2]))
-    roc_auc_metric.attach(engine, 'ap')
+    ap_metric = AveragePrecision(output_transform=lambda x: (torch.softmax(x[1], dim=1), x[2]))
+    ap_metric.attach(engine, 'ap')
 
     data = list(range(size // batch_size))
     ap = engine.run(data, max_epochs=1).metrics['ap']
diff --git a/tests/ignite/contrib/metrics/test_roc_auc.py b/tests/ignite/contrib/metrics/test_roc_auc.py
index 253f5fac..ba42a679 100644
--- a/tests/ignite/contrib/metrics/test_roc_auc.py
+++ b/tests/ignite/contrib/metrics/test_roc_auc.py
@@ -52,36 +52,47 @@ def test_roc_auc_score_2():
     assert roc_auc == np_roc_auc
 
 
-def test_roc_auc_score_with_activation():
+def test_integration_roc_auc_score_with_output_transform():
 
+    np.random.seed(1)
     size = 100
     np_y_pred = np.random.rand(size, 1)
-    np_y_pred_sigmoid = torch.sigmoid(torch.from_numpy(np_y_pred)).numpy()
     np_y = np.zeros((size,), dtype=np.long)
     np_y[size // 2:] = 1
-    np_roc_auc = roc_auc_score(np_y, np_y_pred_sigmoid)
+    np.random.shuffle(np_y)
 
-    roc_auc_metric = ROC_AUC(activation=torch.sigmoid)
-    y_pred = torch.from_numpy(np_y_pred)
-    y = torch.from_numpy(np_y)
+    np_roc_auc = roc_auc_score(np_y, np_y_pred)
 
-    roc_auc_metric.reset()
-    roc_auc_metric.update((y_pred, y))
-    roc_auc = roc_auc_metric.compute()
+    batch_size = 10
+
+    def update_fn(engine, batch):
+        idx = (engine.state.iteration - 1) * batch_size
+        y_true_batch = np_y[idx:idx + batch_size]
+        y_pred_batch = np_y_pred[idx:idx + batch_size]
+        return idx, torch.from_numpy(y_pred_batch), torch.from_numpy(y_true_batch)
+
+    engine = Engine(update_fn)
+
+    roc_auc_metric = ROC_AUC(output_transform=lambda x: (x[1], x[2]))
+    roc_auc_metric.attach(engine, 'roc_auc')
+
+    data = list(range(size // batch_size))
+    roc_auc = engine.run(data, max_epochs=1).metrics['roc_auc']
 
     assert roc_auc == np_roc_auc
 
 
-def test_integration_roc_auc_score_with_output_transform():
+def test_integration_roc_auc_score_with_activated_output_transform():
 
     np.random.seed(1)
     size = 100
     np_y_pred = np.random.rand(size, 1)
+    np_y_pred_sigmoid = torch.sigmoid(torch.from_numpy(np_y_pred)).numpy()
     np_y = np.zeros((size,), dtype=np.long)
     np_y[size // 2:] = 1
     np.random.shuffle(np_y)
 
-    np_roc_auc = roc_auc_score(np_y, np_y_pred)
+    np_roc_auc = roc_auc_score(np_y, np_y_pred_sigmoid)
 
     batch_size = 10
 
@@ -93,7 +104,7 @@ def test_integration_roc_auc_score_with_output_transform():
 
     engine = Engine(update_fn)
 
-    roc_auc_metric = ROC_AUC(output_transform=lambda x: (x[1], x[2]))
+    roc_auc_metric = ROC_AUC(output_transform=lambda x: (torch.sigmoid(x[1]), x[2]))
     roc_auc_metric.attach(engine, 'roc_auc')
 
     data = list(range(size // batch_size))
diff --git a/tests/ignite/metrics/test_metrics_lambda.py b/tests/ignite/metrics/test_metrics_lambda.py
index 32b660b8..8eaeebea 100644
--- a/tests/ignite/metrics/test_metrics_lambda.py
+++ b/tests/ignite/metrics/test_metrics_lambda.py
@@ -3,7 +3,6 @@ from ignite.metrics import Metric, MetricsLambda, Precision, Recall
 from pytest import approx
 from sklearn.metrics import precision_score, recall_score, f1_score
 import numpy as np
-from numpy.testing import assert_allclose
 import torch
 
 
@@ -123,6 +122,47 @@ def test_integration():
     assert f1_true == approx(state.metrics['f1']), "{} vs {}".format(f1_true, state.metrics['f1'])
 
 
+def test_integration_ingredients_not_attached():
+    np.random.seed(1)
+
+    n_iters = 10
+    batch_size = 10
+    n_classes = 10
+
+    y_true = np.arange(0, n_iters * batch_size) % n_classes
+    y_pred = 0.2 * np.random.rand(n_iters * batch_size, n_classes)
+    for i in range(n_iters * batch_size):
+        if np.random.rand() > 0.4:
+            y_pred[i, y_true[i]] = 1.0
+        else:
+            j = np.random.randint(0, n_classes)
+            y_pred[i, j] = 0.7
+
+    y_true_batch_values = iter(y_true.reshape(n_iters, batch_size))
+    y_pred_batch_values = iter(y_pred.reshape(n_iters, batch_size, n_classes))
+
+    def update_fn(engine, batch):
+        y_true_batch = next(y_true_batch_values)
+        y_pred_batch = next(y_pred_batch_values)
+        return torch.from_numpy(y_pred_batch), torch.from_numpy(y_true_batch)
+
+    evaluator = Engine(update_fn)
+
+    precision = Precision(average=False)
+    recall = Recall(average=False)
+
+    def Fbeta(r, p, beta):
+        return torch.mean((1 + beta ** 2) * p * r / (beta ** 2 * p + r)).item()
+
+    F1 = MetricsLambda(Fbeta, recall, precision, 1)
+    F1.attach(evaluator, "f1")
+
+    data = list(range(n_iters))
+    state = evaluator.run(data, max_epochs=1)
+    f1_true = f1_score(y_true, np.argmax(y_pred, axis=-1), average='macro')
+    assert f1_true == approx(state.metrics['f1']), "{} vs {}".format(f1_true, state.metrics['f1'])
+
+
 def test_state_metrics():
 
     y_pred = torch.randint(0, 2, size=(15, 10, 4)).float()
@@ -151,3 +191,97 @@ def test_state_metrics():
     state = evaluator.run(d, max_epochs=1)
 
     assert set(state.metrics.keys()) == set(["precision", "recall", "f1"])
+
+
+def test_state_metrics_ingredients_not_attached():
+
+    y_pred = torch.randint(0, 2, size=(15, 10, 4)).float()
+    y = torch.randint(0, 2, size=(15, 10, 4)).long()
+
+    def update_fn(engine, batch):
+        y_pred, y = batch
+        return y_pred, y
+
+    evaluator = Engine(update_fn)
+
+    precision = Precision(average=False)
+    recall = Recall(average=False)
+    F1 = precision * recall * 2 / (precision + recall + 1e-20)
+    F1 = MetricsLambda(lambda t: torch.mean(t).item(), F1)
+
+    F1.attach(evaluator, "F1")
+
+    def data(y_pred, y):
+        for i in range(y_pred.shape[0]):
+            yield (y_pred[i], y[i])
+
+    d = data(y_pred, y)
+    state = evaluator.run(d, max_epochs=1)
+
+    assert set(state.metrics.keys()) == set(["F1"])
+
+
+def test_recursive_attachment():
+
+    def _test(composed_metric, metric_name, compute_true_value_fn):
+
+        metrics = {
+            metric_name: composed_metric,
+        }
+
+        y_pred = torch.randint(0, 2, size=(15, 10, 4)).float()
+        y = torch.randint(0, 2, size=(15, 10, 4)).long()
+
+        def update_fn(engine, batch):
+            y_pred, y = batch
+            return y_pred, y
+
+        validator = Engine(update_fn)
+
+        for name, metric in metrics.items():
+            metric.attach(validator, name)
+
+        def data(y_pred, y):
+            for i in range(y_pred.shape[0]):
+                yield (y_pred[i], y[i])
+
+        d = data(y_pred, y)
+        state = validator.run(d, max_epochs=1)
+
+        assert set(state.metrics.keys()) == set([metric_name, ])
+        np_y_pred = y_pred.numpy().ravel()
+        np_y = y.numpy().ravel()
+        assert state.metrics[metric_name].item() == approx(compute_true_value_fn(np_y_pred, np_y))
+
+    precision_1 = Precision()
+    precision_2 = Precision()
+    summed_precision = precision_1 + precision_2
+
+    def compute_true_summed_precision(y_pred, y):
+        p1 = precision_score(y, y_pred)
+        p2 = precision_score(y, y_pred)
+        return p1 + p2
+
+    _test(summed_precision, "summed precision", compute_true_value_fn=compute_true_summed_precision)
+
+    precision_1 = Precision()
+    precision_2 = Precision()
+    mean_precision = (precision_1 + precision_2) / 2
+
+    def compute_true_mean_precision(y_pred, y):
+        p1 = precision_score(y, y_pred)
+        p2 = precision_score(y, y_pred)
+        return (p1 + p2) * 0.5
+
+    _test(mean_precision, "mean precision", compute_true_value_fn=compute_true_mean_precision)
+
+    precision_1 = Precision()
+    precision_2 = Precision()
+    some_metric = 2.0 + 0.2 * (precision_1 * precision_2 + precision_1 - precision_2) ** 0.5
+
+    def compute_true_somemetric(y_pred, y):
+        p1 = precision_score(y, y_pred)
+        p2 = precision_score(y, y_pred)
+        return 2.0 + 0.2 * (p1 * p2 + p1 - p2) ** 0.5
+
+    _test(some_metric, "some metric", compute_true_somemetric)