【序列召回推荐】(task2)序列召回GRU4Rec模型

news2024/10/6 10:40:07

学习总结:

  • 一般的RNN模型我们的输入和输出是什么,我们对RNN输入一个序列 X = [ x 1 , x 2 , . . . , x n ] X = [x^1,x^2,...,x^n] X=[x1,x2,...,xn] ,注意我们序列中的每一个节点都是一个向量,那么我们的RNN会给我们的输出也是一个序列 Y = [ y 1 , y 2 , . . . , y n ] Y = [y^1,y^2,...,y^n] Y=[y1,y2,...,yn], 那我们如何通过RNN提取出输入序列的特征呢,常用做法有两种(GRU4Rec用的第一种获得序列表征):
    • 取出 y n y^n yn的向量表征作为序列的特征,这里可以认为 x 1 , x 2 , … , x n x^1, x^2, \ldots, x^n x1,x2,,xn的所有信息,所有可以简单的认为 y n y^n yn的结果代表序列的表征
    • 对所有时间步的特征输出做一个Mean Pooling,也就是对 Y = [ y 1 , y 2 , . . . , y n ] Y = [y^1,y^2,...,y^n] Y=[y1,y2,...,yn] 做均值处理,以此得到序列的表征
  • GRU4Rec亮点在于设计了mini-batch和高效的sampling方法。
    • 本质是多分类问题:得到用户embedding后,直接通过多分类进行损失计算,多分类的标签是用户下一次点击的item的index,直接通过User的向量表征和所有的Item的向量做内积算出User对所有Item的点击概率(一个score),然后通过Softmax进行多分类损失计算 。
    • listwise训练模式的Loss 为 torch.nn.CrossEntropyLoss, 即对输出进行 Softmax 处理后取交叉熵。
    • 本task还学习faiss向量检索库和TSNE的Embedding分布可视化。
  • 回顾DIEN:利用 AUGRU(GRU with Attention Update Gate) 组成的序列模型,在兴趣抽取层GRU基础上加入注意力机制,模拟与当前目标广告(Target Ad)相关的兴趣进化过程,兴趣进化层的最后一个状态的输出就是用户当前的兴趣向量 h’(T)。

文章目录

  • 学习总结:
  • 一、序列召回基础
    • 1.1 推荐系统
    • 1.2 推荐系统中的召回
      • (1)基于规则的召回算法
      • (2)基于协调过滤的召回算法
      • (3)基于向量的召回算法
        • 1)基于I2I的召回算法
        • 2)基于U2I的召回算法
      • (4)表征学习 / 匹配函数的学习
  • 二、基于GRU4Rec的基础序列召回
    • 2.1 GRU模型
    • 2.2 基于GRU的序列召回
      • 1)用户嵌入提取和模型训练
      • 2)模型验证和faiss向量召回
      • 3)损失函数
        • (1)Point wise (很多CTR模型使用)
        • (2)Pair wise (BRP损失函数)
        • (3)List wise(softmax后交叉熵)
    • 2.3 GRU4Rec中的trick
  • 三、GRU4Rec代码实践
    • 3.1 Dataset类
    • 3.2 GRU4Rec模型定义
      • (1)paddle版实现
    • 3.3 Pipeline
    • 3.4 基于Faiss的向量召回
    • 3.5 基于TSNE的Item Embedding分布可视化
  • 时间安排
  • Reference

一、序列召回基础

1.1 推荐系统

推荐系统中采用一种“漏斗”的形式,将算法分为多个阶段来进行推荐:

  • 召回:待计算的候选集合大、计算速度快、模型简单、特征较少,尽量让用户感兴趣的物品在这个阶段能够被快速召回,即保证相关物品的召回率。常用的有多路召回(即多种策略进行召回),embedding向量召回
  • 精排:首要目标是得到精准的排序结果。需要处理的物品数量少,可以利用较多的特征,使用比较复杂的模型
  • 重排:为了避免排序阶段的结果趋于同质化,让用户有更好的体验,这里对排序的结果进行多样化考虑,提高用户的使用体验

在这里插入图片描述

1.2 推荐系统中的召回

由于召回算法是面对全部的Item进行计算筛选的,所以我们的召回算法的核心特点如下:算法结构简单;计算效率高;准确率不需要太高;每一种召回算法都会针对性解决某一类问题。一把可以将召回算法分为以下类别:

  • 基于规则
  • 基于协同过滤
  • 向量召回

(1)基于规则的召回算法

基于规则的召回算法比较好理解,就是我们通常的一些运营规则,例如:

  • 召回点击次数最多的Item
  • 召回购买次数最多的Item
  • 召回购买金额最多的Item
  • 召回地域最热的Item

可以看出这种召回算法属于最原始的笨办法,但是这种方法虽然在算法层面的技术难度不高,但是要做出一个非常优秀的策略也是需要极强的业务理解以及极大的人力投入的。

(2)基于协调过滤的召回算法

基于协调过滤的方法有以下的两个大流派:

  • 基于统计的协同过滤
  • 基于MF(Matrix Factorization)的协同过滤

首先我们讨论一下基于统计的协同过滤模型,这种模型是完全基于用户行为记录的统计信息的,不涉及机器学习/深度学习模型的建模以及训练,在基于统计的协同过滤中,一般按照视角分为两种协同过滤模型。

  • 基于User的协同过滤
  • 基于Item的协同过滤

在这里插入图片描述

