DataWhale推荐系统论文组队task3--Session-based Recommendation with Graph Neural Networks


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

一、GNN简介

推荐一个讲的挺不错的视频: GNN简介by 耿直哥、
GNN的提出的最初目的是为了学习特征图中节点的特征表示,个人理解:GNN的核心就是先聚合再传播,通过这种方式来获得图节点的特征表示

二、传统 VS SR-GNN

2.1 传统会话推荐的方法的局限性

  1. 基于RNN方法所生成的潜在向量被当做用户的embedding表示,根据这些embedding来估计用户的偏好,但是在会话推荐的场景中,很多会话属于匿名的,而且会话的数量很多,这两个特点导致一个会话所涉及的用户行为是有限的,所以基于这种方法所能生成的用户embedding数量是有限的;
  2. 传统方法在对项目进行建模时,只考虑两个连续项目之间的模式转换,而且忽略了和会话中其他项目的转换

2.2 Why GNN?

这一小节谈一下选择GNN作为解决上述局限性工具的原因

2.2.1 数据的结构化属性

从在线平台收集的数据有多种形式,包括用户与商品的互动(比如评分、点击、购买),用户概况(比如性别、年龄、收入),物品属性(比如品牌、品类、价格)等等。传统的推荐系统无法利用这些多种形式的数据,并且它们通常关注一个或几个特定的数据源,这影响了推荐系统的性能,因为许多潜在的信息并没有被学习到。然而GNN凭借自己在表征学习方面强大的能力,提供了一种利用上述数据的统一方式,通过将所有数据表示为图上的节点和边,可以获得用户、项目和其他特征的高质量嵌入,这对推荐性能至关重要

2.2.2 可以将高阶的邻居进行连通

在推荐任务中,推荐的准确性依赖于捕捉用户和商品之间的相似性,而这种相似性应该反映在学习的嵌入空间中。具体地说,用户学习到的嵌入类似于用户与之交互的项的嵌入。此外,那些与其他具有相似偏好的用户交互的项目也与该用户相关,这就是所谓的协同过滤(CF)效应,这对于推荐的准确性非常重要。在传统的方法中,由于训练数据主要是只包含直接连接项的交互记录,所以只能隐式捕获协同过滤效果。换句话说,只考虑了一阶连通性。缺乏高阶连通性会极大地损害推荐性能。相比之下,基于GNN的模型可以有效地捕获高阶连通性。具体而言,协同过滤效果可以自然地表示为图上的多跳邻居,并通过嵌入传播和聚合将其纳入到学习表示中。

2.2.3 可以缓解监督信号稀疏的问题

监督信号在收集的数据中通常是稀疏的,而基于GNN的模型可以在表征学习过程中利用半监督信号来缓解这一问题。以电商平台为例:与其他行为相比,目标行为“购买”非常稀少。因此,仅使用目标行为的推荐系统可能具有较差的性能。基于GNN的模型可以通过对图形上的半监督信号进行编码,有效地合并多个非目标行为,如搜索和添加到购物车,这可以显著提高推荐性能。同时,通过在图上设计辅助任务,可以利用自监督信号,进一步提高推荐性能

2.3 本模型的创新点

  1. 将各自独立的会话序列建模为图结构数据,并用GNN来捕获复杂的项目转换,为基于会话的推荐场景中的建模提供了一个新的视角
  2. 和传统的会话推荐方式以来用户表示不同,本文中模型使用会话的embedding来产生推荐

三、SR-GNN模型

3.1 构建基于session的图

根据session本身的特点,构建一个有向图,下图是论文中的一个例子:
在这里插入图片描述
可以看到根据四个项目item1,2,3,4(四个项目属于同一个session)之间的相互连接关系,构建了两个矩阵,左边是输出边的加权连接矩阵,右边是输入边的加权连接矩阵,里面的矩阵元素生成的规则:如果从一个节点出去的边的个数大于1,那么就对其中的元素进行归一化,比如v2对v4和v3都各自有一条边,所以在outgoing和incoming这两个矩阵中,都对其进行了归一化,2和3、4连接在矩阵中的表示元素为二分之一

3.2 GNN学习Item的embedding

