1 # Copyright 2017 The TensorFlow Authors. All Rights Reserved. 2 # 3 # Licensed under the Apache License, Version 2.0 (the "License"); 4 # you may not use this file except in compliance with the License. 5 # You may obtain a copy of the License at 6 # 7 # http://www.apache.org/licenses/LICENSE-2.0 8 # 9 # Unless required by applicable law or agreed to in writing, software 10 # distributed under the License is distributed on an "AS IS" BASIS, 11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 # See the License for the specific language governing permissions and 13 # limitations under the License. 14 # ============================================================================== 15 """Test utils for factorization_ops.""" 16 17 from __future__ import absolute_import 18 from __future__ import division 19 from __future__ import print_function 20 21 import random 22 23 import numpy as np 24 25 from tensorflow.python.framework import constant_op 26 from tensorflow.python.framework import sparse_tensor 27 from tensorflow.python.ops import array_ops 28 from tensorflow.python.ops import math_ops 29 from tensorflow.python.ops import sparse_ops 30 31 32 INPUT_MATRIX = np.array( 33 [[0.1, 0.0, 0.2, 0.0, 0.4, 0.5, 0.0], 34 [0.0, 1.1, 0.0, 1.3, 1.4, 0.0, 1.6], 35 [2.0, 0.0, 0.0, 2.3, 0.0, 2.5, 0.0], 36 [3.0, 0.0, 3.2, 3.3, 0.0, 3.5, 0.0], 37 [0.0, 4.1, 0.0, 0.0, 4.4, 0.0, 4.6]]).astype(np.float32) 38 39 40 def remove_empty_rows_columns(np_matrix): 41 """Simple util to remove empty rows and columns of a matrix. 42 43 Args: 44 np_matrix: A numpy array. 45 Returns: 46 A tuple consisting of: 47 mat: A numpy matrix obtained by removing empty rows and columns from 48 np_matrix. 49 nz_row_ids: A numpy array of the ids of non-empty rows, such that 50 nz_row_ids[i] is the old row index corresponding to new index i. 51 nz_col_ids: A numpy array of the ids of non-empty columns, such that 52 nz_col_ids[j] is the old column index corresponding to new index j. 53 """ 54 nz_row_ids = np.where(np.sum(np_matrix, axis=1) != 0)[0] 55 nz_col_ids = np.where(np.sum(np_matrix, axis=0) != 0)[0] 56 mat = np_matrix[np.ix_(nz_row_ids, nz_col_ids)] 57 return mat, nz_row_ids, nz_col_ids 58 59 60 def np_matrix_to_tf_sparse(np_matrix, 61 row_slices=None, 62 col_slices=None, 63 transpose=False, 64 shuffle=False): 65 """Simple util to slice non-zero np matrix elements as tf.SparseTensor.""" 66 indices = np.nonzero(np_matrix) 67 68 # Only allow slices of whole rows or whole columns. 69 assert not (row_slices is not None and col_slices is not None) 70 71 if row_slices is not None: 72 selected_ind = np.concatenate( 73 [np.where(indices[0] == r)[0] for r in row_slices], 0) 74 indices = (indices[0][selected_ind], indices[1][selected_ind]) 75 76 if col_slices is not None: 77 selected_ind = np.concatenate( 78 [np.where(indices[1] == c)[0] for c in col_slices], 0) 79 indices = (indices[0][selected_ind], indices[1][selected_ind]) 80 81 if shuffle: 82 shuffled_ind = [x for x in range(len(indices[0]))] 83 random.shuffle(shuffled_ind) 84 indices = (indices[0][shuffled_ind], indices[1][shuffled_ind]) 85 86 ind = (np.concatenate((np.expand_dims(indices[1], 1), 87 np.expand_dims(indices[0], 1)), 1).astype(np.int64) if 88 transpose else np.concatenate((np.expand_dims(indices[0], 1), 89 np.expand_dims(indices[1], 1)), 90 1).astype(np.int64)) 91 val = np_matrix[indices].astype(np.float32) 92 shape = (np.array([max(indices[1]) + 1, max(indices[0]) + 1]).astype(np.int64) 93 if transpose else np.array( 94 [max(indices[0]) + 1, max(indices[1]) + 1]).astype(np.int64)) 95 return sparse_tensor.SparseTensor(ind, val, shape) 96 97 98 def calculate_loss(input_mat, row_factors, col_factors, regularization=None, 99 w0=1., row_weights=None, col_weights=None): 100 """Calculates the loss of a given factorization. 101 102 Using a non distributed method, different than the one implemented in the 103 WALS model. The weight of an observed entry (i, j) (i.e. such that 104 input_mat[i, j] is non zero) is (w0 + row_weights[i]col_weights[j]). 105 106 Args: 107 input_mat: The input matrix, a SparseTensor of rank 2. 108 row_factors: The row factors, a dense Tensor of rank 2. 109 col_factors: The col factors, a dense Tensor of rank 2. 110 regularization: the regularization coefficient, a scalar. 111 w0: the weight of unobserved entries. A scalar. 112 row_weights: A dense tensor of rank 1. 113 col_weights: A dense tensor of rank 1. 114 115 Returns: 116 The total loss. 117 """ 118 wr = (array_ops.expand_dims(row_weights, 1) if row_weights is not None 119 else constant_op.constant(1.)) 120 wc = (array_ops.expand_dims(col_weights, 0) if col_weights is not None 121 else constant_op.constant(1.)) 122 reg = (regularization if regularization is not None 123 else constant_op.constant(0.)) 124 125 row_indices, col_indices = array_ops.split(input_mat.indices, 126 axis=1, 127 num_or_size_splits=2) 128 gathered_row_factors = array_ops.gather(row_factors, row_indices) 129 gathered_col_factors = array_ops.gather(col_factors, col_indices) 130 sp_approx_vals = array_ops.squeeze(math_ops.matmul( 131 gathered_row_factors, gathered_col_factors, adjoint_b=True)) 132 sp_approx = sparse_tensor.SparseTensor( 133 indices=input_mat.indices, 134 values=sp_approx_vals, 135 dense_shape=input_mat.dense_shape) 136 137 sp_approx_sq = math_ops.square(sp_approx) 138 row_norm = math_ops.reduce_sum(math_ops.square(row_factors)) 139 col_norm = math_ops.reduce_sum(math_ops.square(col_factors)) 140 row_col_norm = math_ops.reduce_sum(math_ops.square(math_ops.matmul( 141 row_factors, col_factors, transpose_b=True))) 142 143 resid = sparse_ops.sparse_add(input_mat, sp_approx * (-1)) 144 resid_sq = math_ops.square(resid) 145 loss = w0 * ( 146 sparse_ops.sparse_reduce_sum(resid_sq) - 147 sparse_ops.sparse_reduce_sum(sp_approx_sq) 148 ) 149 loss += (sparse_ops.sparse_reduce_sum(wr * (resid_sq * wc)) + 150 w0 * row_col_norm + reg * (row_norm + col_norm)) 151 return loss.eval() 152