Repository: chainer/chainer

Issue:
request function operator : cumprod
My research just need the cumprod (your cupy have this function but chainer.functions does not)
I found cumsum but not found cumprod.
How to implement such function myself?


Discussion:
I checked cumsum function,
https://github.com/chainer/chainer/blob/master/chainer/functions/math/cumsum.py#L39
why there is `flip` in the `backward `?
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.

I tentatively assigned @toslunar san for this issue.

Resolving patch:
diff --git a/chainer/functions/__init__.py b/chainer/functions/__init__.py
--- a/chainer/functions/__init__.py
+++ b/chainer/functions/__init__.py
@@ -118,6 +118,7 @@
 from chainer.functions.math.bias import bias  # NOQA
 from chainer.functions.math.ceil import ceil  # NOQA
 from chainer.functions.math.clip import clip  # NOQA
+from chainer.functions.math.cumprod import cumprod  # NOQA
 from chainer.functions.math.cumsum import cumsum  # NOQA
 from chainer.functions.math.det import batch_det  # NOQA
 from chainer.functions.math.det import det  # NOQA
diff --git a/chainer/functions/math/cumprod.py b/chainer/functions/math/cumprod.py
new file mode 100644
--- /dev/null
+++ b/chainer/functions/math/cumprod.py
@@ -0,0 +1,126 @@
+import chainer
+from chainer.backends import cuda
+from chainer import function_node
+from chainer.functions.array import flip
+from chainer.utils import type_check
+
+
+class Cumprod(function_node.FunctionNode):
+    """Cumulative prod of array elements over a given axis."""
+
+    def __init__(self, axis):
+        if isinstance(axis, int) or axis is None:
+            self.axis = axis
+        else:
+            raise TypeError('axis must be int or None')
+
+    def check_type_forward(self, in_types):
+        type_check.expect(
+            in_types.size() == 1,
+            in_types[0].dtype.kind == 'f',
+        )
+        if self.axis is not None:
+            if self.axis >= 0:
+                type_check.expect(self.axis < in_types[0].ndim)
+            else:
+                type_check.expect(-self.axis - 1 < in_types[0].ndim)
+
+    def forward(self, inputs):
+        self.retain_inputs((0,))
+        self.retain_outputs((0,))
+        x, = inputs
+        xp = cuda.get_array_module(x)
+        return xp.cumprod(x, axis=self.axis),
+
+    def backward(self, indexes, grad_outputs):
+        x, = self.get_retained_inputs()
+        xp = cuda.get_array_module(x)
+        y, = self.get_retained_outputs()
+        gy, = grad_outputs
+        F = chainer.functions
+
+        axis = self.axis
+        if axis is None:
+            shape = x.shape
+            axis = 0
+            x = F.flatten(x)
+        else:
+            shape = None
+            if axis < 0:
+                axis += y.ndim
+
+        if y.shape[axis] <= 1:
+            gx = gy
+        else:
+            _, x = F.split_axis(x, (1,), axis)
+            gx = _flipcumprodsum(x, gy, axis)
+            y, ylast = F.split_axis(y, (-1,), axis)
+            gx *= F.concat([xp.ones_like(ylast.array), y], axis=axis)
+        if shape is not None:
+            gx = F.reshape(gx, shape)
+        return gx,
+
+
+class Cumprodsum(function_node.FunctionNode):
+
+    def __init__(self, axis):
+        self.axis = axis
+
+    def forward(self, inputs):
+        self.retain_inputs((0,))
+        self.retain_outputs((0,))
+        xmul, xadd = inputs
+        xp = cuda.get_array_module(xmul)
+        y = xp.empty_like(xadd)
+
+        axis = self.axis
+        expander = (slice(None),) * axis
+        cum = xp.zeros_like(xadd[expander + (0,)])
+
+        i = 0
+        while True:
+            ix = expander + (i,)
+            cum += xadd[ix]
+            y[ix] = cum
+            if i >= xmul.shape[axis]:
+                break
+            cum *= xmul[ix]
+            i += 1
+
+        return y,
+
+    def backward(self, indexes, grad_outputs):
+        F = chainer.functions
+        xmul, = self.get_retained_inputs()
+        y, = self.get_retained_outputs()
+        gy, = grad_outputs
+        axis = self.axis
+
+        gxadd = _flipcumprodsum(xmul, gy, axis)
+        _, gxmul = F.split_axis(gxadd, (1,), axis)
+        y, _ = F.split_axis(y, (-1,), axis)
+        gxmul *= y
+        return gxmul, gxadd
+
+
+def _flipcumprodsum(xmul, xadd, axis):
+    z, = Cumprodsum(axis).apply((flip.flip(xmul, axis), flip.flip(xadd, axis)))
+    return flip.flip(z, axis)
+
+
+def cumprod(x, axis=None):
+    """Cumulative prod of array elements over a given axis.
+
+    Args:
+        x (:class:`~chainer.Variable` or :class:`numpy.ndarray` or \
+           :class:`cupy.ndarray`):
+            Elements to calculate the cumulative prod.
+        axis (int or None):
+            Axis along which the cumulative prod is taken.
+            If it is not specified, the input is flattened.
+
+    Returns:
+        ~chainer.Variable: Output variable.
+
+    """
+    return Cumprod(axis).apply((x,))[0]


