经典图推荐系统论文Self-supervised Graph Learning for Recommendation算法及代码简介

Self-supervised Graph Learning for Recommendation

论文链接:https://dl.acm.org/doi/abs/10.1145/3404835.3462862

论文背景/动机

得益于图神经网络(Graph Neural Network)的强大图结构建模能力,推荐系统已经从建模单一交互对发展至建模整个用户-物品交互图。通过迭代执行多层图卷积网络,中心节点可以聚合来自多跳节点以外的信息,从而更有效地建模用户和物品的关系。尽管有效,但图卷积网络的信息传递过程会加剧高度节点(high-degree node)传播,这会进一步降低长尾物品被推荐的概率,从而造成流行度偏差(popularity bias);其次,基于图卷积网络的推荐系统更容易收到噪声交互的影响,基于邻域传播的聚合范式进一步放大了噪声交互对整个用户-物品交互图的影响力。此外,此前基于图卷积网络的推荐方法例如NGCF、LightGCN等工作存在难以收敛的现象,究其原因在于单一的推荐损失无法为推荐任务提供足够的监督信号,从而致使训练效率低下。

方法

在这项工作中,作者探索了用户-物品交互图上的自监督学习(Self-Supervised Learning),并提出了SGL模型,以提升推荐的准确性和鲁棒性如下图所示。作者首先指出LightGCN等传统工作的局限性:

  1. 稀疏的监督信号:大多数模型在监督学习范式下进行推荐任务,其中监督信号来自观察到的用户-物品交互。然而,与整个交互空间相比,观察到的交互极其稀疏,使得它不足以学习高质量的嵌入表示。

  1. 倾斜的数据分布:观察到的交互信息通常呈现幂律分布(power-law distribution),其中长尾由缺乏监督信号的低度物品组成。相比之下,高度物品在邻域聚合和监督损失中出现的频率更高,从而对表示学习产生更大的影响。因此,GCN很容易偏向于高度物品,而牺牲长尾物品的性能。

  1. 交互中的噪声:用户提供的大多数反馈是隐式的(例如,点击、查看),而不是显式的(例如,评级、喜欢/不喜欢)。因此,观察到的交互通常包含噪声,例如,用户被误导点击一个项目,并在消费后发现它不感兴趣。GCN中的邻域聚合机制扩大了交互对表示学习的影响,使得训练过程更容易受到交互噪声的影响。

随后作者定义了SGL的两个核心部分:数据增强(data augmentation)和对比学习(contrastive learning)。

数据增强

数据增强是对比学习的前置操作,其对原始数据进行扰动以扩充样本规模,得到原始数据的不同视角(view)。在计算机视觉(CV)领域,常见的数据增强方式是裁剪、旋转;在自然语言处理(NLP)领域,可行的数据增强方式有遮盖、打乱语句顺序等。考虑到推荐场景与CV和NLP等领域的不同,直接应用这些领域的数据增强技术是不合适的。

图推荐系统的唯一输入是用户-物品交互图。在图中,用户和物品的一阶邻居直接描述了用户和物品的属性,即可以将其视为用户或者物品的特征。基于此,对用户-物品交互图进行扰动等同于对用户或物品特征进行扰动。作者提出了三种数据增强操作,分别是节点dropout、边dropout和随机游走。

Node Dropout(ND)

节点dropout核心思想是以随机概率删除用户-物品交互图上的节点以及这些节点所连接的边:

其中分别是两个遮盖向量,其长度与顶点集合的长度一致,随机删除的概率由参数控制。这种数据增强操作期望从不同的增强视图中识别出有影响力的节点,并使表示学习对结构变化不那么敏感。

Edge Dropout(ED)

边dropout核心思想是以随机概率删除用户-物品交互图中的边:

其中分别是两个遮盖向量,其长度为边集合的长度一致,随机删除的概率由参数控制。这种数据增强操作将这两个子图耦合在一起,旨在捕捉节点的局部结构的有用信息,并进一步赋予其对噪声交互的更强鲁棒性。在实际应用中,边dropout的表现最好,因为这种数据增强方法不会显著地改变原始图结构。

