Repository: pytorch/ignite

Issue:
Master branch nightly build depending on torch_nightly
We need a build of ignite to allow people try nightly version of PyTorch together with ignite. Since the PyTorch is nightly built, it might be better to call this package `pytorch-ignite-nightly` which is also a nightly build of master branch, but with the dependencies on `torch` changed to `torch_nightly`.

cc: @vfdev-5 

NB: I'm not sure the dependency should be `torch_nightly` or `torch-nightly`, because the install command has `torch_nightly` but the output of pip says `torch-nightly`:
```
gaoxiang@pc ~ $ sudo pip install torch_nightly -f https://download.pytorch.org/whl/nightly/cpu/torch_nightly.html
Looking in links: https://download.pytorch.org/whl/nightly/cpu/torch_nightly.html
Collecting torch_nightly
  Downloading https://download.pytorch.org/whl/nightly/cpu/torch_nightly-1.0.0.dev20181029-cp37-cp37m-linux_x86_64.whl (85.9MB)
    100% |████████████████████████████████| 85.9MB 698kB/s 
Installing collected packages: torch-nightly
Successfully installed torch-nightly-1.0.0.dev20181029
```
@soumith Do you think we should change these two names to make them consistent?

Discussion:

Patch:
diff --git a/.travis.yml b/.travis.yml
index cb967eb1..aada26e4 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -13,6 +13,7 @@ stages:
   - Test
   - Docs
   - Deploy
+  - Nightly
 
 before_install: &before_install
   - sudo apt-get update
@@ -83,7 +84,7 @@ jobs:
     # GitHub Pages Deployment: https://docs.travis-ci.com/user/deployment/pages/
     - stage: Docs
       python: "3.5"
-
+      if: branch != nightly
       # Use previously defined before_install
       before_install: *before_install
 
@@ -161,3 +162,54 @@ jobs:
 
         on:
           tags: true
