Repository: keras-team/keras

Issue:
KeyError in to_json when model has multiple components
This is ultimately a 'doctor it hurts when I do this problem', but I'm running into an error in to_json when there are multiple model inputs and one of the inputs is not used in the outputs.

Here's an example:

    class MinRep(object):
        def __init__(self,a,b):
            self.a = a
            self.b = b
            self.build()
        def build(self):
            self.c = Lambda(lambda x: x[:, :, :, :1])(self.a)
            self.d = Add()([self.b, self.c])
            self.model = Model(inputs=[self.a, self.b], outputs=self.c)
    a = Input(shape=(256,512,6,))
    b = Input(shape=(256,512,1,))
    fs = MinRep(a,b)
    open("fns.json","w").write(fs.model.to_json())

I get this error:

    Traceback (most recent call last):
      File "./anotherrepro.py", line 24, in <module>
        open("fns.json","w").write(fs.model.to_json())
      File "/Users/jake/ten1.2/lib/python3.6/site-packages/keras/engine/topology.py", line 2618, in to_json
        model_config = self._updated_config()
      File "/Users/jake/ten1.2/lib/python3.6/site-packages/keras/engine/topology.py", line 2585, in _updated_config
        config = self.get_config()
      File "/Users/jake/ten1.2/lib/python3.6/site-packages/keras/engine/topology.py", line 2370, in get_config
        new_node_index = node_conversion_map[node_key]
    KeyError: 'input_2_ib-0'

If I change the output to self.d (so that the output depends on both input parameters), it works fine.
KeyError in to_json when model has multiple components
This is ultimately a 'doctor it hurts when I do this problem', but I'm running into an error in to_json when there are multiple model inputs and one of the inputs is not used in the outputs.

Here's an example:

    class MinRep(object):
        def __init__(self,a,b):
            self.a = a
            self.b = b
            self.build()
        def build(self):
            self.c = Lambda(lambda x: x[:, :, :, :1])(self.a)
            self.d = Add()([self.b, self.c])
            self.model = Model(inputs=[self.a, self.b], outputs=self.c)
    a = Input(shape=(256,512,6,))
    b = Input(shape=(256,512,1,))
    fs = MinRep(a,b)
    open("fns.json","w").write(fs.model.to_json())

I get this error:

    Traceback (most recent call last):
      File "./anotherrepro.py", line 24, in <module>
        open("fns.json","w").write(fs.model.to_json())
      File "/Users/jake/ten1.2/lib/python3.6/site-packages/keras/engine/topology.py", line 2618, in to_json
        model_config = self._updated_config()
      File "/Users/jake/ten1.2/lib/python3.6/site-packages/keras/engine/topology.py", line 2585, in _updated_config
        config = self.get_config()
      File "/Users/jake/ten1.2/lib/python3.6/site-packages/keras/engine/topology.py", line 2370, in get_config
        new_node_index = node_conversion_map[node_key]
    KeyError: 'input_2_ib-0'

If I change the output to self.d (so that the output depends on both input parameters), it works fine.


Discussion:

Resolving patch:
diff --git a/keras/engine/topology.py b/keras/engine/topology.py
--- a/keras/engine/topology.py
+++ b/keras/engine/topology.py
@@ -310,6 +310,21 @@ def __init__(self, **kwargs):
         else:
             self._initial_weights = None
 
+    @staticmethod
+    def _node_key(layer, node_index):
+        """Converts a layer and its index to a unique (immutable type) name.
+        This function is used internally with `self.container_nodes`.
+
+        # Arguments
+            layer: The layer.
+            node_index: The layer's position (e.g. via enumerate) in a list of
+                nodes.
+
+        # Returns
+            The unique name.
+        """
+        return layer.name + '_ib-' + str(node_index)
+
     @property
     def losses(self):
         return self._losses
