【DataWhale推荐系统论文组队task2--session-based RS with RNN】


NOTE:本文附录的代码引用自Datawhale推荐系统论文组队学习task2中的代码实现

一、序列召回基础知识梳理

1.1 推荐系统

为了缓解当下数据过载的现象,使用户更加快速的获取自己想要的信息,这就是推荐系统出现的必要性。推荐系统主要从以下三个阶段从海量的数据信息中筛选出用户可能感兴趣的N个物品,即:

  1. 召回阶段:这一阶段的目的主要是快速地从海量的数据信息中筛选出用户可能感兴趣的物品,通常对于及时性的要求较高,任务重点在于快速,所以会损失一部分精度。常用的召回方法有多路召回和embedding向量召回
  2. 精排阶段:这一阶段的目的是对召回阶段返回得到的若干物品,按照用户可能感兴趣的概率进行排序,然后返回一个列表,任务重点在于精确,因为需要得到一个有顺序的列表,所以涉及到的特征较多,模型复杂度也高
  3. 重排阶段:这一阶段个人认为更像是对整个推荐系统的一个后续优化,因为用户的兴趣可能会不停的发生变化,所以在精排阶段返回的推荐列表,可能在一段时间之后无法迎合用户的需求,这就需要对推荐结果进行一个重排,任务重点在于动态的迎合用户的喜好
    用一张图来直观的表达各阶段的关系:
    在这里插入图片描述

1.2 召回阶段

本次组队学习的任务重点在于召回阶段,所以本博客所记录的主要内容也以召回为主。召回的特点在上一小节中也有了介绍。

1.2.1 召回算法分类

  1. 基于规则的召回算法

    简介:这种方式原理较为简单,就是提前制定好某种规则来统计目标物品,比如就根据点击率来召回若干项目

  2. 基于协同过滤的召回算法

    简介:这种一种非常经典,而且目前仍在使用的召回算法类型,核心思想是物以类聚人以群分,细分为基于项目和基于用户的协同过滤,两种算法形式都以若干用户(或者项目)对目标用户的影响强度为基础来建模,最终得到召回列表
    在这里插入图片描述
    后来衍生出了基于模型的协同过滤算法,最经典的就是MF模型,通过在潜在空间对用户和项目进行建模,得到一个低维的embedding

  3. 基于向量的召回算法

    简介:这类召回算法大多基于深度学习,主要是通过各类神经网络学习到更加精准的用户和项目的embedding表示,比如最近很火的GNN ,通过用户和项目的历史交互数据来完成信息的聚合传播过程,在这一过程中,用户和项目的embedding的精确度得到了很大的提升。

1.3 本文模型的目标

  1. 序列信息的利用和序列特征的提取
  2. 如何通过序列构建用户的embedding

二、Session-based recommendations with Recurrent Neural Networks

2.1 会话推荐任务简介

session recommendation 是推荐系统当中的一个子分支,解决的是匿名用户的推荐问题,也就是在网络当中建立一次会话当中生成即时推荐,换句话说就是不知道用户是谁,这种方法可以对一些不需要登陆,或者无法记录用户浏览行为的网站快速产生有效的推荐。

2.2 本文的创新点

  1. 将RNN引入了推荐系统领域
  2. 对整个会话进行建模,解决了真实世界中推荐系统只能建模短对话的问题
  3. 通过引入排序损失函数(比如BPRloss)对传统的RNN进行了一些改进,使其更适合推荐系统领域
  4. 基于会话推荐任务对传统的GRU进行改造,提出了会话并行小批量采样的策略、基于输出的小批量采样

2.3 RNN模型简介

在B站上看到了一个简短但是很有意思的解释RNN的视频:RNN的通俗解释 制作by up主耿直哥、

2.4 GRU单元

GRU是一个精细的RNN的单元,被提出用来解决梯度消失的问题,其可以了解何时更新单元的隐藏状态以及具体更新多少个单元,GRU的激活是线性的,由上一阶段的激活和候选激活组成:
h t = ( 1 − z t ) h t − 1 + z t h ^ t \mathbf{h}_{\mathbf{t}}=\left(1-\mathbf{z}_{\mathbf{t}}\right) \mathbf{h}_{\mathbf{t}-1}+\mathbf{z}_{\mathbf{t}} \hat{\mathbf{h}}_{\mathbf{t}} ht=(1zt)ht1+zth^t
其中 z t = σ ( W z x t + U z h t − 1 ) \mathbf{z}_{\mathbf{t}}=\sigma\left(W_{z} \mathbf{x}_{\mathbf{t}}+U_{z} \mathbf{h}_{\mathbf{t}-\mathbf{1}}\right) zt=σ(Wzxt+Uzht1) h t ^ = tanh ⁡ ( W x t + U ( r t ⊙ h t − 1 ) ) \hat{\mathbf{h}_{\mathbf{t}}}=\tanh \left(W \mathbf{x}_{\mathbf{t}}+U\left(\mathbf{r}_{\mathbf{t}} \odot \mathbf{h}_{\mathbf{t}-\mathbf{1}}\right)\right) ht^=tanh(Wxt+U(rtht1))
然后最终得到的输出是
r t = σ ( W r x t + U r h t − 1 ) \mathbf{r}_{\mathbf{t}}=\sigma\left(W_{r} \mathbf{x}_{\mathbf{t}}+U_{r} \mathbf{h}_{\mathbf{t}-1}\right) rt=σ(Wrxt+Urht1)
下图是GRU的计算示意图:
在这里插入图片描述