+
+    - stage: Nightly
+      python: "3.5"
+      env:
+        - PYTORCH_PACKAGE=pytorch-nightly-cpu
+      if: branch = nightly
+      # Use previously defined before_install
+      before_install: *before_install
+      install:
+        # Merge master into nightly
+        - git fetch origin master
+        - git merge FETCH_HEAD --no-edit
+        # Copy "nightly" files
+        - cp nightly/setup_nightly.py setup.py
+        - cp -R nightly/conda.recipe .
+        # Push to nightly branch
+        - git remote rm origin && git remote add origin https://$GITHUB_TOKEN@github.com/${TRAVIS_REPO_SLUG}.git
+        - git push origin nightly
+      script: true
+      after_success: # Nothing to do
+      before_deploy:
+        # Conda deploy if on tag
+        # ANACONDA_TOKEN should be provided by Travis
+        # How to generate ANACONDA_TOKEN: https://docs.anaconda.com/anaconda-cloud/user-guide/tasks/work-with-accounts#creating-access-tokens
+        # https://conda.io/docs/user-guide/tasks/build-packages/install-conda-build.html
+        - conda install -y conda-build conda-verify anaconda-client
+        - conda config --set anaconda_upload no
+        - conda build --quiet --no-test --output-folder conda_build conda.recipe -c pytorch
+        # Convert to other platforms: OSX, WIN
+        - conda convert --platform win-64 conda_build/linux-64/*.tar.bz2 -o conda_build/
+        - conda convert --platform osx-64 conda_build/linux-64/*.tar.bz2 -o conda_build/
+        # Upload to Anaconda
+        # We could use --all but too much platforms to uploaded
+# Uncomment when a package is available
+#        - ls conda_build/*/*.tar.bz2 | xargs -I {} anaconda -v -t $ANACONDA_TOKEN upload -u pytorch {}
+        - ls conda_build/*/*.tar.bz2 | xargs -I {} anaconda -v -t $ANACONDA_TOKEN upload {}
+      # PyPI Deployment: https://docs.travis-ci.com/user/deployment/pypi/
+      deploy:
+        provider: pypi
+        user: $PYPI_USER
+        # If password contains non alphanumeric characters
+        # https://github.com/travis-ci/dpl/issues/377
+        # pass it as secured variable
+        password: $PYPI_TOKEN
+        # otherwise, follow "How to encrypt the password": https://docs.travis-ci.com/user/encryption-keys/
+        # `travis encrypt deploy.password="password"`
+        #  secure: "secured_password"
+        skip_cleanup: true
+        distributions: "sdist bdist_wheel"
+        on:
+          branch: nightly
diff --git a/docs/source/metrics.rst b/docs/source/metrics.rst
index b1af9c64..4d8defa7 100644
--- a/docs/source/metrics.rst
+++ b/docs/source/metrics.rst
@@ -37,6 +37,21 @@ use the `output_transform` argument to transform it:
         metric = Accuracy(output_transform=output_transform)
         metric.attach(engine, "accuracy")
 
+Metrics could be combined together to form a new metric through arithmetics,
+for example:
+
+    .. code-block:: python
+
+        precision = Precision(average=False)
+        recall = Recall(average=False)
+        F1 = precision * recall * 2 / (precision + recall)
+
+    .. note::  This example computes F1 for each class separately, rather than
+        the mean of F1 across class. To combine precision and recall to get
+        F1 or other F metrics, we have to be careful that `average=False`, i.e.
+        to use the unaveraged precision and recall, otherwise we will not be
+        computing F metrics.
+
 
 .. currentmodule:: ignite.metrics
 
diff --git a/ignite/handlers/checkpoint.py b/ignite/handlers/checkpoint.py
index 4e68a4e1..57ea8bd0 100644
--- a/ignite/handlers/checkpoint.py
+++ b/ignite/handlers/checkpoint.py
@@ -83,7 +83,7 @@ class ModelCheckpoint(object):
                  create_dir=True,
                  save_as_state_dict=False):
 
-        self._dirname = dirname
+        self._dirname = os.path.expanduser(dirname)
         self._fname_prefix = filename_prefix
         self._n_saved = n_saved
         self._save_interval = save_interval
diff --git a/ignite/metrics/metric.py b/ignite/metrics/metric.py
index 1f367029..0afc8b08 100644
--- a/ignite/metrics/metric.py
+++ b/ignite/metrics/metric.py
@@ -73,3 +73,35 @@ class Metric(with_metaclass(ABCMeta, object)):
             engine.add_event_handler(Events.EPOCH_STARTED, self.started)
         if not engine.has_event_handler(self.iteration_completed, Events.ITERATION_COMPLETED):
             engine.add_event_handler(Events.ITERATION_COMPLETED, self.iteration_completed)
+
+    def __add__(self, other):
+        from ignite.metrics import MetricsLambda
+        return MetricsLambda(lambda x, y: x + y, self, other)
+
+    def __sub__(self, other):
+        from ignite.metrics import MetricsLambda
+        return MetricsLambda(lambda x, y: x - y, self, other)
+
+    def __mul__(self, other):
+        from ignite.metrics import MetricsLambda
+        return MetricsLambda(lambda x, y: x * y, self, other)
+
+    def __pow__(self, other):
+        from ignite.metrics import MetricsLambda
+        return MetricsLambda(lambda x, y: x ** y, self, other)
+
+    def __mod__(self, other):
+        from ignite.metrics import MetricsLambda
+        return MetricsLambda(lambda x, y: x % y, self, other)
+
+    def __div__(self, other):
+        from ignite.metrics import MetricsLambda
+        return MetricsLambda(lambda x, y: x.__div__(y), self, other)
+
+    def __truediv__(self, other):
+        from ignite.metrics import MetricsLambda
+        return MetricsLambda(lambda x, y: x.__truediv__(y), self, other)
+
+    def __floordiv__(self, other):
+        from ignite.metrics import MetricsLambda
+        return MetricsLambda(lambda x, y: x // y, self, other)


Test patch:
diff --git a/tests/ignite/metrics/test_metric.py b/tests/ignite/metrics/test_metric.py
index 371f2581..3fe3b80a 100644
--- a/tests/ignite/metrics/test_metric.py
+++ b/tests/ignite/metrics/test_metric.py
@@ -1,8 +1,12 @@
-from ignite.metrics import Metric
+import sys
+from ignite.metrics import Metric, Precision, Recall
 from ignite.engine import Engine, State
 import torch
 from mock import MagicMock
-from pytest import raises
+
+from pytest import approx, raises
+import numpy as np
+from sklearn.metrics import precision_score, recall_score, f1_score
 
 
 def test_no_transform():
@@ -72,6 +76,133 @@ def test_no_grad():
     metric.iteration_completed(engine)
 
 
+def test_arithmetics():
+    class ListGatherMetric(Metric):
+
+        def __init__(self, index):
+            self.index = index
+            super(ListGatherMetric, self).__init__()
+
+        def reset(self):
+            self.list_ = []
+
+        def update(self, output):
+            self.list_ = output
+
+        def compute(self):
+            print(self.index)
+            return self.list_[self.index]
+
+    m0 = ListGatherMetric(0)
+    m1 = ListGatherMetric(1)
+    m2 = ListGatherMetric(2)
+
+    # __add__
+    m0_plus_m1 = m0 + m1
+    m0.update([1, 10, 100])
+    m1.update([1, 10, 100])
+    assert m0_plus_m1.compute() == 11
+    m0.update([2, 20, 200])
+    m1.update([2, 20, 200])
+    assert m0_plus_m1.compute() == 22
+
+    m2_plus_2 = m2 + 2
+    m2.update([1, 10, 100])
+    assert m2_plus_2.compute() == 102
+
+    # __sub__
+    m0_minus_m1 = m0 - m1
+    m0.update([1, 10, 100])
+    m1.update([1, 10, 100])
+    assert m0_minus_m1.compute() == -9
+    m0.update([2, 20, 200])
+    m1.update([2, 20, 200])
+    assert m0_minus_m1.compute() == -18
+
+    m2_minus_2 = m2 - 2
+    m2.update([1, 10, 100])
+    assert m2_minus_2.compute() == 98
+
+    # __mul__
+    m0_times_m1 = m0 * m1
+    m0.update([1, 10, 100])
+    m1.update([1, 10, 100])
+    assert m0_times_m1.compute() == 10
+    m0.update([2, 20, 200])
+    m1.update([2, 20, 200])
+    assert m0_times_m1.compute() == 40
+
+    m2_times_2 = m2 * 2
+    m2.update([1, 10, 100])
+    assert m2_times_2.compute() == 200
+
+    # __pow__
+    m0_pow_m1 = m0 ** m1
+    m0.update([1, 10, 100])
+    m1.update([1, 10, 100])
+    assert m0_pow_m1.compute() == 1
+    m0.update([2, 20, 200])
+    m1.update([2, 20, 200])
+    assert m0_pow_m1.compute() == 2 ** 20
+
+    m2_pow_2 = m2 ** 2
+    m2.update([1, 10, 100])
+    assert m2_pow_2.compute() == 10000
+
+    # __mod__
+    m0_mod_m1 = m0 % m1
+    m0.update([1, 10, 100])
+    m1.update([1, 10, 100])
+    assert m0_mod_m1.compute() == 1
+    m0.update([2, 20, 200])
+    m1.update([2, 20, 200])
+    assert m0_mod_m1.compute() == 2
+
+    m2_mod_2 = m2 % 2
+    m2.update([1, 10, 100])
+    assert m2_mod_2.compute() == 0
+
+    # __div__, only applicable to python2
+    if sys.version_info[0] < 3:
+        m0_div_m1 = m0.__div__(m1)
+        m0.update([1, 10, 100])
+        m1.update([1, 10, 100])
+        assert m0_div_m1.compute() == 0
+        m0.update([2, 20, 200])
+        m1.update([2, 20, 200])
+        assert m0_div_m1.compute() == 0
+
+        m2_div_2 = m2.__div__(2)
+        m2.update([1, 10, 100])
+        assert m2_div_2.compute() == 50
+
+    # __truediv__
+    m0_truediv_m1 = m0.__truediv__(m1)
+    m0.update([1, 10, 100])
+    m1.update([1, 10, 100])
+    assert m0_truediv_m1.compute() == approx(0.1)
+    m0.update([2, 20, 200])
+    m1.update([2, 20, 200])
+    assert m0_truediv_m1.compute() == approx(0.1)
+
+    m2_truediv_2 = m2.__truediv__(2)
+    m2.update([1, 10, 100])
+    assert m2_truediv_2.compute() == approx(50.0)
+
+    # __floordiv__
+    m0_floordiv_m1 = m0 // m1
+    m0.update([1, 10, 100])
+    m1.update([1, 10, 100])
+    assert m0_floordiv_m1.compute() == 0
+    m0.update([2, 20, 200])
+    m1.update([2, 20, 200])
+    assert m0_floordiv_m1.compute() == 0
+
+    m2_floordiv_2 = m2 // 2
+    m2.update([1, 10, 100])
+    assert m2_floordiv_2.compute() == 50
+
+
 def test_attach():
     class CountMetric(Metric):
         def __init__(self, value):
@@ -116,6 +247,56 @@ def test_attach():
     assert m2.update_count == 50
 
 
+def test_integration():
+    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)
+    F1 = precision * recall * 2 / (precision + recall)
+
+    precision.attach(evaluator, "precision")
+    recall.attach(evaluator, "recall")
+    F1.attach(evaluator, "f1")
+
+    data = list(range(n_iters))
+    state = evaluator.run(data, max_epochs=1)
+
+    precision_true = precision_score(y_true, np.argmax(y_pred, axis=-1), average=None)
+    recall_true = recall_score(y_true, np.argmax(y_pred, axis=-1), average=None)
+    f1_true = f1_score(y_true, np.argmax(y_pred, axis=-1), average=None)
+
+    precision = state.metrics['precision'].numpy()
+    recall = state.metrics['recall'].numpy()
+    f1 = state.metrics['f1'].numpy()
+
+    assert precision_true == approx(precision), "{} vs {}".format(precision_true, precision)
+    assert recall_true == approx(recall), "{} vs {}".format(recall_true, recall)
+    assert f1_true == approx(f1), "{} vs {}".format(f1_true, f1)
+
+
 def test_abstract_class():
     with raises(TypeError):
         Metric()