@@ -1665,9 +1680,8 @@ def build_map_of_graph(tensor, finished_nodes, nodes_in_progress,
             if node in finished_nodes:
                 return
 
-            node_key = layer.name + '_ib-' + str(node_index)
             # Update container_nodes.
-            container_nodes.add(node_key)
+            container_nodes.add(self._node_key(layer, node_index))
 
             # Store the traversal order for layer sorting.
             if layer not in layer_indices:
@@ -1866,7 +1880,7 @@ def updates(self):
                 # Collect updates that are dependent on inputs
                 # that are part of the model.
                 for node_index, node in enumerate(layer.inbound_nodes):
-                    node_key = layer.name + '_ib-' + str(node_index)
+                    node_key = self._node_key(layer, node_index)
                     if node_key in self.container_nodes:
                         # The model owns this layer node.
                         inputs = node.input_tensors
@@ -1894,7 +1908,7 @@ def losses(self):
                 # Collect losses that are dependent on inputs
                 # that are part of the model.
                 for node_index, node in enumerate(layer.inbound_nodes):
-                    node_key = layer.name + '_ib-' + str(node_index)
+                    node_key = self._node_key(layer, node_index)
                     if node_key in self.container_nodes:
                         # The model owns this layer node.
                         inputs = node.input_tensors
@@ -2281,6 +2295,10 @@ def get_config(self):
         config = {
             'name': self.name,
         }
+
+        # Build a map from a layer unique name (self._node_key)
+        # to the index of the nodes that are saved in the config.
+        # Only nodes in container_nodes are saved.
         node_conversion_map = {}
         for layer in self.layers:
             if issubclass(layer.__class__, Container):
@@ -2290,17 +2308,20 @@ def get_config(self):
             else:
                 kept_nodes = 0
             for original_node_index, node in enumerate(layer.inbound_nodes):
-                node_key = layer.name + '_ib-' + str(original_node_index)
+                node_key = self._node_key(layer, original_node_index)
                 if node_key in self.container_nodes:
+                    # i.e. we mark it to be saved
                     node_conversion_map[node_key] = kept_nodes
                     kept_nodes += 1
+
+        # serialize and save the layers in layer_configs
         layer_configs = []
         for layer in self.layers:  # From the earliest layers on.
             layer_class_name = layer.__class__.__name__
             layer_config = layer.get_config()
             filtered_inbound_nodes = []
             for original_node_index, node in enumerate(layer.inbound_nodes):
-                node_key = layer.name + '_ib-' + str(original_node_index)
+                node_key = self._node_key(layer, original_node_index)
                 if node_key in self.container_nodes:
                     # The node is relevant to the model:
                     # add to filtered_inbound_nodes.
@@ -2324,8 +2345,9 @@ def get_config(self):
                             inbound_layer = node.inbound_layers[i]
                             node_index = node.node_indices[i]
                             tensor_index = node.tensor_indices[i]
-                            node_key = inbound_layer.name + '_ib-' + str(node_index)
-                            new_node_index = node_conversion_map.get(node_key, 0)
+
+                            new_node_index = node_conversion_map.get(
+                                self._node_key(inbound_layer, node_index), 0)
                             node_data.append([inbound_layer.name,
                                               new_node_index,
                                               tensor_index,
@@ -2344,7 +2366,10 @@ def get_config(self):
         for i in range(len(self.input_layers)):
             layer = self.input_layers[i]
             node_index = self.input_layers_node_indices[i]
-            node_key = layer.name + '_ib-' + str(node_index)
+
+            node_key = self._node_key(layer, node_index)
+            if node_key not in self.container_nodes:
+                continue
             new_node_index = node_conversion_map[node_key]
             tensor_index = self.input_layers_tensor_indices[i]
             model_inputs.append([layer.name, new_node_index, tensor_index])
@@ -2353,7 +2378,10 @@ def get_config(self):
         for i in range(len(self.output_layers)):
             layer = self.output_layers[i]
             node_index = self.output_layers_node_indices[i]
-            node_key = layer.name + '_ib-' + str(node_index)
+
+            node_key = self._node_key(layer, node_index)
+            if node_key not in self.container_nodes:
+                continue
             new_node_index = node_conversion_map[node_key]
             tensor_index = self.output_layers_tensor_indices[i]
             model_outputs.append([layer.name, new_node_index, tensor_index])


Test patch:
diff --git a/tests/test_model_saving.py b/tests/test_model_saving.py
--- a/tests/test_model_saving.py
+++ b/tests/test_model_saving.py
@@ -158,6 +158,20 @@ def test_saving_right_after_compilation():
     os.remove(fname)
 
 
+@keras_test
+def test_saving_unused_layers_is_ok():
+    a = Input(shape=(256, 512, 6))
+    b = Input(shape=(256, 512, 1))
+    c = Lambda(lambda x: x[:, :, :, :1])(a)
+
+    model = Model(inputs=[a, b], outputs=c)
+
+    _, fname = tempfile.mkstemp('.h5')
+    save_model(model, fname)
+    load_model(fname)
+    os.remove(fname)
+
+
 @keras_test
 def test_loading_weights_by_name():
     """