Repository: chainer/chainer

Issue:
Variable.backward causes an error when composing F.tile, F.sigmoid_cross_entropy, and F.sum.
An error occurs when we compose `F.tile`, `F.sigmoid_cross_entropy` (with `reduce=no`), and `F.sum` and do `Variable.backward` to the output variable.

* Target version: 3.2.0<= v <=master

Code to reproduce
```python
import chainer
from chainer import functions as F
import numpy as np

chainer.print_runtime_info()

y = chainer.Variable(np.random.standard_normal((12, 10)).astype(np.float32))
t = chainer.Variable(np.random.standard_normal((12, 1)).astype(np.int32))
size = 10

t = F.tile(t, (1, size))
loss = F.sigmoid_cross_entropy(y, t, reduce="no")
loss = F.sum(loss)
loss.backward()
```

Log
```
Chainer: 5.0.0b1
NumPy: 1.11.3
CuPy: Not Available
Traceback (most recent call last):
  File "test.py", line 14, in <module>
    loss.backward()
  File "/home/delta/dev/chainer/chainer/variable.py", line 966, in backward
    self._backward_main(retain_grad, loss_scale)
  File "/home/delta/dev/chainer/chainer/variable.py", line 1095, in _backward_main
    target_input_indexes, out_grad, in_grad)
  File "/home/delta/dev/chainer/chainer/function_node.py", line 548, in backward_accumulate
    gxs = self.backward(target_input_indexes, grad_outputs)
  File "/home/delta/dev/chainer/chainer/functions/array/tile.py", line 50, in backward
    for i in range(gy.ndim):
  File "/home/delta/dev/chainer/chainer/variable.py", line 703, in ndim
    return self.data.ndim
AttributeError: 'NoneType' object has no attribute 'ndim'
```

The error did not occur if we change in either way:
- Remove `F.tile`.
- Remove `F.sum` and change `reduce` option of `F.sigmoid_cross_entropy` to `mean`.

I did `git bisect` and found that the first bad commit was #3705. So, I suspect `F.sigmoid_cross_entropy`.



Discussion:
It seems backward of `F.sigmoid_cross_entropy` returns `Variable(None)` for the gradient of `t`, which should be `None`.
```
y = chainer.Variable(np.random.standard_normal((12, 10)).astype(np.float32))
t = chainer.Variable(np.random.standard_normal((12, 10)).astype(np.int32))

loss = F.sigmoid_cross_entropy(y, t, reduce="no")
loss = F.sum(loss)
print(chainer.grad([loss], [t]))  # => [Variable(None)]
```

Resolving patch:
diff --git a/chainer/functions/loss/sigmoid_cross_entropy.py b/chainer/functions/loss/sigmoid_cross_entropy.py
--- a/chainer/functions/loss/sigmoid_cross_entropy.py
+++ b/chainer/functions/loss/sigmoid_cross_entropy.py
@@ -60,8 +60,9 @@ def forward(self, inputs):
     def backward(self, inputs, grad_outputs):
         x, t = self.get_retained_inputs()
         gy, = grad_outputs
-        return SigmoidCrossEntropyGrad(
+        gx, = SigmoidCrossEntropyGrad(
             self.reduce, self.count, self.ignore_mask, t.data).apply((x, gy))
+        return gx, None
 
 
 class SigmoidCrossEntropyGrad(function_node.FunctionNode):
@@ -88,10 +89,10 @@ def forward(self, inputs):
         else:
             gx = (gy * self.ignore_mask * (y - self.t)).astype(y.dtype)
 
-        return gx, None
+        return gx,
 
     def backward(self, indexes, grad_outputs):
-        ggx, _ = grad_outputs
+        ggx, = grad_outputs
         x, gy = self.get_retained_inputs()
         y = chainer.functions.sigmoid(x)
         yp = y * (1 - y)
diff --git a/chainer/gradient_check.py b/chainer/gradient_check.py
--- a/chainer/gradient_check.py
+++ b/chainer/gradient_check.py
@@ -616,6 +616,8 @@ def check_double_backward(func, x_data, y_grad, x_grad_grad, params=(),
     params_grad_grad = _as_tuple(params_grad_grad)
     n_x = len(x_data)
 
+    first_order_no_grads = [x.dtype.kind != 'f' for x in x_data]
+
     def first_order_grad(*inputs):
         xs = inputs[:n_x]
         gys = inputs[n_x:]
@@ -628,7 +630,19 @@ def first_order_grad(*inputs):
 
         y.backward(enable_double_backprop=True)
 
-        return tuple([x.grad_var for x in xs] + [p.grad_var for p in params])
+        gxs = []
+        for skip, x in six.moves.zip(first_order_no_grads, xs):
+            if skip:
+                if x.grad is not None:
+                    raise RuntimeError(
+                        'gradient of int variable must be None')
+            else:
+                if x.grad is None:
+                    raise RuntimeError(
+                        'gradients of some arguments are not calculated')
+                gxs.append(x.grad_var)
+
+        return tuple(gxs + [p.grad_var for p in params])
 
     inputs = x_data + y_grad
     grad_grad = x_grad_grad + params_grad_grad


Test patch:
diff --git a/tests/chainer_tests/functions_tests/loss_tests/test_sigmoid_cross_entropy.py b/tests/chainer_tests/functions_tests/loss_tests/test_sigmoid_cross_entropy.py
--- a/tests/chainer_tests/functions_tests/loss_tests/test_sigmoid_cross_entropy.py
+++ b/tests/chainer_tests/functions_tests/loss_tests/test_sigmoid_cross_entropy.py
@@ -190,7 +190,7 @@ def f(x, t):
                 x, t, normalize=normalize, reduce=reduce)
 
         gradient_check.check_double_backward(
-            f, (x_data, t_data), y_grad, (gx_grad, None),
+            f, (x_data, t_data), y_grad, (gx_grad,),
             **self.check_double_backward_options)
 
     def test_double_backward_cpu(self):