Random Walk(RW)

ND和ED两个数据增强操作均生成了在所有GCN层中共享的子图。作者额外考虑为每一层分配不同的子图,这一过程可以通过随机游走(RW)为每个节点构建单一的子图。具体来说, 可以在每一层执行边dropout:

其中分别是第个GCN层的遮盖向量。

对比学习

在构造了输入图的两个新视角后,SGL可以通过任意GNN模型在新的图结构上学习用户和物品的嵌入表示,这些额外的用户和物品的嵌入表示不会用于主推荐任务,而是作为辅助任务向主任务提供监督信号,这一过程将通过对比学习(CL)来完成。具体来说,对比学习鼓励同一节点在不同视角中的一致性,同时加强了不同节点之间的差异性。SGL利用InfoNCE来定义损失函数:

其中分别是用户在两个视角中通过GNN模型学习得到的嵌入表示,用户衡量两个向量之前的相似性,通常采用内积计算,是softmax函数的温度系数(temperature parameter),该参数对于对比学习具有重要的意义。简单来说,对比损失可以对负样本进行惩罚,以使其在特征空间里远离正样本,而温度系数正好用于控制惩罚力度。通过调节温度系数,可以将特征空间趋向于均匀分布,以此增强对比学习的效果。关于对比损失的更多深入探讨可以参见论文https://openaccess.thecvf.com/content/CVPR2021/papers/Wang_Understanding_the_Behaviour_of_Contrastive_Loss_CVPR_2021_paper.pdf

类似地,可以在物品侧同样计算对比损失,这样完整的对比损失可以被定义为:

多任务学习

为了提升推荐性能,SGL在基础的主推荐任务的基础上引入了自监督任务,这一过程通过多任务学习进行联合优化。在主推荐任务方面,SGL选择LightGCN作为GNN编码器学习用户和物品的嵌入表示。简单来讲,LightGCN是一种轻量级的图推荐方法,其通过迭代地在用户-物品交互图上执行图卷积过程来学习高质量的用户和物品的嵌入表示,并且移除了冗余的特征转换、非线性激活等操作,其核心公式为:

得到用户和物品的多层嵌入后,LightGCN采用带权求和的方式构造最终的用户和物品的嵌入表示,随后通过内积计算用户对物品的预测分数:

为了更新模型参数,LightGCN采取BPR损失进行优化:

其中O是训练集。更多关于LightGCN的细节以及代码实现可以参见https://blog.csdn.net/Blueghost19/article/details/129665502

在主任务之外,每个epoch中SGL首先需要构造两个子图,并在这两个子图上分别执行LightGCN以得到用户和物品在不同视角下的嵌入表示,并将其用于辅助的自监督任务。SGL的完整损失函数定义如下:

其中是对比损失的权重。

综上所述,SGL的每一次训练过程均需完成三次LightGCN操作,三次操作的输入图均不相同,参与计算的用户和物品的初始嵌入表示共享。下图给出了SGL的训练过程,其中得到的嵌入表示包含用户和物品。

理论分析

在这一小节,作者对SGL进行了理论分析,以阐述为什么对比学习可以有助于提升推荐性能。具体来说,给定正样本u和负样本节点v之间的变量,则对比损失的梯度贡献可以显式地定义为:

根据上述定义,所有节点可以被分为两类:

(1)Hard负样本节点:该类负样本节点在特征空间内相似于正样本节点u,即(),这使得推荐系统很难区分他们。

(2)Easy负样本节点:该类负样本节点在特征空间内不相似于正样本节点u,即(),该类节点很容易被区分。

从上式很容易看出梯度收到温度系数的影响,如下图所示:

当给定温度系数为1时,的值的变化范围不大,这表明节点无论是Hard还是Easy,对于梯度的贡献程度都是一样的。