2.5 本文模型概括

模型的初始输入是用项目独热编码生成了初始embedding作为输入层,模型的核心是GRU层,可以在最后一层和输出之间增加额外的全连接层层。输出是项目的预测偏好,即每个项目成为会话中下一个项目的可能性。当使用多个GRU层时,前一层的隐藏状态是下一层的输入,下图是模型的框架:
在这里插入图片描述
此外,为了使RNN更好地适应推荐任务,本模型还做了以下几点改进:

2.5.1 SESSION-PARALLEL MINI-BATCHES

传统的RNN主要用于NLP的任务场景,通常是基于一个滑动窗口在一个句子的单词上进行滑动,将这些窗口片段相邻的位置片段形成一个小批量。但是由于会话推荐任务中,每个会话的长度是不一样的,而且本模型的最终的目的是捕捉一个会话如何随着时间的推移而演变的,所以传统GRU单元并不能很好的处理会话推荐问题。
本文为了解决这个问题,提出了一种并发式的小批量样本采集。首先,我们为会话创建一个顺序。然后,我们使用前X个会话的第一个事件形成第一个小批处理的输入(期望的输出是活动会话的第二个事件)。第二个小批处理由第二个事件形成,以此类推。如果任何一个会话结束,则将下一个可用的会话放入其位置。假定会话是独立的,因此当发生这种切换时,我们重置适当的隐藏状态,下图是本方法的示意图:
在这里插入图片描述

2.5.2 SAMPLING ON THE OUTPUT

因为推荐任务所设计到的物品数量很多,如果单独为每一个项目都计算一个分数,会花费很多时间,所以本文提出的模型采用了一种抽样的方式来计算一小部分的项目分数,具体采样方式是根据物品的受欢迎程度来采样,这和本模型所采用的BPRloss也是遥相呼应的。
(当然组队学习中给出的代码中有基于Faiss的方法,可以有效解决这一问题),具体的在附录的代码中会有所体现

2.5.3 RANKING LOSS

本模型的最终目的是要完成推荐任务,所以损失函数的选取要贴合推荐任务的背景,本文所选取的损失函数是衡量能够有效衡量排序结果好坏的BPR损失函数:
 Loss  = ∑ ( u , i , j ) ∈ O − ln ⁡ σ ( y ( u , i ) − y ( u , j ) ) + β ⋅ ∥ Θ ∥ 2 \text { Loss }=\sum_{(u, i, j) \in O}-\ln \sigma(y(u, i)-y(u, j))+\beta \cdot\|\Theta\|^{2}  Loss =(u,i,j)Olnσ(y(u,i)y(u,j))+βΘ2
其中y(u,i)和y(u,j)各自代表用户对交互过的项目的预测评分,和没有交互过的项目的预测评分, θ \theta θ代表模型中的所有参数, β \beta β是L2正则化参数

三、总结

本文首次将RNN引入到推荐系统领域,利用GRU单元能够捕捉时序关系的功能,来进行会话推荐任务。而且本文为了更好的完成推荐任务,针对传统GRU进行了三方面的改进,从batch的选择到损失函数的选择都做出了很多优化。本文是RNN在推荐系统领域的一个里程碑的文章,对后来的一些模型产生了深刻的影响

附录(基于paddle的改进GRU模型和Faiss方法的实现)

GRU4Rec模型的代码实现

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()

    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():
            paddle.nn.initializer.KaimingNormal(weight)

    def forward(self, item_seq, mask, item, train=True):
        seq_emb = self.item_emb(item_seq)
        seq_emb,_ = self.gru(seq_emb)
        user_emb = seq_emb[:,-1,:] #取GRU输出的最后一个Hidden作为User的Embedding
        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

基于Faiss的向量召回方法

在大规模向量存在的背景下,可以加快对向量的召回效率,更快地找到与目标向量相似的topk个向量,这个方法可以很好的解决本模型在 SAMPLING ON THE OUTPUT 中所提到的问题

def get_predict(model, test_data, hidden_size, topN=20):

    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):

        # 获取用户嵌入
        # 多兴趣模型,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')
            D, I = gpu_index.search(user_embs, topN)  # Inner Product近邻搜索,D为distance,I是index
#             D,I = faiss.knn(user_embs, item_embs, topN,metric=faiss.METRIC_INNER_PRODUCT)
            for i, iid_list in enumerate(targets):  # 每个用户的label列表,此处item_id为一个二维list,验证和测试是多label的
                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')
            D, I = gpu_index.search(user_embs, topN)  # Inner Product近邻搜索,D为distance,I是index
#             D,I = faiss.knn(user_embs, item_embs, topN,metric=faiss.METRIC_INNER_PRODUCT)
            for i, iid_list in enumerate(targets):  # 每个用户的label列表,此处item_id为一个二维list,验证和测试是多label的
                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)  # 降序排序,内积越大,向量越近
                for j in range(len(item_list)):  # 按距离由近到远遍历推荐物品列表,最后选出最近的topN个物品作为最终的推荐物品
                    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
    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)
  • 7
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 5
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值