这两个协同过滤模型的核心思想是统一的,对于基于User的协同过滤模型而言,其核心的假设是:相似的用户可能喜欢相同物品

  • 这一点当然十分符合我们的直观感受了,实际上就是将用户喜欢的商品推荐给和这个用户相似的用户;
  • 所以重点就落在了如何“度量”两个用户的相似度,最直观的做法就是统计两个用户共同喜欢的Item的个数,如果两个用户共同喜欢的Item个数越多,那是不是就代表着两个用户就“越相似”,核心思路是这样的,但是在实际的计算过程中会加一些系数修正。

那么对于基于Item的协同过滤也一样,其核心的假设是:相似的物品可能被同个用户喜欢

  • 我们会对推荐用户喜欢的Item的相似Item,那我们这里的核心其实就是“度量”两个Item的相似度,这里的最直观的做法就是:统计两个Item被用户同时喜欢的次数,可以简单的认为如果两个Item同时在用户喜欢列表中出现的次数越多,就可以认为这两个Item相似,当然了实际实现的时候也会加一些修正系数。
  • 基于MF的协同过滤模型相当于是在基于User-Item的交互矩阵的基础上进行分解的,其示意图如下。也可以简单的认为我们通过Embedding方法对User/Item分别学习到一个向量表征,然后使用内积的方法来计算User-Item的交互概率。

在这里插入图片描述

(3)基于向量的召回算法

基于向量的召回算法算是大量需要深度学习的一个召回算法的流派了,其主要可以分为一下流派:

  • I2I:计算item-item相似度,用于相似推荐、相关推荐、关联推荐
  • U2I:基于矩阵分解,直接给用户推荐item
  • U2U2I:基于用户的协同过滤,先找相似用户,再推荐相似用户喜欢的item
  • U2I2I:基于物品的协同过滤,先统计用户喜爱的item,再推荐他喜欢的item
  • U2TAG2I:基于标签偏好推荐,先统计用户偏好的tag,然后匹配所有的item;其中tag一般是item的标签、分类、关键词等。
  • X2X2X2X:…

其中这里用的最多的就是I2I和U2I这两类算法:

1)基于I2I的召回算法

(1)基于I2I的召回算法大致有如下两个流派:

  • Item-Item相似度可以通过内容理解来生产Item向量,通过向量相似度来度量
  • Item-Item相似度可以通过图表示学习/GNN来生产Item向量,通过向量相似度来度量

这里的I2I向量召回的核心就是通过某种算法生产出Item的向量表征,然后通过向量的相似度来进行召回,其中Faiss可以帮助我们快速的在大规模向量中找出和Query向量最相似的Top-K个向量,这就解决了我们向量召回计算效率的问题。

2)基于U2I的召回算法

(2)基于U2I的召回算法大致有如下两个流派:

  • 输入User特征和Item特征,直接对齐User与Item的向量表征:一般是双塔模型,如DSSM等。
  • 通过User的历史序列提取User的向量表征,然后和Item的表征对齐。即将User的向量在全部的Item向量中进行召回,也是本次学习的主题【序列召回】。核心在两个点:
    • 序列信息的利用以及系列特征的提取
    • 如何通过序列构建User的向量表征

(4)表征学习 / 匹配函数的学习

召回算法一般分类:

  • 表征学习:如下图,input representation可以是无序交互特征、序列特征、多模态、图数据等。

在这里插入图片描述

  • 匹配函数的学习:如下图

在这里插入图片描述
上图摘自《Deep Learning for Matching in Search and Recommendation》李航,何向南 第五章

二、基于GRU4Rec的基础序列召回

2.1 GRU模型

在这里插入图片描述

(1)首先我们明确一下对于一个一般的RNN模型我们的输入和输出是什么,由上图可以看出,我们对RNN输入一个序列 X = [ x 1 , x 2 , . . . , x n ] X = [x^1,x^2,...,x^n] X=[x1,x2,...,xn] ,这里需要注意,我们序列中的每一个节点都是一个向量,那么我们的RNN会给我们的输出也是一个序列 Y = [ y 1 , y 2 , . . . , y n ] Y = [y^1,y^2,...,y^n] Y=[y1,y2,...,yn], 那我们如何通过RNN提取出输入序列的特征呢,这里常用的做法有两种:

  • 取出 y n y^n yn的向量表征作为序列的特征,这里可以认为 x 1 , x 2 , … , x n x^1, x^2, \ldots, x^n x1,x2,,xn的所有信息,所有可以简单的认为 y n y^n yn的结果代表序列的表征
  • 对每一个时间步的特征输出做一个Mean Pooling,也就是对 Y = [ y 1 , y 2 , . . . , y n ] Y = [y^1,y^2,...,y^n] Y=[y1,y2,...,yn] 做均值处理,以此得到序列的表征

(2)这里我们选择第一种方法来获取序列表征,在明确了RNN的一般处理机制之后,我们就会发现,RNN中可以操作的主要集中在对每个时间步i信息和前i-1信息的处理上,这就衍生出LSTM,GRU等一系列方案。GRU的计算逻辑:

  • GRU(Gate Recurrent Unit 门控循环单元)是介于RNN和LSTM的网络,只有两个门(更新门zt和重置门rt,结构比LSTM简单,性能却相当)。可以参考torch官方文档的API介绍。
  • 更新门zt:控制前一时刻的状态信息被带入到当前状态的程度,zt越大则说明带入的信息越多。
  • 重置门rt:控制前一状态有多少信息被写入到当前的候选集 h ~ t \tilde{h}_t h~t上,重置门越小前一状态的信息被写入的就越少。

在这里插入图片描述

2.2 基于GRU的序列召回

