Repository: chainer/chainer

Issue:
ignore_label in Softmax.forward_gpu does not work when it is not -1
`F.SoftMaxCrossEntropy` can specify `ignore_label` in its `__init__` method. But -1 is hard-coded in its `forward_gpu` and `backward_gpu` methods ([here](https://github.com/pfnet/chainer/blob/master/chainer/functions/loss/softmax_cross_entropy.py#L113) and [here](https://github.com/pfnet/chainer/blob/master/chainer/functions/loss/softmax_cross_entropy.py#L195)). Therefore it does not calculate loss value if `ignore_label` is other than `-1`.

This bug is reported by @musyoku. Thank you!


Discussion:

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
@@ -109,10 +109,13 @@ def forward_gpu(self, inputs):
         log_y = cupy.rollaxis(log_y, 1, log_y.ndim)
         if self.reduce == 'mean':
             ret = cuda.reduce(
-                'S t, raw T log_y, int32 n_channel, raw T coeff', 'T out',
-                't == -1 ? T(0) : log_y[_j * n_channel + t]',
+                'S t, raw T log_y, int32 n_channel, raw T coeff, '
+                'S ignore_label',
+                'T out',
+                't == ignore_label ? T(0) : log_y[_j * n_channel + t]',
                 'a + b', 'out = a * -coeff[0]', '0', 'crossent_fwd'
-            )(t, log_y.reduced_view(), log_y.shape[-1], self._coeff)
+            )(t, log_y.reduced_view(), log_y.shape[-1],
+              self._coeff, self.ignore_label)
         else:
             ret = cuda.elementwise(
                 'S t, raw T log_y, int32 n_channel, T ignore', 'T out',
@@ -188,25 +191,27 @@ def backward_gpu(self, inputs, grad_outputs):
 
         if self.class_weight is None:
             gx = cuda.elementwise(
-                'T y, S t, T coeff, S n_channel, S n_unit',
+                'T y, S t, T coeff, S n_channel, S n_unit, S ignore_label',
                 'T gx',
                 '''
                     const int c = (i / n_unit % n_channel);
-                    gx = t == -1 ? 0 : coeff * (y - (c == t));
+                    gx = t == ignore_label ? 0 : coeff * (y - (c == t));
                 ''',
                 'softmax_crossent_bwd')(
-                    y, cupy.expand_dims(t, 1), coeff, x.shape[1], n_unit)
+                    y, cupy.expand_dims(t, 1), coeff, x.shape[1],
+                    n_unit, self.ignore_label)
         else:
             gx = cuda.elementwise(
-                'T y, raw T w, S t, T coeff, S n_channel, S n_unit',
+                'T y, raw T w, S t, T coeff, S n_channel, S n_unit, '
+                'S ignore_label',
                 'T gx',
                 '''
                     const int c = (i / n_unit % n_channel);
-                    gx = t == -1 ? 0 : coeff * (y - (c == t)) * w[t];
+                    gx = t == ignore_label ? 0 : coeff * (y - (c == t)) * w[t];
                 ''',
                 'softmax_crossent_weight_bwd')(
                     y, self.class_weight, cupy.expand_dims(t, 1), coeff,
-                    x.shape[1], n_unit)
+                    x.shape[1], n_unit, self.ignore_label)
 
         return gx, None
 


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
@@ -391,4 +391,69 @@ def test_invalid_reduce_gpu(self):
         self.check_invalid_reduce(cuda.to_gpu(self.x), cuda.to_gpu(self.t))
 
 
+@testing.parameterize(*testing.product({
+    'reduce': ['mean', 'no'],
+    'class_weight': [None, numpy.ones((3,), dtype=numpy.float32)]})
+)
+class TestNonDefaultIgnoreLabel(unittest.TestCase):
+
+    def setUp(self):
+        self.ignore_label = -2
+        self.x = numpy.random.uniform(-1, 1, (2, 3)).astype(numpy.float32)
+        self.t = numpy.full((2,), self.ignore_label, dtype=numpy.int32)
+        if self.reduce == 'mean':
+            gy_shape = ()
+        else:
+            gy_shape = (2,)
+        self.gy = numpy.random.uniform(-1, 1, gy_shape).astype(numpy.float32)
+
+    def check_forward(self, xp):
+        x = xp.asarray(self.x)
+        t = xp.asarray(self.t)
+        if self.class_weight is not None:
+            class_weight = xp.asarray(self.class_weight)
+        else:
+            class_weight = None
+        loss = functions.softmax_cross_entropy(
+            x, t, reduce=self.reduce,
+            class_weight=class_weight,
+            ignore_label=self.ignore_label)
+        if self.reduce == 'mean':
+            expect = 0.
+        else:
+            expect = numpy.zeros((2,), dtype=numpy.float32)
+        testing.assert_allclose(loss.data, expect)
+
+    @condition.retry(3)
+    def test_forward_cpu(self):
+        self.check_forward(numpy)
+
+    @attr.gpu
+    @condition.retry(3)
+    def test_forward_gpu(self):
+        self.check_forward(cuda.cupy)
+
+    def check_backward(self, xp):
+        x = xp.asarray(self.x)
+        t = xp.asarray(self.t)
+        gy = xp.asarray(self.gy)
+        if self.class_weight is not None:
+            class_weight = xp.asarray(self.class_weight)
+        else:
+            class_weight = None
+        f = functions.SoftmaxCrossEntropy(
+            reduce=self.reduce, class_weight=class_weight,
+            ignore_label=self.ignore_label)
+        gradient_check.check_backward(f, (x, t), gy)
+
+    @condition.retry(3)
+    def test_backward_cpu(self):
+        self.check_backward(numpy)
+
+    @attr.gpu
+    @condition.retry(3)
+    def test_backward_gpu(self):
+        self.check_backward(cuda.cupy)
+
+
 testing.run_module(__name__, __file__)