Repository: chainer/chainer

Issue:
F.negative_sampling outputs float32 loss for any input dtypes, only in CPU mode
Version: current master b9e9267237d60b76211f42d13f80938d1b926e74

### Code to reproduce
```py
import chainer
import chainer.functions as F
import numpy
import cupy

batch_size = 2
in_size = 5
n_classes = 3
sample_size = 4

def func(xp, in_dtype, reduce):
    def sampler(shape):
        return xp.ones(shape, xp.int32)

    x_arr = xp.ones((batch_size, in_size), in_dtype)
    w_arr = xp.ones((n_classes, in_size), in_dtype)
    t_arr = xp.ones((batch_size,), numpy.int32)
    x = chainer.Variable(x_arr)
    w = chainer.Variable(w_arr)
    t = chainer.Variable(t_arr)

    y = F.negative_sampling(x, t, w, sampler, sample_size, reduce=reduce)
    print(in_dtype.__name__, ' -> ', y.dtype)


for reduce in ('sum', 'no'):
    print('*** reduce: ', reduce)
    for xp in (numpy, cupy):
        print('xp: ', xp.__name__)
        for in_dtype in (numpy.float16, numpy.float32, numpy.float64):
            func(xp, in_dtype, reduce)
    print()
```

### Result
```
*** reduce:  sum
xp:  numpy
float16  ->  float32
float32  ->  float32
float64  ->  float32
xp:  cupy
float16  ->  float16
float32  ->  float32
float64  ->  float64

*** reduce:  no
xp:  numpy
float16  ->  float16
float32  ->  float32
float64  ->  float64
xp:  cupy
float16  ->  float16
float32  ->  float32
float64  ->  float64
```


Discussion:

Resolving patch:
diff --git a/chainer/functions/loss/negative_sampling.py b/chainer/functions/loss/negative_sampling.py
--- a/chainer/functions/loss/negative_sampling.py
+++ b/chainer/functions/loss/negative_sampling.py
@@ -67,7 +67,7 @@ def forward_cpu(self, inputs):
         loss[self.ignore_mask] = numpy.sum(numpy.logaddexp(wx, 0), axis=1)
 
         if self.reduce == 'sum':
-            loss = numpy.array(loss.sum(), 'f')
+            loss = numpy.array(loss.sum(), x.dtype)
 
         self.samples = samples
         return loss,


Test patch:
diff --git a/tests/chainer_tests/functions_tests/loss_tests/test_negative_sampling.py b/tests/chainer_tests/functions_tests/loss_tests/test_negative_sampling.py
--- a/tests/chainer_tests/functions_tests/loss_tests/test_negative_sampling.py
+++ b/tests/chainer_tests/functions_tests/loss_tests/test_negative_sampling.py
@@ -1,6 +1,7 @@
 import unittest
 
 import numpy
+import pytest
 import six
 
 import chainer
@@ -60,6 +61,7 @@ def setUp(self):
             self.check_double_backward_options['dtype'] = numpy.float64
 
     def check_forward(self, x_data, t_data, w_data, sampler):
+        batch_size = len(self.t)
         x = chainer.Variable(x_data)
         t = chainer.Variable(t_data)
         w = chainer.Variable(w_data)
@@ -67,17 +69,24 @@ def check_forward(self, x_data, t_data, w_data, sampler):
         # return_samples=False
         y = functions.negative_sampling(
             x, t, w, sampler, self.sample_size, reduce=self.reduce)
+        assert y.dtype == self.dtype
 
         # return_samples=True
         y_, samples = functions.negative_sampling(
             x, t, w, sampler, self.sample_size, reduce=self.reduce,
             return_samples=True)
 
+        xp = chainer.backend.get_array_module(x)
+        assert isinstance(samples, xp.ndarray)
+        assert samples.dtype == numpy.int32
+        assert samples.shape == (batch_size, self.sample_size + 1)
+
         # Sampler is deterministic, so y and y_ should equal.
+        assert y.dtype == y_.dtype
         numpy.testing.assert_array_equal(
             cuda.to_cpu(y.array), cuda.to_cpu(y_.array))
 
-        self.assertEqual(y.shape, self.gy.shape)
+        assert y.shape == self.gy.shape
 
         samples = cuda.to_cpu(samples)
 
@@ -98,6 +107,7 @@ def check_forward(self, x_data, t_data, w_data, sampler):
         if self.reduce == 'sum':
             loss = loss.sum()
 
+        assert y.dtype == loss.dtype
         testing.assert_allclose(y.data, loss, **self.check_forward_options)
 
     def test_forward_cpu(self):
@@ -167,7 +177,7 @@ def check_invalid_option(self, xp):
         t = xp.asarray(self.t)
         w = xp.asarray(self.w)
 
-        with self.assertRaises(ValueError):
+        with pytest.raises(ValueError):
             negative_sampling.negative_sampling(
                 x, t, w, make_sampler(xp, 5), 2, reduce='invalid_option')