【代码解析(3)】Federated Social Recommendation with Graph Neural Network

该代码实现了一个用户类,用于处理推荐系统中的用户数据,包括用户ID、交互项、评分、邻居等。用户类包含了构建局部图、获取用户和邻居嵌入、GNN模型以及使用Laplacian噪声进行本地差分隐私保护的梯度更新等功能。
摘要由CSDN通过智能技术生成

user.py

#!/usr/bin/env python
# -*- 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



评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值