user.py
# -*- coding: utf-8 -*-
# Python version: 3.7
import torch
import copy
from random import sample
import torch.nn as nn
import numpy as np
import dgl
import pdb
from model import model
class user():
def __init__(self, id_self, items, ratings, neighbors, embed_size, clip, laplace_lambda, negative_sample):
# 伪项目采样数量可选{10,50,100,500,1000}
self.negative_sample = negative_sample # 默认为10
'''梯度裁剪阈值设置为0.3'''
self.clip = clip
'''拉普拉斯噪声强度为0.1'''
self.laplace_lambda = laplace_lambda
self.id_self = id_self
self.items = items
'''嵌入大小可选{4,8,16,32,64}'''
self.embed_size = embed_size
'''rating = []评分列表'''
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):
# dgl.DGLGraph
G = dgl.DGLGraph()
'''创建一个没有节点和边的空图'''
dic_user = {self.id_self: 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
'''添加指向同一节点的多条边 1->0, 2->0, 3->0, ... n->0,'''
G.add_edges([i for i in range(1, len(dic_user))], 0)
'''添加指向同一节点的多条边 n->0, n+1->0, n+2->0, ... ,'''
G.add_edges(list(dic_item.values()), 0)
G.add_edges(0, 0)
'''DGLGraph不支持删除节点和边'''
return G
'''下面用到embedding[torch.tensor()] Embedding’s forward method will modify the weight tensor in-place'''
'''
embed = embedding(torch.LongTensor(test))
# 将test传入Embedding层,即可构建一个look-up embedding查询
input = torch.LongTensor([[1,2,4,5],[4,3,2,9]])
embedding(input)
tensor([[[-0.0251, -1.6902, 0.7172],
[-0.6431, 0.0748, 0.6969],
[ 1.4970, 1.3448, -0.9685],
[-0.3677, -2.7265, -0.1685]],
[[ 1.4970, 1.3448, -0.9685],
[ 0.4362, -0.4004, 0.9400],
[-0.6431, 0.0748, 0.6969],
[ 0.9124, -2.3616, 1.1151]]])
'''
def user_embedding(self, embedding):
# user_embedding 函数返回用户自身节点嵌入和用户邻居节点嵌入??
'''self.neighbors 和 self.id_self是【id】吧'''
return embedding[torch.tensor(self.neighbors)], embedding[torch.tensor(self.id_self)]
def item_embedding(self, embedding):
# item_embedding函数返回项目节点嵌入
'''self.items是item的【id】吧'''
return embedding[torch.tensor(self.items)]
def GNN(self, embedding_user, embedding_item, sampled_items):
neighbor_embedding, self_embedding = self.user_embedding(embedding_user)
'''下面train函数中调用了GNN(embedding_user, embedding_item, sampled_items)'''
'''【【【【利用user_embedding从embedding_user返回了用户自身节点嵌入和用户邻居节点嵌入吗】】】'''
items_embedding = self.item_embedding(embedding_item)
'''sampled_items为item的数量'''
sampled_items_embedding = embedding_item[torch.tensor(sampled_items)]
'''
真实item嵌入加上伪项目嵌入
'''
items_embedding_with_sampled = torch.cat((items_embedding, sampled_items_embedding), dim=0)
'''
Equation(10)
得到各自嵌入的注意权重,推断用户节点嵌入:
user_feature为最终的推断用户嵌入
'''
user_feature = self.model(self_embedding, neighbor_embedding, items_embedding)
'''
预测层:点积
items_embedding是包含伪项目的
'''
predicted = torch.matmul(user_feature, items_embedding_with_sampled.t())
'''
detach 意为分离,对某个张量调用函数detach() 的作用是返回一个 Tensor,它和原张量的数据相同,
但requires_grad=False,也就意味着detach() 得到的张量不会具有梯度。这一性质即使我们修改其requires_grad 属性也无法改变。
'''
self.user_feature = user_feature.detach()
return predicted
'''更新本地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)
'''self.items是列表吧 len(self.items) > 0表示用户有交互项目'''
if len(self.items) > 0:
items_embedding = self.item_embedding(embedding_item)
else:
items_embedding = False
'''通过调用模型user调用model.py得到本地用户的推断嵌入'''
user_feature = self.model(self_embedding, neighbor_embedding, items_embedding)
self.user_feature = user_feature.detach()
'''Equation(13)'''
'''predicted表示对(真实交互项目集∪伪交互项目集)的预测评分, sampled_rating为对伪项目集的预测评分'''
def loss(self, predicted, sampled_rating):
# 损失函数
'''rating = []评分列表加上对伪项目的评分来构成真实标签'''
'''true_label为了使得predicted与true_label数量一致,要加上sampled_rating为对伪项目集的预测评分'''
true_label = torch.cat((torch.tensor(self.ratings).to(sampled_rating.device), sampled_rating))
'''【【【【【】】】】】'''
'''【【【【比较一下predicted里面伪标签和sampled_rating得到的伪标签】】】】'''
'''
predicted里面的伪标签
def train函数里面
predicted = self.GNN(embedding_user, embedding_item, sampled_items)
def GNN里面
predicted = torch.matmul(user_feature, items_embedding_with_sampled.t())
sampled_rating得到的伪标签: 进行了裁剪和四舍五入
def negative_sample_item函数里:
对伪项预测
predicted = torch.matmul(self.user_feature, sampled_item_embedding.t())
predicted = torch.round(torch.clip(predicted, min=self.rating_min, max=self.rating_max))
'''
# 计算预测分数与真实评分之间的均方根误差
'''predicted表示对(真实交互项目集∪伪交互项目集)的预测评分'''
'''【【伪项目的真实评分是四舍五入得到的分数。预测分数和四舍五入分数之间的差异导致了这些伪项目的梯度。】】'''
'''所以求RMSE中的predicted是直接通过点积得到的,没有进一步处理'''
'''真实频分true_label里面的伪项目标签是裁剪和取整的,所以预测分数和四舍五入分数之间的差异导致了这些伪项目的梯度'''
return torch.sqrt(torch.mean((predicted - true_label) ** 2))
'''这个预测函数没有用到'''
def predict(self, item_id, embedding_user, embedding_item):
'''embedding_user没用到吗?'''
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为序列
'''在不相邻的项目中采样q项'''
ls = [i for i in range(item_num) if i not in self.items]
# sample函数:从序列ls中随机抽取10个元素,并将10个元素生以list形式返回。
sampled_items = sample(ls, self.negative_sample) # negative_sample默认为10
'''伪项目嵌入'''
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))
'''梯度裁剪:torch.clamp()将输入input张量每个元素的范围限制到区间 [min,max],返回结果到一个新张量。'''
tensor = torch.clamp(tensor, min=-self.clip, max=self.clip)
'''经过Laplacian函数,加入基于梯度的动态噪声'''
noise = np.random.laplace(0, tensor_mean * self.laplace_lambda)
'''g=clip(g^(n) * %)+Laplacian(0, & * mean(g^(n)))'''
tensor += noise
return tensor
'''从服务器下载参数'''
def train(self, embedding_user, embedding_item):
embedding_user = torch.clone(embedding_user).detach()
'''
clone:
(1)返回一个新的tensor,这个tensor与原始tensor的数据不共享一个内存
(也就是说, 两者不是同一个数据,修改一个另一个不会变)。
(2)requires_grad属性与原始tensor相同,若requires_grad=True,计算梯度,
但不会保留梯度,梯度会与原始tensor的梯度相加。
detach:
(1)返回一个新的tensor,这个tensor与原始tensor(torch.clone(embedding_user)的数据共享一个内存
(也就是说,两者是同一个数据,修改原始tensor,new tensor也会变; 修改new tensor,原始tensor也会变)。
(2)require_grad设置为False(也就是网上说的从计算图中剥除,不计算梯度)。
'''
embedding_item = torch.clone(embedding_item).detach()
embedding_user.requires_grad = True
embedding_item.requires_grad = True
'''torch.zeros_like生成和括号内变量维度维度一致的全是零的内容'''
'''通过嵌入生成嵌入梯度'''
'''每次训练都会清零,和下面model.zero_grad()对应'''
'''【torch.Tensor.grad】'''
'''print(type(embedding_user)) <class 'torch.Tensor'>'''
'''
【重要】
通过调用tensor的grad属性就可以获取tensor的梯度值,所以,
反向传播过程中计算的梯度值都保存在模型参数或者中间变量中,
通过grad属性就可以调用。
需要通过retain_grad()函数事先声明保留变量的梯度值,
这样在变量使用完后还会保存其梯度值
'''
embedding_user.grad = torch.zeros_like(embedding_user)
embedding_item.grad = torch.zeros_like(embedding_item)
self.model.train()
'''伪项目标签'''
'''negative_sample_item(embedding_item)函数返回伪项目数量、对伪项目的裁剪取整评分'''
sampled_items, sampled_rating = self.negative_sample_item(embedding_item)
returned_items = self.items + sampled_items
'''通过GNN调用model得到推断用户嵌入经过预测层得到预测评分'''
predicted = self.GNN(embedding_user, embedding_item, sampled_items) # sampled_items为数量
'''sampled_rating为对伪项目的预测评分'''
loss = self.loss(predicted, sampled_rating)
'''
model.zero_grad()的作用是将所有模型参数的梯度置为0
optimizer.zero_grad()的作用是清除所有优化的torch.Tensor的梯度
除了优化器中所有x的x.grad
在每次loss.backward()之前,不要忘记使用,否则之前的梯度将会累积
'''
self.model.zero_grad()
'''
loss.backward()故名思义,就是将损失loss 向输入侧进行反向传播,
同时对于需要进行梯度计算的所有变量 x(requires_grad=True)计算梯度d/dx loss
并将其累积到梯度x.grad中备用
x.grad = x.grad + d/dx loss
所有的梯度就会自动运算,loss的梯度将会累加到它的.grad属性里面去
'''
loss.backward()
model_grad = []
'''经过LDP对梯度加密Equation(13)'''
'''对模型梯度本地差分隐私加密'''
'''对项目嵌入梯度本地差分隐私加密'''
'''对用户嵌入梯度本地差分隐私加密'''
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, :])
returned_users = self.neighbors + [self.id_self]
user_grad = self.LDP(embedding_user.grad[returned_users, :])
'''算法15行,返回加密的梯度和交互数量:伪项目数量+真实交互项目'''
'''returned_items交互的项目和采样的项目'''
'''returned_users用户本身和用户邻居'''
'''返回模型梯度,用户嵌入梯度,项目嵌入梯度,用户和邻居总数,交互项目和采样项目总数,损失'''
res = (model_grad, item_grad, user_grad, returned_items, returned_users, loss.detach())
return res