Coverage for hopwise/model/general_recommender/dmf.py: 94%

105 statements  

« prev     ^ index     » next       coverage.py v7.16.2, created at 2026-09-30 13:25 +0000

1# @Time : 2020/8/21 

2# @Author : Kaizhou Zhang 

3# @Email : kaizhou361@163.com 

4 

5# UPDATE 

6# @Time : 2020/08/31 2020/09/18 

7# @Author : Kaiyuan Li Zihan Lin 

8# @email : tsotfsk@outlook.com linzihan.super@foxmail.con 

9 

10r"""DMF 

11################################################ 

12Reference: 

13 Hong-Jian Xue et al. "Deep Matrix Factorization Models for Recommender Systems." in IJCAI 2017. 

14""" 

15 

16import numpy as np 

17import torch 

18from torch import nn 

19from torch.nn.init import normal_ 

20 

21from hopwise.model.abstract_recommender import GeneralRecommender 

22from hopwise.model.layers import MLPLayers 

23from hopwise.utils import InputType 

24 

25 

26class DMF(GeneralRecommender): 

27 r"""DMF is an neural network enhanced matrix factorization model. 

28 The original interaction matrix of :math:`n_{users} \times n_{items}` is set as model input, 

29 we carefully design the data interface and use sparse tensor to train and test efficiently. 

30 We just implement the model following the original author with a pointwise training mode. 

31 

32 Note: 

33 Our implementation is a improved version which is different from the original paper. 

34 For a better performance and stability, we replace cosine similarity to inner-product when calculate 

35 final score of user's and item's embedding. 

36 """ 

37 

38 input_type = InputType.POINTWISE 

39 

40 def __init__(self, config, dataset): 

41 super().__init__(config, dataset) 

42 

43 # load dataset info 

44 self.LABEL = config["LABEL_FIELD"] 

45 self.RATING = config["RATING_FIELD"] 

46 

47 # load parameters info 

48 self.user_embedding_size = config["user_embedding_size"] 

49 self.item_embedding_size = config["item_embedding_size"] 

50 self.user_hidden_size_list = config["user_hidden_size_list"] 

51 self.item_hidden_size_list = config["item_hidden_size_list"] 

52 # The dimensions of the last layer of users and items must be the same 

53 assert self.user_hidden_size_list[-1] == self.item_hidden_size_list[-1] 

54 self.inter_matrix_type = config["inter_matrix_type"] 

55 

56 # generate intermediate data 

57 if self.inter_matrix_type == "01": 

58 ( 

59 self.history_user_id, 

60 self.history_user_value, 

61 _, 

62 ) = dataset.history_user_matrix() 

63 ( 

64 self.history_item_id, 

65 self.history_item_value, 

66 _, 

67 ) = dataset.history_item_matrix() 

68 self.interaction_matrix = dataset.inter_matrix(form="csr").astype(np.float32) 

69 elif self.inter_matrix_type == "rating": 

70 ( 

71 self.history_user_id, 

72 self.history_user_value, 

73 _, 

74 ) = dataset.history_user_matrix(value_field=self.RATING) 

75 ( 

76 self.history_item_id, 

77 self.history_item_value, 

78 _, 

79 ) = dataset.history_item_matrix(value_field=self.RATING) 

80 self.interaction_matrix = dataset.inter_matrix(form="csr", value_field=self.RATING).astype(np.float32) 

81 else: 

82 raise ValueError(f"The inter_matrix_type must in ['01', 'rating'] but get {self.inter_matrix_type}") 

83 self.max_rating = self.history_user_value.max() 

84 # tensor of shape [n_items, H] where H is max length of history interaction. 

85 self.history_user_id = self.history_user_id.to(self.device) 

86 self.history_user_value = self.history_user_value.to(self.device) 

87 self.history_item_id = self.history_item_id.to(self.device) 

88 self.history_item_value = self.history_item_value.to(self.device) 

89 

90 # define layers 

91 self.user_linear = nn.Linear(in_features=self.n_items, out_features=self.user_embedding_size, bias=False) 

92 self.item_linear = nn.Linear(in_features=self.n_users, out_features=self.item_embedding_size, bias=False) 

93 self.user_fc_layers = MLPLayers([self.user_embedding_size] + self.user_hidden_size_list) 

94 self.item_fc_layers = MLPLayers([self.item_embedding_size] + self.item_hidden_size_list) 

95 self.sigmoid = nn.Sigmoid() 

96 self.bce_loss = nn.BCEWithLogitsLoss() 

97 

98 # Save the item embedding before dot product layer to speed up evaluation 

99 self.i_embedding = None 

100 

101 # parameters initialization 

102 self.apply(self._init_weights) 