当给定温度系数为0.1时,的规模开始明显增加,当Hard负样本节点越相似于正样本节点时(即越接近1),能够提供的梯度值更大。

以上分析表示通过调节温度系数,可以使推荐模型有效地区分Hard负样本,并且能够提供更多监督信号,从而实现更快的收敛速度。同时,由于对比损失本身旨在让正样本节点远离所有负样本节点,这也会使得特征空间内的所有嵌入表示接近于均匀分布。从推荐的角度来说,这使得长尾物品有更多机会被用户发现,从而有效地缓解流行度偏差问题。关于上述结论,可以参见以下论文:

特征空间均匀分布:

https://dl.acm.org/doi/abs/10.1145/3477495.3531937

http://proceedings.mlr.press/v119/wang20k.html

https://openaccess.thecvf.com/content/CVPR2021/papers/Wang_Understanding_the_Behaviour_of_Contrastive_Loss_CVPR_2021_paper.pdf

流行度偏差:

https://dl.acm.org/doi/10.1145/3447548.3467289

时间复杂度

在这一节,作者分析了SGL-ED和LightGCN的时间复杂度。具体来说,给定用户-物品交互图,顶点数和边数分别为表示总迭代次数(epoch),表示每个批次(batch)的抽样数,表示GCN层数,表示嵌入大小,表示SGL-ED的边保留概率,则LightGCN和SGL-ED在构造邻接矩阵、图卷积操作和计算BPR损失以及对比损失等方面的时间复杂度对比如下表所示:

  • 构造邻接矩阵:LightGCN需要构造完整的图拉普拉斯矩阵(包含用户-物品图和物品-用户图),则总复杂度为2,该过程在整个训练过程中只完成一次。SGL-ED在每个epoch中需要构造两个子图视角,每个子图的复杂度为2(即在总边数中以概率保留一部分边)。

  • 图卷积操作:LightGCN和SGL-ED均为mini-batch抽样训练的策略,即一个完整的epoch被划分为个batch,每个batch中需要完成一次整个图的图卷积操作,每次图卷积操作的复杂度为。SGL-ED需要进行两次额外的图卷积操作。

  • 计算损失:LightGCN和SGL-ED均需要计算一次BPR损失,其包含每个交互和对应负样本的内积计算过程。SGL-ED需要额外计算对比损失。如果将所有其他用户节点均视为负样本节点来计算对比损失,则额外的时间复杂度为,如果只将每个batch里的其他用户作为负样本,则额外的时间复杂度降低为

综上所述,通过引入对比学习,SGL的时间复杂度相比于LightGCN有了明显的增加。作者表示SGL的时间复杂度是LightGCN的3.7倍。虽然SGL具有更高的时间复杂度,但得益于对比损失提供的额外监督信号,这使得SGL的收敛速度远远快于LightGCN,这使得SGL的总训练用时低于LightGCN,如下图所示:

实验

为了验证SGL的有效性,作者在以下三个大规模推荐数据集上进行实验:

作者首先给出了SGL-ND,SGL-ED和SGL-RW与LightGCN的性能对比:

从上图可以看到SGL在所有设置下均优于LightGCN,在高层时更为明显,这表明了SGL的有效性。需要指出的是,SGL-ED在18组对比中有10组都取得了最优表现,这表明了边dropout可以有效地捕获图结构的内在模式。另一方面,SGL-ND的表现不尽如人意,并且极其不稳定。究其原因在于ND操作会随机删除大量节点以及这些节点对应的边,如果删除高度节点将会严重破坏图结构,从而造成训练阻碍

随后作者进行了长尾物品的推荐实验。具体来说,作者将每个数据集中的物品根据流行度划分为10组,每一组的交互数相同。在该设置下LightGCN和SGL-ED在不同流行度组情况的Recall对比如下所示(其中GroupID越大表明物品越流行):