公式第一行通过连接矩阵聚合邻居结点信息(包含了outgoing和Incoming两个矩阵), 剩余过程类似上一篇博客中GRU参数的更新:
a s , i t = A s , i : [ v 1 t − 1 , … , v n t − 1 ] T H + b z s , i t = σ ( W z a s , i t + U z v i t − 1 ) r s , i t = σ ( W r a s , i t + U r v i t − 1 ) v i t ‾ = tanh ⁡ ( W o a s , i t + U o ( r s , i t ⊙ v i t − 1 ) ) v i t = ( 1 − z s , i t ) ⊙ v i t − 1 ) + z s , i t ⊙ v i t ‾ \begin{array}{c} a_{s, i}^{t}=A_{s, i:}\left[v_{1}^{t-1}, \ldots, v_{n}^{t-1}\right]^{T} H+b \\ z_{s, i}^{t}=\sigma\left(W_{z} a_{s, i}^{t}+U_{z} v_{i}^{t-1}\right) \\ r_{s, i}^{t}=\sigma\left(W_{r} a_{s, i}^{t}+U_{r} v_{i}^{t-1}\right) \\ \overline{v_{i}^{t}}=\tanh \left(W_{o} a_{s, i}^{t}+U_{o}\left(r_{s, i}^{t} \odot v_{i}^{t-1}\right)\right) \\ \left.v_{i}^{t}=\left(1-z_{s, i}^{t}\right) \odot v_{i}^{t-1}\right)+z_{s, i}^{t} \odot \overline{v_{i}^{t}} \end{array} as,it=As,i:[v1t1,,vnt1]TH+bzs,it=σ(Wzas,it+Uzvit1)rs,it=σ(Wras,it+Urvit1)vit=tanh(Woas,it+Uo(rs,itvit1))vit=(1zs,it)vit1)+zs,itvit
v i t v_{i}^{t} vit就是经过聚合得到的项目embedding表示

3.3 session的embedding表示

获得item embedding向量后, 接着生成session embedding. 取session内最后一个交互的结点向量作为用户当前兴趣向量(local embedding), 以凸显最后交互item的重要性. 接着, 基于注意力机制获得global embedding以表征session长期兴趣. 最后, 通过一个简单线性函数做融合, 得到session的混合embedding
α i = q T σ ( W 1 v n + W 2 v i + c ) s l = v n s g = ∑ i = 1 n α i v i s h = W 3 [ s l : s g ] \begin{array}{c} \alpha_{i}=q^{T} \sigma\left(W_{1} v_{n}+W_{2} v_{i}+c\right) \\ s_{l}=v_{n} \\ s_{g}=\sum_{i=1}^{n} \alpha_{i} v_{i} \\ s_{h}=W_{3}\left[s_{l}: s_{g}\right] \end{array} αi=qTσ(W1vn+W2vi+c)sl=vnsg=i=1nαivish=W3[sl:sg]
在学习到session的embedding表示之后,将其与要预测的item的embedding做内积得到一个预测分数,再对Top-k个项目评分进行排序,得到最终的推荐列表

3.4 SR-GNN的模型图

下面是SR-GNN的整体模型框架,里面详细地讲解了数据流动方向,以及主要模块
在这里插入图片描述

附录(SR-GNN模型基于paddle框架的实现)

#基础GNN组件的定义
class GNN(nn.Layer):
    def __init__(self, embedding_size, step=1):
        super(GNN, self).__init__()
        self.step = step
        self.embedding_size = embedding_size
        self.input_size = embedding_size * 2
        self.gate_size = embedding_size * 3
        
        self.w_ih = self.create_parameter(shape=[self.input_size, self.gate_size]) 
        self.w_hh = self.create_parameter(shape=[self.embedding_size, self.gate_size])
        self.b_ih = self.create_parameter(shape=[self.gate_size])
        self.b_hh = self.create_parameter(shape=[self.gate_size])
        self.b_iah = self.create_parameter(shape=[self.embedding_size])
        self.b_ioh = self.create_parameter(shape=[self.embedding_size])

        self.linear_edge_in = nn.Linear(self.embedding_size, self.embedding_size)
        self.linear_edge_out = nn.Linear(self.embedding_size, self.embedding_size)

    def GNNCell(self, A, hidden):
        input_in = paddle.matmul(A[:, :, :A.shape[1]], self.linear_edge_in(hidden)) + self.b_iah
        input_out = paddle.matmul(A[:, :, A.shape[1]:], self.linear_edge_out(hidden)) + self.b_ioh
        # [batch_size, max_session_len, embedding_size * 2]
        inputs = paddle.concat([input_in, input_out], 2)

        # gi.size equals to gh.size, shape of [batch_size, max_session_len, embedding_size * 3]
        gi = paddle.matmul(inputs, self.w_ih) + self.b_ih
        gh = paddle.matmul(hidden, self.w_hh) + self.b_hh
        # (batch_size, max_session_len, embedding_size)
        i_r, i_i, i_n = gi.chunk(3, 2)
        h_r, h_i, h_n = gh.chunk(3, 2)
        reset_gate = F.sigmoid(i_r + h_r)
        input_gate = F.sigmoid(i_i + h_i)
        new_gate = paddle.tanh(i_n + reset_gate * h_n)
        hy = (1 - input_gate) * hidden + input_gate * new_gate
        return hy

    def forward(self, A, hidden):
        for i in range(self.step):
            hidden = self.GNNCell(A, hidden)
        return hidden

