Coverage for hopwise/model/knowledge_aware_recommender/usermkr.py: 0%
122 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/10/08
2# @Author : Xinyan Fan
3# @Email : xinyan.fan@ruc.edu.cn
5r"""
6MKR
7#####################################################
8Reference:
9 Hongwei Wang et al. "Multi-Task Feature Learning for Knowledge Graph Enhanced Recommendation." in WWW 2019.
11Reference code:
12 https://github.com/hsientzucheng/MKR.PyTorch
13"""
15import torch
16from torch import nn
18from hopwise.model.abstract_recommender import KnowledgeRecommender
19from hopwise.model.init import xavier_normal_initialization
20from hopwise.model.layers import MLPLayers
21from hopwise.utils import InputType
24class UserMKR(KnowledgeRecommender):
25 r"""MKR is a Multi-task feature learning approach for Knowledge graph enhanced Recommendation. It is a deep
26 end-to-end framework that utilizes knowledge graph embedding task to assist recommendation task. The two
27 tasks are associated by cross&compress units, which automatically share latent features and learn high-order
28 interactions between items in recommender systems and entities in the knowledge graph.
29 """
31 input_type = InputType.POINTWISE
33 def __init__(self, config, dataset):
34 super().__init__(config, dataset)
36 # load parameters info
37 self.LABEL = config["LABEL_FIELD"]
38 self.embedding_size = config["embedding_size"]
39 self.kg_embedding_size = config["kg_embedding_size"]
40 self.L = config["low_layers_num"] # the number of low layers
41 self.H = config["high_layers_num"] # the number of high layers
42 self.reg_weight = config["reg_weight"]
43 self.use_inner_product = config["use_inner_product"]
44 self.dropout_prob = config["dropout_prob"]
46 # init embeddings
47 self.user_embeddings_lookup = nn.Embedding(self.n_entities, self.embedding_size)
48 self.item_embeddings_lookup = nn.Embedding(self.n_entities, self.embedding_size)
49 self.entity_embeddings_lookup = nn.Embedding(self.n_entities, self.embedding_size)
50 self.relation_embeddings_lookup = nn.Embedding(self.n_relations, self.embedding_size)
52 # define layers
53 lower_mlp_layers = []
54 high_mlp_layers = []
55 for i in range(self.L + 1):
56 lower_mlp_layers.append(self.embedding_size)
57 for i in range(self.H):
58 high_mlp_layers.append(self.embedding_size * 3)
60 self.user_mlp = MLPLayers(lower_mlp_layers, self.dropout_prob, "sigmoid")
61 self.tail_mlp = MLPLayers(lower_mlp_layers, self.dropout_prob, "sigmoid")
62 self.user_cc_unit = nn.Sequential()
63 for i_cnt in range(self.L):
64 self.user_cc_unit.add_module(f"user_cc_unit{i_cnt}", CrossCompressUnit(self.embedding_size))
65 self.item_cc_unit = nn.Sequential()
66 for i_cnt in range(self.L):
67 self.item_cc_unit.add_module(f"item_cc_unit{i_cnt}", CrossCompressUnit(self.embedding_size))
68 self.kge_mlp = MLPLayers(high_mlp_layers, self.dropout_prob, "sigmoid")
69 self.kge_pred_mlp = MLPLayers([self.embedding_size * 3, self.embedding_size], self.dropout_prob, "sigmoid")
70 if not self.use_inner_product:
71 self.rs_pred_mlp = MLPLayers([self.embedding_size * 2, 1], self.dropout_prob, "sigmoid")
72 self.rs_mlp = MLPLayers(high_mlp_layers, self.dropout_prob, "sigmoid")
74 # loss
75 self.sigmoid_BCE = nn.BCEWithLogitsLoss()
77 # parameters initialization
78 self.apply(xavier_normal_initialization)
80 def forward(
81 self,
82 user_indices=None,
83 item_indices=None,
84 head_indices=None,
85 relation_indices=None,
86 tail_indices=None,
87 task="rs",
88 ):
89 self.user_embeddings = self.user_embeddings_lookup(user_indices)
90 self.item_embeddings = self.item_embeddings_lookup(item_indices)
91 head_embeddings = self.entity_embeddings_lookup(head_indices)
92 self.item_embeddings, item_head_embeddings = self.item_cc_unit(
93 [self.item_embeddings, head_embeddings]
94 ) # calculate feature interactions between items and entities
95 self.user_embeddings, user_head_embeddings = self.user_cc_unit(
96 [self.user_embeddings, head_embeddings]
97 ) # calculate feature interactions between items and entities
99 self.head_embeddings = torch.cat([item_head_embeddings, user_head_embeddings], 1)
101 if task == "rs":
102 # RS
103 self.user_embeddings = self.user_mlp(self.user_embeddings)
105 if self.use_inner_product: # get scores by inner product.
106 self.scores = torch.sum(self.user_embeddings * self.item_embeddings, 1) # [batch_size]
107 else: # get scores by mlp layers
108 self.user_item_concat = torch.cat(
109 [self.user_embeddings, self.item_embeddings], 1
110 ) # [batch_size, emb_dim*2]
111 self.user_item_concat = self.rs_mlp(self.user_item_concat)
113 self.scores = torch.squeeze(self.rs_pred_mlp(self.user_item_concat)) # [batch_size]
114 self.scores_normalized = torch.sigmoid(self.scores)
115 outputs = [
116 self.user_embeddings,
117 self.item_embeddings,
118 self.scores,
119 self.scores_normalized,
120 ]
122 if relation_indices is not None and task == "kge":
123 # KGE
124 self.tail_embeddings = self.entity_embeddings_lookup(tail_indices)
125 self.relation_embeddings = self.relation_embeddings_lookup(relation_indices)
126 self.tail_embeddings = self.tail_mlp(self.tail_embeddings)
128 self.head_relation_concat = torch.cat(
129 [self.head_embeddings, self.relation_embeddings], 1
130 ) # [batch_size, emb_dim*2]
131 self.head_relation_concat = self.kge_mlp(self.head_relation_concat)
133 self.tail_pred = self.kge_pred_mlp(self.head_relation_concat) # [batch_size, 1]
134 self.tail_pred = torch.sigmoid(self.tail_pred)
135 self.scores_kge = torch.sigmoid(torch.sum(self.tail_embeddings * self.tail_pred, 1))
136 self.rmse = torch.mean(
137 torch.sqrt(torch.sum(torch.pow(self.tail_embeddings - self.tail_pred, 2), 1) / self.embedding_size)
138 )
139 outputs = [
140 self.head_embeddings,
141 self.tail_embeddings,
142 self.scores_kge,
143 self.rmse,
144 ]
146 return outputs
148 def _l2_loss(self, inputs):
149 return torch.sum(inputs**2) / 2
151 def calculate_rs_loss(self, interaction):
152 r"""Calculate the training loss for a batch data of RS."""
153 # inputs
154 self.user_indices = interaction[self.USER_ID]
155 self.item_indices = interaction[self.ITEM_ID]
156 self.head_indices = interaction[self.ITEM_ID]
157 self.labels = interaction[self.LABEL]
158 # RS model
159 user_embeddings, item_embeddings, scores, scores_normalized = self.forward(
160 user_indices=self.user_indices,
161 item_indices=self.item_indices + self.n_users,
162 head_indices=self.head_indices + self.n_users,
163 relation_indices=None,
164 tail_indices=None,
165 task="rs",
166 )
167 # loss
168 base_loss_rs = torch.mean(self.sigmoid_BCE(scores, self.labels))
169 l2_loss_rs = self._l2_loss(user_embeddings) + self._l2_loss(item_embeddings)
170 loss_rs = base_loss_rs + l2_loss_rs * self.reg_weight
172 return loss_rs
174 def calculate_kg_loss(self, interaction):
175 r"""Calculate the training loss for a batch data of KG."""
176 # inputs
177 self.user_indices = interaction[self.HEAD_ENTITY_ID]
178 self.item_indices = interaction[self.HEAD_ENTITY_ID]
179 self.head_indices = interaction[self.HEAD_ENTITY_ID]
180 self.relation_indices = interaction[self.RELATION_ID]
181 self.tail_indices = interaction[self.TAIL_ENTITY_ID]
182 # KGE model
183 head_embeddings, tail_embeddings, scores_kge, rmse = self.forward(
184 user_indices=self.user_indices,
185 item_indices=self.item_indices,
186 head_indices=self.head_indices,
187 relation_indices=self.relation_indices,
188 tail_indices=self.tail_indices,
189 task="kge",
190 )
191 # loss
192 base_loss_kge = -scores_kge
193 l2_loss_kge = self._l2_loss(head_embeddings) + self._l2_loss(tail_embeddings)
194 loss_kge = base_loss_kge + l2_loss_kge * self.reg_weight
196 return loss_kge.sum()
198 def predict(self, interaction):
199 user = interaction[self.USER_ID]
200 item = interaction[self.ITEM_ID]
201 head = interaction[self.ITEM_ID]
203 outputs = self.forward(user, item + self.n_users, head + self.n_users, task="rs")
204 _, _, scores, _ = outputs
206 return scores
209class CrossCompressUnit(nn.Module):
210 r"""This is Cross&Compress Unit for MKR model to model feature interactions between items and entities."""
212 def __init__(self, dim):
213 super().__init__()
214 self.dim = dim
215 self.fc_vv = nn.Linear(dim, 1, bias=True)
216 self.fc_ev = nn.Linear(dim, 1, bias=True)
217 self.fc_ve = nn.Linear(dim, 1, bias=True)
218 self.fc_ee = nn.Linear(dim, 1, bias=True)
220 def forward(self, inputs):
221 v, e = inputs
222 # [batch_size, dim, 1], [batch_size, 1, dim]
223 v = torch.unsqueeze(v, 2)
224 e = torch.unsqueeze(e, 1)
225 # [batch_size, dim, dim]
226 c_matrix = torch.matmul(v, e)
227 c_matrix_transpose = c_matrix.permute(0, 2, 1)
228 # [batch_size * dim, dim]
229 c_matrix = c_matrix.view(-1, self.dim)
230 c_matrix_transpose = c_matrix_transpose.contiguous().view(-1, self.dim)
231 # [batch_size, dim]
232 v_intermediate = self.fc_vv(c_matrix) + self.fc_ev(c_matrix_transpose)
233 e_intermediate = self.fc_ve(c_matrix) + self.fc_ee(c_matrix_transpose)
234 v_output = v_intermediate.view(-1, self.dim)
235 e_output = e_intermediate.view(-1, self.dim)
237 return v_output, e_output