从上图可以看出LightGCN更倾向于推荐最流行的物品,而长尾物品很难被推荐,这表明LightGCN很难从极其稀疏的交互中为长尾物品学习到高质量的嵌入表示。而SGL则展现了良好的抗稀疏性,联合对比上图和性能对比表,可以发现SGL的性能提升主要来自于长尾物品,这表明了监督信号对于学习高质量嵌入表示的重要性。

最后一个比较重要的实验是鲁棒性实验,在该实验中,作者为训练集加入了一定比例的对抗样本(即不存在的用户-物品交互对),同时保持测试集不变。在该设置下LightGCN和SGL-ED的性能对比如下图所示(柱状图表示Recall,折线图表示性能下降的比率):

增加噪声后LightGCN和SGL-ED的性能均在一定程度上下降,但SGL-ED性能下降的速度远低于LightGCN。随着噪声比率的增加,LightGCN和SGL-ED的性能下降曲线之间的差距被进一步拉大。这表明SGL通过对比同一节点的不同视角,可以有效地识别有用的交互信息,从而增强了推荐的鲁棒性。

代码

SGL的官方开源代码如下:

TensorFlow:https://github.com/wujcan/SGL-TensorFlow

PyTorch:https://github.com/wujcan/SGL-Torch

以PyTorch版本的代码为例,简要介绍SGL的核心操作,即数据增强和对比学习。

数据增强

SGL提出了三种数据增强策略,即节点dropout(ND),边dropout(ED)和随机游走(RW),相关代码如下:

def create_adj_mat(self, is_subgraph=False, aug_type='ed'):
    n_nodes = self.num_users + self.num_items
    users_items = self.dataset.train_data.to_user_item_pairs()
    users_np, items_np = users_items[:, 0], users_items[:, 1]

    if is_subgraph and self.ssl_ratio > 0:
        if aug_type == 'nd':
            drop_user_idx = randint_choice(self.num_users, size=self.num_users * self.ssl_ratio, replace=False)
            drop_item_idx = randint_choice(self.num_items, size=self.num_items * self.ssl_ratio, replace=False)
            indicator_user = np.ones(self.num_users, dtype=np.float32)
            indicator_item = np.ones(self.num_items, dtype=np.float32)
            indicator_user[drop_user_idx] = 0.
            indicator_item[drop_item_idx] = 0.
            diag_indicator_user = sp.diags(indicator_user)
            diag_indicator_item = sp.diags(indicator_item)
            R = sp.csr_matrix(
                (np.ones_like(users_np, dtype=np.float32), (users_np, items_np)), 
                shape=(self.num_users, self.num_items))
            R_prime = diag_indicator_user.dot(R).dot(diag_indicator_item)
            (user_np_keep, item_np_keep) = R_prime.nonzero()
            ratings_keep = R_prime.data
            tmp_adj = sp.csr_matrix((ratings_keep, (user_np_keep, item_np_keep+self.num_users)), shape=(n_nodes, n_nodes))
        if aug_type in ['ed', 'rw']:
            keep_idx = randint_choice(len(users_np), size=int(len(users_np) * (1 - self.ssl_ratio)), replace=False)
            user_np = np.array(users_np)[keep_idx]
            item_np = np.array(items_np)[keep_idx]
            ratings = np.ones_like(user_np, dtype=np.float32)
            tmp_adj = sp.csr_matrix((ratings, (user_np, item_np+self.num_users)), shape=(n_nodes, n_nodes))
    else:
        ratings = np.ones_like(users_np, dtype=np.float32)
        tmp_adj = sp.csr_matrix((ratings, (users_np, items_np+self.num_users)), shape=(n_nodes, n_nodes))
    adj_mat = tmp_adj + tmp_adj.T

    # normalize adjcency matrix
    rowsum = np.array(adj_mat.sum(1))
    d_inv = np.power(rowsum, -0.5).flatten()
    d_inv[np.isinf(d_inv)] = 0.
    d_mat_inv = sp.diags(d_inv)
    norm_adj_tmp = d_mat_inv.dot(adj_mat)
    adj_matrix = norm_adj_tmp.dot(d_mat_inv)

    return adj_matrix

