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
« 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
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
10r"""DMF
11################################################
12Reference:
13 Hong-Jian Xue et al. "Deep Matrix Factorization Models for Recommender Systems." in IJCAI 2017.
14"""
16import numpy as np
17import torch
18from torch import nn
19from torch.nn.init import normal_
21from hopwise.model.abstract_recommender import GeneralRecommender
22from hopwise.model.layers import MLPLayers
23from hopwise.utils import InputType
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.
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 """
38 input_type = InputType.POINTWISE
40 def __init__(self, config, dataset):
41 super().__init__(config, dataset)
43 # load dataset info
44 self.LABEL = config["LABEL_FIELD"]
45 self.RATING = config["RATING_FIELD"]
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"]
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)
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()
98 # Save the item embedding before dot product layer to speed up evaluation
99 self.i_embedding = None
101 # parameters initialization
102 self.apply(self._init_weights)
103 self.other_parameter_name = ["i_embedding"]
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)
114 def forward(self, user, item):
115 user = self.get_user_embedding(user)
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)
126 user = self.user_fc_layers(user)
127 item = self.item_fc_layers(item)
129 # cosine distance is replaced by dot product according the result of our experiments.
130 vector = torch.mul(user, item).sum(dim=1)
132 return vector
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
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)
147 label = label / self.max_rating # normalize the label to calculate BCE loss.
148 loss = self.bce_loss(output, label)
149 return loss
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
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.
160 Args:
161 user (torch.LongTensor): The input tensor that contains user's id, shape: [batch_size, ]
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)
174 return user
176 def get_item_embedding(self):
177 r"""Get all item's embedding with history interaction matrix.
179 Considering the RAM of device, we use matrix multiply on sparse tensor for generalization.
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())
194 item = self.item_fc_layers(item)
195 return item
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)
202 if self.i_embedding is None:
203 self.i_embedding = self.get_item_embedding()
205 similarity = torch.mm(u_embedding, self.i_embedding.t())
206 similarity = self.sigmoid(similarity)
207 return similarity.view(-1)