1)用户嵌入提取和模型训练

在这里插入图片描述
我们这里首先对Item进行Embedding操作,这里的Embedding层可以认为是一张表,他内部存储的是每一个Item ID到其向量表征的映射,例如:我有10个Item,我想对每个Item表征成一个4维的向量,那么我们可以有如下的Embedding层:

#声明一个10个Item,维度为4的Embedding表
emb_layer = nn.Embedding(10,4) 
# 比如我查询index为 [1,0,1,2,3]的向量
query_index = paddle.to_tensor([1,0,1,2,3]) 
# 可以看出embedding内部的存储是一个10x4的二维矩阵,其中每一行都是一个4维的向量,也就是一个item的向量表征
print(emb_layer.weight) 
# 查询结果是对应index对应在embedding里面的行向量
print(emb_layer(query_index)) 

在获取Item的Embedding向量之后,我们就对序列进行GRU特征提取,注意我们选择的是GRU输出的最后一个节点的向量,我们将这个向量作为序列整体的特征表达:

seq_emb = self.item_emb(item_seq)
seq_emb,_ = self.gru(seq_emb)
user_emb = seq_emb[:,-1,:] #取GRU输出的最后一个Hidden作为User的Embedding

得到用户embedding后,直接通过多分类进行损失计算,多分类的标签是用户下一次点击的item的index,直接通过User的向量表征和所有的Item的向量做内积算出User对所有Item的点击概率,然后通过Softmax进行多分类损失计算:

def calculate_loss(self,user_emb,pos_item):
    all_items = self.item_emb.weight
    scores = paddle.matmul(user_emb, all_items.transpose([1, 0]))
    return self.loss_fun(scores,pos_item)

2)模型验证和faiss向量召回

  • 数据集是:Movielens-20M,我们对其按照User进行了数据划分,按照8:1:1的比例做成了train/valid/test数据集。
  • 对于测试阶段,我们选择用户的前80%的行为作为序列输入,我们的标签是用户后20%的行为,使用Faiss进行向量召回:
# 第一步:我们获取所有Item的Embedding表征,然后将其插入Faiss(向量数据库)中
item_embs = model.output_items().cpu().detach().numpy()
item_embs = normalize(item_embs, norm='l2')
gpu_index = faiss.IndexFlatIP(hidden_size)
gpu_index.add(item_embs)

# 第二步:根据用户的行为序列生产User的向量表征
user_embs = model(item_seq,mask,None,train=False)['user_emb']
user_embs = user_embs.cpu().detach().numpy()

# 第三步:对User的向量表征在所有Item的向量中进行Top-K检索
D, I = gpu_index.search(user_embs, topN)  # Inner Product近邻搜索,D为distance,I是index

3)损失函数

pointwise排序不适合该类网络,作者使用了pairwise(正样本的score大于负样本即可)方式训练,并且损失函数为BPR(Bayesian Personalized Ranking)或TOP1。

(1)BPR考虑用户物品交互中的可观察项和不可观察项的相对顺序,BPR假定更能反映出用户偏好的可观察项的交互相较于那些不可观察项来说应该赋予高的预测值。BPR损失函数:
 Loss  = ∑ ( u , i , j ) ∈ O − ln ⁡ σ ( y ^ u i − y ^ u j ) + λ ∥ Θ ∥ 2 2 \text { Loss }=\sum_{(u, i, j) \in O}-\ln \sigma\left(\hat{y}_{u i}-\hat{y}_{u j}\right)+\lambda\|\Theta\|_2^2  Loss =(u,i,j)Olnσ(y^uiy^uj)+λΘ22
(2)TOP1 loss是对bpr loss的一个改进,相比bpr 在loss 后面加了一项负样本正则项(二范数),希望负样本评分越小越好: L s = 1 N s ∑ j = 1 N s σ ( r ^ s , j − r ^ s , i ) + σ ( r ^ s , j 2 ) L_s=\frac{1}{N_s} \sum_{j=1}^{N_s} \sigma\left(\hat{r}_{s, j}-\hat{r}_{s, i}\right)+\sigma\left(\hat{r}_{s, j}^2\right) Ls=Ns1j=1Nsσ(r^s,jr^s,i)+σ(r^s,j2)

TOP1损失函数:使用传统的bpr loss进行优化时,可能会面临正负样本的评分都持续增长的问题。比如1 5 8的例子,优化1的时候,一定希望1对于第一个序列评分高,5较低;而优化5的时候,一定希望1评分低,5评分高。当GRU面临可能的梯度消失问题时,可能丧失user的序列特征,只关注于当下的物品,如果序列的局部比较接近,就会使模型优化在1,5之间陷入两难局面。所以才会正负采样的评分同步增长。

为了后面代码的复用,下面和后面的task都是使用listwise进行模型训练。关于三种训练方式这里做一个回顾:【召回中的三种训练方式】,召回中,一般的训练方式分为三种:point-wise、pair-wise、list-wise。每一种不同的训练方式也对应不同的Loss。参考下图,其中a表示user的embedding,b+表示正样本的embedding,b-表示负样本的embedding。
在这里插入图片描述

(1)Point wise (很多CTR模型使用)

思想:将召回视作二分类,独立看待每个正样本、负样本。

对于一个召回模型:

  • 输入二元组<User, Item>,
  • 输出 P ( U s e r P(U s e r P(User, Item ) ) ), 表示 User 对 Item 的感兴趣程度。
  • 训练目标为: 若物品为正样本, 输出 cos ⁡ ( a , b ) \cos (\mathbf{a}, \mathbf{b}) cos(a,b)应尽可能接近 1 , 负样本则输出尽可能接近 0 。 采用的 Loss 最常见的就是 BCELoss(Binary Cross Entropy Loss)。
  • 控制正负样本数量为1:2或者1:3

