Repository: chainer/chainer

Issue:
Inverse function of stack
I often concatenate and split `Variable`s with `stack` and `split_axis` function. As `split_axis` is not an inverse of `stack`, I need to call `reshape` for split variables.
I need an inverse function of `stack`, that split a `(N, S, T)`-shape variable to a list of N `(S, T)`-shape variables. I don't know its corresponding function in NumPy.



Discussion:

Resolving patch:
diff --git a/chainer/functions/__init__.py b/chainer/functions/__init__.py
--- a/chainer/functions/__init__.py
+++ b/chainer/functions/__init__.py
@@ -24,6 +24,7 @@
 from chainer.functions.array import permutate
 from chainer.functions.array import reshape
 from chainer.functions.array import select_item
+from chainer.functions.array import separate
 from chainer.functions.array import split_axis
 from chainer.functions.array import stack
 from chainer.functions.array import swapaxes
@@ -144,6 +145,7 @@
 split_axis = split_axis.split_axis
 SelectItem = select_item.SelectItem
 select_item = select_item.select_item
+separate = separate.separate
 stack = stack.stack
 Swapaxes = swapaxes.Swapaxes
 swapaxes = swapaxes.swapaxes
diff --git a/chainer/functions/array/separate.py b/chainer/functions/array/separate.py
new file mode 100644
--- /dev/null
+++ b/chainer/functions/array/separate.py
@@ -0,0 +1,27 @@
+from chainer.functions.array import reshape
+from chainer.functions.array import split_axis
+
+
+def separate(x, axis=0):
+    """Separates an array along a given axis.
+
+    This function separates an array along a given axis. For example, shape of
+    an array is ``(2, 3, 4)``. When it separates the array with ``axis=1``, it
+    returns three ``(2, 4)`` arrays.
+
+    This function is an inverse of :func:`chainer.functions.stack`.
+
+    Args:
+        x (chainer.Variable): Variable to be separated.
+        axis (int): Axis alog which variables are separated.
+
+    Returns:
+        tuple of chainer.Variable: Output variables.
+
+    .. seealso:: :func:`chainer.functions.stack`
+
+    """
+    shape = list(x.data.shape)
+    del shape[axis]
+    ys = split_axis.split_axis(x, x.data.shape[axis], axis, force_tuple=True)
+    return tuple(reshape.reshape(y, shape) for y in ys)


Test patch:
diff --git a/tests/chainer_tests/functions_tests/array_tests/test_separate.py b/tests/chainer_tests/functions_tests/array_tests/test_separate.py
new file mode 100644
--- /dev/null
+++ b/tests/chainer_tests/functions_tests/array_tests/test_separate.py
@@ -0,0 +1,74 @@
+import unittest
+
+import numpy
+
+import chainer
+from chainer import cuda
+from chainer.functions.array import separate
+from chainer import gradient_check
+from chainer import testing
+from chainer.testing import attr
+
+
+@testing.parameterize(*testing.product_dict(
+    [
+        {'shape': (2, 3, 4), 'axis': 0},
+        {'shape': (2, 3, 4), 'axis': 1},
+        {'shape': (2, 3, 4), 'axis': 2},
+        {'shape': (2, 3, 4), 'axis': -1},
+        {'shape': (2,), 'axis': 0},
+        {'shape': (2,), 'axis': -1},
+    ],
+    [
+        {'dtype': numpy.float16},
+        {'dtype': numpy.float32},
+        {'dtype': numpy.float64},
+    ]
+))
+class TestSeparate(unittest.TestCase):
+
+    def setUp(self):
+        self.x = numpy.random.uniform(-1, 1, self.shape).astype(self.dtype)
+        yshape = list(self.shape)
+        del yshape[self.axis]
+        self.gys = [numpy.random.uniform(-1, 1, yshape).astype(self.dtype)
+                    for _ in range(self.shape[self.axis])]
+        self.check_backward_options = {}
+        if self.dtype == numpy.float16:
+            self.check_backward_options = {
+                'eps': 2 ** -5, 'atol': 1e-3, 'rtol': 1e-2}
+
+    def check_forward(self, x_data):
+        x = chainer.Variable(x_data)
+        ys = separate.separate(x, self.axis)
+
+        self.assertIsInstance(ys, tuple)
+        self.assertEqual(len(ys), self.shape[self.axis])
+        for i in range(self.shape[self.axis]):
+            expect = self.x.take(i, axis=self.axis)
+            testing.assert_allclose(ys[i].data, expect)
+
+    def test_forward_cpu(self):
+        self.check_forward(self.x)
+
+    @attr.gpu
+    def test_forward_gpu(self):
+        self.check_forward(cuda.to_gpu(self.x))
+
+    def check_backward(self, x_data, gys_data):
+        def f(x):
+            return separate.separate(x, self.axis)
+
+        gradient_check.check_backward(
+            f, x_data, gys_data, **self.check_backward_options)
+
+    def test_backward_cpu(self):
+        self.check_backward(self.x, self.gys)
+
+    @attr.gpu
+    def test_backward_gpu(self):
+        self.check_backward(cuda.to_gpu(self.x),
+                            [cuda.to_gpu(g) for g in self.gys])
+
+
+testing.run_module(__name__, __file__)