Federated Social Recommendation with Graph Neural Network

基于图神经网络的联合社交推荐

ACM-TIST CCF_B类
论文链接
代码地址

模型中梯度和embedding的聚合

在FeSog中,Server端维护一个整体的model,由于这里的model层网络和GraphAttentionLayer层网络中一共有10个要更新参数,所以当每次server端将当前的model分发到各个user后,各个user利用本地的的评分数据来获得要更新的梯度,然后加到整体的梯度里面,这个梯度也有一个权重,那就是user交互的所有的项目的数量。利用这个来实现聚合每个客户端对于整体server端梯度的更新。
对于embedding的聚合则是server端维护一个大的item_embeddinguser_embedding,item_embedding的向量维度是 1957 × 8 1957 \times 8 1957×8,user_embedding的向量维度是 874 × 8 874 \times 8 874×8
embedding分发到各个user端之后,user端会先取出于当前的用户有关的社交关系和有关的项目相关的嵌入向量。

    def user_embedding(self, embedding):
        return embedding[torch.tensor(self.neighbors)], embedding[torch.tensor(self.id_self)]
    def item_embedding(self, embedding):
        return embedding[torch.tensor(self.items)]

最后返回给server端的数据更新也只是基于user
端自己拥有的那些数据相应的用户和项目的embedding的更新。


当你创建一个PyTorch实例并调用它的方法时,你传递的参数实际上是传给了模型内部的forward函数。这是因为在pytorch中,当你调用一个模型实例,实际上是在调用它的forward方法。


对于GraphAttentionLayer中的代码解释
尤其是这里使用了注意力机制,通过对代码详细分析解释什么是注意力机制。

 def forward(self, h, adj):
        W_h = torch.matmul(h, self.W)
        print(h,W_h,self.W)
        W_adj = torch.mm(adj, self.W)
        a_input = torch.cat((W_h.repeat(W_adj.shape[0], 1), W_adj), dim = 1)
        attention = self.leakyrelu(torch.matmul(a_input, self.a)).squeeze(-1)
        attention = F.softmax(attention, dim = -1)
        W_adj_transform = torch.mm(adj, self.W_1)
        h = torch.matmul(attention, W_adj_transform)
        return h

这里在model.py中传给forward的参数是

 f_n = self.GAT_neighbor(feature_self, feature_neighbor)
 f_i = self.GAT_item(feature_self, feature_item)

我们以feature_selffeature_neighbor为例分析

  W_h = torch.matmul(h, self.W)

这里第一步对h进行一个线性变换,变换后的矩阵和原矩阵有相同的形状。
进行线性变换的目的是在一个新的坐标系统中,数据的某些性质(比如类别间的区分度)能够被模型更容易地识别和学习。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

 W_adj = torch.mm(adj, self.W)

这行代码和前面的道理是一致的。

 a_input = torch.cat((W_h.repeat(W_adj.shape[0], 1), W_adj), dim = 1)
 attention = self.leakyrelu(torch.matmul(a_input, self.a)).squeeze(-1)
 attention = F.softmax(attention, dim = -1)

这里W_h.repeat是为了让W_h按照行多复制几遍,让每一行W_hW_adj能按行横向拼接起来,横向拼接起来这个向量对可以代表一个user-user或者user-item对。接着下面attention代码可以对每一个向量对分配不同的权重,最终attention对的向量形式应该是(m*1)其中m是有多少个这样的向量对。然后使用非线性激活函数self.leakyrelu非线性激活函数处理这些注意力分数并帮助稳定梯度下降过程。接下来,使用 softmax 函数对这些分数进行标准化,确保所有注意力系数加起来等于 1。这样,每个分数就变成了一个概率值,代表相应节点对的相对重要性。最后将注意力分数和W_adj_transform相乘来对邻居或者项目的特征向量进行加权得到一个新的特征向量。
在这里插入图片描述
在这里插入图片描述
图神经网络中节点用向量可以表示,边也可以用向量表示。

   if type(feature_item) == torch.Tensor:
            f_n = self.GAT_neighbor(feature_self, feature_neighbor)
            f_i = self.GAT_item(feature_self, feature_item)
            e_n = torch.matmul(self.c, torch.cat((f_n, self.relation_neighbor)))
            e_i = torch.matmul(self.c, torch.cat((f_i, self.relation_item)))
            e_s = torch.matmul(self.c, torch.cat((feature_self, self.relation_self)))
            m = nn.Softmax(dim = -1)
            e_tensor = torch.stack([e_n, e_i, e_s])
            e_tensor = m(e_tensor)
            r_n, r_i, r_s = e_tensor
            user_embedding = r_s * feature_self + r_n * f_n + r_i * f_i

下面这三行代码

 e_n = torch.matmul(self.c, torch.cat((f_n, self.relation_neighbor)))
            e_i = torch.matmul(self.c, torch.cat((f_i, self.relation_item)))
            e_s = torch.matmul(self.c, torch.cat((feature_self, self.relation_self)))

这3个代码考虑了用户的节点特征和用户和节点之间的关系特征。
在这里插入图片描述
这里的torch.matmul()执行向量的点积计算出来的e_n,e_i,e_s。这是哪个值分别表示了用户-邻居用户-项目,用户-自身三种关系的重要程度。
在这里插入图片描述
每种关系的重要程度是不同的。
在这里插入图片描述
FeSog这个模型框架中,存在两种注意力机制,一种是计算每种关系中具体哪个用户对哪个邻居或者哪个用户对哪个项目更重要的注意力,还有一种是用户与用户的关系,用户与项目的关系,以及用户自身的关系这三种关系哪个对于求解最后的向量表示更重要的注意力关系。
在这里插入图片描述

推荐系统中节点自身到自身的连接表示什么?
在这里插入图片描述

论文中使用的数据集介绍

论文一共使用了ciao,epinions,filmtrust三个数据集。
filmtrust数据集中有以下几个关系。
train_data,valid_data,test_data,user_id_list,item_id_list,social
其中train_data,valid_data,test_data三者是含有874个元素的字典。其中每个字典的键是用户的id,每一个键对应一个三元组,三元组第一位表示item编号,第二位表示useritem的rate分数。第三位没啥用。在这里插入图片描述
film数据集中有

  • 1957个item
  • 874个user
    user_id_list中有874个user编号,item_id_list有1957个item编号。
    social中是每个用户的社交关系。
    rating的评分是1到8,1表示最不喜欢,8表示最喜欢。
    在这里插入图片描述

epinions数据集中有18069Users,有261246个Items,有762938个社交关系。其中数据的组织形式和前面介绍的Filmtrust数据集差不多,都包含6个组成部分。
Ciao数据集中有7317个Users,有104975个Items,包含了283320个打分。
这两个数据集

LeakyRelu激活函数

LeakyReLU(Leaky Rectified Linear Unit)是ReLU(Rectified Linear Unit)激活函数的一个变种,用于解决ReLU激活函数中的所谓“死神经元”问题。以下是LeakyReLU的主要特点和工作原理。
在这里插入图片描述在这里插入图片描述

论文分析

FeSoG的提出的三个主要创新来解决的三个主要问题

  • 数据的异构性
    • 本地GNN的关系注意力和聚合区分了社交邻居和项目邻居
  • 本地建模的个性化要求
    • 本地用户嵌入推理为客户端保留个性化信息
  • 通信的隐私保护
    • 伪项标记以及动态LDP技术保护梯度

代码分析

Pytorch常用代码分析

model.eval():调用后,模型进入评估模式,在这个模式下,所有的训练特有的行为会被禁用。比如 Dropout 层会停止工作,即不会丢弃任何激活,而是会传递所有的输入.BatchNorm 层会使用在训练阶段计算得到的运行均值和方差来标准化数据,而不是当前批次的均值和方差。
评估模式的用途在于,当你需要进行模型验证、测试或实际预测时,你通常需要模型表现出稳定的行为,不受训练过程中某些随机性的影响。因此,eval() 模式确保了模型的前向传播是确定性的,并且某些层(如 Dropout 和 BatchNorm)的行为也是确定性的。

model.train():将模型设置为训练模式。在这种模式下,模型会正常更新权重,且某些层(如DropoutBatchNorm等)会按照训练时的行为运行。model.eval():将模型设置为评估模式。在这种模式下,所有的训练特定层(如Dropout、BatchNorm等)会设置为评估状态,不会进行权重更新,也不会进行梯度计算。


clip=0.3表示梯度裁剪的阈值设定为0.3。梯度裁剪是一种针对梯度爆炸问题的常用技术,它通过设定一个阈值来限制梯度的最大值,保持梯度在一个合理的范围内。
具体到这个值(0.3):

  • 当计算得到的梯度的绝对值超过0.3时,这些梯度将被缩放到不超过0.3。
  • 梯度的方向保持不变,只是大小被限制在指定的范围内。

self.model = copy.deepcopy(global_model)这个代码可以生成global_model的深拷贝,这样修改新的model不会改变原来的model。否则,修改客户端的model会把服务器端的model也给修改了。


PyTorch中,.detach() 方法被用于将一个变量从当前的计算图中分离出来。当你调用 .detach() 后,原变量所得到的新变量将不会在其上进行梯度计算,也就是说它不会在反向传播中被跟踪。这通常用于冻结某些层的参数或在评估模型时防止梯度计算。在代码 self.user_feature = user_feature.detach() 中,user_feature.detach() 创建了 user_feature 的一个副本,该副本与原计算图无关,这意味着对 self.user_feature 的任何操作都不会影响到原本的梯度计算。这样做通常是为了避免在反向传播时计算那些不应该或不需要计算梯度的变量。


torch.clone().detach()的含义
torch.clone(embedding_user).detach()首先创建embedding_user的一个副本,然后通过.detach()将其从当前计算图中分离出来,使得副本不会参与梯度计算。这样做通常是为了在执行操作时不影响原始Tensor的梯度。


torch.clone().detach()copy.deepcopy()有什么区别?
copy.deepcopy()是Python标准库中的一个函数,它会递归地复制Python对象,包括对象内部嵌套的对象。在PyTorch中,copy.deepcopy()也可以用来复制一个模型或Tensor,但它不仅复制了数据还包括了Tensor的所有历史和计算图。如果对一个Tensor使用copy.deepcopy(),则得到的副本仍然会保留梯度计算的历史。
简而言之,torch.clone().detach()用于创建一个无梯度的Tensor副本,适用于PyTorch中的Tensor对象;copy.deepcopy()用于创建一个有梯度历史的深层副本,适用于包括PyTorch模型在内的任意Python对象。


share_memory_()PyTorch中的一个函数,它用于将Tensor数据从共享内存中共享给其他进程。这对于多进程并行计算非常有用,可以提高数据传输的效率。通过使用share_memory_(),多个进程可以直接访问相同的Tensor数据,而无需进行数据的复制或传输。

self.model.parameters
PyTorch中,self.model.parameters() 是一个方法,用于获取模型self.model中所有的可训练参数。这个方法返回一个迭代器,遍历这个迭代器可以访问模型中定义的所有参数。这些参数通常包括神经网络层的权重和偏置等。以下是一些关键点:
直接使用print输出self.model.parameters()会输出一个封装对象,要想输出里面的内容,可以使用
print(list(self.model.parameters()))
PyTorch中,要获取每个参数的名字,你可以使用 model.named_parameters() 方法,这将返回一个生成器,生成的每个元素都是参数的名字和对应的张量。

for name, param in self.model.named_parameters():
    print(name, param.size())

self.model.named_parameters()中的参数数量是和model类中定义的需要被更新的参数数量一致。
以这个代码的model类为例:
model类里面有GraphAttentionLayer类的代码:

class model(nn.Module):
    def __init__(self, embed_size):
        super().__init__()
        self.GAT_neighbor = GraphAttentionLayer(embed_size, embed_size)
        self.GAT_item = GraphAttentionLayer(embed_size, embed_size)
        self.relation_neighbor = nn.Parameter(torch.randn(embed_size))
        self.relation_item = nn.Parameter(torch.randn(embed_size))
        self.relation_self = nn.Parameter(torch.randn(embed_size))
        self.c = nn.Parameter(torch.randn(2 * embed_size))
class GraphAttentionLayer(nn.Module):
    def __init__(self, in_features, out_features, alpha = 0.1):
        super().__init__()
        self.in_features = in_features
        self.out_features = out_features
        self.alpha = alpha
        self.W = nn.Parameter(torch.empty(size = (in_features, out_features)))
        nn.init.xavier_uniform_(self.W.data)
        self.a = nn.Parameter(torch.empty(size = (2 * out_features, 1)))
        nn.init.xavier_uniform_(self.a.data)
        self.W_1 = nn.Parameter(torch.randn(in_features, out_features))
        self.leakyrelu = nn.LeakyReLU(self.alpha)

首先分析GraphAttentionLayer类里面有哪些训练参数(初始化定义为nn.paramameter的为训练参数):
self.W,self.a,self.W_1
self.GAT_item 和 self.GAT_neighbor每一个都有三个训练参数,两个有六个训练参数
model类接着定义了self.relation_neighbor,self.relation_item,self.relation_self,self.c四个参数,所以self.model.named_parameters()里面一共有3*2+4=10个训练参数。
在pycharm的控制台中打印model.parameters()参可以验证一共有10个训练参数。
在这里插入图片描述


torch.matmul和torch.mm的区别
在这里插入图片描述在这里插入图片描述

PyTorch中,param.grad存储的是param的梯度消息,这些梯度消息用于优化param.data,即参数的实际值。


PyTorch中,param是一个torch.nn.Parameter 对象,它是torch.Tensor的一个子类。因此,你可以调用所有适用于torch.Tensor的方法和属性。下面是一些常用的参数和方法:
param.data:访问数据
param.dtype:参数的数据类型
param.size()或者param.shape:参数的形状
param.grad:参数的梯度
param.requires_grad()一个参数值表示是否需要计算这个参数的梯度。
param.device参数所在的设备。
param.zero_()将参数中的所有元素置0
param.copy_()复制另一个张量的数据到这个参数中。

GAT.py
import numpy as np
import torch
import torch.nn as nn
import torch.nn.functional as F
import pdb

class GraphAttentionLayer(nn.Module):
    def __init__(self, in_features, out_features, alpha = 0.1):
        super().__init__()
        self.in_features = in_features
        self.out_features = out_features
        self.alpha = alpha

        self.W = nn.Parameter(torch.empty(size = (in_features, out_features)))
        nn.init.xavier_uniform_(self.W.data)
        self.a = nn.Parameter(torch.empty(size = (2 * out_features, 1)))
        nn.init.xavier_uniform_(self.a.data)
        self.W_1 = nn.Parameter(torch.randn(in_features, out_features))
        self.leakyrelu = nn.LeakyReLU(self.alpha)

    def forward(self, h, adj):
        W_h = torch.matmul(h, self.W)
        W_adj = torch.mm(adj, self.W)
        a_input = torch.cat((W_h.repeat(W_adj.shape[0], 1), W_adj), dim = 1)
        attention = self.leakyrelu(torch.matmul(a_input, self.a)).squeeze(-1)

        attention = F.softmax(attention, dim = -1)
        W_adj_transform = torch.mm(adj, self.W_1)
        h = torch.matmul(attention, W_adj_transform)
        return h

一步步解读GAT.py
首先,我们导入了必要的Python库:
numpy 是一个科学计算库。
torchPyTorch库,一个流行的深度学习库。
torch.nntorch.nn.functionalPyTorch中用于构建网络层的模块。
pdbPython的调试器。

import numpy as np
import torch
import torch.nn as nn
import torch.nn.functional as F
import pdb

定义了一个类 GraphAttentionLayer,它继承自 nn.Module,这是所有神经网络模块的基类。

class GraphAttentionLayer(nn.Module):

在类的初始化函数中,我们有三个参数:in_features 指输入特征的维数,out_features 指输出特征的维数,alpha 是用于LeakyReLU激活函数的负斜率。

def __init__(self, in_features, out_features, alpha = 0.1):

这里定义了一个权重矩阵 W 并用Xavier均匀初始化方法进行初始化。nn.Parameter 表示这是一个模型可学习的参数。

self.W = nn.Parameter(torch.empty(size = (in_features, out_features)))
nn.init.xavier_uniform_(self.W.data)`在这里插入代码片`

定义了另一个可学习的参数 a,它是用于计算注意力系数的向量,并且也使用Xavier均匀初始化。

self.a = nn.Parameter(torch.empty(size = (2 * out_features, 1)))
nn.init.xavier_uniform_(self.a.data)

Xavier均匀初始化
在这里插入图片描述
定义了另一个可学习的权重矩阵 W 1 W_1 W1,使用正态分布进行初始化。

self.W_1 = nn.Parameter(torch.randn(in_features, out_features))

定义了LeakyReLU激活函数,并设置其负斜率。

self.leakyrelu = nn.LeakyReLU(self.alpha)

定义了 forward 函数,这是模型的前向传播函数。它接收输入特征 h 和邻接矩阵 adj

def forward(self, h, adj):

这里,输入特征通过权重矩阵 W 转换,并将结果存储在 W h W_h Wh 中。同样,邻接矩阵与权重矩阵 W 相乘,结果存储在 W a d j W_{adj} Wadj 中。

W_h = torch.matmul(h, self.W)
W_adj = torch.mm(adj, self.W)

将转换后的特征 W h W_h Wh W a d j W_{adj} Wadj 拼接起来,作为注意力机制的输入。

a_input = torch.cat((W_h.repeat(W_adj.shape[0], 1), W_adj), dim = 1)

计算注意力系数,应用LeakyReLU激活函数,并去掉最后一个维度。
LeakyReLU函数
在这里插入图片描述

attention = self.leakyrelu(torch.matmul(a_input, self.a)).squeeze(-1)

应用softmax函数归一化注意力系数。

attention = F.softmax(attention, dim = -1)

将邻接矩阵与权重 W 1 W_1 W1 相乘得到新的节点表示,然后通过注意力权重与这个表示相乘,得到最终的节点表示。

W_adj_transform = torch.mm(adj, self.W_1)
h = torch.matmul(attention, W_adj_transform)

GAT.py代码的作用
这个图注意力网络层计算每个节点的新表示,方法是考虑其邻居的特征以及与这些邻居之间的注意力权重。注意力机制允许模型动态地关注那些对当前任务更重要的邻居节点。

model.py
import torch
import torch.nn as nn
from GAT import GraphAttentionLayer

class model(nn.Module):
    def __init__(self, embed_size):
        super().__init__()
        self.GAT_neighbor = GraphAttentionLayer(embed_size, embed_size)
        self.GAT_item = GraphAttentionLayer(embed_size, embed_size)
        self.relation_neighbor = nn.Parameter(torch.randn(embed_size))
        self.relation_item = nn.Parameter(torch.randn(embed_size))
        self.relation_self = nn.Parameter(torch.randn(embed_size))
        self.c = nn.Parameter(torch.randn(2 * embed_size))

    def predict(self, user_embedding, item_embedding):
        return torch.matmul(user_embedding, item_embedding.t())


    def forward(self, feature_self, feature_neighbor, feature_item):
        if type(feature_item) == torch.Tensor:
            f_n = self.GAT_neighbor(feature_self, feature_neighbor)
            f_i = self.GAT_item(feature_self, feature_item)
            e_n = torch.matmul(self.c, torch.cat((f_n, self.relation_neighbor)))
            e_i = torch.matmul(self.c, torch.cat((f_i, self.relation_item)))
            e_s = torch.matmul(self.c, torch.cat((feature_self, self.relation_self)))
            m = nn.Softmax(dim = -1)
            e_tensor = torch.stack([e_n, e_i, e_s])
            e_tensor = m(e_tensor)
            r_n, r_i, r_s = e_tensor
            user_embedding = r_s * feature_self + r_n * f_n + r_i * f_i
        else:
            f_n = self.GAT_neighbor(feature_self, feature_neighbor)
            e_n = torch.matmul(self.c, torch.cat((f_n, self.relation_neighbor)))
            e_s = torch.matmul(self.c, torch.cat((feature_self, self.relation_self)))
            m = nn.Softmax(dim = -1)
            e_tensor = torch.stack([e_n, e_s])
            e_tensor = m(e_tensor)
            r_n, r_s = e_tensor
            user_embedding = r_s * feature_self + r_n * f_n

        return user_embedding

代码详细解释
首先,导入了PyTorch库及其神经网络模块,并从GAT模块导入了GraphAttentionLayer类。

import torch
import torch.nn as nn
from GAT import GraphAttentionLayer

接着定义了一个名为model的类,该类继承自nn.Module,表示它是一个可训练的神经网络模型。

class model(nn.Module):

model类的构造函数中,定义了图注意力层和三种关系向量(relation_neighbor,relation_item,relation_self),这些向量用于对不同类型的连接进行建模。每个GraphAttentionLayer实例化时都传入embed_size,这是输入和输出特征向量的大小。

    def __init__(self, embed_size):
        super().__init__()
        self.GAT_neighbor = GraphAttentionLayer(embed_size, embed_size)
        self.GAT_item = GraphAttentionLayer(embed_size, embed_size)
        self.relation_neighbor = nn.Parameter(torch.randn(embed_size))
        self.relation_item = nn.Parameter(torch.randn(embed_size))
        self.relation_self = nn.Parameter(torch.randn(embed_size))
        self.c = nn.Parameter(torch.randn(2 * embed_size))

predict方法中,通过计算用户嵌入与物品嵌入的矩阵乘积来预测用户对物品的评分或偏好。

    def predict(self, user_embedding, item_embedding):
        return torch.matmul(user_embedding, item_embedding.t())

forward方法是执行模型前向传播的主要函数,它接受自身特征、邻居特征和物品特征作为输入

    def forward(self, feature_self, feature_neighbor, feature_item):

模型首先检查feature_item是否是一个张量。如果是,则计算邻居和物品的图注意力输出,然后将注意力分数与各自的关系向量连接并通过参数c进行转换。之后,使用softmax函数来规范化这些分数,得到每种连接的权重。

        if type(feature_item) == torch.Tensor:
            f_n = self.GAT_neighbor(feature_self, feature_neighbor)
            f_i = self.GAT_item(feature_self, feature_item)
            e_n = torch.matmul(self.c, torch.cat((f_n, self.relation_neighbor)))
            e_i = torch.matmul(self.c, torch.cat((f_i, self.relation_item)))
            e_s = torch.matmul(self.c, torch.cat((feature_self, self.relation_self)))
            m = nn.Softmax(dim = -1)
            e_tensor = torch.stack([e_n, e_i, e_s])
            e_tensor = m(e_tensor)
            r_n, r_i, r_s = e_tensor
            user_embedding = r_s * feature_self + r_n * f_n + r_i * f_i
        else:
            f_n = self.GAT_neighbor(feature_self, feature_neighbor)
            e_n = torch.matmul(self.c, torch.cat((f_n, self.relation_neighbor)))
            e_s = torch.matmul(self.c, torch.cat((feature_self, self.relation_self)))
            m = nn.Softmax(dim = -1)
            e_tensor = torch.stack([e_n, e_s])
            e_tensor = m(e_tensor)
            r_n, r_s = e_tensor
            user_embedding = r_s * feature_self + r_n * f_n

这样的设计提供了模型的灵活性,使它能够根据可用的数据或具体任务的需求来调整其行为。在实际应用中,这可以根据数据的可用性或特定的应用场景来决定是否提供feature_item

server.py
import torch
import os
import numpy as np
import torch.nn as nn
import dgl
from random import sample
from multiprocessing import Pool, Manager
# from torch.multiprocessing import Pool, Manager
from model import model
import pdb
torch.multiprocessing.set_sharing_strategy('file_system')

class server():
    def __init__(self, user_list, user_batch, users, items, embed_size, lr, device, rating_max, rating_min, weight_decay):
        self.user_list_with_coldstart = user_list
        self.user_list = self.generate_user_list(self.user_list_with_coldstart)
        self.batch_size = user_batch
        self.user_embedding = torch.randn(len(users), embed_size).share_memory_()
        self.item_embedding = torch.randn(len(items), embed_size).share_memory_()
        self.model = model(embed_size)
        self.lr = lr
        self.rating_max = rating_max
        self.rating_min = rating_min
        self.distribute(self.user_list_with_coldstart)
        self.weight_decay = weight_decay
    def generate_user_list(self, user_list_with_coldstart):
        ls = []
        for user in user_list_with_coldstart:
            if len(user.items) > 0:
                ls.append(user)
        return ls

    def aggregator(self, parameter_list):
        flag = False
        number = 0
        gradient_item = torch.zeros_like(self.item_embedding)
        gradient_user = torch.zeros_like(self.user_embedding)
        loss = 0
        item_count = torch.zeros(self.item_embedding.shape[0])
        user_count = torch.zeros(self.user_embedding.shape[0])

        for parameter in parameter_list:
            [model_grad, item_grad, user_grad, returned_items, returned_users, loss_user] = parameter
            num = len(returned_items)
            item_count[returned_items] += 1
            user_count[returned_users] += num
            loss += loss_user ** 2 * num

            number += num
            if not flag:
                flag = True
                gradient_model = []
                gradient_item[returned_items, :] += item_grad * num
                gradient_user[returned_users, :] += user_grad * num
                for i in range(len(model_grad)):
                    gradient_model.append(model_grad[i] * num)
            else:
                gradient_item[returned_items, :] += item_grad * num
                gradient_user[returned_users, :] += user_grad * num
                for i in range(len(model_grad)):
                    gradient_model[i] += model_grad[i] * num
        loss = torch.sqrt(loss / number)
        print('trianing average loss:', loss)
        item_count[item_count == 0] = 1
        user_count[user_count == 0] = 1
        gradient_item /= item_count.unsqueeze(1)
        gradient_user /= user_count.unsqueeze(1)
        for i in range(len(gradient_model)):
            gradient_model[i] = gradient_model[i] / number
        return gradient_model, gradient_item, gradient_user

    def distribute(self, users):
        for user in users:
            user.update_local_GNN(self.model, self.rating_max, self.rating_min, self.user_embedding, self.item_embedding)

    def distribute_one(self, user):
        user.update_local_GNN(self.model)

    def predict(self, valid_data):
        # print('predict')
        users = valid_data[:, 0]
        items = valid_data[:, 1]
        res = []
        self.distribute([self.user_list_with_coldstart[i] for i in set(users)])

        for i in range(len(users)):
            res_temp = self.user_list_with_coldstart[users[i]].predict(items[i], self.user_embedding, self.item_embedding)
            res.append(float(res_temp))
        return np.array(res)

    def train_one(self, user, user_embedding, item_embedding):
        print(user)
        self.parameter_list.append(user.train(user_embedding, item_embedding))

    def train(self):
        parameter_list = []
        users = sample(self.user_list, self.batch_size)
        # print('distribute')
        self.distribute(users)

        for user in users:
            parameter_list.append(user.train(self.user_embedding, self.item_embedding))

        # print('aggregate')
        gradient_model, gradient_item, gradient_user = self.aggregator(parameter_list)

        ls_model_param = list(self.model.parameters())

        item_index = gradient_item.sum(dim = -1) != 0
        user_index = gradient_user.sum(dim = -1) != 0
        # print('renew')
        for i in range(len(ls_model_param)):
            ls_model_param[i].data = ls_model_param[i].data - self.lr * gradient_model[i] - self.weight_decay * ls_model_param[i].data
        self.item_embedding[item_index] = self.item_embedding[item_index] -  self.lr * gradient_item[item_index] - self.weight_decay * self.item_embedding[item_index]
        self.user_embedding[user_index] = self.user_embedding[user_index] -  self.lr * gradient_user[user_index] - self.weight_decay * self.user_embedding[user_index]

aggregator函数
定义了一个聚合器函数,用于聚合梯度。flag变量用于跟踪是否已经处理了第一个参数集(用于初始化梯度累积)。

def aggregator(self, parameter_list):
    flag = False

初始化一个计数器,用于累积处理的物品总数。

    number = 0

创建与物品和用户嵌入维度相同的全零张量,用于累积每个物品和用户的梯度。

    gradient_item = torch.zeros_like(self.item_embedding)
    gradient_user = torch.zeros_like(self.user_embedding)

初始化损失累计值。

    loss = 0

创建与物品和用户嵌入向量长度相同的计数器张量,用于记录每个物品和用户的更新次数。

    item_count = torch.zeros(self.item_embedding.shape[0])
    user_count = torch.zeros(self.user_embedding.shape[0])

遍历传入的参数列表,每个参数包含了一个用户的模型梯度、物品梯度、用户梯度、返回物品、返回用户和用户损失,并将列表中的每个元素(parameter)解构到对应的变量中。

    for parameter in parameter_list:
  [model_grad, item_grad, user_grad, returned_items, returned_users, loss_user] = parameter

获取返回物品列表的长度,即这次更新中有多少个物品的梯度需要被累积。

 num = len(returned_items)

根据返回的物品和用户索引,更新item_count和user_count计数器。(这种尤其注意returned_items是一个列表)

        item_count[returned_items] += 1
        user_count[returned_users] += num

累积调整过的用户损失,通过将单个用户损失的平方乘以涉及的物品数。

        loss += loss_user ** 2 * num

累积调整过的用户损失,通过将单个用户损失的平方乘以涉及的物品数。

        number += num

更新总数,将当前的物品数累加到number上。检查是否是第一次迭代。

if not flag:

如果是第一次迭代,设置flagTrue

             flag = True

初始化模型梯度列表。

            gradient_model = []

将物品和用户的梯度乘以物品数量,然后累加到对应的梯度张量中。

            gradient_item[returned_items, :] += item_grad * num
            gradient_user[returned_users, :] += user_grad * num

对于模型的每个梯度,将它乘以物品数量并添加到gradient_model列表中。

            for i in range(len(model_grad)):
                gradient_model.append(model_grad[i] * num)

如果不是第一次对待,则不用初始化gradient_model,直接累加就可以了。

            gradient_item[returned_items, :] += item_grad * num
            gradient_user[returned_users, :] += user_grad * num

同样,累加物品和用户的梯度乘以物品数量到对应梯度张量中。

            for i in range(len(model_grad)):
                gradient_model[i] += model_grad[i] * num

累加模型梯度到gradient_model列表中的对应梯度。
接着,计算所有用户损失的均方根。

   loss = torch.sqrt(loss / number)

将计数器中的零值替换为一,避免后续的除法操作中出现除以零的情况。

    item_count[item_count == 0] = 1
    user_count[user_count == 0] = 1

torch.zeros_like()torch.zeros()都是PyTorch中PyTorch中创建全零张量的函数,但它们在使用上有一些区别:
在这里插入图片描述
在这里插入图片描述
user.py代码注释

import torch  # 导入PyTorch库
import copy  # 导入copy模块,用于深度复制对象
from random import sample  # 导入random模块的sample函数,用于随机采样
import torch.nn as nn  # 导入PyTorch的神经网络模块
import numpy as np  # 导入NumPy库,用于数学运算
import dgl  # 导入DGL库,用于图神经网络
import pdb  # 导入Python调试器
from model import model  # 从model模块中导入model类

# 定义一个用户类
class user():
    # 类初始化函数
    def __init__(self, id_self, items, ratings, neighbors, embed_size, clip, laplace_lambda, negative_sample):
        # 下面是用户对象的属性
        self.negative_sample = negative_sample  # 负采样数量
        self.clip = clip  # 用于梯度裁剪的阈值
        self.laplace_lambda = laplace_lambda  # 拉普拉斯噪声的lambda值
        self.id_self = id_self  # 用户的ID
        self.items = items  # 用户交互的物品ID列表
        self.embed_size = embed_size  # 嵌入的维度
        self.ratings = ratings  # 用户对物品的评分
        self.neighbors = neighbors  # 用户的邻居节点ID列表
        self.model = model(embed_size)  # 创建一个图神经网络模型实例
        self.graph = self.build_local_graph(id_self, items, neighbors)  # 构建局部图
        self.graph = dgl.add_self_loop(self.graph)  # 为图添加自环
        self.user_feature = torch.randn(self.embed_size)  # 随机初始化用户特征

    # 构建局部图的函数
    def build_local_graph(self, id_self, items, neighbors):
        G = dgl.DGLGraph()  # 创建一个DGL图
        dic_user = {self.id_self: 0}  # 创建一个字典,将用户ID映射到0
        dic_item = {}  # 创建一个字典,用于存储物品ID和图中的节点ID的映射
        count = 1  # 计数器,用于给用户和物品的节点分配ID
        for n in neighbors:  # 遍历邻居用户
            dic_user[n] = count  # 给邻居用户分配节点ID
            count += 1
        for item in items:  # 遍历物品
            dic_item[item] = count  # 给物品分配节点ID
            count += 1
        # 为用户和物品之间添加边
        G.add_edges([i for i in range(1, len(dic_user))], 0)
        G.add_edges(list(dic_item.values()), 0)
        G.add_edges(0, 0)
        return G  # 返回构建好的图

    # 获取用户嵌入的函数
    def user_embedding(self, embedding):
        return embedding[torch.tensor(self.neighbors)], embedding[torch.tensor(self.id_self)]

    # 获取物品嵌入的函数
    def item_embedding(self, embedding):
        return embedding[torch.tensor(self.items)]

    # 图神经网络的前向传播函数
    def GNN(self, embedding_user, embedding_item, sampled_items):
        # 获取邻居和自己的嵌入
        neighbor_embedding, self_embedding = self.user_embedding(embedding_user)
        items_embedding = self.item_embedding(embedding_item)
        sampled_items_embedding = embedding_item[torch.tensor(sampled_items)]
        items_embedding_with_sampled = torch.cat((items_embedding, sampled_items_embedding), dim=0)
        user_feature = self.model(self_embedding, neighbor_embedding, items_embedding)
        predicted = torch.matmul(user_feature, items_embedding_with_sampled.t())
        self.user_feature = user_feature.detach()  # 分离出用户特征,用于防止反向传播时计算其梯度
# 更新局部GNN模型的函数
def update_local_GNN(self, global_model, rating_max, rating_min, embedding_user, embedding_item):
    self.model = copy.deepcopy(global_model)  # 深拷贝全局模型参数到本地用户模型
    self.rating_max = rating_max  # 设定评分的最大值
    self.rating_min = rating_min  # 设定评分的最小值
    neighbor_embedding, self_embedding = self.user_embedding(embedding_user)  # 获取用户的邻居和自身的嵌入向量
    items_embedding = self.item_embedding(embedding_item) if len(self.items) > 0 else False  # 如果用户有评分的物品,则获取这些物品的嵌入向量
    user_feature = self.model(self_embedding, neighbor_embedding, items_embedding)  # 使用GNN模型计算用户特征
    self.user_feature = user_feature.detach()  # 分离计算得到的用户特征,以便停止梯度追踪

# 计算预测损失的函数
def loss(self, predicted, sampled_rating):
    true_label = torch.cat((torch.tensor(self.ratings).to(sampled_rating.device), sampled_rating))  # 将真实评分和负采样评分合并
    return torch.sqrt(torch.mean((predicted - true_label) ** 2))  # 计算预测评分与真实评分之间的均方根误差

# 预测单个物品评分的函数
def predict(self, item_id, embedding_user, embedding_item):
    self.model.eval()  # 将模型设置为评估模式
    item_embedding = embedding_item[item_id]  # 获取待预测物品的嵌入向量
    return torch.matmul(self.user_feature, item_embedding.t())  # 计算用户特征向量与物品嵌入向量的点积,得到预测评分

# 负采样物品并预测评分的函数
def negative_sample_item(self, embedding_item):
    item_num = embedding_item.shape[0]  # 获取嵌入向量中的物品总数
    ls = [i for i in range(item_num) if i not in self.items]  # 生成非用户已评分的物品列表
    sampled_items = sample(ls, self.negative_sample)  # 随机负采样指定数量的物品
    sampled_item_embedding = embedding_item[torch.tensor(sampled_items)]  # 获取负采样物品的嵌入向量
    predicted = torch.matmul(self.user_feature, sampled_item_embedding.t())  # 计算用户特征与负采样物品嵌入向量的点积得到评分预测
    predicted = torch.round(torch.clip(predicted, min=self.rating_min, max=self.rating_max))  # 对预测评分进行裁剪并四舍五入到最近的整数
    return sampled_items, predicted

# 添加拉普拉斯噪声实现局部差分隐私的函数
def LDP(self, tensor):
    tensor_mean = torch.abs(torch.mean(tensor))  # 计算张量的绝对均值
    tensor = torch.clamp(tensor, min=-self.clip, max=self.clip)  # 将张量中的每个元素裁剪到指定范围以限制噪声
    noise = np.random.laplace(0, tensor_mean * self.laplace_lambda)  # 生成拉普拉斯噪声
    tensor += noise  # 将噪声添加到张量中
    return tensor

# 训练模型的函数
def train(self, embedding_user, embedding_item):
    embedding_user = torch.clone(embedding_user).detach()  # 复制用户嵌入向量并从计算图中分离
    embedding_item = torch.clone(embedding_item).detach()  # 复制物品嵌入向量并从计算图中分离
    embedding_user.requires_grad = True  # 设置用户
User.py
import torch  # 导入PyTorch库用于张量计算
import copy  # 导入copy模块用于执行深拷贝
from random import sample  # 导入sample函数用于从列表中随机抽样
import torch.nn as nn  # 从PyTorch中导入神经网络模块
import numpy as np  # 导入NumPy库用于数值计算
import dgl  # 导入DGL库用于图神经网络构建
import pdb  # 导入pdb模块用于程序调试
from model import model  # 从model文件导入model类

class user():  # 定义一个用户类
    def __init__(self, id_self, items, ratings, neighbors, embed_size, clip, laplace_lambda, negative_sample):
        # 类初始化函数
        self.negative_sample = negative_sample  # 负采样数量
        self.clip = clip  # 梯度裁剪阈值
        self.laplace_lambda = laplace_lambda  # 拉普拉斯噪声的参数
        self.id_self = id_self  # 用户自身的ID
        self.items = items  # 用户交互过的物品列表
        self.embed_size = embed_size  # 嵌入向量的维度
        self.ratings = ratings  # 用户对物品的评分
        self.neighbors = neighbors  # 用户的邻居节点列表
        self.model = model(embed_size)  # 初始化用户的模型
        self.graph = self.build_local_graph(id_self, items, neighbors)  # 构建用户的局部图
        self.graph = dgl.add_self_loop(self.graph)  # 为图添加自环
        self.user_feature = torch.randn(self.embed_size)  # 初始化用户特征向量

    def build_local_graph(self, id_self, items, neighbors):
        # 构建局部图的函数
        G = dgl.DGLGraph()  # 创建一个DGL图
        dic_user = {self.id_self: 0}  # 创建一个用户字典,将自身ID映射为0
        dic_item = {}  # 创建一个物品字典
        count = 1  # 计数器,用于给节点编号
        for n in neighbors:
            dic_user[n] =  count  # 将邻居节点加入用户字典,并编号
            count += 1
        for item in items:
            dic_item[item] = count  # 将物品节点加入物品字典,并编号
            count += 1
        G.add_edges([i for i in range(1, len(dic_user))], 0)  # 添加用户节点之间的边
        G.add_edges(list(dic_item.values()), 0)  # 添加用户节点到物品节点的边
        G.add_edges(0, 0)  # 添加用户自身的边
        return G  # 返回构建的局部图

    def user_embedding(self, embedding):
        # 获取用户嵌入的函数
        return embedding[torch.tensor(self.neighbors)], embedding[torch.tensor(self.id_self)]

    def item_embedding(self, embedding):
        # 获取物品嵌入的函数
        return embedding[torch.tensor(self.items)]

    def GNN(self, embedding_user, embedding_item, sampled_items):
    # 获取用户邻居和自身的嵌入向量
	    neighbor_embedding, self_embedding = self.user_embedding(embedding_user)
	    # 获取用户交互过的物品的嵌入向量
	    items_embedding = self.item_embedding(embedding_item)
	    # 从全体物品嵌入中获取负采样物品的嵌入向量
	    sampled_items_embedding = embedding_item[torch.tensor(sampled_items)]
	    # 将用户交互过的物品和负采样物品的嵌入向量进行拼接
	    items_embedding_with_sampled = torch.cat((items_embedding, sampled_items_embedding), dim=0)
	    # 使用模型处理用户和物品嵌入向量,生成用户特征
	    user_feature = self.model(self_embedding, neighbor_embedding, items_embedding)
	    # 使用用户特征和物品嵌入向量计算预测评分
	    predicted = torch.matmul(user_feature, items_embedding_with_sampled.t())
	    # 将用户特征向量从计算图中分离出来
	    self.user_feature = user_feature.detach()
	    # 返回预测评分
	    return predicted
	def update_local_GNN(self, global_model, rating_max, rating_min, embedding_user, embedding_item):
	    # 使用全局模型更新本地模型
	    self.model = copy.deepcopy(global_model)
	    # 设置评分的最大和最小值
	    self.rating_max = rating_max
	    self.rating_min = rating_min
	    # 获取用户和邻居的嵌入向量
	    neighbor_embedding, self_embedding = self.user_embedding(embedding_user)
	    # 如果用户有交互过的物品,则获取这些物品的嵌入向量,否则为False
	    items_embedding = self.item_embedding(embedding_item) if len(self.items) > 0 else False
	    # 使用模型处理用户和物品嵌入向量,生成用户特征
	    user_feature = self.model(self_embedding, neighbor_embedding, items_embedding)
	    # 将用户特征向量从计算图中分离出来
	    self.user_feature = user_feature.detach()
	def loss(self, predicted, sampled_rating):
	    # 将真实评分和负采样评分拼接成一个向量
	    true_label = torch.cat((torch.tensor(self.ratings).to(sampled_rating.device), sampled_rating))
	    # 计算预测评分和真实评分的均方根误差
	    return torch.sqrt(torch.mean((predicted - true_label) ** 2))
	def predict(self, item_id, embedding_user, embedding_item):
	    # 将模型设置为评估模式
	    self.model.eval()
	    # 获取指定物品的嵌入向量
	    item_embedding = embedding_item[item_id]
	    # 使用用户特征和物品嵌入向量计算预测评分
	    return torch.matmul(self.user_feature, item_embedding.t())
	def negative_sample_item(self, embedding_item):
	    # 确定物品嵌入向量的总数
	    item_num = embedding_item.shape[0]
	    # 选择非用户交互物品作为负样本
	    ls = [i for i in range(item_num) if i not in self.items]
	    sampled_items = sample(ls, self.negative_sample)
	    # 获取负样本物品的嵌入向量
	    sampled_item_embedding = embedding_item[torch.tensor(sampled_items)]
	    # 计算负样本物品的预测评分,并将评分限制在最大和最小值之间
	    predicted = torch.matmul(self.user_feature, sampled_item_embedding.t())
	    predicted = torch.round(torch.clip(predicted, min=self.rating_min, max=self.rating_max))
	    # 返回负样本物品和它们的预测评分
	    return sampled_items, predicted
	def LDP(self, tensor):
	     # 计算张量的均值的绝对值
	     tensor_mean = torch.abs(torch.mean(tensor))
	     # 将张量的值限制在负裁剪值和正裁剪值之间
	     tensor = torch.clamp(tensor, min=-self.clip, max=self.clip)
	     # 添加拉普拉斯噪声,噪声的比例由拉普拉斯参数和张量均值决定
	     noise = np.random.laplace(0, tensor_mean * self.laplace_lambda)
	     # 将噪声加到张量上,用于差分隐私
	     tensor += noise
	     # 返回添加噪声后的张量
	     return tensor
	
	 def train(self, embedding_user, embedding_item):
	     # 克隆并分离用户和物品的嵌入向量,以便于训练
	     embedding_user = torch.clone(embedding_user).detach()
	     embedding_item = torch.clone(embedding_item).detach()
	     # 设置嵌入向量的梯度计算为True
	     embedding_user.requires_grad = True
	     embedding_item.requires_grad = True
	     # 初始化用户和物品嵌入向量的梯度
	     embedding_user.grad = torch.zeros_like(embedding_user)
	     embedding_item.grad = torch.zeros_like(embedding_item)
	
	     # 设置模型为训练模式
	     self.model.train()
	     # 使用负采样函数获取负样本物品和它们的预测评分
	     sampled_items, sampled_rating = self.negative_sample_item(embedding_item)
	     # 计算用户交互和负采样物品的并集
	     returned_items = self.items + sampled_items
	     # 使用GNN函数计算预测评分
	     predicted = self.GNN(embedding_user, embedding_item, sampled_items)
	     # 使用损失函数计算预测评分和真实评分的损失
	     loss = self.loss(predicted, sampled_rating)
	     # 清零模型当前的梯度
	     self.model.zero_grad()
	     # 反向传播损失
	     loss.backward()
	     # 使用差分隐私技术对模型参数的梯度进行噪声添加
	     model_grad = []
	     for param in list(self.model.parameters()):
	         grad = self.LDP(param.grad)
	         model_grad.append(grad)
	
	     # 对物品嵌入向量的梯度进行噪声添加
	     item_grad = self.LDP(embedding_item.grad[returned_items, :])
	     # 获取用户及其邻居的ID列表
	     returned_users = self.neighbors + [self.id_self]
	     # 对用户嵌入向量的梯度进行噪声添加
	     user_grad = self.LDP(embedding_user.grad[returned_users, :])
	     # 将模型梯度、物品梯度、用户梯度以及相关ID和损失值打包返回
	     res = (model_grad, item_grad, user_grad, returned_items, returned_users, loss.detach())
	     return res
论文中的主要创新结合代码说明:

动态差分隐私在这段代码中体现在LDP 方法中。这个方法的目的是为了在不显著改变数据统计学特性的前提下,对个体数据进行保护。方法如下:
动态差分隐私
其中动态体现在tensor_mean * self.laplace_lambda,这里基于梯度的大小均值来决定添加的噪声的大小,也就是动态噪声

def LDP(self, tensor):
    tensor_mean = torch.abs(torch.mean(tensor))
    tensor = torch.clamp(tensor, min = -self.clip, max = self.clip)
    noise = np.random.laplace(0, tensor_mean * self.laplace_lambda)
    tensor += noise
    return tensor

tensor_mean是输入张量的均值的绝对值,这是为了确定噪声的规模。
tensor 是经过裁剪的,以确保每个元素都在 -self.clipself.clip之间。这是一种常见的做法,用于限制梯度的大小,防止过大的梯度对模型训练的影响。
noise 是根据拉普拉斯分布生成的,其位置参数(mean)为0,尺度参数(scale)为 tensor_mean * self.laplace_lambda。这里,self.laplace_lambda 是拉普拉斯噪声的多样性控制参数,它和隐私预算成反比。
最后,将噪声添加到原张量中,这样就在保护隐私的同时保留了原始数据的一些特性。

这个方法被用在以下上下文中:
在计算模型参数梯度后,通过调用 self.LDP(param.grad) 为梯度添加噪声。
同样,在计算用户和项目嵌入的梯度时,也添加了噪声。
这种做法可以在一定程度上防止过拟合,并且是在对抗推理攻击(例如,试图从梯度中恢复训练数据)时保护用户隐私的重要策略。

伪项目标签
其实就是推荐系统中常用的负采样技术

伪项目标签技术与 negative_sample_item 方法有关。在推荐系统中,负采样是一种处理未观察数据(用户未与之互动的项目)的方法。由于推荐系统的数据通常是正样本偏差的(大量的未互动数据通常被认为是负样本),因此引入负样本可以提高模型的泛化能力。具体代码如下:

def negative_sample_item(self, embedding_item):
    item_num = embedding_item.shape[0]
    ls = [i for i in range(item_num) if i not in self.items]
    sampled_items = sample(ls, self.negative_sample)
    sampled_item_embedding = embedding_item[torch.tensor(sampled_items)]
    predicted = torch.matmul(self.user_feature, sampled_item_embedding.t())
    predicted = torch.round(torch.clip(predicted, min = self.rating_min, max = self.rating_max))
    return sampled_items, predicted

在训练过程中,模型通过以下步骤使用随机选择的负样本:

  • 负采样(Negative Sampling):
    模型从那些用户未曾互动过的项目中随机选择一些项目作为负样本。这是在negative_sample_item函数中执行的。
  • 预测(Prediction):对这些负样本的用户偏好进行预测,生成一个预测分数。这通常涉及到使用用户的特征和项目的特征来计算一个得分,该得分表征了用户对该项目的偏好程度。
  • 训练(Training): 将这些负样本和它们的预测标签一起用于模型训练。模型会试图区分用户已知的正样本(用户已经互动过的项目)和这些负样本。
  • 损失计算(Loss Calculation): 通过比较模型对所有正样本和负样本的预测得分与实际标签之间的差异来计算损失。损失函数的目标是减少这个差异,从而使模型能够更准确地预测用户的偏好。

通过这种方式,模型不仅学习了用户可能喜欢的项目(通过正样本),还学习了用户可能不喜欢的项目(通过负样本)。这有助于模型更好地理解用户的整体偏好,并且在推荐新项目时能够做出更准确的推断。

main.py

import pickle  # 导入pickle模块用于数据序列化和反序列化
import torch  # 导入PyTorch库
import numpy as np  # 导入NumPy库
from user import user  # 从user模块导入user类
from server import server  # 从server模块导入server类
from sklearn import metrics  # 导入sklearn.metrics用于评估模型
import math  # 导入math库
import argparse  # 导入argparse库用于处理命令行参数
import warnings  # 导入warnings库用于警告控制
import sys  # 导入sys库用于系统相关的操作
import faulthandler  # 导入faulthandler库用于错误处理

faulthandler.enable()  # 启用故障处理器,帮助调试程序崩溃问题
warnings.filterwarnings('ignore')  # 忽略警告信息

# 设置命令行参数解析
parser = argparse.ArgumentParser(description="args for FedGNN")
parser.add_argument('--embed_size', type=int, default=8)  # 嵌入向量的维度
parser.add_argument('--lr', type=float, default=0.1)  # 学习率
parser.add_argument('--data', default='filmtrust')  # 数据集名称
parser.add_argument('--user_batch', type=int, default=256)  # 用户批次大小
parser.add_argument('--clip', type=float, default=0.3)  # 梯度裁剪阈值
parser.add_argument('--laplace_lambda', type=float, default=0.1)  # 拉普拉斯噪声参数
parser.add_argument('--negative_sample', type=int, default=10)  # 负采样数量
parser.add_argument('--valid_step', type=int, default=20)  # 验证步数
parser.add_argument('--weight_decay', type=float, default=0.001)  # 权重衰减率
parser.add_argument('--device', type=str, default='cpu')  # 计算设备
args = parser.parse_args()  # 解析命令行参数

# 初始化参数变量
embed_size = args.embed_size
user_batch = args.user_batch
lr = args.lr
device = torch.device('cpu')  # 默认设备为CPU
if args.device != 'cpu':
    device = torch.device('cuda:0')  # 如果指定设备不是CPU,则使用CUDA

def processing_valid_data(valid_data):
    # 初始化一个空列表,用于存储处理后的数据
    res = []
    # 遍历验证数据的每个键(用户ID)
    for key in valid_data.keys():
        # 如果当前键(用户ID)对应的数据不为空
        if len(valid_data[key]) > 0:
            # 再次遍历当前键(用户ID)下的评分数据
            for ratings in valid_data[key]:
                # 解包评分数据,得到物品ID、评分值、其他信息(此处未使用)
                item, rate, _ = ratings
                # 将用户ID、物品ID和评分值作为元组添加到结果列表中
                res.append((int(key), int(item), rate))
    # 将结果列表转换为NumPy数组并返回
    return np.array(res)


def loss(server, valid_data):
    # 从验证数据中提取实际的评分值作为标签
    label = valid_data[:, -1]
    # 使用服务器实例对验证数据进行预测,得到预测评分
    predicted = server.predict(valid_data)
    # 计算真实评分和预测评分之间的平均绝对误差(MAE)
    mae = sum(abs(label - predicted)) / len(label)
    # 计算真实评分和预测评分之间的均方根误差(RMSE)
    rmse = math.sqrt(sum((label - predicted) ** 2) / len(label))
    # 返回MAE和RMSE两个性能指标
    return mae, rmse


# 读取数据
data_file = open('../data/' + args.data + '_FedMF.pkl', 'rb')
[train_data, valid_data, test_data, user_id_list, item_id_list, social] = pickle.load(data_file)
data_file.close()
valid_data = processing_valid_data(valid_data)  # 处理验证数据
test_data = processing_valid_data(test_data)  # 处理测试数据

# 构建用户列表
rating_max = -9999  # 初始化最大评分
rating_min = 9999  # 初始化最小评分
user_list = []  # 用户列表
for u in user_id_list:
    ratings = train_data[u]
    items = []  # 物品列表
    rating = []  # 评分列表
    for i in range(len(ratings)):
        item, rate, _ = ratings[i]
        items
        items.append(item)  # 添加物品ID到列表
        rating.append(rate)  # 添加评分到列表

    # 更新评分的最大值和最小值
    if len(rating) > 0:
        rating_max = max(rating_max, max(rating))
        rating_min = min(rating_min, min(rating))
    # 创建一个用户实例,并添加到用户列表中
    user_list.append(user(u, items, rating, list(social[u]), embed_size, args.clip, args.laplace_lambda, args.negative_sample))

# 创建服务器实例
server = server(user_list, user_batch, user_id_list, item_id_list, embed_size, lr, device, rating_max, rating_min, args.weight_decay)
count = 0  # 初始化计数器,用于早停

# 训练和评估模型
rmse_best = 9999  # 初始化最佳RMSE评分
while True:  # 开始训练循环
    for i in range(args.valid_step):  # 按照验证步数进行训练
        server.train()  # 调用服务器的训练函数
    print('valid')  # 输出验证信息
    mae, rmse = loss(server, valid_data)  # 计算验证集上的MAE和RMSE
    print('valid mae: {}, valid rmse:{}'.format(mae, rmse))  # 打印验证结果
    if rmse < rmse_best:  # 如果当前RMSE比最佳RMSE小
        rmse_best = rmse  # 更新最佳RMSE
        count = 0  # 重置计数器
        mae_test, rmse_test = loss(server, test_data)  # 在测试集上计算MAE和RMSE
    else:  # 如果没有改进
        count += 1  # 计数器加一
    if count > 5:  # 如果连续5次迭代没有改进
        print('not improved for 5 epochs, stop training')  # 输出停止训练的信息
        break  # 跳出循环
print('final test mae: {}, test rmse: {}'.format(mae_test, rmse_test))  # 打印最终的测试集上的MAE和RMSE

论文实验中所选取的损失函数参数

mae和rmse
在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/141538.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

对测试职业发展的思考

虽然在测试行业摸爬滚打了很年&#xff0c;随着年龄的增长&#xff0c;职位的升迁&#xff0c;似乎已经走到了尽头&#xff0c;因而还是时不时觉得自己的职业发展目标很模糊&#xff0c;这是最近对自己职业发展的一些思考&#xff0c;希望与大家进行分享和探讨&#xff1a; 1、…

3、Linux库的生成和使用(核心代码是程序员不可公开的小秘密)

目录 Linux库的概念 Linux 静态库 Linux 静态库作用 Linux 静态库的创建 1. 将.c文件生成.o文件 ​编辑 2. 将所有的.o文件归档为一个静态库.a文件 Linux 静态库的使用 Linux 动态库&#xff1a; Linux 动态库作用 Linux 动态库的创建 生成.so动态库文件 ​编辑 …

curl(八)时间和环境变量以及配置

一 时间 ① --connect-timeout 连接超时时间 ② -m | --max-time 数据最大传输时间 -m&#xff1a; 限制curl 完成时间(overall time limit)-m,--max-time <seconds> 整个交互完成的超时时间场景&#xff1a; 通过设置-m参数,可以避免请求时间过长而导致的超时错误…

jstack java堆栈跟踪工具

jstack java堆栈跟踪工具 1、jstack介绍 jstack&#xff08;stack trace for java&#xff09;是java虚拟机自带的一种堆栈跟踪工具。 jstack主要用于生成java虚拟机当前时刻的线程快照&#xff0c;线程快照是当前java虚拟机内每一条线程正在执行的方法 堆栈的集合&#xf…

MacOS Ventura 13 优化配置(ARM架构新手向导)

一、系统配置 1、About My MacBook Pro 2、在当前标签打开新窗口 桌面上创建目录的文件夹&#xff0c;每次新打开一个目录&#xff0c;就会创建一个窗口&#xff0c;这就造成窗口太多&#xff0c;不太好查看和管理&#xff0c;我们可以改成在新标签处打开新目录。需要在&…

数字滤波器设计---IIR 滤波器设计

数字滤波器设计---IIR 滤波器设计 IIR 与 FIR 滤波器的比较 与 FIR 滤波器相比&#xff0c;IIR 滤波器的主要优点是&#xff0c;要满足同一组设定&#xff0c;它的滤波器阶数通常远远低于 FIR 滤波器。虽然 IIR 滤波器具有非线性相位&#xff0c;但 MATLAB 软件中的数据处理通…

【数据分享】我国雏鹰企业数据(excel格式\shp格式)

企业是经济活动的参与主体。一个城市的企业数量决定了这个城市的经济发展水平&#xff01;比如一个城市的金融企业较多&#xff0c;那这个城市的金融产业肯定比较发达&#xff1b;一个城市的制造业企业较多&#xff0c;那这个城市的制造业肯定比较发达。 本次我们为大家带来的…

jumpserver任意密码重置漏洞-CVE-2023-42820

目录 jumpserver 环境搭建 这里用的是vulhub靶场 进入 jumpserver 的目录 修改配置文件 config.env 里面的 DOMAINS 参数为kali的地址 运行环境&#xff0c;第一次运行的话会拉取文件&#xff0c;要耐心等待。 命令&#xff1a; 查看docker容器 命令&#xff1a; 用浏…

【分享】Excel“只读方式”的两种模式

查阅Excel表格的时候&#xff0c;担心不小心修改了内容&#xff0c;可以给Excel设置以“只读方式”打开&#xff0c;这样就算修改了内容也不能直接保存表格。Excel表格可以设置两种“只读方式”&#xff0c;一起来看看吧&#xff01; “只读方式” 1&#xff1a; 打开Excel表…

找工作什么平台最可靠

吉鹿力招聘网是最可靠的找工作平台。可以直接和HR沟通岗位情况&#xff0c;方便快捷。同时&#xff0c;吉鹿力招聘网还有一些其他功能&#xff0c;比如可以找到更精准的人才&#xff0c;以及专业的招聘网站&#xff0c;可以帮助求职者找到合适的职位。吉鹿力招聘网还有一个特点…

【Linux进程】进程控制

目录 一、进程创建 1.2 fork函数初识 1.2 fork函数返回值 1.3 写时拷贝 1.4 fork常规用法 1.5 fork调用失败的原因 二、进程终止 2.1 进程退出场景 2.2 进程退出码 2.2.1 用strerror打印错误信息 2.2.2 errno全局变量 2.3 进程常见退出方法 2.3.1 进程正常退出 2…

Python高级语法----Python的元编程

文章目录 装饰器元类反射使用 `__getattr__`, `__setattr__`, 和 `__delattr__`元编程是一种编程技术,它允许程序员在运行时修改、增加或操作程序的结构。在Python中,元编程通常涉及到对类和函数的动态创建和修改,这是通过使用诸如装饰器、元类和反射等高级功能来实现的。 …

Java整合Redis实现坐标附近查询

目录 一、GEO用法引入 二、引入依赖 三、实体类 四、添加位置信息 五、查询位置信息 一、GEO用法引入 GEO&#xff0c;全称Geolocation&#xff0c;代表地理坐标。可以在其中存储地理坐标信息&#xff0c;帮助我们根据经纬度来检索数据。常见的命令有&#xff1a;GEOADD&…

数列计算

题目描述 有一列数是 : 请找出这个数列的规律&#xff0c;编写程序计算并输出这个数列的第项&#xff0c;要求是分数形式&#xff0c;并计算这个数列的前项和 ( 结果四舍五入保留两位小数 ) 输入格式 第一行仅有一个正整数 &#xff08;) 。 输出格式 共有 行&#xff0c;第一…

5+干湿结合的佳作,可另外添加分析升级

今天给同学们分享一篇生信文章“PCTAIRE Protein Kinase 1 (PCTK1) Suppresses Proliferation, Stemness,and Chemoresistance in Colorectal Cancer through the BMPR1B-Smad1/5/8 Signaling Pathway”&#xff0c;这篇文章发表在Int J Mol Sci期刊上&#xff0c;影响因子为5.…

Web APIs——综合案例学生就业统计表

1、学生就业统计表 2、渲染业务 根据持久化数据渲染页面 步骤&#xff1a; ①&#xff1a;读取localstorage本地数据 如果有数据则转换为对象放到变量里面一会使用它渲染页面如果没有则用默认空数组[]为了测试效果&#xff0c;可以先把initData存入本地存储看效果 ②&…

Monarch Mixer: A Simple Sub-Quadratic GEMM-Based Architecture

Monarch Mixer: A Simple Sub-Quadratic GEMM-Based Architecture 发于 2023年AI顶会 NeurIPS。 sub-quadratic primitive(次二次原语) GEMMs&#xff08;General Matrix Multiply algorithms&#xff09;是指在许多核心系统上执行的通用矩阵乘法操作的模型。 “causal model…

idea2023启动springboot项目如何指定配置文件

方法一&#xff1a; 方法二&#xff1a; 举例&#xff1a;

blockly编程简介

blockly编程简介 blockly是google发布的可视化编程工具&#xff0c;基于web技术构建。 在功能/设计上和MIT的儿童编程语言Scratch类似&#xff0c;采用可视化搭积木编程方式。 可以将基于block程序转化为基于普通文本的常规代码&#xff08;如JavaScript、Python等&#xff…

基于flask+bootstrap4实现的注重创作的轻博客系统项目源码

一个注重创作的轻博客系统 作为一名技术人员一定要有自己的博客&#xff0c;用来记录平时技术上遇到的问题&#xff0c;把技术分享出去就像滚雪球一样会越來越大&#xff0c;于是我在何三博客的基础上开发了[l4blog]&#xff0c;一个使用python开发的轻量博客系统&#xff0c;…