(2)Pair wise (BRP损失函数)

思想:用户对正样本感兴趣的程度应该大于负样本
在这里插入图片描述
在这里插入图片描述
改进的损失函数Logistic loss:
在这里插入图片描述

对于一个召回模型:

  • 输入三元组<User, ItemPositive, ItemNegative>,
  • 输出兴趣得分 P ( P( P( User, ItemPositive ) , P ( ), P( ),P( User, ItemNegative ) ) ), 表示用户对正样本物品和负样 本物品的兴趣得分。
  • 训练目标为:正样本的兴趣得分应尽可能大于负样本的兴趣得分。

Loss 为 BPRLoss(Bayes Personalized Ranking Loss)。Loss 的公式这里放 一个公式, 详细可以参考【贝叶斯个性化排序(BPR)算法小结】(链接里的内容和下面的公式有些细微的差别, 但是思想是一 样的)。公式: L o s s = 1 N ∑ N i i = 1 − log ⁡ ( L o s s=\frac{1}{N} \sum^{N} i_{i=1}-\log ( Loss=N1Nii=1log( sigmoid ( ( ( pos_score − - neg_score ) ) )) ))

(3)List wise(softmax后交叉熵)

思想:思想同Pair wise,但是实现上不同。
在这里插入图片描述
引入Softmax 激活函数,最小化交叉熵损失,相当于最大化 S + \mathrm{S}^{+} S+,也即最大化正样本的相似度。
在这里插入图片描述

对于一个召回模型:

  • 输入 N + 2 \boldsymbol{N}+2 N+2 元 组 ⟨ \langle User, ItemPositive, ItemNeg_1,…, ItemNeg_N ⟩ \rangle
  • 输出用户对 1 个正样本和 N \mathrm{N} N 个负样本的兴趣得分。
  • 训练目标为:对正样本的兴趣得分应该尽可能大于其他所有负样本的兴趣得分。 Loss 为 torch.nn.CrossEntropyLoss, 即对输出进行 Softmax 处理后取交叉熵。

PS: 召回的 List wise 方式容易和 Ranking 中的 List wise 混淆, 虽然二者名字一样, 但 ranking 的 List wise 考虑了样本之间的顺序关系。例如 ranking 中会考虑 MAP、NDCP 等考虑顺序的指标作为评价指标, 而 Matching 中的 List wise 没有考虑顺序

2.3 GRU4Rec中的trick

(1)session-parallel mini-batch:一般RNN如果为了用batch加速,需要统一长度(长度短的就进行padding),但是由于推荐系统中item长尾分布,长序列和短序列相差很大,padding方法不合适。

下图中是五个并行的session, 每次输入到GRU中的是每一个序列 s s s t s t_s ts 时刻的行为, GRU预测序列在 t s + 1 t_s+1 ts+1 的行为, 由于序列有长有短, 所以当一个序列并行结束后, 将新的序列补进来, 这样可以最大限度提高并行效率, 甚至不需要padding
在这里插入图片描述
(2)基于流行度的负采样,并且在batch中负采样:在目标样本中根据热门程度进行采样,采样后将同一个mini-batch中,但是是其他session内的next-item作为负样本,用这些正负样本进行训练模型。如下图: 序列的next-item的gt是1, 5, 8对于1来讲 5,8是负样本。
在这里插入图片描述

和随机抽样相比,优势有两个: 一个是基于pop的采样,可解释性更强,另一个是对于batch内需要计算的item是一致的,那么可以将batch内所有序列的next-item预测,统一成一个矩阵运算,大大降低显存占用。

三、GRU4Rec代码实践

3.1 Dataset类

这里的hist_mask_list是像transformer一样的mask处理序列长短不一致的问题。和task1说的一样,在自定义的数据类中__getitem__需要返回样本集合中的样本对,并且可以进行一定的转换。

这里就是返回三个元素:
(1)用户历史序列hist_item_list
(2)掩码矩阵
(3)标签即下一个点击的item_id,因为是多分类任务,预测下一个会点击的item,这里的label就是最后的一个item

class SeqnenceDataset(Dataset):
    def __init__(self, config, df, phase='train'):
        self.config = config
        self.df = df
        self.max_length = self.config['max_length']
        self.df = self.df.sort_values(by=['user_id', 'timestamp'])
        self.user2item = self.df.groupby('user_id')['item_id'].apply(list).to_dict()
        self.user_list = self.df['user_id'].unique()
        self.phase = phase

    def __len__(self, ):
        return len(self.user2item)

    def __getitem__(self, index):
        if self.phase == 'train':
            user_id = self.user_list[index]
            item_list = self.user2item[user_id]
            hist_item_list = []
            hist_mask_list = []

            k = random.choice(range(4, len(item_list)))  # 从[8,len(item_list))中随机选择一个index
            # k = np.random.randint(2,len(item_list))
            item_id = item_list[k]  # 该index对应的item加入item_id_list

            if k >= self.max_length:  # 选取seq_len个物品
                hist_item_list.append(item_list[k - self.max_length: k])
                hist_mask_list.append([1.0] * self.max_length)
            else:
                hist_item_list.append(item_list[:k] + [0] * (self.max_length - k))
                hist_mask_list.append([1.0] * k + [0.0] * (self.max_length - k))

            return paddle.to_tensor(hist_item_list).squeeze(0), paddle.to_tensor(hist_mask_list).squeeze(
                0), paddle.to_tensor([item_id])
        else:
            user_id = self.user_list[index]
            item_list = self.user2item[user_id]
            hist_item_list = []
            hist_mask_list = []

            k = int(0.8 * len(item_list))
            # k = len(item_list)-1

            if k >= self.max_length:  # 选取seq_len个物品
                hist_item_list.append(item_list[k - self.max_length: k])
                hist_mask_list.append([1.0] * self.max_length)
            else:
                hist_item_list.append(item_list[:k] + [0] * (self.max_length - k))
                hist_mask_list.append([1.0] * k + [0.0] * (self.max_length - k))

            return paddle.to_tensor(hist_item_list).squeeze(0), paddle.to_tensor(hist_mask_list).squeeze(
                0), item_list[k:]
	# 用后20%作为测试集
    def get_test_gd(self):
        self.test_gd = {}
        for user in self.user2item:
            item_list = self.user2item[user]
            test_item_index = int(0.8 * len(item_list))
            self.test_gd[user] = item_list[test_item_index:]
        return self.test_gd