103 self.other_parameter_name = ["i_embedding"] 

104 

105 def _init_weights(self, module): 

106 # We just initialize the module with normal distribution as the paper said 

107 if isinstance(module, nn.Linear): 

108 normal_(module.weight.data, 0, 0.01) 

109 if module.bias is not None: 

110 module.bias.data.fill_(0.0) 

111 elif isinstance(module, nn.Embedding): 

112 normal_(module.weight.data, 0, 0.01) 

113 

114 def forward(self, user, item): 

115 user = self.get_user_embedding(user) 

116 

117 # Following lines construct tensor of shape [B,n_users] using the tensor of shape [B,H] 

118 col_indices = self.history_user_id[item].flatten() 

119 row_indices = ( 

120 torch.arange(item.shape[0]).to(self.device).repeat_interleave(self.history_user_id.shape[1], dim=0) 

121 ) 

122 matrix_01 = torch.zeros(1).to(self.device).repeat(item.shape[0], self.n_users) 

123 matrix_01.index_put_((row_indices, col_indices), self.history_user_value[item].flatten()) 

124 item = self.item_linear(matrix_01) 

125 

126 user = self.user_fc_layers(user) 

127 item = self.item_fc_layers(item) 

128 

129 # cosine distance is replaced by dot product according the result of our experiments. 

130 vector = torch.mul(user, item).sum(dim=1) 

131 

132 return vector 

133 

134 def calculate_loss(self, interaction): 

135 # when starting a new epoch, the item embedding we saved must be cleared. 

136 if self.training: 

137 self.i_embedding = None 

138 

139 user = interaction[self.USER_ID] 

140 item = interaction[self.ITEM_ID] 

141 if self.inter_matrix_type == "01": 

142 label = interaction[self.LABEL] 

143 elif self.inter_matrix_type == "rating": 

144 label = interaction[self.RATING] * interaction[self.LABEL] 

145 output = self.forward(user, item) 

146 

147 label = label / self.max_rating # normalize the label to calculate BCE loss. 

148 loss = self.bce_loss(output, label) 

149 return loss 

150 

151 def predict(self, interaction): 

152 user = interaction[self.USER_ID] 

153 item = interaction[self.ITEM_ID] 

154 predict = self.sigmoid(self.forward(user, item)) 

155 return predict 

156 

157 def get_user_embedding(self, user): 

158 r"""Get a batch of user's embedding with the user's id and history interaction matrix. 

159 

160 Args: 

161 user (torch.LongTensor): The input tensor that contains user's id, shape: [batch_size, ] 

162 

163 Returns: 

164 torch.FloatTensor: The embedding tensor of a batch of user, shape: [batch_size, embedding_size] 

165 """ 

166 # Following lines construct tensor of shape [B,n_items] using the tensor of shape [B,H] 

167 col_indices = self.history_item_id[user].flatten() 

168 row_indices = torch.arange(user.shape[0]).to(self.device) 

169 row_indices = row_indices.repeat_interleave(self.history_item_id.shape[1], dim=0) 

170 matrix_01 = torch.zeros(1).to(self.device).repeat(user.shape[0], self.n_items) 

171 matrix_01.index_put_((row_indices, col_indices), self.history_item_value[user].flatten()) 

172 user = self.user_linear(matrix_01) 

173 

174 return user 

175 

176 def get_item_embedding(self): 

177 r"""Get all item's embedding with history interaction matrix. 

178 

179 Considering the RAM of device, we use matrix multiply on sparse tensor for generalization. 

180 

181 Returns: 

182 torch.FloatTensor: The embedding tensor of all item, shape: [n_items, embedding_size] 

183 """ 

184 interaction_matrix = self.interaction_matrix.tocoo() 

185 row = interaction_matrix.row 

186 col = interaction_matrix.col 

187 i = torch.LongTensor([row, col]) 

188 data = torch.FloatTensor(interaction_matrix.data) 

189 item_matrix = ( 

190 torch.sparse.FloatTensor(i, data, torch.Size(interaction_matrix.shape)).to(self.device).transpose(0, 1) 

191 ) 

192 item = torch.sparse.mm(item_matrix, self.item_linear.weight.t()) 

193 

194 item = self.item_fc_layers(item) 

195 return item 

196 

197 def full_sort_predict(self, interaction): 

198 user = interaction[self.USER_ID] 

199 u_embedding = self.get_user_embedding(user) 

200 u_embedding = self.user_fc_layers(u_embedding) 

201 

202 if self.i_embedding is None: 

203 self.i_embedding = self.get_item_embedding() 

204 

205 similarity = torch.mm(u_embedding, self.i_embedding.t()) 

206 similarity = self.sigmoid(similarity) 

207 return similarity.view(-1)