Repository: chainer/chainer

Issue:
ChildSumTreeLSTM cannot accept less than 2 children
ChildSumTreeLSTM cannot accept less than 2 children.
For example, for 1 child case,
```
self.encoder(None, None, x)
```
an error
```
Expect: in_types.size >= 3
Actual: 2 < 3
```

occurs.
Modifying [tree_lstm function](https://github.com/chainer/chainer/blob/v3.0.0/chainer/functions/activation/tree_lstm.py#L68) is required.

kc reported this issue on [Chainer Slack](https://chainer.slack.com/archives/C3TDS9QGK/p1508909406000009), thanks.


Discussion:
This issue has been automatically marked as stale because it has not had recent activity. It will be closed after 30 days if no further activity occurs. Thank you for your contributions.

@soskek Can you make a PR to resolve this issue?

Resolving patch:
diff --git a/chainer/functions/activation/tree_lstm.py b/chainer/functions/activation/tree_lstm.py
--- a/chainer/functions/activation/tree_lstm.py
+++ b/chainer/functions/activation/tree_lstm.py
@@ -65,7 +65,7 @@ class TreeLSTM(function.Function):
     """
 
     def check_type_forward(self, in_types):
-        type_check.expect(in_types.size() >= 3)
+        type_check.expect(in_types.size() >= 2)
         c_types = in_types[:-1]
         x_type = in_types[-1]
         n_ary = len(c_types)
diff --git a/chainer/links/connection/tree_lstm.py b/chainer/links/connection/tree_lstm.py
--- a/chainer/links/connection/tree_lstm.py
+++ b/chainer/links/connection/tree_lstm.py
@@ -1,3 +1,5 @@
+from chainer.functions.activation import sigmoid
+from chainer.functions.activation import tanh
 from chainer.functions.activation import tree_lstm
 from chainer.functions.array import concat
 from chainer.functions.array import split_axis
@@ -81,8 +83,7 @@ def __call__(self, *cshsx):
         cs = cshsx[:len(cshsx) // 2]
         hs = cshsx[len(cshsx) // 2:-1]
         x = cshsx[-1]
-        assert(len(cs) >= 1)
-        assert(len(hs) >= 1)
+        assert(len(cshsx) % 2 == 1)
         assert(len(cs) == len(hs))
 
         if x is None:
@@ -91,7 +92,7 @@ def __call__(self, *cshsx):
             elif any(h is not None for h in hs):
                 base = [h for h in hs if h is not None][0]
             else:
-                raise ValueError('All inputs are None.')
+                raise ValueError('All inputs (cs, hs, x) are None.')
             batchsize, dtype = base.shape[0], base.dtype
             x = self.xp.zeros(
                 (batchsize, self.in_size), dtype=dtype)
@@ -100,6 +101,13 @@ def __call__(self, *cshsx):
         W_x_aio_in, W_x_f_in = split_axis.split_axis(
             W_x_in, [3 * self.state_size], axis=1)
 
+        if len(hs) == 0:
+            aio_in = W_x_aio_in
+            a, i, o = split_axis.split_axis(aio_in, 3, axis=1)
+            c = sigmoid.sigmoid(i) * tanh.tanh(a)
+            h = sigmoid.sigmoid(o) * tanh.tanh(c)
+            return c, h
+
         hs = self._pad_zero_nodes(
             hs, (x.shape[0], self.state_size), dtype=x.dtype)
         cs = self._pad_zero_nodes(
@@ -107,7 +115,8 @@ def __call__(self, *cshsx):
 
         aio_in = self.W_h_aio(sum(hs)) + W_x_aio_in
         W_h_fs_in = concat.concat(split_axis.split_axis(
-            self.W_h_f(concat.concat(hs, axis=0)), len(hs), axis=0), axis=1)
+            self.W_h_f(concat.concat(hs, axis=0)), len(hs), axis=0),
+            axis=1)
         f_in = W_h_fs_in + \
             concat.concat([W_x_f_in] * len(hs), axis=1)
         tree_lstm_in = concat.concat([aio_in, f_in], axis=1)
@@ -179,7 +188,7 @@ class NaryTreeLSTM(link.Chain):
     """
 
     def __init__(self, in_size, out_size, n_ary=2):
-        assert(n_ary >= 2)
+        assert(n_ary >= 1)
         super(NaryTreeLSTM, self).__init__()
         with self.init_scope():
             self.W_x = linear.Linear(in_size, (3 + n_ary) * out_size)
@@ -220,7 +229,7 @@ def __call__(self, *cshsx):
             elif any(h is not None for h in hs):
                 base = [h for h in hs if h is not None][0]
             else:
-                raise ValueError('All inputs are None.')
+                raise ValueError('All inputs (cs, hs, x) are None.')
             batchsize, dtype = base.shape[0], base.dtype
             x = self.xp.zeros(
                 (batchsize, self.in_size), dtype=dtype)


Test patch:
diff --git a/tests/chainer_tests/links_tests/connection_tests/test_tree_lstm.py b/tests/chainer_tests/links_tests/connection_tests/test_tree_lstm.py
--- a/tests/chainer_tests/links_tests/connection_tests/test_tree_lstm.py
+++ b/tests/chainer_tests/links_tests/connection_tests/test_tree_lstm.py
@@ -33,19 +33,27 @@ def _child_sum_tree_lstm(func, *inputs):
         W_ha, W_hi, W_ho = xp.split(W_h_aio, 3, 1)
         W_hf = W_h_f
 
-        sum_h = sum(hs)
-        a = x.dot(W_xa) + sum_h.dot(W_ha) + b_a
-        i = x.dot(W_xi) + sum_h.dot(W_hi) + b_i
-        o = x.dot(W_xo) + sum_h.dot(W_ho) + b_o
-        f_list = [x.dot(W_xf) + h.dot(W_hf) + b_f for h in hs]
-
+        if len(hs) >= 1:
+            sum_h = sum(hs)
+            a = x.dot(W_xa) + sum_h.dot(W_ha) + b_a
+            i = x.dot(W_xi) + sum_h.dot(W_hi) + b_i
+            o = x.dot(W_xo) + sum_h.dot(W_ho) + b_o
+            f_list = [x.dot(W_xf) + h.dot(W_hf) + b_f for h in hs]
+        else:
+            a = x.dot(W_xa) + b_a
+            i = x.dot(W_xi) + b_i
+            o = x.dot(W_xo) + b_o
         a = xp.tanh(a)
         i = _sigmoid(i)
         o = _sigmoid(o)
-        f_list = [_sigmoid(f) for f in f_list]
 
-        c_next = a * i + sum(f * c for f, c in zip(f_list, cs))
-        y = o * xp.tanh(c_next)
+        if len(hs) >= 1:
+            f_list = [_sigmoid(f) for f in f_list]
+            c_next = sum([f * c for f, c in zip(f_list, cs)], a * i)
+            y = o * xp.tanh(c_next)
+        else:
+            c_next = a * i
+            y = o * xp.tanh(c_next)
     return c_next, y
 
 
@@ -97,7 +105,7 @@ def _nary_tree_lstm(func, *inputs):
 
 @testing.parameterize(*testing.product({
     'dtype': [numpy.float32],
-    'n_ary': [2, 3],
+    'n_ary': [0, 1, 2, 3],
     'in_size': [6, 9],
     'out_size': [9],
     'model_type': ['ChildSumTreeLSTM', 'NaryTreeLSTM'],
@@ -109,6 +117,9 @@ def setUp(self):
             self.link = links.ChildSumTreeLSTM(
                 self.in_size, self.out_size)
         elif self.model_type == 'NaryTreeLSTM':
+            if self.n_ary == 0:
+                # n_ary=0 test should be skipped for NaryTreeLSTM
+                self.n_ary = 1
             self.link = links.NaryTreeLSTM(
                 self.in_size, self.out_size, n_ary=self.n_ary)
         else:
@@ -180,29 +191,34 @@ def test_forward_gpu_multi(self):
     def check_forward_valid_none(self, *inputs_data):
         inputs_variable = [chainer.Variable(v)
                            if v is not None else v for v in inputs_data]
-        base = [v for v in inputs_data if v is not None][0]
-        xp = cuda.get_array_module(base)
+        xp = self.link.xp
         inputs_data = [xp.zeros(self.h_prevs[0].shape, dtype=self.dtype)
                        if v is None else v for v in inputs_data[:-1]] + \
             [xp.zeros(self.x.shape, dtype=self.dtype)
              if inputs_data[-1] is None else inputs_data[-1]]
 
-        c, h = self.link(*inputs_variable)
-        self.assertEqual(c.data.dtype, self.dtype)
-        self.assertEqual(h.data.dtype, self.dtype)
-
-        # Compute expected out
-        if self.model_type == 'ChildSumTreeLSTM':
-            c_expect, h_expect = _child_sum_tree_lstm(self.link, *inputs_data)
-        elif self.model_type == 'NaryTreeLSTM':
-            c_expect, h_expect = _nary_tree_lstm(self.link, *inputs_data)
+        if self.n_ary == 0:
+            # in this case for link(x) without cs and hs,
+            # it does not include any None.
+            pass
         else:
-            NotImplementedError()
-
-        testing.assert_allclose(
-            c_expect, c.data, **self.check_forward_options)
-        testing.assert_allclose(
-            h_expect, h.data, **self.check_forward_options)
+            c, h = self.link(*inputs_variable)
+            self.assertEqual(c.data.dtype, self.dtype)
+            self.assertEqual(h.data.dtype, self.dtype)
+
+            # Compute expected out
+            if self.model_type == 'ChildSumTreeLSTM':
+                c_expect, h_expect = _child_sum_tree_lstm(
+                    self.link, *inputs_data)
+            elif self.model_type == 'NaryTreeLSTM':
+                c_expect, h_expect = _nary_tree_lstm(self.link, *inputs_data)
+            else:
+                NotImplementedError()
+
+            testing.assert_allclose(
+                c_expect, c.data, **self.check_forward_options)
+            testing.assert_allclose(
+                h_expect, h.data, **self.check_forward_options)
 
     def test_forward_none_ch_cpu(self):
         inputs = [None] * len(self.c_prevs) + \