• 推荐系统笔记(十三):SGL算法的代码实现


    前言

            本算法是在LightGCN的代码上的基础实现的,LightGCN的代码和原理参考我前面几篇的博客:推荐系统笔记(六):LightGCN代码实现_甘霖那的博客-CSDN博客

           由于LightGCN等传统基于图神经网络的算法有以下局限性:

            (1)高度节点对表征学习的影响更大,低度(长尾)节点的推荐效果更差;

            (2)表示容易受到噪声交互的影响,因为邻域聚合方案进一步扩大了观察到的边的影响。

            (3)目前大多数推荐学习任务都是基于监督学习的范式,其中监督信号一般指用户和物品的交互数据。然而这些交互数据通常来说是异常稀疏的,不足以学习高质量的表征。
           因此,将自监督学习(Self-supervised Learning, SSL)在用户-物品二部图上的应用,辅助推荐模型训练学习,应用self-discrimination来学习更加鲁棒的节点表征是必要的。

    代码讲解

    损失函数

            由于SGL中的损失函数由BPR+InfoNCE+Reg三部分组成,即:

            因此需要将损失函数该为如下形式:

    1. def BPR_InfoNCE_Reg_loss(self, S, emb, emb1,emb2, init_emb):
    2. S = np.array(S).astype('int') # [64,3]
    3. # print("S:{}".format(S.shape))
    4. all_user_emb, all_item_emb = torch.split(emb, [self.n_users, self.n_items])
    5. all_user_emb0, all_item_emb0 = torch.split(init_emb, [self.n_users, self.n_items])
    6. sub_all_user_emb1, sub_all_item_emb1 = torch.split(emb1, [self.n_users, self.n_items])
    7. sub_all_user_emb2, sub_all_item_emb2 = torch.split(emb2, [self.n_users, self.n_items])
    8. # print("all_user_emd:{}".format(all_user_emb.shape)) # [610,64]
    9. # print("all_item_emd:{}".format(all_item_emb.shape)) # [9724,64]
    10. pos_emb = all_item_emb[S[:, 1]] # [64,64]
    11. neg_emb = all_item_emb[S[:, 2]] # [64,64]
    12. user_emb = all_user_emb[S[:, 0]] # [64,64]
    13. # print("pos norm",torch.norm(pos_emb).item())
    14. # print("neg norm",torch.norm(neg_emb).item())
    15. # print("user norm",torch.norm(user_emb).item())
    16. # print(pos_emb.shape,neg_emb.shape,user_emb.shape)
    17. pos_emb0 = all_item_emb0[S[:, 1]]
    18. neg_emb0 = all_item_emb0[S[:, 2]]
    19. user_emb0 = all_user_emb0[S[:, 0]]
    20. user_embeddings1 = F.normalize(sub_all_user_emb1, dim=1) # [610,64]
    21. item_embeddings1 = F.normalize(sub_all_item_emb1, dim=1) # [9724,64]
    22. user_embeddings2 = F.normalize(sub_all_user_emb2, dim=1)
    23. item_embeddings2 = F.normalize(sub_all_item_emb2, dim=1)
    24. # print("user_embedding:{}".format(user_embeddings1.shape))
    25. # print("item_embedding:{}".format(item_embeddings1.shape))
    26. user_embs1 = user_embeddings1[S[:, 0]] # [64,64]
    27. item_embs1 = item_embeddings1[S[:, 1]] # [64,64]
    28. user_embs2 = user_embeddings2[S[:, 0]] # [64,64]
    29. item_embs2 = item_embeddings2[S[:, 1]] # [64,64]
    30. # print(user_embs1.shape,item_embs1.shape)
    31. similar_users = torch.sum(user_embs1*user_embs2,dim=-1)
    32. similar_items = torch.sum(item_embs1*item_embs2,dim=-1)
    33. total_user = torch.matmul(user_embs1,
    34. torch.transpose(user_embeddings2, 0, 1))
    35. total_item = torch.matmul(item_embs1,
    36. torch.transpose(item_embeddings2, 0, 1))
    37. # BPR Loss
    38. loss = (F.softplus(torch.sum(user_emb * neg_emb, dim=1) - torch.sum(user_emb * pos_emb, dim=1))).sum()
    39. loss=loss
    40. # print("begin:{}".format(loss))
    41. # InfoNCE Loss
    42. SGL_logits_user = total_user - similar_users[:, None]
    43. SGL_logits_item = total_item - similar_items[:, None]
    44. InfoLoss_user = torch.logsumexp(SGL_logits_user / self.temperature, dim=1)
    45. InfoLoss_item = torch.logsumexp(SGL_logits_item / self.temperature, dim=1)
    46. InfoNCE_loss = 0.005*torch.sum(InfoLoss_user + InfoLoss_item)
    47. loss += InfoNCE_loss
    48. # print(InfoNCE_loss)
    49. # Reg Loss
    50. loss += self.lamda * (
    51. torch.norm(pos_emb0) ** 2 + torch.norm(neg_emb0) ** 2 + torch.norm(user_emb0) ** 2) / float(
    52. len(pos_emb))
    53. # print(loss)
    54. return loss

            其中的参数emb、emb1、emb2分别是未被drop边或者点的原始图网络、和被随机drop后的网络经过forward后得到的表示编码结果。S是对一个batch的数据进行索引取出对应的数据进行训练。

     而又在论文中提出了三种不同的数据增强方法:

            Node Dropout(ND):图中每个节点都可能以概率 ρ 被舍弃,连带其连接的边一起舍弃。具体 s1 和 s2 如下建模:

            因为需要两个图才能进行对比,因此需要构建两个不同的子图,M’和M'’分别去drop掉数据。

            Edge Dropout(ED):图中每条边都可能以概率 ρ被舍弃。具体如下表示:

             Random Walk (RW):上述两个操作符生成的子图在图卷积的所有层中保持一致。而这里的RW是指的每一层的M'和M''都是不同的,即层与层之间的dropout不共享:

            上述操作符只有dropout和masking操作,没有增加任何模型参数。

    ND:随即删除点和对应的连边

    1. def create_ND(self,ratio,mat1):
    2. mat = mat1
    3. drop_user_idx = self.random_choice(self.n_users, int(self.n_users * ratio))
    4. drop_item_idx = self.random_choice(self.n_items, int(self.n_items * ratio))
    5. indicator_user = np.ones(self.n_users)
    6. indicator_item = np.ones(self.n_items)
    7. indicator_user[drop_user_idx] = 0
    8. indicator_item[drop_item_idx] = 0
    9. mask = diags(np.hstack((indicator_user, indicator_item)))
    10. mat=(mask @ mat @ mask)
    11. # print(mat)
    12. d_mat = mat.sum(axis=1)
    13. d_mat = np.sqrt(d_mat)
    14. d_mat = np.array(d_mat)
    15. d_mat = 1 / (d_mat.reshape(-1))
    16. d_mat[np.isinf(d_mat)]=0.
    17. d_mat = diags(d_mat)
    18. d_mat = d_mat.tocoo()
    19. final = (d_mat @ mat @ d_mat).tocoo()
    20. # print(final.shape)
    21. rows = torch.tensor(final.row)
    22. cols = torch.tensor(final.col)
    23. index = torch.cat([rows.reshape(1, -1), cols.reshape(1, -1)], dim=0)
    24. return torch.sparse_coo_tensor(index, torch.tensor(final.data)).to(self.device)

            传入要drop掉的比例和对应的邻接矩阵的表示,维度为(M+N,M+N),M代表用户个数,N代表物品个数。这里直接构建了两个mask矩阵,q是两个对角阵,唯独分别为M+N和N+M,对应要drop掉的用户的元素置为零,其他对角元素为,然后对应相乘即可实现随机drop掉点和边。

    ED和RW:随机删除边

    1. def create_ED_RW(self, count, ratio):
    2. row_arr = np.zeros(2 * count, dtype=np.int32)
    3. col_arr = np.zeros(2 * count, dtype=np.int32)
    4. data = np.ones(2 * count, dtype=np.int32)
    5. # 随机drop
    6. drop_data_index = self.random_choice(count, int(count * ratio))
    7. count = 0
    8. for key in self.train.keys():
    9. for value in self.train[key]:
    10. if count in drop_data_index:
    11. data[count]=0
    12. row_arr[count] = int(key)
    13. col_arr[count] = self.n_users + int(value)
    14. count += 1
    15. count1=count
    16. for key in self.train.keys():
    17. for value in self.train[key]:
    18. if count-count1 in drop_data_index:
    19. data[count]=0
    20. row_arr[count] = self.n_users + int(value)
    21. col_arr[count] = int(key)
    22. count += 1
    23. # print("row:{} colum:{}".format(len(row_arr),len(col_arr)))
    24. mat = coo_matrix((data, (row_arr, col_arr)),shape=(self.n_users+self.n_items,self.n_users+self.n_items))
    25. d_mat = mat.sum(axis=1)
    26. d_mat = np.sqrt(d_mat)
    27. d_mat = np.array(d_mat)
    28. d_mat = 1 / (d_mat.reshape(-1))
    29. d_mat[np.isinf(d_mat)] = 0.
    30. d_mat = diags(d_mat)
    31. d_mat = d_mat.tocoo()
    32. final = (d_mat @ mat @ d_mat).tocoo()
    33. # print(final.shape)
    34. rows = torch.tensor(final.row)
    35. cols = torch.tensor(final.col)
    36. # print(rows.reshape(1, -1).shape,cols.reshape(1, -1).shape)
    37. index = torch.cat([rows.reshape(1, -1), cols.reshape(1, -1)], dim=0)
    38. return torch.sparse_coo_tensor(index, torch.tensor(final.data)).to(self.device)

            随即将coo_matrix的data置为零即可实现边的drop,值得注意的是RW是每一层的layer都不同,因此需要循环多次实现:

            对应的在forward的步骤中也要设置多次循环传播,并且取结果平均值:

    1. # print(emb.shape)
    2. for i in range(stages):
    3. if isinstance(mat, list):
    4. # print(emb.shape,mat[i].shape)
    5. emb = torch.sparse.mm(mat[i], emb)
    6. emb_list.append(emb)
    7. # print("emb norm", torch.norm(emb).item())
    8. else:
    9. emb = torch.sparse.mm(mat, emb)
    10. emb_list.append(emb)

             其他评估函数的不变或者变化不大,这里不再做讲解。

    运行文件:train.py

    1. from SGL import SGL
    2. import torch
    3. device = torch.device('cuda') if torch.cuda.is_available() else torch.device('cpu')
    4. print('device:',device)
    5. model = SGL('Movielens/ml-latest-small/train.txt', lr = 1e-3,device = device,stages=3,augmentation='ED',ratio=0.1)
    6. model.to(device)
    7. model.load_test_data('Movielens/ml-latest-small/test.txt')
    8. # model.load_test_data('yelp2018/test.txt')
    9. # model.load_test_data('gowalla/test.txt')
    10. model.train_model(stages=3,logger_path='ED_result.pkl')
    11. model.evaluate(20)
    12. model1 = SGL('Movielens/ml-latest-small/train.txt', lr = 1e-3,device = device,stages=3,augmentation='ND',ratio=0.1)
    13. model1.to(device)
    14. model1.load_test_data('Movielens/ml-latest-small/test.txt')
    15. model1.train_model(stages=3,logger_path='ND_result.pkl')
    16. model1.evaluate(20)
    17. model1 = SGL('Movielens/ml-latest-small/train.txt', lr = 1e-3,device = device,stages=3,augmentation='ND',ratio=0.1)
    18. model1.to(device)
    19. model1.load_test_data('Movielens/ml-latest-small/test.txt')
    20. model1.train_model(stages=3,logger_path='RW_result.pkl')
    21. model1.evaluate(20)

            分别测试三种随机数据增强的方式的结果,并且保存到对应的pkl文件中以供数据可视化。

    运行结果展示:

     

  • 相关阅读:
    《最新出炉》系列入门篇-Python+Playwright自动化测试-48-Route类拦截修改请求-上篇
    nginx配置新的SSL证书后浏览器仍显示之前的旧SSL证书
    文娱行业搜索最佳实践
    MyBatis-Plus-入门操作(1)
    电脑文件误删除如何恢复?
    数据库1= =
    后端开发怎么做得更优秀?记住这15个好习惯
    WIFI6 TWT机制介绍
    面试官:Dubbo一次RPC请求经历哪些环节?
    C#的Console 类使用说明
  • 原文地址:https://blog.csdn.net/qq_46006468/article/details/126147815