Repository: pyro-ppl/pyro

Issue:
[bug] TraceGraph_ELBO inconsistent in sign of ELBO
### Issue Description
 + `TraceGraph_ELBO.loss()` returns the negative ELBO (as expected).
 + `TraceGraph_ELBO.loss_and_grads()` returns the positive ELBO.

This doesn't affect the optimisation as the surrogate loss is based on the negative ELBO, but is confusing when monitoring convergence. May well happen in other ELBO classes, this is just the one that I've been using and hence spotted it in.

### Environment
MacOS, Python 3.7.1, PyTorch 1.1.0, Pyro 0.3.4



Discussion:
I agree it seems a bit confusing. Not fully knowing the reasons behind the implementation, I would hazard a guess that pyro use a negative ELBO loss for minimization and use with optimizers, but ELBO (normally) is positive which is still helpful and accessible through `TraceGraph_ELBO.loss_and_grads()`?
This is just a bug, any `ELBO` implementation should return the "elbo loss" = -elbo from `.loss_and_grads()`. Anyone have time to submit a quick fix :smile:?
😂 - I'm happy to fix it quickly.

Resolving patch:
diff --git a/pyro/infer/trace_elbo.py b/pyro/infer/trace_elbo.py
--- a/pyro/infer/trace_elbo.py
+++ b/pyro/infer/trace_elbo.py
@@ -108,7 +108,7 @@ def differentiable_loss(self, model, guide, *args, **kwargs):
             surrogate_loss += surrogate_loss_particle / self.num_particles
             loss += loss_particle / self.num_particles
         warn_if_nan(surrogate_loss, "loss")
-        return loss + (surrogate_loss - surrogate_loss.detach())
+        return loss + (surrogate_loss - torch_item(surrogate_loss))
 
     def loss_and_grads(self, model, guide, *args, **kwargs):
         """
@@ -132,7 +132,6 @@ def loss_and_grads(self, model, guide, *args, **kwargs):
             if trainable_params and getattr(surrogate_loss_particle, 'requires_grad', False):
                 surrogate_loss_particle = surrogate_loss_particle / self.num_particles
                 surrogate_loss_particle.backward(retain_graph=self.retain_graph)
-
         warn_if_nan(loss, "loss")
         return loss
 
diff --git a/pyro/infer/tracegraph_elbo.py b/pyro/infer/tracegraph_elbo.py
--- a/pyro/infer/tracegraph_elbo.py
+++ b/pyro/infer/tracegraph_elbo.py
@@ -249,12 +249,12 @@ def loss_and_grads(self, model, guide, *args, **kwargs):
         Performs backward on the latter. Num_particle many samples are used to form the estimators.
         If baselines are present, a baseline loss is also constructed and differentiated.
         """
-
-        loss, surrogate_loss = self._loss_and_surrogate_loss(model, guide, *args, **kwargs)
+        elbo, surrogate_loss = self._loss_and_surrogate_loss(model, guide, *args, **kwargs)
 
         torch_backward(surrogate_loss, retain_graph=self.retain_graph)
 
-        loss = torch_item(loss)
+        elbo = torch_item(elbo)
+        loss = -elbo
         warn_if_nan(loss, "loss")
         return loss
 


Test patch:
diff --git a/tests/infer/test_valid_models.py b/tests/infer/test_valid_models.py
--- a/tests/infer/test_valid_models.py
+++ b/tests/infer/test_valid_models.py
@@ -11,8 +11,10 @@
 from pyro.distributions.testing import fakes
 from pyro.infer import (SVI, Trace_ELBO, TraceEnum_ELBO, TraceGraph_ELBO, TraceMeanField_ELBO, TraceTailAdaptive_ELBO,
                         config_enumerate)
+from pyro.infer.util import torch_item
 from pyro.ops.indexing import Vindex
 from pyro.optim import Adam
+from tests.common import assert_close
 
 logger = logging.getLogger(__name__)
 
@@ -26,6 +28,23 @@ def assert_ok(model, guide, elbo, **kwargs):
     pyro.clear_param_store()
     inference = SVI(model, guide, Adam({"lr": 1e-6}), elbo)
     inference.step(**kwargs)
+    try:
+        pyro.set_rng_seed(0)
+        loss = elbo.loss(model, guide, **kwargs)
+        if hasattr(elbo, "differentiable_loss"):
+            try:
+                pyro.set_rng_seed(0)
+                differentiable_loss = torch_item(elbo.differentiable_loss(model, guide, **kwargs))
+            except ValueError:
+                pass  # Ignore cases where elbo cannot be differentiated
+            else:
+                assert_close(differentiable_loss, loss, atol=0.01)
+        if hasattr(elbo, "loss_and_grads"):
+            pyro.set_rng_seed(0)
+            loss_and_grads = elbo.loss_and_grads(model, guide, **kwargs)
+            assert_close(loss_and_grads, loss, atol=0.01)
+    except NotImplementedError:
+        pass  # Ignore cases where loss isn't implemented, eg. TraceTailAdaptive_ELBO
 
 
 def assert_error(model, guide, elbo, match=None):