原论文是按照8:1:1划分训练集、验证集、测试集(按照用户划分,更具泛化能力)。

  • 训练样本:全部点击序列 ( e 1 ( u ) , e 2 ( u ) , … , e k ( u ) , … , e n ( u ) ) \left(e_1^{(u)}, e_2^{(u)}, \ldots, e_k^{(u)}, \ldots, e_n^{(u)}\right) (e1(u),e2(u),,ek(u),,en(u)),用前(n-1)个item预测第n个;
  • 验证集合测试集则是使用用户前80%的点击序列作为模型输入,然后预测后面的点击序列。所以后面定义valid_dataloadertest_dataloader时都是需要加上collate_fn函数参数,对于每个用户的item历史行为,后20%的item数量不一定相同。

3.2 GRU4Rec模型定义

(1)paddle版实现

其实这里paddle和torch一样有nn.GRU可以直接调用。模型forward中取GRU输出的最后一个Hidden作为User的Embedding。注意nn.GRU层中需要将time_major设置为false,即为[batch, seq_len, emb_size],如果在torch中则是batch_first参数:
在这里插入图片描述

class GRU4Rec(nn.Layer):
    def __init__(self, config):
        super(GRU4Rec, self).__init__()

        self.config = config
        self.embedding_dim = self.config['embedding_dim']
        self.max_length = self.config['max_length']
        self.n_items = self.config['n_items']
        self.num_layers = self.config['num_layers']

        self.item_emb = nn.Embedding(self.n_items, self.embedding_dim, padding_idx=0)
        self.gru = nn.GRU(
            input_size=self.embedding_dim,
            hidden_size=self.embedding_dim,
            num_layers=self.num_layers,
            time_major=False,
        )
        self.loss_fun = nn.CrossEntropyLoss()
        self.reset_parameters()
	
	# 计算损失函数,使用listwise模型训练,loss_fun(dot_res, pos_item)
    def calculate_loss(self,user_emb,pos_item):
        all_items = self.item_emb.weight
        scores = paddle.matmul(user_emb, all_items.transpose([1, 0]))
        return self.loss_fun(scores,pos_item)

    def output_items(self):
        return self.item_emb.weight

    def reset_parameters(self, initializer=None):
        for weight in self.parameters():
            # hekaiming参数初始化
            paddle.nn.initializer.KaimingNormal(weight)

    def forward(self, item_seq, mask, item, train=True):
        seq_emb = self.item_emb(item_seq) # (b,s,e)
        seq_emb,_ = self.gru(seq_emb)
        # 取GRU输出的最后一个Hidden作为User的Embedding
        user_emb = seq_emb[:,-1,:] 
        if train:
            loss = self.calculate_loss(user_emb,item)
            output_dict = {
                'user_emb':user_emb,
                'loss':loss
            }
        else:
            output_dict = {
                'user_emb':user_emb
            }
        return output_dict

取GRU输出的最后一个Hidden作为序列的embedding,即User的Embedding。

3.3 Pipeline

定义一些模型参数、保存模型、导入模型的操作。

(1)额外注意my_collate函数,是为了将后20%作为测试集时,__getitem__函数返回值之一item_list[k:]长度可能不一致的一个处理。

def my_collate(batch):
    hist_item, hist_mask, item_list = list(zip(*batch))

    hist_item = [x.unsqueeze(0) for x in hist_item]
    hist_mask = [x.unsqueeze(0) for x in hist_mask]

    hist_item = paddle.concat(hist_item,axis=0)
    hist_mask = paddle.concat(hist_mask,axis=0)
    return hist_item,hist_mask,item_list

(2)早停策略:patience=5只如果连续5个epoch指标都没上升则停止训练。
(3)训练后的指标结果如下,这里发现第5个epoch就停止了,说明已经连续5个epoch指标没有涨了,所以停止训练模型,具体指标如下,完整代码参考paddle传统序列召回实践:GRU4Rec。这里第5个epoch的test指标用的第0个epoch的结果(权重)进行测试。
在这里插入图片描述

3.4 基于Faiss的向量召回

在这里插入图片描述

  • Faiss:Facebook 开源的向量相似检索库:https://github.com/facebookresearch/faiss,类似的有annoy向量检索库。从多媒体文档中快速搜索出相似的条目——这个场景下的挑战是基于查询的传统搜索引擎无法解决的。Faiss使用的数据流如上图所示。可以通过如下命令下载:
#cpu 版本
conda install faiss-cpu -c pytorch
# GPU 版本
conda install faiss-gpu cudatoolkit=8.0 -c pytorch # For CUDA8
conda install faiss-gpu cudatoolkit=9.0 -c pytorch # For CUDA9
conda install faiss-gpu cudatoolkit=10.0 -c pytorch # For CUDA10
  • 下面代码中get_predict函数使用了faiss向量相似检索库;evaluate函数计算recall、hitrate、ndcg。hitrate指标在top N推荐中经常使用:
    H R =  测试集中的item出现在  T o p − N  推荐列表中的用户数量   用户总数  H R=\frac{\text { 测试集中的item出现在 } T o p-N \text { 推荐列表中的用户数量 }}{\text { 用户总数 }} HR= 用户总数  测试集中的item出现在 TopN 推荐列表中的用户数量 
def get_predict(model, test_data, hidden_size, topN=20):
	# 1. 获得所有item的embedding,然后插入faiss向量数据库中
    item_embs = model.output_items().cpu().detach().numpy()
    item_embs = normalize(item_embs, norm='l2')
    gpu_index = faiss.IndexFlatIP(hidden_size)
    gpu_index.add(item_embs)
    
    test_gd = dict()
    preds = dict()
    
    user_id = 0

    for (item_seq, mask, targets) in tqdm(test_data):

        # 获取用户嵌入
        # 2. 多兴趣模型,shape=(batch_size, num_interest, embedding_dim)
        # 其他模型,shape=(batch_size, embedding_dim)
        user_embs = model(item_seq,mask,None,train=False)['user_emb']
        user_embs = user_embs.cpu().detach().numpy()

        # 用内积来近邻搜索,实际是内积的值越大,向量越近(越相似)
        # 非多兴趣模型评估
        if len(user_embs.shape) == 2:  
            user_embs = normalize(user_embs, norm='l2').astype('float32')
            # 3. Inner Product近邻搜索,D为distance,I是index
            D, I = gpu_index.search(user_embs, topN)  
#             D,I = faiss.knn(user_embs, item_embs, topN,metric=faiss.METRIC_INNER_PRODUCT)
			# 每个用户的label列表,此处item_id为一个二维list,验证和测试是多label的
            for i, iid_list in enumerate(targets):  
                test_gd[user_id] = iid_list
                preds[user_id] = I[i,:]
                user_id +=1
        else:  # 多兴趣模型评估
            ni = user_embs.shape[1]  # num_interest
            user_embs = np.reshape(user_embs,
                                   [-1, user_embs.shape[-1]])  # shape=(batch_size*num_interest, embedding_dim)
            user_embs = normalize(user_embs, norm='l2').astype('float32')
            # Inner Product近邻搜索,D为distance,I是index
            D, I = gpu_index.search(user_embs, topN)  
#             D,I = faiss.knn(user_embs, item_embs, topN,metric=faiss.METRIC_INNER_PRODUCT)
            # 每个用户的label列表,此处item_id为一个二维list,验证和测试是多label的
            for i, iid_list in enumerate(targets):  
                recall = 0
                dcg = 0.0
                item_list_set = []

                # 将num_interest个兴趣向量的所有topN近邻物品(num_interest*topN个物品)集合起来按照距离重新排序
                item_list = list(
                    zip(np.reshape(I[i * ni:(i + 1) * ni], -1), np.reshape(D[i * ni:(i + 1) * ni], -1)))
                # 降序排序,内积越大,向量越近
                item_list.sort(key=lambda x: x[1], reverse=True)  
                # 按距离由近到远遍历推荐物品列表,最后选出最近的topN个物品作为最终的推荐物品
                for j in range(len(item_list)):  
                    if item_list[j][0] not in item_list_set and item_list[j][0] != 0:
                        item_list_set.append(item_list[j][0])
                        if len(item_list_set) >= topN:
                            break
                test_gd[user_id] = iid_list
                preds[user_id] = item_list_set
                user_id +=1
    return test_gd, preds

def evaluate(preds,test_gd, topN=50):
    total_recall = 0.0
    total_ndcg = 0.0
    total_hitrate = 0
    for user in test_gd.keys():
        recall = 0
        dcg = 0.0
        item_list = test_gd[user]
        for no, item_id in enumerate(item_list):
            if item_id in preds[user][:topN]:
                recall += 1
                dcg += 1.0 / math.log(no+2, 2)
            idcg = 0.0
            for no in range(recall):
                idcg += 1.0 / math.log(no+2, 2)
        total_recall += recall * 1.0 / len(item_list)
        if recall > 0:
            total_ndcg += dcg / idcg
            total_hitrate += 1
    # recall/ndcg/hitrate
    total = len(test_gd)
    recall = total_recall / total
    ndcg = total_ndcg / total
    hitrate = total_hitrate * 1.0 / total
    return {f'recall@{topN}': recall, f'ndcg@{topN}': ndcg, f'hitrate@{topN}': hitrate}

# 指标计算
def evaluate_model(model, test_loader, embedding_dim,topN=20):
    test_gd, preds = get_predict(model, test_loader, embedding_dim, topN=topN)
    return evaluate(preds, test_gd, topN=topN)

3.5 基于TSNE的Item Embedding分布可视化

t-SNE(t-Distributed Stochastic Neighbor Embedding)是一种非线性降维技术,主要用途为对高维数据进行可视化。下面这里首先进行了归一化操作。

