Repository: tensorly/tensorly

Issue:
Masked CP decomposition returns error in numpy backend kr
Hi, like milanlanlan in [Handling missing data in decomposition #4](https://github.com/tensorly/tensorly/issues/4#issuecomment-557899752) I am trying to decompose a tensor using the CP decomposition with missing values. Unfortunately, I also receive this error message: 

>    5 mask_parafac[score_sparse > 0] = 1
      6 print(mask_parafac)
      7 rec_score_tensor = parafac(score_sparse, rank=4, mask = mask)
      8 
      9 print(rec_score_tensor)
tensorly/decomposition/candecomp_parafac.py in parafac(tensor, rank, n_iter_max, init, svd, normalize_factors, tol, orthogonalise, random_state, verbose, return_errors, non_negative, mask)
    183 
    184             if mask is not None:
    185                 tensor = tensor*mask + tl.kruskal_to_tensor((None, factors), mask=1-mask)
    186 
    187             mttkrp = unfolding_dot_khatri_rao(tensor, (None, factors), mode)
tensorly/kruskal_tensor.py in kruskal_to_tensor(kruskal_tensor, mask)
    186                              T.transpose(khatri_rao(factors, skip_matrix=0)))
    187     else:
    188         full_tensor = T.sum(khatri_rao([factors[0]*weights]+factors[1:], mask=mask), axis=1)
    189 
    190     return fold(full_tensor, 0, shape)
tensorly/tenalg/_khatri_rao.py in khatri_rao(matrices, weights, skip_matrix, reverse, mask)
     96         # Note: we do NOT use .reverse() which would reverse matrices even outside this function
     97 
     98     return T.kr(matrices, weights=weights, mask=mask)
tensorly/backend/__init__.py in inner(*args, **kwargs)
    158 
    159     def inner(*args, **kwargs):
    160         return _get_backend_method(name)(*args, **kwargs)
    161 
    162     # We don't use `functools.wraps` here because some of the dispatched
tensorly/backend/numpy_backend.py in kr(self, matrices, weights, mask)
     67             matrices = [m if i else m*self.reshape(weights, (1, -1)) for i, m in enumerate(matrices)]
     68 
     69         return np.einsum(operation, *matrices).reshape((-1, n_columns))*mask
     70         #tensor = np.einsum(operation, *matrices).reshape((-1, n_columns))
     71         #return tensor* mask.reshape(tensor.shape)
ValueError: operands could not be broadcast together with shapes (80,4) (5,4,4) <

As the number of elements in both arrays is not equal it seems like a simple reshape will not solve the problem.

Comments:

Patch:
diff --git a/tensorly/backend/numpy_backend.py b/tensorly/backend/numpy_backend.py
index 45aead6..2688ae9 100644
--- a/tensorly/backend/numpy_backend.py
+++ b/tensorly/backend/numpy_backend.py
@@ -53,7 +53,6 @@ class NumpyBackend(Backend):
             return np.sum(np.abs(tensor)**order, axis=axis)**(1 / order)
 
     def kr(self, matrices, weights=None, mask=None):
-        if mask is None: mask = 1
         n_columns = matrices[0].shape[1]
         n_factors = len(matrices)
 
@@ -66,7 +65,8 @@ class NumpyBackend(Backend):
         if weights is not None:
             matrices = [m if i else m*self.reshape(weights, (1, -1)) for i, m in enumerate(matrices)]
 
-        return np.einsum(operation, *matrices).reshape((-1, n_columns))*mask
+        m = mask.reshape((-1, 1)) if mask is not None else 1
+        return np.einsum(operation, *matrices).reshape((-1, n_columns))*m
 
     @property
     def SVD_FUNS(self):


Test patch:
diff --git a/tensorly/decomposition/tests/test_candecomp_parafac.py b/tensorly/decomposition/tests/test_candecomp_parafac.py
index e537743..dcd8c45 100644
--- a/tensorly/decomposition/tests/test_candecomp_parafac.py
+++ b/tensorly/decomposition/tests/test_candecomp_parafac.py
@@ -75,6 +75,21 @@ def test_parafac():
     error = T.norm(tensor - rec, 2)/T.norm(tensor)
     assert_(error < tol)
 
+
+def test_masked_parafac():
+    """Test for the masked CANDECOMP-PARAFAC decomposition.
+    This checks that a mask of 1's is identical to the unmasked case.
+    """
+    rng = check_random_state(1234)
+    tensor = T.tensor(rng.random_sample((3, 3, 3)))
+    mask = T.tensor(np.ones((3, 3, 3)))
+
+    mask_fact = parafac(tensor, rank=2, mask=mask)
+    fact = parafac(tensor, rank=2)
+    diff = kruskal_to_tensor(mask_fact) - kruskal_to_tensor(fact)
+    assert_(T.norm(diff) < 0.01, 'norm 2 of reconstruction higher than 0.01')
+
+
 def test_non_negative_parafac():
     """Test for non-negative PARAFAC