Repository: chainer/chainer

Issue:
ZeroDivisionError in softmax_cross_entropy when I pass masked array
  - Chainer version: `origin/master`
  - CuPy version: `2.0.0b1`
  - OS/Platform: `Ubuntu 14.04`
  - CUDA/cuDNN version: `CUDA-8.0.61-1/cudnn5.1.5-1`

## Problem

When I mask two arrays and calculate `softmax_cross_entroy`, `ZeroDivisionError` occurs when masked array's data is `[]`
I thought it should be checked in ndim and shape check at the beginning of the function, but the array's shape was something like `(0, 2, 2)`

## Reproduction

```python
import chainer
import cupy
import numpy as np


data = np.random.random((10, 2, 2))
t = cupy.ones((data.shape[0], data.shape[2]), dtype=cupy.int32)
x = chainer.Variable(data)
x.to_gpu()
mask = t[:, 0] == 0
x_masked = x[mask]
print('=================')
print('x_masked')
print('    data: {}'.format(x_masked.data))
print('    shape: {}'.format(x_masked.shape))
print('=================')
t_masked = t[mask]
print('=================')
print('t_masked')
print('    data: {}'.format(t_masked))
print('    shape: {}'.format(t_masked.shape))
print('=================')
chainer.functions.softmax_cross_entropy(x_masked, t_masked)
```
* Shape
```
=================
x_masked
    data: []
    shape: (0, 2, 2)
=================
=================
t_masked
    data: []
    shape: (0, 2)
=================
```
* Error
```bash
Traceback (most recent call last):
  File "spam.py", line 25, in <module>
    chainer.functions.softmax_cross_entropy(x_masked, t_masked)
  File "/home/shingo/chainer/chainer/functions/loss/softmax_cross_entropy.py", line 275, in softmax_cross_entropy
    normalize, cache_score, class_weight, ignore_label, reduce)(x, t)
  File "/home/shingo/chainer/chainer/function.py", line 212, in __call__
    ret = node.apply(inputs)
  File "/home/shingo/chainer/chainer/function_node.py", line 219, in apply
    outputs = self.forward(in_data)
  File "/home/shingo/chainer/chainer/function.py", line 117, in forward
    return self._function.forward(inputs)
  File "/home/shingo/chainer/chainer/function.py", line 319, in forward
    return self.forward_gpu(inputs)
  File "/home/shingo/chainer/chainer/functions/loss/softmax_cross_entropy.py", line 102, in forward_gpu
    log_y = log_softmax._log_softmax(x)
  File "/home/shingo/chainer/chainer/functions/activation/log_softmax.py", line 36, in _log_softmax
    x.reshape(x.shape[:2] + (-1, 1)))
  File "cupy/core/core.pyx", line 461, in cupy.core.core.ndarray.reshape (cupy/core/core.cpp:11315)
  File "cupy/core/core.pyx", line 434, in cupy.core.core.ndarray._reshape (cupy/core/core.cpp:10994)
  File "cupy/core/internal.pyx", line 139, in cupy.core.internal.infer_unknown_dimension (cupy/core/internal.cpp:3060)
ZeroDivisionError: integer division or modulo by zero
```






Discussion:
my question is whether shape `(0, 2, 2)` is proper or not.
If it is proper, it can pass `ndim` check.
Thank you for reporting the problem. I suppose we have not determined the specification when 0-sized array is fed to the function. In my opinion, we should forbid it at first because we can extend acceptable shapes afterwards if necessary.
> In my opinion, we should forbid it at first because we can extend acceptable shapes afterwards if necessary.

You mean it raises some error when shape contains `0`?
It occurs this error when I masked an array, and sometimes shape can be like `(0, 2, 2)`.
I used to use `mean_squared_error` with shape `(0, 2, 2)` array, and it doesn't matter. (However, it means nothing because there is nothing to back-propagate)
In my opinion, the function have no need to raise error.
I see. Then another alternative is to output a scalar 0 when `reduce` is `mean` and a 0-sized array when `reduce` is `no`.

Resolving patch:
diff --git a/chainer/functions/loss/softmax_cross_entropy.py b/chainer/functions/loss/softmax_cross_entropy.py
--- a/chainer/functions/loss/softmax_cross_entropy.py
+++ b/chainer/functions/loss/softmax_cross_entropy.py
@@ -113,6 +113,14 @@ def forward_gpu(self, inputs):
         if chainer.is_debug():
             _check_input_values(x, t, self.ignore_label)
 
+        if x.size == 0:
+            y = cupy.zeros(t.shape, dtype=x.dtype)
+            if self.cache_score:
+                self.y = y
+            if self.reduce == 'mean':
+                return y.sum(),
+            else:
+                return y,
         log_y = log_softmax._log_softmax(x)
         if self.cache_score:
             self.y = cupy.exp(log_y)
@@ -154,6 +162,8 @@ def forward_gpu(self, inputs):
     def backward_cpu(self, inputs, grad_outputs):
         x, t = inputs
         gloss = grad_outputs[0]
+        if x.size == 0:
+            return numpy.zeros(x.shape, dtype=x.dtype), None
         if hasattr(self, 'y'):
             y = self.y.copy()
         else:
@@ -195,6 +205,8 @@ def backward_cpu(self, inputs, grad_outputs):
     def backward_gpu(self, inputs, grad_outputs):
         cupy = cuda.cupy
         x, t = inputs
+        if x.size == 0:
+            return cupy.zeros(x.shape, dtype=x.dtype), None
         if hasattr(self, 'y'):
             y = self.y
         else:


Test patch:
diff --git a/tests/chainer_tests/functions_tests/loss_tests/test_softmax_cross_entropy.py b/tests/chainer_tests/functions_tests/loss_tests/test_softmax_cross_entropy.py
--- a/tests/chainer_tests/functions_tests/loss_tests/test_softmax_cross_entropy.py
+++ b/tests/chainer_tests/functions_tests/loss_tests/test_softmax_cross_entropy.py
@@ -29,6 +29,14 @@
     'dtype': [numpy.float16, numpy.float32, numpy.float64],
     'weight_apply': [False, True],
     'enable_double_backprop': [False, True],
+}) + testing.product({
+    'shape': [(0, 3), (0, 3, 2), (0, 3, 2, 2)],
+    'cache_score': [True, False],
+    'normalize': [True, False],
+    'ignore_index': [None],
+    'dtype': [numpy.float16, numpy.float32, numpy.float64],
+    'weight_apply': [False, True],
+    'enable_double_backprop': [False],
 })))
 class TestSoftmaxCrossEntropy(unittest.TestCase):
 
@@ -95,7 +103,10 @@ def check_forward(self, x_data, t_data, class_weight, use_cudnn='always'):
             else:
                 loss_expect /= count
         else:
-            loss_expect /= len(t_data)
+            if len(t_data) == 0:
+                loss_expect = 0.0
+            else:
+                loss_expect /= len(t_data)
 
         testing.assert_allclose(
             loss_expect, loss_value, **self.check_forward_options)