Repository: chainer/chainer

Issue:
Empty Sequential has different semantics than other cases
As you can see from below, an empty `chainer.Sequential()` has different semantics: it wraps an argument with a tuple and return it. If at least one built-in links or functions exist, it returns a single `Variable` object.

I think it's better to forbid calling an empty `Sequential`.

#### Code
```py
import numpy
import chainer
from chainer import functions as F

x = numpy.array([1, 2], numpy.float32)
seq = chainer.Sequential()
ret = seq(x)

print(ret)
print(type(ret))
print(type(ret[0]))
print()

seq = chainer.Sequential(F.tanh)
ret = seq(x)
print(type(ret))
```
#### Result
```
(array([1., 2.], dtype=float32),)
<class 'tuple'>
<class 'numpy.ndarray'>

<class 'chainer.variable.Variable'>
```


Discussion:

Resolving patch:
diff --git a/chainer/sequential.py b/chainer/sequential.py
--- a/chainer/sequential.py
+++ b/chainer/sequential.py
@@ -205,6 +205,9 @@ def forward(self, *x):
             The output of the final layer in the given layers.
 
         """
+        if len(self._layers) == 0:
+            raise RuntimeError('Sequential does not have any layer.')
+
         for layer in self._layers:
             if isinstance(x, tuple):
                 x = layer(*x)


Test patch:
diff --git a/tests/chainer_tests/test_sequential.py b/tests/chainer_tests/test_sequential.py
--- a/tests/chainer_tests/test_sequential.py
+++ b/tests/chainer_tests/test_sequential.py
@@ -5,12 +5,13 @@
 
 import mock
 import numpy
+import pytest
 import six
 
+import chainer
 from chainer import cuda
 from chainer import functions
 from chainer import links
-from chainer import sequential
 from chainer import testing
 from chainer.testing import attr
 from chainer import variable
@@ -23,10 +24,10 @@ def setUp(self):
         self.l2 = links.Linear(3, 2)
         self.l3 = links.Linear(2, 3)
         # s1: l1 -> l2
-        self.s1 = sequential.Sequential(self.l1)
+        self.s1 = chainer.Sequential(self.l1)
         self.s1.append(self.l2)
         # s2: s1 (l1 -> l2) -> l3
-        self.s2 = sequential.Sequential(self.s1)
+        self.s2 = chainer.Sequential(self.s1)
         self.s2.append(self.l3)
 
     def test_init(self):
@@ -35,7 +36,7 @@ def test_init(self):
         self.assertIs(self.s2[0], self.s1)
         self.assertEqual(self.s1.name, '0')
         with self.assertRaises(ValueError):
-            sequential.Sequential(0)
+            chainer.Sequential(0)
 
     def test_append(self):
         self.assertIs(self.s2[1], self.l3)
@@ -256,8 +257,8 @@ def test_copyparams(self):
         l1 = links.Linear(None, 3)
         l2 = links.Linear(3, 2)
         l3 = links.Linear(2, 3)
-        s1 = sequential.Sequential(l1, l2)
-        s2 = sequential.Sequential(s1, l3)
+        s1 = chainer.Sequential(l1, l2)
+        s2 = chainer.Sequential(s1, l3)
         l1.b.data.fill(0)
         l2.W.data.fill(1)
         l2.b.data.fill(2)
@@ -298,8 +299,8 @@ def test_addgrads(self):
         l1 = links.Linear(2, 3)
         l2 = links.Linear(3, 2)
         l3 = links.Linear(2, 3)
-        s1 = sequential.Sequential(l1, l2)
-        s2 = sequential.Sequential(s1, l3)
+        s1 = chainer.Sequential(l1, l2)
+        s2 = chainer.Sequential(s1, l3)
         l1.b.grad.fill(1)
         l2.W.grad.fill(2)
         l2.b.grad.fill(3)
@@ -327,7 +328,7 @@ def test_serialize(self):
         l2 = links.Linear(None, 3)
         with l2.init_scope():
             l2.x = variable.Parameter(0, 2)
-        s1 = sequential.Sequential(l1, l2)
+        s1 = chainer.Sequential(l1, l2)
         mocks = {'0': mock.MagicMock(), '1': mock.MagicMock()}
         serializer = mock.MagicMock()
         serializer.__getitem__.side_effect = lambda k: mocks[k]
@@ -371,7 +372,7 @@ def test_contains(self):
     def test_add(self):
         l1 = links.Linear(3, 2)
         l2 = functions.relu
-        other = sequential.Sequential(l1, l2)
+        other = chainer.Sequential(l1, l2)
         added = self.s2 + other
         self.assertEqual(len(added), 4)
         self.assertIs(added[0], self.s1)
@@ -383,7 +384,7 @@ def test_add(self):
 
     def test_iadd(self):
         l4 = links.Linear(3, 1)
-        self.s2 += sequential.Sequential(l4)
+        self.s2 += chainer.Sequential(l4)
         self.assertIs(self.s2[0], self.s1)
         self.assertIs(self.s2[1], self.l3)
         self.assertIs(self.s2[2], l4)
@@ -394,7 +395,7 @@ def test_call(self):
         l1 = mock.MagicMock()
         l2 = mock.MagicMock()
         l3 = mock.MagicMock()
-        model = sequential.Sequential(l1, l2, l3)
+        model = chainer.Sequential(l1, l2, l3)
         x = numpy.arange(2).reshape(1, 2).astype('f')
         y = model(x)
         l1.assert_called_once()
@@ -404,7 +405,7 @@ def test_call(self):
         self.assertIs(y.creator.inputs[1].data, self.l2.W.data)
 
     def test_call_with_multiple_inputs(self):
-        model = sequential.Sequential(
+        model = chainer.Sequential(
             lambda x, y: (x * 2, y * 3, x + y),
             lambda x, y, z: x + y + z
         )
@@ -414,7 +415,7 @@ def test_call_with_multiple_inputs(self):
     def test_extend(self):
         l1 = links.Linear(3, 2)
         l2 = links.Linear(2, 3)
-        s3 = sequential.Sequential(l1, l2)
+        s3 = chainer.Sequential(l1, l2)
         self.s2.extend(s3)
         self.assertEqual(len(self.s2), 4)
         self.assertIs(self.s2[2], s3[0])
@@ -581,4 +582,12 @@ def test_flatten(self):
         self.assertIs(flattened_s2[2], self.l3)
 
 
+class TestEmptySequential(unittest.TestCase):
+    def test_empty_sequential(self):
+        seq = chainer.Sequential()
+        x = numpy.ones((2, 3), numpy.float32)
+        with pytest.raises(RuntimeError):
+            seq(x)
+
+
 testing.run_module(__name__, __file__)