def plot_embedding(data, title):
    x_min, x_max = np.min(data, 0), np.max(data, 0)
    data = (data - x_min) / (x_max - x_min)
 
    fig = plt.figure(dpi=120)
    plt.scatter(data[:,0], data[:,1], marker='.')
    
    plt.xticks([])
    plt.yticks([])
    plt.title(title)
    plt.show()

在这里插入图片描述

时间安排

任务信息截止时间完成情况
11月14日周一正式开始
Task01:Paddle开发深度学习模型快速入门11月14、15、16日周三完成
Task02:传统序列召回实践:GRU4Rec11月17、18、19日周六完成
Task03:GNN在召回中的应用:SR-GNN11月20、21、22日周二
Task04:多兴趣召回实践:MIND11月23、24、25、26日周六
Task05:多兴趣召回实践:Comirec-DR11月27、28日周一
Task06:多兴趣召回实践:Comirec-SA11月29日周二

Reference

[1] paddle传统序列召回实践:GRU4Rec
https://github.com/THUDM/ComiRec/blob/a576eed8b605a531f2971136ce6ae87739d47693/src/model.py
https://github.com/RUCAIBox/RecBole/blob/master/recbole/model/sequential_recommender/gru4rec.py
[2] 什么是推荐系统?推荐系统类型、用例和应用
[3] 推荐系统2–隐语义模型(LFM)和矩阵分解(MF)
[4] 推荐场景中召回模型的演化过程. 京东大佬
[5] 论文阅读笔记一:SESSION-BASED RECOMMENDATIONS WITHRECURRENT NEURAL NETWORKS
[6] GRU4Rec2 《Recurrent Neural Networks with Top-k Gains for Session-based Recommendations》
[7] DeepLearning-500-questions
[8] GRU4Rec入门
[9] 序列推荐模型(四): GRU4Rec
[10] Deep Learning for Matching in Search and Recommendation.李航,何向南
[11] 深度学习之GRU网络
[12] 向量检索库Faiss使用指北
[13] https://pytorch.org/docs/stable/generated/torch.nn.GRU.html?highlight=gru#torch.nn.GRU
[14] word2vec Parameter Learning Explained.Xin Rong
[15] 【RS样本选择与构造】正负样本的选择 | 负采样 | pointwise、pairwise、listwise
[16] 推荐系统中如何评价embedding的质量
[17] 贝叶斯个性化排序(BPR)算法小结
[18] 图像检索:再叙ANN Search
[19] Faiss:https://blog.razrlele.com/p/2594
[20] Faiss官方文档:https://github.com/facebookresearch/faiss
[21] 深度学习在序列化推荐中的应用 (1)-GRU4REC 以及扩展
[22] GRU4Rec v2 - Recurrent Neural Networks with Top-k Gains for Session-based Recommendations
[23] Faiss从入门到实战精通
[24] 时间序列数据的预处理方法总结

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【python黑帽子】——(一)搭建扫描器入门介绍

作者名&#xff1a;Demo不是emo 主页面链接&#xff1a;主页传送门创作初心&#xff1a;舞台再大&#xff0c;你不上台&#xff0c;永远是观众&#xff0c;没人会关心你努不努力&#xff0c;摔的痛不痛&#xff0c;他们只会看你最后站在什么位置&#xff0c;然后羡慕或鄙夷座右…

Python安装教程

Python安装 1.浏览器打开网址:www.python.org 2.根据电脑系统选择下载 3.确定电脑系统属性&#xff0c;此处我们以win10的64位操作系统为例 4.安装python 3.6.3 双击下载的安装包 python-3.6.3.exe 注意要勾选&#xff1a;Add Python 3.6 to PATH 点击 Customize installat…

4 种经典方法IB 数学证明题分享给大家

学习数学时感觉最有意思的题目就是证明题了&#xff0c;证明题能练习一种能力&#xff1a; 你知道一件事情时对的&#xff0c;怎么说清楚它是对的&#xff1b;你认为一件事情时错的&#xff0c;怎么说清楚它是错的。 这和生活中的辩论有点像&#xff0c;要有理有据地说清楚原因…

[附源码]Node.js计算机毕业设计蛋糕店会员系统Express

项目运行 环境配置&#xff1a; Node.js最新版 Vscode Mysql5.7 HBuilderXNavicat11Vue。 项目技术&#xff1a; Express框架 Node.js Vue 等等组成&#xff0c;B/S模式 Vscode管理前后端分离等等。 环境需要 1.运行环境&#xff1a;最好是Nodejs最新版&#xff0c;我…

Servlet的生命周期

servlet 1.servlet是什么 2.servlet生命周期 3. servlet 工作原理 4 .ServletContextListener 什么是Servlet&#xff1f; Servlet是JavaWeb的 三大组件之一 &#xff0c;它属于动态资源。Servlet的作用是处理请求&#xff0c;服务器会把接收到的请求交给Servlet来处理&…

基于RSS和TOA两种方法的无线传感器网络定位测量算法matlab仿真

up目录 一、理论基础 二、核心程序 三、测试结果 一、理论基础 无线传感器网络(Wireless Sensor Networks, WSN)是一种分布式传感网络&#xff0c;它的末梢是可以感知和检查外部世界的传感器。WSN中的传感器通过无线方式通信&#xff0c;因此网络设置灵活&#xff0c;设备位…

去哪儿旅行微服务架构实践,全文带图加详细解析,带你多方面了解

文章目录一、背景介绍二、微服务架构模式的最佳实践三、微服务开发效率提升实践四、服务治理实践五、ServiceMesh 尝试六、总结今天我带来的主题是去哪儿旅行 微服务架构实践。我将从以下几个方面进行介绍&#xff1a;背景介绍微服务架构模式的最佳实践微服务开发效率的提升实践…

