Repository: chainer/chainer

Issue:
F.det should not raise error in forward propagation when input matrix is singular
Currently, `F.det` raises `ValueError` in forward propagation when input matrix is singular [here](https://github.com/pfnet/chainer/pull/613/files#diff-4d0027dd8b2ecc0d9eae073ddfefdb26R65).

It is beneficial when we do backward propagation because it calculates the inverse of the input matrix. But some users can use `F.det` without backward propagation, in which case singular matrices are not exceptional. So we should not raise errors.



Discussion:

Resolving patch:
diff --git a/chainer/functions/math/det.py b/chainer/functions/math/det.py
--- a/chainer/functions/math/det.py
+++ b/chainer/functions/math/det.py
@@ -22,11 +22,11 @@ def _det_gpu(b):
     # Output array
     # These arrays hold information on the execution success
     # or if the matrix was singular.
-    info1 = cuda.cupy.zeros(n_matrices, dtype=numpy.intp)
+    info = cuda.cupy.zeros(n_matrices, dtype=numpy.intp)
     ap = matmul._mat_ptrs(a)
     _, lda = matmul._get_ld(a)
     cuda.cublas.sgetrfBatched(cuda.Device().cublas_handle, n, ap.data.ptr, lda,
-                              p.data.ptr, info1.data.ptr, n_matrices)
+                              p.data.ptr, info.data.ptr, n_matrices)
     det = cuda.cupy.prod(a.diagonal(axis1=1, axis2=2), axis=1)
     # The determinant is equal to the product of the diagonal entries
     # of `a` where the sign of `a` is flipped depending on whether
@@ -34,8 +34,7 @@ def _det_gpu(b):
     rng = cuda.cupy.arange(1, n + 1, dtype='int32')
     parity = cuda.cupy.sum(p != rng, axis=1) % 2
     sign = 1. - 2. * parity.astype('float32')
-    success = cuda.cupy.all(info1 == 0)
-    return det * sign, success
+    return det * sign, info
 
 
 class BatchDet(function.Function):
@@ -60,23 +59,26 @@ def forward_cpu(self, x):
         return self.detx,
 
     def forward_gpu(self, x):
-        self.detx, success = _det_gpu(x[0])
-        if not success:
-            raise ValueError('Singular Matrix')
+        self.detx, _ = _det_gpu(x[0])
         return self.detx,
 
     def backward_cpu(self, x, gy):
         x, = x
         gy, = gy
-        grad = (gy[:, None, None] * self.detx[:, None, None] *
-                numpy.linalg.inv(x.transpose((0, 2, 1))))
+        try:
+            inv_x = numpy.linalg.inv(x.transpose((0, 2, 1)))
+        except numpy.linalg.LinAlgError:
+            raise ValueError('Input has singular matrices.')
+        grad = gy[:, None, None] * self.detx[:, None, None] * inv_x
         return utils.force_array(grad),
 
     def backward_gpu(self, x, gy):
         x, = x
         gy, = gy
-        grad = (gy[:, None, None] * self.detx[:, None, None] *
-                inv._inv_gpu(x.transpose((0, 2, 1))))
+        inv_x, info = inv._inv_gpu(x.transpose((0, 2, 1)))
+        if cuda.cupy.any(info != 0):
+            raise ValueError('Input has singular matrices.')
+        grad = gy[:, None, None] * self.detx[:, None, None] * inv_x
         return utils.force_array(grad),
 
 
diff --git a/chainer/functions/math/inv.py b/chainer/functions/math/inv.py
--- a/chainer/functions/math/inv.py
+++ b/chainer/functions/math/inv.py
@@ -32,7 +32,7 @@ def _inv_gpu(b):
     cuda.cublas.sgetriBatched(
         handle, n, ap.data.ptr, lda, p.data.ptr, cp.data.ptr, ldc,
         info.data.ptr, n_matrices)
-    return c
+    return c, info
 
 
 class Inv(function.Function):
@@ -52,7 +52,7 @@ def forward_cpu(self, x):
 
     def forward_gpu(self, x):
         shape = x[0].shape
-        self.invx = _inv_gpu(x[0].reshape(1, *shape)).reshape(shape)
+        self.invx = _inv_gpu(x[0].reshape(1, *shape))[0].reshape(shape)
         return self.invx,
 
     def backward(self, x, gy):
@@ -80,7 +80,7 @@ def forward_cpu(self, x):
         return self.invx,
 
     def forward_gpu(self, x):
-        self.invx = _inv_gpu(x[0])
+        self.invx, _ = _inv_gpu(x[0])
         return self.invx,
 
     def backward_cpu(self, x, gy):


Test patch:
diff --git a/tests/chainer_tests/functions_tests/math_tests/test_det.py b/tests/chainer_tests/functions_tests/math_tests/test_det.py
--- a/tests/chainer_tests/functions_tests/math_tests/test_det.py
+++ b/tests/chainer_tests/functions_tests/math_tests/test_det.py
@@ -155,6 +155,22 @@ def test_single_matrix_cpu(self):
     def test_expect_scalar_gpu(self):
         self.check_single_matrix(cuda.to_gpu(self.x))
 
+    def check_singular_matrix(self, x):
+        if self.batched:
+            x[0, ...] = 0.0
+        else:
+            x[...] = 0.0
+        x = chainer.Variable(x)
+        # it checks no errors are raised
+        self.det(x)
+
+    def test_singular_matrix_cpu(self):
+        self.check_singular_matrix(self.x)
+
+    @attr.gpu
+    def test_singular_matrix_gpu(self):
+        self.check_singular_matrix(cuda.to_gpu(self.x))
+
     def check_zero_det(self, x, gy, err):
         if self.batched:
             x[0, ...] = 0.0
@@ -164,7 +180,7 @@ def check_zero_det(self, x, gy, err):
             gradient_check.check_backward(self.det, x, gy)
 
     def test_zero_det_cpu(self):
-        self.check_zero_det(self.x, self.gy, numpy.linalg.LinAlgError)
+        self.check_zero_det(self.x, self.gy, ValueError)
 
     @attr.gpu
     def test_zero_det_gpu(self):