考虑到随机游走的实验过程式逐层做边dropout,所以RW和ED的计算过程被归为一类(第23~28行),而第7~22行为ND的构造过程。

为了随机删除节点和对应的边,SGL将这一过程转换为保留节点和对应的边首先以比率self.ssl_ratio随机选择用户和物品的节点(第8~9行),其中randint_choice()函数返回随机抽样列表。得到抽样列表后构造值全为1的列表(第10~11行),并将抽样的对应位置置为0(第12~13行),最后变为对角矩阵(第14~15行),该用户和物品对角矩阵用于过滤所有抽样的用户和物品的交互信息。第16~18行用于定义用户-物品交互矩阵R。将用户对角矩阵、用户-物品交互矩阵R和物品对角矩阵依次进行稀疏矩阵乘法,即可得到删除节点和对应边的新用户-物品交互矩阵(第19行),随后将该新交互矩阵极其转置矩阵合并以得到新构造的邻接矩阵(第32行),上述过程如下图所示(蓝色代表1,白色代表0):

其中计算稀疏矩阵乘法步骤进一步细化如下(其中红色边框为删除的交互):

边dropout和随机游走的构造过程较为简单,即对原始的交互列表进行抽样(第24~26行),随机根据抽样得到的新交互列表(第27行)重构构造交互矩阵(第28行)。

在得到新视角的邻居矩阵后需要计算图拉普拉斯矩阵以进行后续的图卷积操作(第35~40行),该过程为GCN的基础操作。此外SGL选用LightGCN作为GNN编码器,关于图拉普拉斯矩阵和LightGCN的代码介绍参见https://blog.csdn.net/Blueghost19/article/details/129665502

对比学习

SGL将原始图输入主推荐任务以计算BPR损失,将两个新视角输入辅助任务以计算对比损失,该过程的代码如下:

def InfoNCE(self, embedding_1, embedding_2, temperature):
    embedding_1 = torch.nn.functional.normalize(embedding_1)
    embedding_2 = torch.nn.functional.normalize(embedding_2)

    pos_score = (embedding_1 * embedding_2).sum(dim=-1)
    pos_score = torch.exp(pos_score / temperature)

    ttl_score = torch.matmul(embedding_1, embedding_2.transpose(0, 1))
    ttl_score = torch.exp(ttl_score / temperature).sum(dim=1)

    loss = - torch.log(pos_score / ttl_score + 10e-6)
    return torch.mean(loss)

该代码块中embedding_1和embedding_2即为通过不同视角学习到的嵌入表示,temperature为温度系数。InfoNCE的计算过程首先包括对原始输入进行正则化(第2~3行),随后计算正样本分数(第5~6行),随后计算负样本分数,在该过程中embedding_1需要和embedding_2做矩阵乘法操作,以分别计算用户和其他所有用户的相似度分数。在得到正负样本分数后即可计算对比损失(第11行),需要注意的是对比损失中包含对数函数,为了防止出现0,需要在最后增加一个极小项(例如10e-6)。

总结

作者在本文中通过引入对比学习提出了SGL模型。通过一系列分析和实验,作者得出结论:推荐场景中稀疏的交互数据无法学习到高质量的用户和物品的嵌入表示,这使得推荐系统很难为冷启动(或极其稀疏)的用户或者物品进行推荐。通过引入基于对比学习的辅助任务,推荐系统可以在主推荐任务的基础上得到额外的监督信号,这使得推荐模型在收敛速度、鲁棒性和推荐性能方面均有了明显提升。

SGL是对比学习在图推荐领域的首次尝试,该论文虽然对对比损失进行了一定的理论分析,但没有完全解释清楚对比损失对于推荐性能的证明影响,并且额外的数据增强操作会显著地增加推荐模型训练的空间与时间复杂度。

  • 11
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值