Test patch:
diff --git a/tests/chainer_tests/functions_tests/math_tests/test_cumprod.py b/tests/chainer_tests/functions_tests/math_tests/test_cumprod.py
new file mode 100644
--- /dev/null
+++ b/tests/chainer_tests/functions_tests/math_tests/test_cumprod.py
@@ -0,0 +1,138 @@
+import unittest
+
+import numpy
+
+import chainer
+from chainer.backends import cuda
+from chainer import functions
+from chainer import gradient_check
+from chainer import testing
+from chainer.testing import attr
+from chainer.utils import type_check
+
+
+@testing.parameterize(*(testing.product_dict(
+    [
+        {'shape': (1,), 'axis': 0},
+        {'shape': (2, 3, 4), 'axis': 0},
+        {'shape': (2, 3, 4), 'axis': 1},
+        {'shape': (2, 3, 4), 'axis': 2},
+        {'shape': (2, 3, 4), 'axis': -3},
+        {'shape': (2, 3, 4), 'axis': -2},
+        {'shape': (2, 3, 4), 'axis': -1},
+        {'shape': (2, 3, 4), 'axis': None},
+    ],
+    testing.product({
+        'dtype': [numpy.float16, numpy.float32, numpy.float64],
+        'contain_zero': [True, False],
+    }),
+) + testing.product({
+    'shape': [(0, 3)],
+    'axis': [-2, 1, None],
+    'dtype': [numpy.float64],
+    'contain_zero': [False],
+})))
+class TestCumprod(unittest.TestCase):
+
+    def setUp(self):
+        self.x = numpy.random.uniform(-2, 2, self.shape).astype(self.dtype)
+        if self.contain_zero:
+            index = numpy.random.choice(self.x.size)
+            self.x.ravel()[index] = 0
+        self.gy = numpy.random.uniform(-1, 1, self.shape).astype(self.dtype)
+        self.ggx = numpy.random.uniform(-1, 1, self.shape).astype(self.dtype)
+        if self.axis is None:
+            self.gy = self.gy.ravel()
+
+        self.check_forward_options = {}
+        self.check_backward_options = {}
+        self.check_double_backward_options = {}
+        if self.dtype == numpy.float16:
+            self.check_forward_options = {'atol': 1e-2, 'rtol': 1e-2}
+            self.check_backward_options = {'atol': 1e-2, 'rtol': 1e-2}
+            self.check_double_backward_options = {
+                'atol': 1e-1, 'rtol': 1e-1, 'eps': 0.01}
+        elif self.dtype == numpy.float32:
+            self.check_double_backward_options = {'atol': 1e-3, 'rtol': 1e-3}
+
+    def check_forward(self, x_data, axis):
+        xp = cuda.get_array_module(x_data)
+        x = chainer.Variable(x_data)
+        y = functions.cumprod(x, axis=axis)
+        assert y.data.dtype == self.dtype
+        y_expect = xp.asarray(numpy.cumprod(self.x, axis=axis))
+        testing.assert_allclose(y_expect, y.data,
+                                **self.check_forward_options)
+
+    def test_forward_cpu(self):
+        self.check_forward(self.x, self.axis)
+
+    @attr.gpu
+    def test_forward_gpu(self):
+        self.check_forward(cuda.to_gpu(self.x), self.axis)
+
+    def check_backward(self, x_data, axis, y_grad):
+        gradient_check.check_backward(
+            lambda x: functions.cumprod(x, axis), x_data, y_grad,
+            dtype=numpy.float64, **self.check_backward_options)
+
+    def test_backward_cpu(self):
+        self.check_backward(self.x, self.axis, self.gy)
+
+    @attr.gpu
+    def test_backward_gpu(self):
+        self.check_backward(
+            cuda.to_gpu(self.x), self.axis, cuda.to_gpu(self.gy))
+
+    def check_double_backward(self, x_data, axis, y_grad, x_grad_grad):
+        def f(x):
+            return functions.cumprod(x, axis)
+
+        gradient_check.check_double_backward(
+            f, x_data, y_grad, x_grad_grad, dtype=numpy.float64,
+            **self.check_double_backward_options)
+
+    def test_double_backward_cpu(self):
+        self.check_double_backward(self.x, self.axis, self.gy, self.ggx)
+
+    @attr.gpu
+    def test_double_backward_gpu(self):
+        self.check_double_backward(cuda.to_gpu(self.x), self.axis,
+                                   cuda.to_gpu(self.gy),
+                                   cuda.to_gpu(self.ggx))
+
+
+@testing.parameterize(
+    {'axis': 3},
+    {'axis': -4},
+)
+class TestCumprodInvalidTypeAxis(unittest.TestCase):
+
+    def setUp(self):
+        self.x = numpy.random.uniform(-1, 1, (2, 3, 4)).astype('f')
+
+    def check_type_error(self, x):
+        with self.assertRaises(type_check.InvalidType):
+            functions.cumprod(x, self.axis)
+
+    def test_type_error_cpu(self):
+        self.check_type_error(self.x)
+
+    @attr.gpu
+    def test_type_error_gpu(self):
+        self.check_type_error(cuda.to_gpu(self.x))
+
+
+class TestCumprodInvalidTypeError(unittest.TestCase):
+
+    def setUp(self):
+        self.x = numpy.random.uniform(-1, 1, (2, 3, 4)).astype('f')
+
+    def test_invalid_type_axis(self):
+        with self.assertRaises(TypeError):
+            functions.cumprod(self.x, [0])
+        with self.assertRaises(TypeError):
+            functions.cumprod(self.x, (0,))
+
+
+testing.run_module(__name__, __file__)