Repository: pytorch/ignite

Issue:
Add helper ops for metrics arithmetics
Following the discussion from [here](https://github.com/pytorch/ignite/pull/410#issuecomment-457105415)

> I was wondering whether it is possible to introduce a metric that performs mean, Mean, and compact the following piece of code:

```python
    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)
```
> to:
```python
    precision = Precision(average=False)
    recall = Recall(average=False)
    metrics = Mean(precision * recall * 2 / (precision + recall + 1e-20))
```

Idea is to provide some helper classes like `Mean`, `Sum` and others.

cc @amitibo 



Discussion:
Another possibility is:
```
    precision = Precision(average=False)
    recall = Recall(average=False)
    metrics = (precision * recall * 2 / (precision + recall + 1e-20)).mean()
```

Resolving patch:
diff --git a/ignite/metrics/metric.py b/ignite/metrics/metric.py
--- a/ignite/metrics/metric.py
+++ b/ignite/metrics/metric.py
@@ -65,7 +65,10 @@ def iteration_completed(self, engine):
         self.update(output)
 
     def completed(self, engine, name):
-        engine.state.metrics[name] = self.compute()
+        result = self.compute()
+        if torch.is_tensor(result) and len(result.shape) == 0:
+            result = result.item()
+        engine.state.metrics[name] = result
 
     def attach(self, engine, name):
         engine.add_event_handler(Events.EPOCH_COMPLETED, self.completed, name)
@@ -129,3 +132,13 @@ def __rtruediv__(self, other):
     def __floordiv__(self, other):
         from ignite.metrics import MetricsLambda
         return MetricsLambda(lambda x, y: x // y, self, other)
+
+    def __getattr__(self, attr):
+        from ignite.metrics import MetricsLambda
+
+        def fn(x, *args, **kwargs):
+            return getattr(x, attr)(*args, **kwargs)
+
+        def wrapper(*args, **kwargs):
+            return MetricsLambda(fn, self, *args, **kwargs)
+        return wrapper
diff --git a/ignite/metrics/metrics_lambda.py b/ignite/metrics/metrics_lambda.py
--- a/ignite/metrics/metrics_lambda.py
+++ b/ignite/metrics/metrics_lambda.py
@@ -1,5 +1,6 @@
 from ignite.metrics.metric import Metric
 from ignite.engine import Events
+import itertools
 
 
 class MetricsLambda(Metric):
@@ -33,13 +34,14 @@ def Fbeta(r, p, beta):
         F3 = MetricsLambda(Fbeta, recall, precision, 3)
         F4 = MetricsLambda(Fbeta, recall, precision, 4)
     """
-    def __init__(self, f, *args):
+    def __init__(self, f, *args, **kwargs):
         self.function = f
         self.args = args
+        self.kwargs = kwargs
         super(MetricsLambda, self).__init__()
 
     def reset(self):
-        for i in self.args:
+        for i in itertools.chain(self.args, self.kwargs.values()):
             if isinstance(i, Metric):
                 i.reset()
 
@@ -51,10 +53,11 @@ def update(self, output):
 
     def compute(self):
         materialized = [i.compute() if isinstance(i, Metric) else i for i in self.args]
-        return self.function(*materialized)
+        materialized_kwargs = {k: (v.compute() if isinstance(v, Metric) else v) for k, v in self.kwargs.items()}
+        return self.function(*materialized, **materialized_kwargs)
 
     def _internal_attach(self, engine):
-        for index, metric in enumerate(self.args):
+        for index, metric in enumerate(itertools.chain(self.args, self.kwargs.values())):
             if isinstance(metric, MetricsLambda):
                 metric._internal_attach(engine)
             elif isinstance(metric, Metric):


Test patch:
diff --git a/tests/ignite/metrics/test_metric.py b/tests/ignite/metrics/test_metric.py
--- a/tests/ignite/metrics/test_metric.py
+++ b/tests/ignite/metrics/test_metric.py
@@ -323,3 +323,68 @@ def update_fn(engine, batch):
 def test_abstract_class():
     with raises(TypeError):
         Metric()
+
+
+def test_pytorch_operators():
+
+    def _test(composed_metric, metric_name, compute_true_value_fn):
+
+        metrics = {
+            metric_name: composed_metric,
+        }
+
+        y_pred = torch.rand(15, 10, 5).float()
+        y = torch.randint(0, 5, size=(15, 10)).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 = np.argmax(y_pred.numpy(), axis=-1).ravel()
+        np_y = y.numpy().ravel()
+        assert state.metrics[metric_name] == approx(compute_true_value_fn(np_y_pred, np_y))
+
+    precision_1 = Precision(average=False)
+    precision_2 = Precision(average=False)
+    norm_summed_precision = (precision_1 + precision_2).norm(p=10)
+
+    def compute_true_norm_summed_precision(y_pred, y):
+        p1 = precision_score(y, y_pred, average=None)
+        p2 = precision_score(y, y_pred, average=None)
+        return np.linalg.norm(p1 + p2, ord=10)
+
+    _test(norm_summed_precision, "mean summed precision", compute_true_value_fn=compute_true_norm_summed_precision)
+
+    precision = Precision(average=False)
+    recall = Recall(average=False)
+    sum_precision_recall = (precision + recall).sum()
+
+    def compute_sum_precision_recall(y_pred, y):
+        p = precision_score(y, y_pred, average=None)
+        r = recall_score(y, y_pred, average=None)
+        return np.sum(p + r)
+
+    _test(sum_precision_recall, "sum precision recall", compute_true_value_fn=compute_sum_precision_recall)
+
+    precision = Precision(average=False)
+    recall = Recall(average=False)
+    f1 = (precision * recall * 2 / (precision + recall + 1e-20)).mean()
+
+    def compute_f1(y_pred, y):
+        f1 = f1_score(y, y_pred, average='macro')
+        return f1
+
+    _test(f1, "f1", compute_true_value_fn=compute_f1)
diff --git a/tests/ignite/metrics/test_metrics_lambda.py b/tests/ignite/metrics/test_metrics_lambda.py
--- a/tests/ignite/metrics/test_metrics_lambda.py
+++ b/tests/ignite/metrics/test_metrics_lambda.py
@@ -31,8 +31,12 @@ def process_function(engine, data):
         return data
 
     engine = Engine(process_function)
-    m0_plus_m1 = MetricsLambda(lambda x, y: x + y, m0, m1)
-    m2_plus_2 = MetricsLambda(lambda x, y: x + y, m2, 2)
+
+    def plus(this, other):
+        return this + other
+
+    m0_plus_m1 = MetricsLambda(plus, m0, other=m1)
+    m2_plus_2 = MetricsLambda(plus, m2, 2)
     m0_plus_m1.attach(engine, 'm0_plus_m1')
     m2_plus_2.attach(engine, 'm2_plus_2')
 
@@ -52,7 +56,10 @@ def test_metrics_lambda_reset():
     m1.update([1, 10, 100])
     m2.update([1, 10, 100])
 
-    m = MetricsLambda(lambda x, y, z, t: 1, m0, m1, m2, 0)
+    def fn(x, y, z, t):
+        return 1
+
+    m = MetricsLambda(fn, m0, m1, z=m2, t=0)
 
     # initiating a new instance of MetricsLambda must reset
     # its argument metrics
@@ -251,7 +258,7 @@ def data(y_pred, y):
         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))
+        assert state.metrics[metric_name] == approx(compute_true_value_fn(np_y_pred, np_y))
 
     precision_1 = Precision()
     precision_2 = Precision()