如果觉得我的分享有一定帮助,欢迎关注我的微信公众号 “码农的科研笔记”,了解更多我的算法和代码学习总结记录。或者点击链接扫码关注【RecBole-GNN/源码】RecBole-GNN中NCL源码解析
【RecBole-GNN/源码】RecBole-GNN中NCL源码解析
原文:https://arxiv.org/abs/2202.06200
源码:https://github.com/rucaibox/ncl
1 数据
方法开始经过数据处理等进入 ncl.py
的 calculate_loss(self, interaction)
方法。interaction数据形式如下
首先获得交互正负样本对数据
#根据交互interaction数据,获得交互正对和负对
user = interaction[self.USER_ID] #2048*1
pos_item = interaction[self.ITEM_ID] #2048*1
neg_item = interaction[self.NEG_ITEM_ID] #2048*1
2 GCN嵌入表示
利用GCN进行前向传播得到user和item的嵌入表示
#获得所有节点(user和item)的嵌入表示
all_embeddings = self.get_ego_embeddings() #9748*64
embeddings_list = [all_embeddings]
for layer_idx in range(max(self.n_layers, self.hyper_layers * 2)): #max(3,2)
#采用了LightGCNConv的方式
all_embeddings = self.gcn_conv(all_embeddings, self.edge_index, self.edge_weight) # 9748*64/2*1610886/1610886*1
embeddings_list.append(all_embeddings)
#得到的embeddings_list是4个9748*64.
lightgcn_all_embeddings = torch.stack(embeddings_list[:self.n_layers + 1], dim=1)
lightgcn_all_embeddings = torch.mean(lightgcn_all_embeddings, dim=1)
user_all_embeddings, item_all_embeddings = torch.split(lightgcn_all_embeddings, [self.n_users, self.n_items])
3 Loss计算
#embeddings_list是4个9748*64.
center_embedding = embeddings_list[0] #得到初始的embedding,9748*64
context_embedding = embeddings_list[self.hyper_layers * 2] #得到第3个embedding,9748*64
#基于SSL(结构)得到loss (偶数跳的作为邻居正例),user:2048*1,pos_item:2048*1
ssl_loss = self.ssl_layer_loss(context_embedding, center_embedding, user, pos_item)
#基于语义计算loss,
proto_loss = self.ProtoNCE_loss(center_embedding, user, pos_item)
u_embeddings = user_all_embeddings[user]
pos_embeddings = item_all_embeddings[pos_item]
neg_embeddings = item_all_embeddings[neg_item]
# calculate BPR Loss
pos_scores = torch.mul(u_embeddings, pos_embeddings).sum(dim=1)
neg_scores = torch.mul(u_embeddings, neg_embeddings).sum(dim=1)
mf_loss = self.mf_loss(pos_scores, neg_scores)
u_ego_embeddings = self.user_embedding(user)
pos_ego_embeddings = self.item_embedding(pos_item)
neg_ego_embeddings = self.item_embedding(neg_item)
reg_loss = self.reg_loss(u_ego_embeddings, pos_ego_embeddings, neg_ego_embeddings)
3.1【ssl_layer_loss方法介绍】
该代码首先从当前嵌入中提取用户和商品的表征。然后,将用户和商品的表征分别与基于GCN的第二跳表征进行匹配,计算其相似度。接着,使用softmax函数将所有商品与用户的相似度加权平均,得到用户对所有商品的兴趣得分。然后,对每个商品,同样计算其与当前和基于GCN的第二跳表征之间的相似度,并使用softmax函数将其与所有商品的相似度加权平均,得到商品的受欢迎度得分。
接下来,将用户和商品的得分分别用作分子和分母,计算用户和商品的损失。最后,将两个损失加权相加,并乘以一个正则化参数,得到最终的ssl损失。
注意:context_embedding, center_embedding是包含用户和item的embedding。
def ssl_layer_loss(self, current_embedding, previous_embedding, user, item):
#user和item表征分开
current_user_embeddings, current_item_embeddings = torch.split(current_embedding, [self.n_users, self.n_items])
previous_user_embeddings_all, previous_item_embeddings_all = torch.split(previous_embedding, [self.n_users, self.n_items])
###################
#获取当前user对应embedding
current_user_embeddings = current_user_embeddings[user]
previous_user_embeddings = previous_user_embeddings_all[user]
#将这两个表征进行归一化,以保证它们具有相同的尺度,并计算它们的点积作为相似度得分。
norm_user_emb1 = F.normalize(current_user_embeddings)
norm_user_emb2 = F.normalize(previous_user_embeddings)
norm_all_user_emb = F.normalize(previous_user_embeddings_all)
pos_score_user = torch.mul(norm_user_emb1, norm_user_emb2).sum(dim=1)
#计算用户对所有用户的得分。transpose(0, 1)表示对norm_all_user_emb的第0维和第1维进行转置操作,即将所有用户的表征转置,以便与当前用户的表征进行矩阵乘法。乘积的结果是一个大小为(n_items, 1)的向量,表示当前用户对所有所有用户的得分。
ttl_score_user = torch.matmul(norm_user_emb1, norm_all_user_emb.transpose(0, 1))
#self.ssl_temp=0.1
pos_score_user = torch.exp(pos_score_user / self.ssl_temp)
ttl_score_user = torch.exp(ttl_score_user / self.ssl_temp).sum(dim=1)
#
ssl_loss_user = -torch.log(pos_score_user / ttl_score_user).sum()
####################
#同理计算
current_item_embeddings = current_item_embeddings[item]
previous_item_embeddings = previous_item_embeddings_all[item]
norm_item_emb1 = F.normalize(current_item_embeddings)
norm_item_emb2 = F.normalize(previous_item_embeddings)
norm_all_item_emb = F.normalize(previous_item_embeddings_all)
pos_score_item = torch.mul(norm_item_emb1, norm_item_emb2).sum(dim=1)
ttl_score_item = torch.matmul(norm_item_emb1, norm_all_item_emb.transpose(0, 1))
pos_score_item = torch.exp(pos_score_item / self.ssl_temp)
ttl_score_item = torch.exp(ttl_score_item / self.ssl_temp).sum(dim=1)
ssl_loss_item = -torch.log(pos_score_item / ttl_score_item).sum()
ssl_loss = self.ssl_reg * (ssl_loss_user + self.alpha * ssl_loss_item)
return ssl_loss
3.2【ProtoNCE_loss方法介绍】
给定一个节点嵌入向量 node_embedding,以及一个用户 ID user 和一个物品 ID item,该函数会使用这些输入计算出用户和物品的 proto-contrastive loss。proto-contrastive loss 是一个用于无监督学习的损失函数,用于在嵌入空间中学习出具有相似性质的节点之间的距离,并且可以通过聚类算法来获取节点的簇标签。
- 这个函数首先将 node_embedding 分成两部分,分别对应于所有用户和所有物品的嵌入向量 user_embeddings_all 和 item_embeddings_all。
- 然后,它会从 user_embeddings_all 中选择 user 对应的嵌入向量 user_embeddings,并对其进行归一化处理,得到 norm_user_embeddings。接着,函数会获取 user 对应的簇标签 user2cluster,并使用它来获取对应的簇中心 user2centroids。然后,函数会计算出用户与其所属簇中心的内积,得到 pos_score_user。pos_score_user 会经过指数函数处理,并除以一个温度参数 self.ssl_temp。接着,函数会计算用户与所有簇中心的内积,得到 ttl_score_user。ttl_score_user 也会经过指数函数处理,并按行求和。然后,函数会使用 pos_score_user 和 ttl_score_user 来计算用户的 proto-contrastive loss,将其取负数并求和。
- 函数接下来会处理物品的嵌入向量 item_embeddings_all,方法与处理用户的嵌入向量类似,得到 pos_score_item 和 ttl_score_item,并计算出物品的 proto-contrastive loss。
- 最后,函数会将用户和物品的 proto-contrastive loss 加权求和,得到最终的 proto-contrastive loss。这个加权系数由参数 proto_reg 决定。函数返回最终的 proto-contrastive loss。
def ProtoNCE_loss(self, node_embedding, user, item):
user_embeddings_all, item_embeddings_all = torch.split(node_embedding, [self.n_users, self.n_items])
user_embeddings = user_embeddings_all[user] # [B, e]
norm_user_embeddings = F.normalize(user_embeddings)
user2cluster = self.user_2cluster[user] # [B,]
user2centroids = self.user_centroids[user2cluster] # [B, e]
pos_score_user = torch.mul(norm_user_embeddings, user2centroids).sum(dim=1)
pos_score_user = torch.exp(pos_score_user / self.ssl_temp)
ttl_score_user = torch.matmul(norm_user_embeddings, self.user_centroids.transpose(0, 1))
ttl_score_user = torch.exp(ttl_score_user / self.ssl_temp).sum(dim=1)
proto_nce_loss_user = -torch.log(pos_score_user / ttl_score_user).sum()
item_embeddings = item_embeddings_all[item]
norm_item_embeddings = F.normalize(item_embeddings)
item2cluster = self.item_2cluster[item] # [B, ]
item2centroids = self.item_centroids[item2cluster] # [B, e]
pos_score_item = torch.mul(norm_item_embeddings, item2centroids).sum(dim=1)
pos_score_item = torch.exp(pos_score_item / self.ssl_temp)
ttl_score_item = torch.matmul(norm_item_embeddings, self.item_centroids.transpose(0, 1))
ttl_score_item = torch.exp(ttl_score_item / self.ssl_temp).sum(dim=1)
proto_nce_loss_item = -torch.log(pos_score_item / ttl_score_item).sum()
proto_nce_loss = self.proto_reg * (proto_nce_loss_user + proto_nce_loss_item)
return proto_nce_loss
3.3【簇标签以及簇中心】
在每个epoch的时候都会开始进行self.model.e_step()(Running E-step )
def e_step(self):
user_embeddings = self.user_embedding.weight.detach().cpu().numpy() #
item_embeddings = self.item_embedding.weight.detach().cpu().numpy()
#self.user_centroids:1000*64 ,self.user_2cluster:6041*1
#self.item_centroids:1000*64, self.item_2cluster:3707*1
# 给予了user对应的簇,以及簇中心
self.user_centroids, self.user_2cluster = self.run_kmeans(user_embeddings)
self.item_centroids, self.item_2cluster = self.run_kmeans(item_embeddings)
# self.k=1000
def run_kmeans(self, x):
"""Run K-means algorithm to get k clusters of the input tensor x
"""
import faiss
kmeans = faiss.Kmeans(d=self.latent_dim, k=self.k, gpu=True)
kmeans.train(x)
cluster_cents = kmeans.centroids
_, I = kmeans.index.search(x, 1)
# convert to cuda Tensors for broadcast
centroids = torch.Tensor(cluster_cents).to(self.device)
centroids = F.normalize(centroids, p=2, dim=1)
node2cluster = torch.LongTensor(I).squeeze().to(self.device)
return centroids, node2cluster