Repository: GPflow/GPflow

Issue:
Error when computing log likelihood with prior
The MWE below results in 
TypeError: Cannot interpret feed_dict key as Tensor: Tensor Tensor("X:0", shape=(?, ?), dtype=float64) is not an element of this graph.
If the _compile call is commented in the code works.

```import GPflow
import numpy as np
N = 10
X = np.random.rand(N, 1)
Y = np.sin(X) + np.random.randn(N, 1)
ki = GPflow.kernels.Matern32(1)
m = GPflow.gpr.GPR(X, Y, ki)
print('needs recompile', m._needs_recompile)
objInt = m.compute_log_likelihood()-m.compute_log_prior()
# m._compile()
objInt = m.compute_log_likelihood()-m.compute_log_prior()
print('needs recompile now?', m._needs_recompile)```


Discussion:
I can simplify slightly

``` python
N = 10
X = np.random.rand(N, 1)
Y = np.sin(X) + np.random.randn(N, 1)
ki = GPflow.kernels.Matern32(1)
m = GPflow.gpr.GPR(X, Y, ki)

m.compute_log_likelihood()
m.compute_log_likelihood()
m.compute_log_prior()
# m._compile()
m.compute_log_likelihood()
```

 It seems that a second call to the AutoFlow function fails if there's another AutoFlow in the middle. I agree that this is a bug.


Resolving patch:
diff --git a/GPflow/model.py b/GPflow/model.py
--- a/GPflow/model.py
+++ b/GPflow/model.py
@@ -103,7 +103,7 @@ def __getstate__(self):
         This method is necessary for pickling objects
         """
         d = Parameterized.__getstate__(self)
-        for key in ['_graph', '_session', '_free_vars', '_objective', '_minusF', '_minusG']:
+        for key in ['_graph', '_session', '_free_vars', '_objective', '_minusF', '_minusG', '_feed_dict_keys']:
             try:
                 d.pop(key)
             except:
@@ -146,9 +146,10 @@ def _compile(self, optimizer=None):
             print("compiling tensorflow function...")
         sys.stdout.flush()
 
+        self._feed_dict_keys = self.get_feed_dict_keys()
         def obj(x):
             feed_dict = {self._free_vars: x}
-            feed_dict.update(self.get_feed_dict())
+            self.update_feed_dict(self._feed_dict_keys, feed_dict)
             f, g = self._session.run([self._minusF, self._minusG],
                                      feed_dict=feed_dict)
             return f.astype(np.float64), g.astype(np.float64)
@@ -221,11 +222,13 @@ def _optimize_tf(self, method, callback, maxiter):
         Optimize the model using a tensorflow optimizer. See self.optimize()
         """
         opt_step = self._compile(optimizer=method)
+        feed_dict = {}
+        self.update_feed_dict(self._feed_dict_keys, feed_dict)
 
         try:
             iteration = 0
             while iteration < maxiter:
-                self._session.run(opt_step, feed_dict=self.get_feed_dict())
+                self._session.run(opt_step, feed_dict=feed_dict)
                 if callback is not None:
                     callback(self._session.run(self._free_vars))
                 iteration += 1
diff --git a/GPflow/param.py b/GPflow/param.py
--- a/GPflow/param.py
+++ b/GPflow/param.py
@@ -139,7 +139,7 @@ class Param(Parentable):
 
     There is a self.fixed flag, in which case the parameter does not get
     optimized. To enable this, during make_tf_array, a fixed parameter will be
-    ignored, and a placeholder returned via get_feed_dict instead.
+    ignored, and a placeholder added to the feed_dict instead.
 
     Fixes and transforms can be used together, in the sense that fixes take
     priority over transforms, so unfixing a parameter is as simple as setting
@@ -166,7 +166,7 @@ class Param(Parentable):
     vector representing the free state. In this case, the parameter is
     represented as part of the 'free-state' vector associated with a model.
     However, if the parameters is fixed, then a placeholder is returned during
-    a call to get_feed_dict, and the parameter is represented that way instead.
+    a call to update_feed_dict, and the parameter is represented that way instead.
 
     **Priors and transforms**
 
@@ -246,15 +246,27 @@ def get_free_state(self):
             return np.empty((0,), np_float_type)
         return self.transform.backward(self.value.flatten())
 
-    def get_feed_dict(self):
+    def get_feed_dict_keys(self):
         """
-        Return a dictionary matching up any fixed-placeholders to their values
+        If this parameter is fixed, Return a dictionary mapping from self to self.value.
+        Else return an empty dictionary.
         """
         d = {}
         if self.fixed:
-            d[self._tf_array] = self.value
+            d[self] = self._tf_array
         return d
 
+    def update_feed_dict(self, key_dict, feed_dict):
+        """
+        key_dict is a dictionary which maps from objects (including self) to tensorflow placeholders.
+        feed_dict is a dictionary which will be fed to tensorflow.
+
+        If this parameter is fixed, we add self.value to the feed dict, paired
+        with the appropriate placeholder from the key_dict.
+        """
+        if self.fixed:
+            feed_dict[key_dict[self]] = self.value
+
     def set_state(self, x):
         """
         Given a vector x representing the 'free' state of this Param, transform
@@ -343,7 +355,7 @@ class DataHolder(Parentable):
     An object to represent data which needs to be passed to tensorflow for computation.
 
     This behaves in much the same way as a Param (above), but is always
-    'fixed'. On a call to get_feed_dict, a placeholder-numpy pair is returned.
+    'fixed'. On a call to update_feed_dict, a placeholder-numpy pair is added to the feed_dict.
 
     Getting and setting values
     --
@@ -387,8 +399,11 @@ def _get_type(self, array):
         else:
             raise NotImplementedError("unknown dtype")
 
-    def get_feed_dict(self):
-        return {self._tf_array: self._array}
+    def get_feed_dict_keys(self):
+        return {self: self._tf_array}
+
+    def update_feed_dict(self, key_dict, feed_dict):
+        feed_dict[key_dict[self]] = self._array
 
     def __getstate__(self):
         d = Parentable.__getstate__(self)
@@ -511,10 +526,13 @@ def runnable(instance, *np_args):
                     instance.make_tf_array(storage['free_vars'])
                     with instance.tf_mode():
                         storage['tf_result'] = tf_method(instance, *storage['tf_args'])
-                    storage['session'].run(tf.initialize_all_variables(), feed_dict=instance.get_feed_dict())
+                    storage['feed_dict_keys'] = instance.get_feed_dict_keys()
+                    feed_dict = {}
+                    instance.update_feed_dict(storage['feed_dict_keys'], feed_dict)
+                    storage['session'].run(tf.initialize_all_variables(), feed_dict=feed_dict)
             feed_dict = dict(zip(storage['tf_args'], np_args))
             feed_dict[storage['free_vars']] = instance.get_free_state()
-            feed_dict.update(instance.get_feed_dict())
+            instance.update_feed_dict(storage['feed_dict_keys'], feed_dict)
             return storage['session'].run(storage['tf_result'], feed_dict=feed_dict)
 
         return runnable
@@ -753,15 +771,20 @@ def get_free_state(self):
         return np.hstack([p.get_free_state() for p in self.sorted_params] +
                          [np.empty(0, np_float_type)])
 
-    def get_feed_dict(self):
+    def get_feed_dict_keys(self):
         """
-        Recursively fetch a dictionary matching up fixed-placeholders to associated values
+        Recursively generate a dictionary of {object: _tf_array} pairs that can be used in update_feed_dict
         """
         d = {}
         for p in self.sorted_params + self.data_holders:
-            d.update(p.get_feed_dict())
+            d.update(p.get_feed_dict_keys())
         return d
 
+    def update_feed_dict(self, key_dict, feed_dict):
+        for p in self.sorted_params + self.data_holders:
+            p.update_feed_dict(key_dict, feed_dict)
+        return feed_dict
+
     def set_state(self, x):
         """
         Set the values of all the parameters by recursion
diff --git a/GPflow/svgp.py b/GPflow/svgp.py
--- a/GPflow/svgp.py
+++ b/GPflow/svgp.py
@@ -26,7 +26,7 @@
 
 class MinibatchData(DataHolder):
     """
-    A special DataHolder class which feeds a minibatch to tensorflow via get_feed_dict().
+    A special DataHolder class which feeds a minibatch to tensorflow via update_feed_dict().
     """
     def __init__(self, array, minibatch_size, rng=None):
         """
@@ -48,8 +48,8 @@ def generate_index(self):
             # uncommon when using SVI.
             return self.rng.randint(self._array.shape[0], size=self.minibatch_size)
 
-    def get_feed_dict(self):
-        return {self._tf_array: self._array[self.generate_index()]}
+    def update_feed_dict(self, key_dict, feed_dict):
+        feed_dict[key_dict[self]] = self._array[self.generate_index()]
 
 
 class SVGP(GPModel):


Test patch:
diff --git a/testing/test_autoflow.py b/testing/test_autoflow.py
--- a/testing/test_autoflow.py
+++ b/testing/test_autoflow.py
@@ -126,6 +126,11 @@ def test_predict_y(self):
     def test_predict_density(self):
         self.m.predict_density(self.Xtest, self.Ytest)
 
+    def test_multiple_AFs(self):
+        self.m.compute_log_likelihood()
+        self.m.compute_log_prior()
+        self.m.compute_log_likelihood()
+
 
 class TestResetGraph(unittest.TestCase):
     def setUp(self):
diff --git a/testing/test_data_object.py b/testing/test_data_object.py
--- a/testing/test_data_object.py
+++ b/testing/test_data_object.py
@@ -26,17 +26,23 @@ def test_types(self):
     def test_same_shape(self):
         new_X = self.rng.randn(2, 2)
         self.m.X = new_X
-        assert np.all(self.m.get_feed_dict()[self.m.X._tf_array] == new_X)
+        fd = {}
+        self.m.update_feed_dict(self.m.get_feed_dict_keys(), fd)
+        assert np.all(fd[self.m.X._tf_array] == new_X)
         self.assertFalse(self.m._needs_recompile)
 
         new_Y = self.rng.randn(2, 2)
         self.m.Y = new_Y
-        assert np.all(self.m.get_feed_dict()[self.m.Y._tf_array] == new_Y)
+        fd = {}
+        self.m.update_feed_dict(self.m.get_feed_dict_keys(), fd)
+        assert np.all(fd[self.m.Y._tf_array] == new_Y)
         self.assertFalse(self.m._needs_recompile)
 
         new_Z = self.rng.randn(2, 2)
         self.m.Z = new_Z
-        assert np.all(self.m.get_feed_dict()[self.m.Z._tf_array] == new_Z)
+        fd = {}
+        self.m.update_feed_dict(self.m.get_feed_dict_keys(), fd)
+        assert np.all(fd[self.m.Z._tf_array] == new_Z)
         self.assertFalse(self.m._needs_recompile)
 
     def test_pass(self):
@@ -66,7 +72,9 @@ def test_types(self):
     def test_same_shape(self):
         new_X = self.rng.randint(0, 10, (2, 2))
         self.m.X = new_X
-        assert np.all(self.m.get_feed_dict()[self.m.X._tf_array] == new_X)
+        fd = {}
+        self.m.update_feed_dict(self.m.get_feed_dict_keys(), fd)
+        assert np.all(fd[self.m.X._tf_array] == new_X)
         self.assertFalse(self.m._needs_recompile)