Repository: chainer/chainer

Issue:
Make broadcast function
We need a broadcast function which is called _explicitly_.
For example, shape of `x` is `()` and shape of `y` is (3, 4), user can call multiplication like this:

```
bx, by = F.broadcast(x, y)
xy = bx * by
```

Of course implicit broadcasting which is implemented in numpy and cupy is useful, but it is another problem. We need to discuss about implicit broadcasting in issue #228.



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
@@ -9,6 +9,7 @@
 from chainer.functions.activation import softmax
 from chainer.functions.activation import softplus
 from chainer.functions.activation import tanh
+from chainer.functions.array import broadcast
 from chainer.functions.array import concat
 from chainer.functions.array import copy
 from chainer.functions.array import reshape
@@ -64,6 +65,8 @@
 Tanh = tanh.Tanh
 tanh = tanh.tanh
 
+Broadcast = broadcast.Broadcast
+broadcast = broadcast.broadcast
 Concat = concat.Concat
 concat = concat.concat
 Copy = copy.Copy
diff --git a/chainer/functions/array/broadcast.py b/chainer/functions/array/broadcast.py
new file mode 100644
--- /dev/null
+++ b/chainer/functions/array/broadcast.py
@@ -0,0 +1,63 @@
+import six
+
+from chainer import cuda
+from chainer import function
+from chainer.utils import type_check
+
+
+def _backward_one(x, g):
+    if g is None:
+        xp = cuda.get_array_module(x)
+        return xp.zeros_like(x)
+
+    if g.ndim != x.ndim:
+        g = g.sum(axis=tuple(range(g.ndim - x.ndim)))
+        # An input variable is always an array, not a scalar.
+        # We need to convert a scalar value to a zero-dim array.
+        xp = cuda.get_array_module(x)
+        if xp.isscalar(g):
+            g = xp.array(g)
+
+    axis = tuple(i for i, sx in enumerate(x.shape) if sx == 1)
+    if len(axis) > 0:
+        return g.sum(keepdims=True, axis=axis)
+    else:
+        return g
+
+
+class Broadcast(function.Function):
+
+    """Function that broadcasts given arrays."""
+
+    def check_type_forward(self, in_types):
+        type_check.expect(in_types.size() > 0)
+
+        shapes = [t.eval().shape for t in in_types]
+        r_shapes = [s[::-1] for s in shapes]
+        r_filled = six.moves.zip_longest(*r_shapes, fillvalue=1)
+        for ss in r_filled:
+            d = max(ss)
+            if not all(s == d or s == 1 for s in ss):
+                expect = 'each dimension has the same size or is 1'
+                actual = 'shapes: ' + ', '.join(map(str, shapes))
+                raise type_check.InvalidType(expect, actual)
+
+    def forward(self, xs):
+        xp = cuda.get_array_module(*xs)
+        return tuple(xp.broadcast_arrays(*xs))
+
+    def backward(self, xs, grads):
+        return tuple(_backward_one(x, g) for x, g in six.moves.zip(xs, grads))
+
+
+def broadcast(*args):
+    """Broadcast given variables.
+
+    Args:
+      args (Variables): Variables to be broadcasted.
+
+    Returns:
+      ``tuple``: Tuple of :class:`~chainer.Variable` objects which are
+          broadcasted from given arguments.
+    """
+    return Broadcast()(*args)


Test patch:
diff --git a/tests/chainer_tests/functions_tests/array_tests/test_broadcast.py b/tests/chainer_tests/functions_tests/array_tests/test_broadcast.py
new file mode 100644
--- /dev/null
+++ b/tests/chainer_tests/functions_tests/array_tests/test_broadcast.py
@@ -0,0 +1,102 @@
+import unittest
+
+import numpy
+
+import chainer
+from chainer 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(
+    {'in_shapes': [(3, 1, 5), (1, 2, 5)], 'out_shape': (3, 2, 5)},
+    {'in_shapes': [(3, 2, 5), (5,)],      'out_shape': (3, 2, 5)},
+    {'in_shapes': [(3, 2, 5), ()],        'out_shape': (3, 2, 5)},
+    {'in_shapes': [(3, 2, 5), (3, 2, 5)], 'out_shape': (3, 2, 5)},
+    {'in_shapes': [(), ()],               'out_shape': ()},
+    {'in_shapes': [(1, 1, 1), (1,)],      'out_shape': (1, 1, 1)},
+    {'in_shapes': [(1, 1, 1), ()],        'out_shape': (1, 1, 1)},
+    {'in_shapes': [(3, 2, 5)],            'out_shape': (3, 2, 5)},
+    {'in_shapes': [(3, 1, 5), (1, 2, 5), (3, 2, 1)],
+     'out_shape': (3, 2, 5)},
+)
+class TestBroadcast(unittest.TestCase):
+
+    def setUp(self):
+        uniform = numpy.random.uniform
+        self.data = [uniform(0, 1, shape).astype(numpy.float32)
+                     for shape in self.in_shapes]
+        self.grads = [uniform(0, 1, self.out_shape).astype(numpy.float32)
+                      for _ in range(len(self.in_shapes))]
+
+    def check_forward(self, data):
+        xs = [chainer.Variable(x) for x in data]
+        bxs = functions.broadcast(*xs)
+
+        # When len(xs) == 1, function returns a Variable object
+        if isinstance(bxs, chainer.Variable):
+            bxs = (bxs,)
+
+        for bx in bxs:
+            self.assertEqual(bx.data.shape, self.out_shape)
+
+    def test_forward_cpu(self):
+        self.check_forward(self.data)
+
+    @attr.gpu
+    def test_forward_gpu(self):
+        self.check_forward([cuda.to_gpu(x) for x in self.data])
+
+    def check_backward(self, data, grads):
+        xs = [chainer.Variable(x) for x in data]
+        bxs = functions.broadcast(*xs)
+
+        # When len(xs) == 1, function returns a Variable object
+        if isinstance(bxs, chainer.Variable):
+            bxs = (bxs,)
+
+        func = bxs[0].creator
+        f = lambda: func.forward(data)
+
+        for i, (bx, grad) in enumerate(zip(bxs, grads)):
+            bx.grad = grad
+            bx.backward()
+            gxs = gradient_check.numerical_grad(
+                f, data, tuple(bx.grad for bx in bxs))
+            gradient_check.assert_allclose(gxs[i], xs[i].grad)
+
+    def test_backward_cpu(self):
+        self.check_backward(self.data, self.grads)
+
+    @attr.gpu
+    def test_backward_gpu(self):
+        self.check_backward([cuda.to_gpu(x) for x in self.data],
+                            [cuda.to_gpu(x) for x in self.grads])
+
+
+class TestBroadcastTypeError(unittest.TestCase):
+
+    def test_invalid_shape(self):
+        x_data = numpy.zeros((3, 2, 5), dtype=numpy.int32)
+        y_data = numpy.zeros((1, 3, 4), dtype=numpy.float32)
+        x = chainer.Variable(x_data)
+        y = chainer.Variable(y_data)
+
+        with self.assertRaises(type_check.InvalidType):
+            functions.broadcast(x, y)
+
+    def test_invalid_shape_fill(self):
+        x_data = numpy.zeros((3, 2, 5), dtype=numpy.int32)
+        y_data = numpy.zeros((4), dtype=numpy.float32)
+        x = chainer.Variable(x_data)
+        y = chainer.Variable(y_data)
+
+        with self.assertRaises(type_check.InvalidType):
+            functions.broadcast(x, y)
+
+    def test_no_args(self):
+        with self.assertRaises(type_check.InvalidType):
+            functions.broadcast()