{"action":"created","issue":{"url":"https://api.github.com/repos/Lasagne/Lasagne/issues/678","repository_url":"https://api.github.com/repos/Lasagne/Lasagne","labels_url":"https://api.github.com/repos/Lasagne/Lasagne/issues/678/labels{/name}","comments_url":"https://api.github.com/repos/Lasagne/Lasagne/issues/678/comments","events_url":"https://api.github.com/repos/Lasagne/Lasagne/issues/678/events","html_url":"https://github.com/Lasagne/Lasagne/issues/678","id":153784393,"number":678,"title":"New Layer base class","user":{"login":"f0k","id":629706,"avatar_url":"https://avatars.githubusercontent.com/u/629706?v=3","gravatar_id":"","url":"https://api.github.com/users/f0k","html_url":"https://github.com/f0k","followers_url":"https://api.github.com/users/f0k/followers","following_url":"https://api.github.com/users/f0k/following{/other_user}","gists_url":"https://api.github.com/users/f0k/gists{/gist_id}","starred_url":"https://api.github.com/users/f0k/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/f0k/subscriptions","organizations_url":"https://api.github.com/users/f0k/orgs","repos_url":"https://api.github.com/users/f0k/repos","events_url":"https://api.github.com/users/f0k/events{/privacy}","received_events_url":"https://api.github.com/users/f0k/received_events","type":"User","site_admin":false},"labels":[],"state":"open","locked":false,"assignee":null,"milestone":null,"comments":4,"created_at":"2016-05-09T14:06:40Z","updated_at":"2016-05-15T10:58:11Z","closed_at":null,"body":"This is a proposal to resolve the long-standing discussions on multiple-output layers (#337) and merge layers accepting dictionaries (#537), both required for recurrent containers (#629). It was originally formulated in https://github.com/Lasagne/Lasagne/issues/537#issuecomment-211954226; I'll give some more detail here and hope to reignite the discussion.\r\n\r\nMain idea: Create a new `BaseLayer` class, with `Layer`, `InputLayer` and `MergeLayer` subclassing it.\r\nThe `BaseLayer` would be something quite universal: its `incomings` could be a single layer, or a list of layers, or a dictionary of layers, or a list of dictionaries of layers, or anything along those lines. Similarly, its output shape could be a single tuple, or a list of tuples, or a dictionary of tuples, or a list of dictionaries of tuples, or anything along those lines. Its output would match its output shape definition (i.e., it would produce a tensor, or a list of tensors, or a dictionary of tensors, you get it).\r\nThe `Layer` subclass would check that, after construction, its input shape is a single shape tuple.\r\nThe `InputLayer` subclass would further check that its input layer is `None`. It can subclass `Layer`.\r\nThe `MergeLayer` subclass would instead check that its input shape is a list of tuples. We can have another subclass, `DictMergeLayer`, that checks that its input shape is a dictionary of tuples.\r\n`lasagne.layers.get_output_for()` would only need to know how to handle a `BaseLayer` and an `InputLayer`.\r\n\r\nThis change would require `lasagne.layers.get_output_for()` to become quite a bit more generic, as it needs to deal with nested dictionaries and lists. However, it would avoid further `isinstance` checks. The only special case would be for `InputLayer`, in case an input is passed to `get_output_for()` and we need to find the layer it should be linked to (and we don't want to link it to any free-floating layer we can find). While nested dictionaries and lists seem unnecessarily complex, they would seamlessly allow `MergeLayer`s of multiple-output layers. But we don't necessarily have to allow them.\r\n\r\nThe gist of this is that any `BaseLayer` will just have to rely on its `input_shapes` property to see what to expect in its `get_output_for()` method. It doesn't have to care whether it is linked to a single layer producing multiple outputs, or multiple layers producing single outputs, or anything more complex.\r\n\r\nSketch of the code:\r\n```python\r\nclass BaseLayer(object):\r\n    def __init__(self, incomings, name=None):\r\n        # assign self.input_shapes and self.input_layers accordingly\r\n        self.input_shapes = ...\r\n        self.input_layers = ...\r\n        self.name = name\r\n\r\nclass Layer(BaseLayer):\r\n    def __init__(self, incoming, **kwargs):\r\n        super(Layer, self).__init__(incoming, **kwargs)\r\n        if not isinstance(self.input_shapes, tuple):\r\n            raise TypeError(\"%s expects a single input only\" % self.__class__.__name__)\r\n    @property\r\n    def input_shape(self):\r\n        return self.input_shapes\r\n    @property\r\n    def input_layer(self):\r\n        return self.input_layers\r\n\r\nclass InputLayer(Layer):\r\n    def __init__(self, shape, input_var=None, **kwargs):\r\n        super(InputLayer, self).__init__(shape, **kwargs)\r\n        if self.input_layer is not None:\r\n            raise TypeError(\"InputLayer shape must be a shape tuple, not a %r\" % type(shape))\")\r\n        if input_var is None:\r\n            ...\r\n        self.input_var = input_var\r\n    def get_output_for(self, input, **kwargs):\r\n        assert input is None\r\n        return self.input_var\r\n\r\nclass MergeLayer(BaseLayer):\r\n    def __init__(self, incomings, **kwargs):\r\n        super(MergeLayer, self).__init__(incomings, **kwargs)\r\n        if not isinstance(self.input_shapes, list) or any(not isinstance(shape, tuple) for shape in self.input_shapes):\r\n            raise TypeError(\"%s expects a list of inputs, got %r\" % (self.__class__.__name__, self.input_shapes))\r\n    def get_output_for(self, inputs, **kwargs):\r\n        # the MergeLayer base class just returns a list of incoming tensors\r\n        return inputs\r\n\r\nclass DictMergeLayer(BaseLayer):\r\n    def __init__(self, incomings, **kwargs):\r\n        super(DictMergeLayer, self).__init__(incomings, **kwargs)\r\n        if not isinstance(self.input_shapes, dict) or any(not isinstance(shape, tuple) for shape in self.input_shapes.values()):\r\n            raise TypeError(\"%s expects a dict of inputs, got %r\" % (self.__class__.__name__, self.input_shapes))\r\n    def get_output_for(self, inputs, **kwargs):\r\n        # the DictMergeLayer base class just returns a dict of incoming tensors\r\n        return inputs\r\n```\r\n\r\nNote that this will not change anything for existing `Layer` and `MergeLayer` subclasses -- they can still access their incomings via `self.input_shape` or `self.input_shapes` and know they're of the expected form (a single shape or a list of shapes). New code can continue to use either of these base classes, or start with `BaseLayer` if they want to support wild combinations of multiple inputs.\r\n\r\nIf you're still with me, what do you think of this proposal?\r\n\r\n---\r\n\r\nSomething more technical that's just relevant to the concrete implementation: For full backward compatibility, we would need to have `MergeLayer` subclass `Layer` -- user code may have `isinstance(..., Layer)` in place and expect to catch everything. This is annoying, as we can't do the \"is it a single input tensor\" check in the `Layer` constructor then, but will require a separate `self.check_inputs` method called from the constructor that subclasses can override. If we're that far, we can also turn things around, ditch the `BaseLayer` and have `Layer` implement all its functionality, with a new `UniversalLayer` that subclasses `Layer` just to override the `check_inputs` method with something more liberal (`return True`). Either this, or we tell users to replace `isinstance(..., Layer)` with `isinstance(..., BaseLayer)` as needed.\r\n\r\n---\r\n\r\nAs another variation, `MergeLayer` could accept either a list or a dict, and `DictMergeLayer` / `ListMergeLayer` would restrict this further."},"comment":{"url":"https://api.github.com/repos/Lasagne/Lasagne/issues/comments/219278568","html_url":"https://github.com/Lasagne/Lasagne/issues/678#issuecomment-219278568","issue_url":"https://api.github.com/repos/Lasagne/Lasagne/issues/678","id":219278568,"user":{"login":"benanne","id":43829,"avatar_url":"https://avatars.githubusercontent.com/u/43829?v=3","gravatar_id":"","url":"https://api.github.com/users/benanne","html_url":"https://github.com/benanne","followers_url":"https://api.github.com/users/benanne/followers","following_url":"https://api.github.com/users/benanne/following{/other_user}","gists_url":"https://api.github.com/users/benanne/gists{/gist_id}","starred_url":"https://api.github.com/users/benanne/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/benanne/subscriptions","organizations_url":"https://api.github.com/users/benanne/orgs","repos_url":"https://api.github.com/users/benanne/repos","events_url":"https://api.github.com/users/benanne/events{/privacy}","received_events_url":"https://api.github.com/users/benanne/received_events","type":"User","site_admin":false},"created_at":"2016-05-15T10:58:11Z","updated_at":"2016-05-15T10:58:11Z","body":"> Maybe that's really something we shouldn't care too much about.\r\n\r\nIf we decide not to worry about it, that would mean we are free to rename `Layer`, right? Or is there something else I'm missing?"}}