前台用户注册_发送邮件配置

在用户注册成功后&#xff0c;要向用户的邮箱发送一封激活邮件&#xff0c;发送邮件需要在系统中配置发件人&#xff0c;同学们使用自己的邮箱作为发件人即可。 配置邮箱第三方登录。 我们在系统中使用邮箱发送邮件属于第三方登录&#xff0c;而市面上的邮箱默认是不能第三方…

自监督学习系列(四):基于蒸馏的图片掩码学习

前文 好久不见&#xff01;自监督系列文章继续更新啦&#xff01;在前几期的文章我们介绍了基于辅助任务&#xff0c;对比学习&#xff0c;和图片掩码学习的自监督学习范式 (对比学习&#xff0c;图片掩码学习其实也可以归属于基于辅助任务的自监督学习&#xff0c;由于这两类…

百度安全怎么查询,怎么彻底解决百度安全弹出的风险提示

当我们在百度搜索自己的网站时&#xff0c;搜索结果中出现各种风险提示&#xff0c;比如安全联盟提醒您&#xff1a;该网站可能存在安全风险&#xff0c;请谨慎访问&#xff01; 别慌&#xff01;今天我们就来解决百度安全弹出的风险提示的问题。 第一步&#xff1a;查询网站…

Python 自动化测试框架unittest与pytest的区别

这篇文章主要讲unittest与pytest的区别&#xff0c;pytest相对unittest而言&#xff0c;代码简洁&#xff0c;使用便捷灵活&#xff0c;并且插件很丰富。 Unittest vs Pytest 主要从用例编写规则、用例的前置和后置、参数化、断言、用例执行、失败重运行和报告这几个方面比较…

传奇GOM引擎微端架设教程

传奇GOM引擎微端架设教程 GOM引擎架设微端需要准备好微端程序&#xff0c;用网站下载在服务器的版本 &#xff08;注&#xff1a;本文章图有打码处因平台GZ原因需打码望读者理解&#xff09; Mirserver文件一般都是自带微端程序的&#xff0c;偶尔也有版本没有微端程序那我们…

基于多目标遗传算法(NSGA-II)和多目标粒子群算法(MOPSO)的分布式仿真系统双目标负载平衡模型【Matlab代码实现】

&#x1f4a5;&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️❤️&#x1f4a5;&#x1f4a5;&#x1f4a5;&#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜密&#xff0c;逻辑清…

Oracle单机部署:GI安装

Oracle单机部署&#xff1a;GI安装存储配置ASM磁盘空间评估GI单机安装配置GI图形化安装流程安装后测试&#x1f42c; 使用grid用户来安装GI。 存储配置 Oracle存储支持Oracle ASM、Oracle ACFS、本地文件系统、网络文件系统&#xff08;NFS/NAS&#xff09;、Oracle Memory S…

RK3588平台开发系列讲解(RTC篇)RTC的使用

平台内核版本安卓版本RK3588Linux 5.10Android 12文章目录 一、RTC简介二、HYM8563驱动配置2.1、设备树配置2.1、驱动代码三、RTC的使用3.1、SYSFS接口3.2、PROCFS接口3.3、IOCTL接口沉淀、分享、成长,让自己和他人都能有所收获!😄 📢本篇将介绍RK3588平台RTC驱动及使用方…

玩以太坊链上项目的必备技能(基本类型转换以及推断-Solidity之旅六)

基本类型之间的转换 熟悉过其他编程语言的您&#xff0c;对基本类型之间的转换并不陌生吧&#xff01;当然&#xff0c;这基本类型进行转换可分为隐式转换和显示转换。 隐式转换 Solidity 支持隐式转换&#xff0c;通过编译器自动进行转换&#xff0c;而不要开发人员的干涉&…

信道估计算法误码率仿真,对比不同导频长度,对比不同信道估计算法包括CS-OMP,LS,MMSE

目录 1.算法描述 2.仿真效果预览 3.MATLAB核心程序 4.完整MATLAB 1.算法描述 所谓信道估计&#xff0c;就是从接收数据中将假定的某个信道模型的模型参数出来的过程。如果信道是线性的话&#xff0c;那么信道估计就是对系统冲激响应进行估计。 CS-OMP 正则正交匹配追踪(Re…

分布式操作系统 - 5.分布式命名管理

文章目录1.基本概念2.非结构化命名管理2.1 简单的实体定位方法&#xff1a;广播和多播方法&#xff08;1&#xff09;广播方法&#xff08;broadcast&#xff09;&#xff08;2&#xff09;多播方法&#xff08;multicast&#xff09;&#xff08;3&#xff09;问题&#xff1a…

图形API学习工程(30):尝试使用panorama来代替Cubemap作为全景图

工程GIT地址&#xff1a;https://gitee.com/yaksue/yaksue-graphics 前言 为了能得到全方位的光照数据&#xff0c;我之前学习了使用CubeMap作为全景图。CubeMap包含六张贴图对应了上下左右前后六个方向的数据。但是最近在下载全景图的资源时&#xff0c;看到很多并非是CubeM…

Python基于Django的毕业设计论文提交过程管理系统

项目介绍 在各学校的教学过程中&#xff0c;django过程管理系统是一项非常重要的事情。随着计算机多媒体技术的发展和网络的普及。采用当前流行的B/S模式以及3层架构的设计思想通过Python技术来开发此系统的目的是建立一个配合网络环境的django过程管理系统的平台&#xff0c;这…