Repository: chainer/chainer

Issue:
L.Linear without in_size raises ZeroDivisionError on first input if the batchsize is 0
This is probably fixed by changing `x.size // x.shape[0]` to `numpy.prod(x.shape[1:])` though it requires numpy.  (Note: The current code is no better than `x[0].size`.)

Motivation: I'd like to initialize a Chain by a 'dry run'.

To reproduce:
```
>>> import chainer
>>> import numpy as np
>>> l=chainer.links.Linear(4)
>>> l(np.ones((2, 3), dtype=np.float32))
variable([[-0.93691719,  1.47195649, -0.66054785,  0.62899256],
          [-0.93691719,  1.47195649, -0.66054785,  0.62899256]])
>>> l(np.ones((0, 3), dtype=np.float32))
variable([], shape=(0, 4))
>>> l=chainer.links.Linear(4)
>>> l(np.ones((0, 3), dtype=np.float32))
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/Users/****/lib/python3.6/site-packages/chainer/links/connection/linear.py", line 126, in __call__
    self._initialize_params(x.size // x.shape[0])
ZeroDivisionError: integer division or modulo by zero
>>> chainer.__version__
'3.0.0'
```


Discussion:

Resolving patch:
diff --git a/chainer/links/connection/linear.py b/chainer/links/connection/linear.py
--- a/chainer/links/connection/linear.py
+++ b/chainer/links/connection/linear.py
@@ -1,3 +1,6 @@
+import functools
+import operator
+
 from chainer.functions.connection import linear
 from chainer import initializers
 from chainer import link
@@ -120,5 +123,6 @@ def __call__(self, x):
 
         """
         if self.W.data is None:
-            self._initialize_params(x.size // x.shape[0])
+            in_size = functools.reduce(operator.mul, x.shape[1:], 1)
+            self._initialize_params(in_size)
         return linear.linear(x, self.W, self.b)
diff --git a/chainer/links/connection/lstm.py b/chainer/links/connection/lstm.py
--- a/chainer/links/connection/lstm.py
+++ b/chainer/links/connection/lstm.py
@@ -1,3 +1,6 @@
+import functools
+import operator
+
 import numpy
 import six
 
@@ -298,7 +301,7 @@ def __call__(self, x):
         """
         if self.upward.W.data is None:
             with cuda.get_device_from_id(self._device_id):
-                in_size = x.size // x.shape[0]
+                in_size = functools.reduce(operator.mul, x.shape[1:], 1)
                 self.upward._initialize_params(in_size)
                 self._initialize_params()
 
diff --git a/chainer/links/normalization/layer_normalization.py b/chainer/links/normalization/layer_normalization.py
--- a/chainer/links/normalization/layer_normalization.py
+++ b/chainer/links/normalization/layer_normalization.py
@@ -1,3 +1,6 @@
+import functools
+import operator
+
 from chainer.functions.normalization import layer_normalization
 from chainer import link
 from chainer import utils
@@ -78,7 +81,8 @@ def __call__(self, x):
 
         """
         if self.gamma.data is None:
-            self._initialize_params(x.size // x.shape[0])
+            in_size = functools.reduce(operator.mul, x.shape[1:], 1)
+            self._initialize_params(in_size)
 
         return layer_normalization.layer_normalization(
             x, self.gamma, self.beta, self.eps)


Test patch:
diff --git a/tests/chainer_tests/links_tests/connection_tests/test_linear.py b/tests/chainer_tests/links_tests/connection_tests/test_linear.py
--- a/tests/chainer_tests/links_tests/connection_tests/test_linear.py
+++ b/tests/chainer_tests/links_tests/connection_tests/test_linear.py
@@ -154,6 +154,17 @@ def test_serialization(self):
         self.assertEqual((w1 == w2).all(), True)
 
 
+class TestEmptyBatchInitialize(unittest.TestCase):
+
+    def setUp(self):
+        self.link = links.Linear(4)
+        self.x = numpy.random.uniform(-1, 1, (0, 3)).astype(numpy.float32)
+
+    def test_empty_batch_dim(self):
+        y = self.link(chainer.Variable(self.x))
+        assert y.shape == (0, 4)
+
+
 class TestInvalidLinear(unittest.TestCase):
 
     def setUp(self):
diff --git a/tests/chainer_tests/links_tests/connection_tests/test_lstm.py b/tests/chainer_tests/links_tests/connection_tests/test_lstm.py
--- a/tests/chainer_tests/links_tests/connection_tests/test_lstm.py
+++ b/tests/chainer_tests/links_tests/connection_tests/test_lstm.py
@@ -248,6 +248,17 @@ def test_initialize_bias_default(self):
             numpy.array([0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0]))
 
 
+class TestLSTMEmptyBatchInitialize(unittest.TestCase):
+
+    def setUp(self):
+        self.link = links.LSTM(4)
+        self.x = numpy.random.uniform(-1, 1, (0, 3)).astype(numpy.float32)
+
+    def test_empty_batch_dim(self):
+        y = self.link(chainer.Variable(self.x))
+        assert y.shape == (0, 4)
+
+
 @testing.parameterize(*testing.product_dict(
     [
         {'in_size': 10, 'out_size': 10},
diff --git a/tests/chainer_tests/links_tests/normalization_tests/test_layer_normalization.py b/tests/chainer_tests/links_tests/normalization_tests/test_layer_normalization.py
--- a/tests/chainer_tests/links_tests/normalization_tests/test_layer_normalization.py
+++ b/tests/chainer_tests/links_tests/normalization_tests/test_layer_normalization.py
@@ -153,6 +153,18 @@ def test_initialize_gpu(self):
             numpy.zeros(self.size), self.link.beta.data)
 
 
+class TestEmptyBatchInitialize(unittest.TestCase):
+
+    def setUp(self):
+        self.link = _create_ln()
+        self.shape = (0, 3)
+        self.x = numpy.random.uniform(-1, 1, self.shape).astype(numpy.float32)
+
+    def test_empty_batch_dim(self):
+        y = self.link(chainer.Variable(self.x))
+        assert y.shape == self.shape
+
+
 @testing.parameterize(*testing.product({
     'shape': [(2, 4, 3), (2, 5, 3, 4)],
 }))