论文:https://arxiv.org/pdf/2305.18238.pdf
这是我写的第一篇代码阅读,在第一篇中我会对讲的稍微细致一点,并将代码与论文中的公式相结合起来,这样才方便理解,如果有理解错的地方欢迎各位指正(才疏学浅) 我会从数据处理部分开始,然后根据论文里的章节来讲代码,可以结合我的论文阅读一起看,地址:论文阅读总结:SIGIR 2023 Multi-behavior Self-supervised Learning for Recommendation_推荐系统YYDS的博客-CSDN博客
首先还是放一下模型图
数据预处理(不想看的可以直接跳的模型部分)
在代码中数据预处理的部分在load_data文件中,(这部分代码太长,大致讲一下流程)
for i in range(len(self.behs) - 1):
beh = self.behs[i]
file_name = self.predir + '/' + beh + '.txt'
with open(file_name) as f:
for l in f.readlines():
if len(l) > 0:
l = l.strip('\n').split(' ')
items = [int(i) for i in l[1:]]
uid = int(l[0])
self.n_items = max(self.n_items, max(items))
self.n_users = max(self.n_users, uid)
加载行为数据文件,并统计用户数和物品数。循环遍历行为列表中的元素,并打开相应的数据文件。然后,对于每一行数据,获取用户ID和物品列表,并更新self.n_items
和self.n_users
的最大值。
self.trnMats = [sp.dok_matrix((self.n_users, self.n_items), dtype=np.float32) for i in
range(len(self.behs))]
self.tstMats = sp.dok_matrix((self.n_users, self.n_items), dtype=np.float32)
self.trnDicts = [dict() for i in range(len(self.behs))]
self.trnDicts_item = [dict() for i in range(len(self.behs))]
self.tstDicts = dict()
self.interNum = [0 for i in range(len(self.behs))]
介绍几个重要变量:训练和测试的稀疏矩阵self.trnMats
和self.tstMats
,以及用于存储训练和测试数据的字典self.trnDicts
、self.trnDicts_item
和self.tstDicts
。还有一个列表self.interNum
用于存储每种行为的交互数量。
for i in range(len(self.behs) - 1):
beh = self.behs[i]
beh_filename = self.predir + '/' + beh + '.txt'
with open(beh_filename) as f:
for l in f.readlines():
if len(l) == 0: break
l = l.strip('\n')
items = [int(i) for i in l.split(' ')]
uid, items_list = items[0], items[1:]
self.interNum[i] += len(items_list)
for item in items_list:
self.trnMats[i][uid, item] = 1.
self.trnDicts[i][uid] = items_list
读取行为文件中的数据,并更新训练矩阵和训练字典。通过循环遍历行为列表self.behs
中的元素,并打开对应的行为文件。然后,对于每一行数据,解析出用户ID和物品列表,并将训练矩阵中相应的位置置为1,同时更新训练字典和交互数量。
def get_adj_mat(self):
self.saveAdjMatPath = 'Adj_Mats/' + self.dataset_name
os.makedirs(self.saveAdjMatPath, exist_ok=True)
adj_mat_list = []
norm_adj_mat_list = []
mean_adj_mat_list = []
pre_adj_mat_list = []
try:
t1 = time()
for i in range(len(self.behs)):
beh = self.behs[i]
adj_mat = sp.load_npz(self.saveAdjMatPath + '/s_adj_mat_' + beh + '.npz')
norm_adj_mat = sp.load_npz(self.saveAdjMatPath + '/s_norm_adj_mat_' + beh + '.npz')
mean_adj_mat = sp.load_npz(self.saveAdjMatPath + '/s_mean_adj_mat_' + beh + '.npz')
adj_mat_list.append(adj_mat)
norm_adj_mat_list.append(norm_adj_mat)
mean_adj_mat_list.append(mean_adj_mat)
print('already load adj matrix', time() - t1)
except Exception:
for i in range(len(self.behs)):
beh = self.behs[i]
adj_mat, norm_adj_mat, mean_adj_mat = self.create_adj_mat(self.trnMats[i])
adj_mat_list.append(adj_mat)
norm_adj_mat_list.append(norm_adj_mat)
mean_adj_mat_list.append(mean_adj_mat)
sp.save_npz(self.saveAdjMatPath + '/s_adj_mat_' + beh + '.npz', adj_mat)
sp.save_npz(self.saveAdjMatPath + '/s_norm_adj_mat_' + beh + '.npz', norm_adj_mat)
sp.save_npz(self.saveAdjMatPath + '/s_mean_adj_mat_' + beh + '.npz', mean_adj_mat)
try:
for i in range(len(self.behs)):
beh = self.behs[i]
pre_adj_mat = sp.load_npz(self.saveAdjMatPath + '/s_pre_adj_mat_' + beh + '.npz')
pre_adj_mat_list.append(pre_adj_mat)
print('already load pre adj matrix')
except Exception:
for i in range(len(self.behs)):
beh = self.behs[i]
adj_mat = adj_mat_list[i]
rowsum = np.array(adj_mat.sum(1))
d_inv = np.power(rowsum, -0.5).flatten()
d_inv[np.isinf(d_inv)] = 0.
d_mat_inv = sp.diags(d_inv)
norm_adj = d_mat_inv.dot(adj_mat)
norm_adj = norm_adj.dot(d_mat_inv)
print('generate pre adjacency matrix.')
pre_adj_mat = norm_adj.tocsr()
pre_adj_mat_list.append(pre_adj_mat)
sp.save_npz(self.saveAdjMatPath + '/s_pre_adj_mat_' + beh + '.npz', norm_adj)
return pre_adj_mat_list
这部分是邻接矩阵的代码。首先加载邻接矩阵。从保存的文件中加载已有的邻接矩阵。对于每种行为类型,使用sp.load_npz
函数加载对应的邻接矩阵、规范化邻接矩阵和均值邻接矩阵,并将它们添加到对应的列表中。如果加载成功,打印已加载邻接矩阵的耗时。如果加载失败,调用self.create_adj_mat
方法创建邻接矩阵,并将它们保存到文件中。得到了邻接矩阵和预处理邻接矩阵。返回三个邻接矩阵:原始邻接矩阵 adj_mat
、单一归一化邻接矩阵 norm_adj_mat
和均值归一化邻接矩阵 mean_adj_mat
。
代码太长了。。。我直接放函数名字(模型部分会全放的)
get_unified_sim:获取统一相似度矩阵
create_adj_mat:返回三个邻接矩阵:原始邻接矩阵adj_mat 归一化邻接矩阵norm_adj_mat
和均值归一化邻接矩阵mean_adj_mat
negative_pool:为每个用户生成负样本池
sample:根据 batch_size
的大小确定需要采样的用户数量采样用户、正样本和负样本,之后定义了三个函数,分别是:
从用户的训练集中采样正样本物品sample_pos_items_for_u,随机选择一个索引,从用户的正样本列表中获取相应的物品ID;
从所有物品中采样负样本物品sample_neg_items_for_u,随机选择一个索引,从0到总物品数之间进行采样,并确保采样的物品既不在用户的训练集中;
从用户的负样本池中采样负样本物品sample_neg_items_for_u_from_pools,首先根据用户ID获取对应的负样本池,然后从负样本池中去除用户的训练集中已有的物品,并从剩余的负样本物品中随机采样指定数量的物品。
get_sparsity_split:获取稀疏性划分的用户列表,将用户分为不同的组或子集,使得每个子集中的用户具有相似的稀疏性特征(可以降低复杂度,提升模型预测的准确性)。
augment_adj_mat:数据增强。根据给定的数据增强类型和数据增强比例,判断是否需要进行数据增强操作。0表示Node Dropout(节点丢弃),1表示Edge Dropout(边丢弃),2表示Random Walk(随机游走)。之后得到对称的邻接矩阵adj_mat。进行归一化操作,计算行和、取行和的倒数、构建对角矩阵,并进行矩阵运算得到最终的归一化邻接矩阵adj_matrix。(归一化操作是为了在图数据中考虑节点的度信息,并使得节点之间的关系更加平衡。可以消除节点度数的差异对GNN的影响,提高算法的鲁棒性)
模型部分
class MBSSL(nn.Module):
name = 'MBSSL'
def __init__(self, max_item_list, data_config, args):
super(MBSSL, self).__init__()
# ********************** input data *********************** #
self.max_item_list = max_item_list
self.n_users = data_config['n_users'] 用户数量
self.n_items = data_config['n_items'] 物品数量
self.num_nodes = self.n_users + self.n_items 图中节点(用户和物品)的总数
self.pre_adjs = data_config['pre_adjs'] 预处理的邻接矩阵
self.pre_adjs_tensor = [self._convert_sp_mat_to_sp_tensor(adj).to(device) for adj in self.pre_adjs] 将预处理的邻接矩阵列表转换为稀疏张量
self.behs = data_config['behs']
self.n_relations = len(self.behs) 行为的数量
self.coefficient = torch.tensor(eval(args.coefficient)).view(1, -1).to(device)
self.emb_dim = args.embed_size 嵌入维度
self.batch_size = args.batch_size 批处理大小
self.weight_size = eval(args.layer_size)图卷积层的输入和输出维度
self.n_layers = len(self.weight_size)图卷积层层数
self.mess_dropout = eval(args.mess_dropout) 图卷积层的消息传递层的dropout
self.aug_type = args.aug_type 数据增强类型
self.nhead = args.nhead 注意力头数
self.att_dim = args.att_dim 注意力维度
可学习的参数:
self.all_weights = {}
self.all_weights['user_embedding'] = Parameter(torch.FloatTensor(self.n_users, self.emb_dim))
self.all_weights['item_embedding'] = Parameter(torch.FloatTensor(self.n_items, self.emb_dim))
self.all_weights['relation_embedding'] = Parameter(torch.FloatTensor(self.n_relations, self.emb_dim))
self.weight_size_list = [self.emb_dim] + self.weight_size
for k in range(self.n_layers):
self.all_weights['W_gc_%d' % k] = Parameter(
torch.FloatTensor(self.weight_size_list[k], self.weight_size_list[k + 1]))
self.all_weights['W_rel_%d' % k] = Parameter(
torch.FloatTensor(self.weight_size_list[k], self.weight_size_list[k + 1]))
self.all_weights['trans_weights_s1'] = Parameter(
torch.FloatTensor(self.n_relations, self.emb_dim, self.att_dim))
self.all_weights['trans_weights_s2'] = Parameter(torch.FloatTensor(self.n_relations, self.att_dim, 1))
user_embeddin 用户嵌入矩阵,形状为(n_users, emb_dim)
item_embedding 项目嵌入矩阵,形状为(n_items, emb_dim)
relation_embedding同理
W_gc 图卷积层的权重矩阵 W_rel 行为嵌入矩阵与图卷积层的权重矩阵 根据GCN层计算得到
trans_weights_s1,2 图卷积层的输入和输出维度
def reset_parameters(self):
nn.init.xavier_uniform_(self.all_weights['user_embedding'])
nn.init.xavier_uniform_(self.all_weights['item_embedding'])
nn.init.xavier_uniform_(self.all_weights['relation_embedding'])
nn.init.xavier_uniform_(self.all_weights['trans_weights_s1'])
nn.init.xavier_uniform_(self.all_weights['trans_weights_s2'])
for k in range(self.n_layers):
nn.init.xavier_uniform_(self.all_weights['W_gc_%d' % k])
nn.init.xavier_uniform_(self.all_weights['W_rel_%d' % k])
def _convert_sp_mat_to_sp_tensor(self, X):
coo = X.tocoo()
values = coo.data
indices = np.vstack((coo.row, coo.col))
shape = coo.shape
return torch.sparse.FloatTensor(torch.LongTensor(indices), torch.FloatTensor(values), torch.Size(shape))
对各个参数进行Xavier初始化并将稀疏矩阵转换为稀疏张量
行为感知图神经网络
def forward(self, sub_mats, device):
self.sub_mat = {}
for k in range(1, self.n_layers + 1):
if self.aug_type in [0, 1]:
self.sub_mat['sub_mat_1%d' % k] = sub_mats['sub1'].to(device)
self.sub_mat['sub_mat_2%d' % k] = sub_mats['sub2'].to(device)
else:
self.sub_mat['sub_mat_1%d' % k] = sub_mats['sub1%d' % k].to(device)
self.sub_mat['sub_mat_2%d' % k] = sub_mats['sub2%d' % k].to(device)
ego_embeddings = torch.cat((self.all_weights['user_embedding'], self.all_weights['item_embedding']),
dim=0).unsqueeze(1).repeat(1, self.n_relations, 1)
ego_embeddings_sub1 = ego_embeddings
ego_embeddings_sub2 = ego_embeddings
all_embeddings = ego_embeddings
all_embeddings_sub1 = ego_embeddings_sub1
all_embeddings_sub2 = ego_embeddings_sub2
all_rela_embs = {}
for i in range(self.n_relations):
beh = self.behs[i]
rela_emb = self.all_weights['relation_embedding'][i]
rela_emb = torch.reshape(rela_emb, (-1, self.emb_dim))
all_rela_embs[beh] = [rela_emb]
total_mm_time = 0.
初始化了self.sub_mat
字典,用于存储矩阵。根据aug_type
的不同取值,从sub_mats
中获取不同的矩阵。初始化了用户物品和关系的嵌入向量。在这里作者首先将用户和物品的嵌入拼接成ego_embeds,在将ego_embeds赋值给ego_embeds_sub1,2;和all_ego_embeds_sub1,2;这里应该对应行为内监督学习,将分为了两个视图(后续还有操作的)
for k in range(0, self.n_layers):
embeddings_list = []
for i in range(self.n_relations):
st = time()
embeddings_ = torch.matmul(self.pre_adjs_tensor[i], ego_embeddings[:, i, :])
total_mm_time += time() - st
rela_emb = all_rela_embs[self.behs[i]][k]
embeddings_ = self.leaky_relu(
torch.matmul(torch.mul(embeddings_, rela_emb), self.all_weights['W_gc_%d' % k]))
embeddings_list.append(embeddings_)
embeddings_st = torch.stack(embeddings_list, dim=1)
embeddings_list = []
attention_list = []
这部分代码首先初始化了一个空的embeddings_list
列表,用于存储每个行为对应的嵌入向量。然后,通过矩阵乘法计算节点的嵌入向量更新(预处理的邻接矩阵和ego_embeds),并将结果存储在embeddings_
中。接着,将embeddings_
通过LeakyReLU激活函数进行非线性变换。最后,将所有行为的嵌入向量拼接成一个张量embeddings_st
。对应公式如下:
for i in range(self.n_relations):
attention = F.softmax(
torch.matmul(
torch.tanh(torch.matmul(embeddings_st, self.all_weights['trans_weights_s1'][i])),
self.all_weights['trans_weights_s2'][i]
).squeeze(2),
dim=1
).unsqueeze(1)
attention_list.append(attention)
embs_cur_rela = torch.matmul(attention, embeddings_st).squeeze(1)
embeddings_list.append(embs_cur_rela)
embeddings_list.append(embs_cur_rela)
ego_embeddings = torch.stack(embeddings_list, dim=1)
attn = torch.cat(attention_list, dim=1)
ego_embeddings = self.dropout(ego_embeddings)
all_embeddings = all_embeddings + ego_embeddings
在这段代码中,首先通过矩阵乘法计算行为之间的注意力权重(建模跨行为依赖性)公式(2)中的Wb对应trans_weight,ek对应embeddings_st,并经过Softmax函数归一化得到权重au,k(attention list)。最后将attention, embeddings_st相乘,更新eu,k。然后将所有行为的嵌入向量拼接成一个张量ego_embeddings
,并进行dropout操作。最后,将更新后的嵌入向量加到all_embeddings
中。对应公式如下:(在之后作者使用相同的操作应用于两个扩充视图ego_embeds_sub1,2,all_ego_embeds_sub1,2)
all_embeddings /= self.n_layers + 1
u_g_embeddings, i_g_embeddings = torch.split(all_embeddings, [self.n_users, self.n_items], 0)
token_embedding = torch.zeros([1, self.n_relations, self.emb_dim], device=device)
i_g_embeddings = torch.cat((i_g_embeddings, token_embedding), dim=0)
all_embeddings_sub1 /= self.n_layers + 1
u_g_embeddings_sub1, i_g_embeddings_sub1 = torch.split(all_embeddings_sub1, [self.n_users, self.n_items], 0)
i_g_embeddings_sub1 = torch.cat((i_g_embeddings_sub1, token_embedding), dim=0)
all_embeddings_sub2 /= self.n_layers + 1
u_g_embeddings_sub2, i_g_embeddings_sub2 = torch.split(all_embeddings_sub2, [self.n_users, self.n_items], 0)
i_g_embeddings_sub2 = torch.cat((i_g_embeddings_sub2, token_embedding), dim=0)
attn_user, attn_item = torch.split(attn, [self.n_users, self.n_items], 0)
for i in range(self.n_relations):
all_rela_embs[self.behs[i]] = torch.mean(torch.stack(all_rela_embs[self.behs[i]], 0), 0)
return u_g_embeddings, i_g_embeddings, u_g_embeddings_sub1, i_g_embeddings_sub1, u_g_embeddings_sub2, i_g_embeddings_sub2, all_rela_embs, attn_user, attn_item
最后对应于一个主视图和两个扩充视图进行相同的特征分割拼接的工作,将all_embeddings分割为u_g_embeddings, i_g_embeddings,并mean pool集成GCN所有层的嵌入,对于公式如下:
行为内自监督学习
class SSLoss(nn.Module):
def __init__(self, data_config, args):
super(SSLoss, self).__init__()
self.ssl_temp = args.ssl_temp
self.ssl_reg = args.ssl_reg
self.ssl_mode = args.ssl_mode
ssl_temp
(温度参数,用于调整相似度分数的分布)、ssl_reg
(对比学习损失的正则化系数)和 ssl_mode
(对比学习的模式,用户or物品)下面我只介绍用户的,因为物品的是一样的
def forward(self, input_u_list, input_i_list, ua_embeddings_sub1, ua_embeddings_sub2, ia_embeddings_sub1,
ia_embeddings_sub2):
if self.ssl_mode in ['user_side', 'both_side']:
user_emb1 = ua_embeddings_sub1[input_u_list]
user_emb2 = ua_embeddings_sub2[input_u_list] # [B, dim]
normalize_user_emb1 = F.normalize(user_emb1, dim=1)
normalize_user_emb2 = F.normalize(user_emb2, dim=1)
normalize_all_user_emb2 = F.normalize(ua_embeddings_sub2, dim=1)
pos_score_user = torch.sum(torch.mul(normalize_user_emb1, normalize_user_emb2),
dim=1)
pos_score_user = torch.exp(pos_score_user / self.ssl_temp)
ttl_score_user = torch.matmul(normalize_user_emb1,
normalize_all_user_emb2.T)
ttl_score_user = torch.sum(torch.exp(ttl_score_user / self.ssl_temp), dim=1)
ssl_loss_user = -torch.sum(torch.log(pos_score_user / ttl_score_user))
输入用户列表 input_u_list
、输入的物品列表 input_i_list
,以及两个扩充子图的用户嵌入和物品嵌入 ua_embeddings_sub1, ua_embeddings_sub2, ia_embeddings_sub1, ia_embeddings_sub2
。
首先,对user_emb1
和 user_emb2
进行归一化操作得到normalize_user_emb1
和 normalize_user_emb2
。同时归一化了所有的 ua_embeddings_sub2
,得到 normalize_all_user_emb2
。接下来,计算正样本的相似度得分 pos_score_user
,通过对 normalize_user_emb1
和 normalize_user_emb2
进行逐元素相乘并求和得到。将 pos_score_user
除以 ssl_temp
并进行指数化。计算负样本的相似度得分 ttl_score_user
,通过对 normalize_user_emb1
和 normalize_all_user_emb2
(转置)进行矩阵乘法,并对结果进行逐元素指数化并求和得到。最后,计算用户对比学习损失 ssl_loss_user
,通过对正样本得分 pos_score_user
除以正样本得分得分 ttl_score_user
取对数并求和的负数。公式如下
if self.ssl_mode == 'user_side':
ssl_loss = self.ssl_reg * ssl_loss_user
elif self.ssl_mode == 'item_side':
ssl_loss = self.ssl_reg * ssl_loss_item
else:
ssl_loss = self.ssl_reg * (ssl_loss_user + ssl_loss_item)
return ssl_loss
用户端的损失+项目端的损失 ,公式里没写的就是行为内的对比学习还乘了非采样损失Lrec,问题不大
行为间自监督学习
lass SSLoss2(nn.Module):
def __init__(self, data_config, args):
super(SSLoss2, self).__init__()
self.config = data_config
self.ssl_temp = args.ssl_temp
self.ssl_reg_inter = eval(args.ssl_reg_inter)
self.ssl_mode_inter = args.ssl_inter_mode
self.topk1_user = args.topk1_user
self.topk1_item = args.topk1_item
self.user_indices_remove, self.item_indices_remove = None, None
ssl_temp
是温度参数,ssl_reg_inter
是一个字典,用于指定不同辅助行为的对比学习正则化权重,ssl_mode_inter
是指定对比学习模式的参数,可选值为user_side
、item_side
或both_sidetopk1_user
和topk1_item
是指定用户和物品推荐的Top-K值
def forward(self, input_u_list, input_i_list, ua_embeddings, ia_embeddings, aux_beh, user_batch_indices=None,
item_batch_indices=None):
ssl2_loss = 0.
if self.ssl_mode_inter in ['user_side', 'both_side']:
emb_tgt = ua_embeddings[input_u_list, -1, :] # [B, d]
normalize_emb_tgt = F.normalize(emb_tgt, dim=1)
emb_aux = ua_embeddings[input_u_list, aux_beh, :] # [B, d]
normalize_emb_aux = F.normalize(emb_aux, dim=1) # [B, dim]
normalize_all_emb_aux = F.normalize(ua_embeddings[:, aux_beh, :], dim=1) # [N, dim]
pos_score = torch.sum(torch.mul(normalize_emb_tgt, normalize_emb_aux),
dim=1) # [B, ]
ttl_score = torch.matmul(normalize_emb_tgt, normalize_all_emb_aux.T) # [B, N]
ttl_score[user_batch_indices] = 0.
pos_score = torch.exp(pos_score / self.ssl_temp)
ttl_score = torch.sum(torch.exp(ttl_score / self.ssl_temp), dim=1)
ssl2_loss += -torch.sum(torch.log(pos_score / ttl_score)) * self.ssl_reg_inter[aux_beh]
输入参数包括input_u_list
(用户输入列表)、input_i_list
(物品输入列表)、ua_embeddings
(用户嵌入向量)、ia_embeddings
(物品嵌入向量)、aux_beh
(辅助行为)、user_batch_indices
(用户批次索引)和item_batch_indices
(物品批次索引)。
从ua_embeddings
中获取目标用户嵌入向量emb_tgt和
辅助用户嵌入向量emb_aux
,并进行归一化操作得到normalize_emb_tgt,normalize_emb_aux
。正样本分数由目标行为和辅助行为的归一化嵌入向量相乘得到,负样本分数由目标行为和所有辅助行为的归一化嵌入的转置嵌入相乘得到,最后对正负样本分数应用温度参数进行指数化操作(增强正样本之间的相似度,从而使它们在相似度计算中占据更高的权重)得到行为间的对比损失。公式如下:
非采样损失Lrec
class RecLoss(nn.Module):
def __init__(self, data_config, args):
super(RecLoss, self).__init__()
self.behs = data_config['behs']
self.n_relations = len(self.behs)
self.n_users = data_config['n_users']
self.n_items = data_config['n_items']
self.emb_dim = args.embed_size
self.coefficient = eval(args.coefficient)
self.wid = eval(args.wid)
def forward(self, input_u, label_phs, ua_embeddings, ia_embeddings, rela_embeddings):
uid = ua_embeddings[input_u]
uid = torch.reshape(uid, (-1, self.n_relations, self.emb_dim))
pos_r_list = []
for i in range(self.n_relations):
beh = self.behs[i]
pos_beh = ia_embeddings[:, i, :][label_phs[i]] # [B, max_item, dim]
pos_num_beh = torch.ne(label_phs[i], self.n_items).float()
pos_beh = torch.einsum('ab,abc->abc', pos_num_beh,
pos_beh) # [B, max_item] * [B, max_item, dim] -> [B, max_item, dim]
pos_r = torch.einsum('ac,abc->abc', uid[:, i, :],
pos_beh) # [B, dim] * [B, max_item, dim] -> [B, max_item, dim]
pos_r = torch.einsum('ajk,lk->aj', pos_r, rela_embeddings[beh])
pos_r_list.append(pos_r)
loss = 0.
for i in range(self.n_relations):
beh = self.behs[i]
temp = torch.einsum('ab,ac->bc', ia_embeddings[:, i, :], ia_embeddings[:, i, :]) \
* torch.einsum('ab,ac->bc', uid[:, i, :], uid[:, i, :]) # [B, dim]' * [B, dim] -> [dim, dim]
tmp_loss = self.wid[i] * torch.sum(
temp * torch.matmul(rela_embeddings[beh].T, rela_embeddings[beh]))
tmp_loss += torch.sum((1.0 - self.wid[i]) * torch.square(pos_r_list[i]) - 2.0 * pos_r_list[i])
loss += self.coefficient[i] * tmp_loss
regularizer = torch.sum(torch.square(uid)) * 0.5 + torch.sum(torch.square(ia_embeddings)) * 0.5
emb_loss = args.decay * regularizer
return loss, emb_loss
一开始就是用户数量行为数量等。输入用户,物品,行为嵌入矩阵,在 ua_embeddings
中获取输入用户 input_u
的嵌入向量 uid
。然后,将 uid
进行形状重塑,变成一个三维的 (batch_size, n_relations, emb_dim)
遍历每个行为,计算正样本的嵌入向量 pos_r
。对于每个行为,首先从物品嵌入矩阵 ia_embeddings
中选取对应关系的物品嵌入向量 pos_beh
,索引为label_phs[i]
。然后,将物品嵌入进行加权,得到加权后的物品嵌入 pos_beh
。接下来,将用户嵌入 uid
和加权后的物品嵌入 pos_beh
进行逐元素相乘,得到 pos_r
。最后,将 pos_r
与关系嵌入 rela_embeddings[beh]
进行点积,得到最终的正样本嵌入向量 pos_r
。将所有的 pos_r
存储在列表 pos_r_list
中。
根据temp
矩阵 和关系嵌入计算 tmp_loss
。这里使用了点积运算 torch.matmul(rela_embeddings[beh].T, rela_embeddings[beh])
,将关系嵌入进行转置后与自身相乘,得到一个形状为 (emb_dim, emb_dim)
的矩阵。将temp
矩阵与点积结果相乘,并乘以权重 self.wid[i]
,得到tmp_loss
的第一部分。
接下来,计算tmp_loss
的第二部分。首先,将正样本嵌入向量 pos_r_list[i]
的平方项和两倍的负样本嵌入项进行计算。然后,将权重 乘以该结果,并累加到 tmp_loss
上。
最后,将 tmp_loss
乘以权重 self.coefficient[i]
,并累加到总损失 loss
上。最后,将正则化项乘以超参数 args.decay
,得到嵌入损失 emb_loss
模型损失函数计算如下: