基于图神经网络的联合社交推荐
ACM-TIST CCF_B类
论文链接
代码地址
模型中梯度和embedding的聚合
在FeSog中,Server端维护一个整体的model
,由于这里的model
层网络和GraphAttentionLayer
层网络中一共有10个要更新参数,所以当每次server
端将当前的model
分发到各个user
后,各个user
利用本地的的评分数据来获得要更新的梯度,然后加到整体的梯度里面,这个梯度也有一个权重,那就是user
交互的所有的项目的数量。利用这个来实现聚合每个客户端对于整体server端梯度的更新。
对于embedding
的聚合则是server
端维护一个大的item_embedding
和user_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_self
和feature_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_h
和W_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
编号,第二位表示user
对item
的rate分数。第三位没啥用。
film数据集中有
- 1957个item
- 874个user
user_id_list
中有874个user
编号,item_id_list
有1957个item编号。
social
中是每个用户的社交关系。
rating的评分是1到8,1表示最不喜欢,8表示最喜欢。
epinions
数据集中有18069
个Users
,有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():
将模型设置为训练模式。在这种模式下,模型会正常更新权重,且某些层(如Dropout
、BatchNorm
等)会按照训练时的行为运行。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
是一个科学计算库。
torch
是PyTorch库
,一个流行的深度学习库。
torch.nn
和 torch.nn.functional
是PyTorch
中用于构建网络层的模块。
pdb
是Python
的调试器。
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
接着定义了一个名为mode
l的类,该类继承自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:
如果是第一次迭代,设置flag
为True
。
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.clip
和self.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