#基于上述GNN模块的SR-GNN模块的实现
class SRGNN(nn.Layer):
	def __init__(self, config):
        super(SRGNN, self).__init__()

        # load parameters info
        self.config = config
        self.embedding_size = config['embedding_dim']
        self.step = config['step']
        self.n_items = self.config['n_items']

        # define layers and loss
        # item embedding
        self.item_emb = nn.Embedding(self.n_items, self.embedding_size, padding_idx=0)
        # define layers and loss
        self.gnn = GNN(self.embedding_size, self.step)
        self.linear_one = nn.Linear(self.embedding_size, self.embedding_size)
        self.linear_two = nn.Linear(self.embedding_size, self.embedding_size)
        self.linear_three = nn.Linear(self.embedding_size, 1, bias_attr=False)
        self.linear_transform = nn.Linear(self.embedding_size * 2, self.embedding_size)
        self.loss_fun = nn.CrossEntropyLoss()


        # parameters initialization
        self.reset_parameters()

    def gather_indexes(self, output, gather_index):
        """Gathers the vectors at the specific positions over a minibatch"""
#         gather_index = gather_index.view(-1, 1, 1).expand(-1, -1, output.shape[-1])
        gather_index = gather_index.reshape([-1, 1, 1])
        gather_index = paddle.repeat_interleave(gather_index,output.shape[-1],2)
        output_tensor = paddle.take_along_axis(output, gather_index, 1)
        return output_tensor.squeeze(1)

    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 _get_slice(self, item_seq):
        # Mask matrix, shape of [batch_size, max_session_len]
        mask = (item_seq>0).astype('int32')
        items, n_node, A, alias_inputs = [], [], [], []
        max_n_node = item_seq.shape[1]
        item_seq = item_seq.cpu().numpy()
        for u_input in item_seq:
            node = np.unique(u_input)
            items.append(node.tolist() + (max_n_node - len(node)) * [0])
            u_A = np.zeros((max_n_node, max_n_node))

            for i in np.arange(len(u_input) - 1):
                if u_input[i + 1] == 0:
                    break

                u = np.where(node == u_input[i])[0][0]
                v = np.where(node == u_input[i + 1])[0][0]
                u_A[u][v] = 1

            u_sum_in = np.sum(u_A, 0)
            u_sum_in[np.where(u_sum_in == 0)] = 1
            u_A_in = np.divide(u_A, u_sum_in)
            u_sum_out = np.sum(u_A, 1)
            u_sum_out[np.where(u_sum_out == 0)] = 1
            u_A_out = np.divide(u_A.transpose(), u_sum_out)
            u_A = np.concatenate([u_A_in, u_A_out]).transpose()
            A.append(u_A)

            alias_inputs.append([np.where(node == i)[0][0] for i in u_input])
        # The relative coordinates of the item node, shape of [batch_size, max_session_len]
        alias_inputs = paddle.to_tensor(alias_inputs)
        # The connecting matrix, shape of [batch_size, max_session_len, 2 * max_session_len]
        A = paddle.to_tensor(A)
        # The unique item nodes, shape of [batch_size, max_session_len]
        items = paddle.to_tensor(items)

        return alias_inputs, A, items, mask
        def forward(self, item_seq, mask, item, train=True):
        if train:
            alias_inputs, A, items, mask = self._get_slice(item_seq)
            hidden = self.item_emb(items)
            hidden = self.gnn(A, hidden)
            alias_inputs = alias_inputs.reshape([-1, alias_inputs.shape[1],1])
            alias_inputs = paddle.repeat_interleave(alias_inputs, self.embedding_size, 2)
            seq_hidden = paddle.take_along_axis(hidden,alias_inputs,1)
            # fetch the last hidden state of last timestamp
            item_seq_len = paddle.sum(mask,axis=1)
            ht = self.gather_indexes(seq_hidden, item_seq_len - 1)
            q1 = self.linear_one(ht).reshape([ht.shape[0], 1, ht.shape[1]])
            q2 = self.linear_two(seq_hidden)

            alpha = self.linear_three(F.sigmoid(q1 + q2))
            a = paddle.sum(alpha * seq_hidden * mask.reshape([mask.shape[0], -1, 1]), 1)
            user_emb = self.linear_transform(paddle.concat([a, ht], axis=1))

            loss = self.calculate_loss(user_emb,item)
            output_dict = {
                'user_emb': user_emb,
                'loss': loss
            }
        else:
            alias_inputs, A, items, mask = self._get_slice(item_seq)
            hidden = self.item_emb(items)
            hidden = self.gnn(A, hidden)
            alias_inputs = alias_inputs.reshape([-1, alias_inputs.shape[1],1])
            alias_inputs = paddle.repeat_interleave(alias_inputs, self.embedding_size, 2)
            seq_hidden = paddle.take_along_axis(hidden, alias_inputs,1)
            # fetch the last hidden state of last timestamp
            item_seq_len = paddle.sum(mask, axis=1)
            ht = self.gather_indexes(seq_hidden, item_seq_len - 1)
            q1 = self.linear_one(ht).reshape([ht.shape[0], 1, ht.shape[1]])
            q2 = self.linear_two(seq_hidden)

            alpha = self.linear_three(F.sigmoid(q1 + q2))
            a = paddle.sum(alpha * seq_hidden * mask.reshape([mask.shape[0], -1, 1]), 1)
            user_emb = self.linear_transform(paddle.concat([a, ht], axis=1))
            output_dict = {
                'user_emb': user_emb,
            }
        return output_dict
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值