论文下载地址:https://arxiv.org/abs/1808.09781
论文原版代码:GitHub - kang205/SASRec: SASRec: Self-Attentive Sequential Recommendation
pytorch实现:GitHub - pmixer/SASRec.pytorch: PyTorch(1.6+) implementation of https://github.com/kang205/SASRec
原作者给出的代码是基于tensorflow框架的,我对这个框架不熟悉,所以使用了pytorch框架。
一、论文复现遇到的问题
1、由于从未接触过推荐系统相关知识,首先对于模型评价指标不了解。
于是去查询了什么是Hit Rate@10和NDCG@10
- Hit Rate@10:
定义:Hit Rate@10(在前10个推荐中的命中率)是指在推荐列表的前10个推荐项中,至少有一个是用户实际感兴趣的(即用户点击或购买的)的比例。这个指标衡量的是推荐系统能否在前10个推荐中至少命中一个用户感兴趣的项目。
- NDCG@10:
定义:Normalized Discounted Cumulative Gain@10(归一化折扣累积增益@10)是一个更复杂的指标,它不仅考虑了推荐列表中用户感兴趣的项目,还考虑了这些项目的排名。NDCG@10通过为列表中的每个项目分配一个分数(通常是项目的相关性),然后根据项目的排名对这些分数进行折扣,最后对折扣后的分数进行累积,得到一个值。这个值会被归一化,以便与理想情况下的最大值进行比较。
计算:NDCG的计算涉及到对每个推荐项目的相关性进行评分(例如,1表示完全相关,0表示不相关),然后对每个位置的项目分数进行折扣(通常使用对数折扣函数),最后将折扣后的分数累加并归一化。
2、不了解推荐系统所需的数据格式是怎样的
于是去了解了ml-1m.txt的数据格式以及含义,每一行代表一个用户的行为,第一列为user_id,第二列行为id
# 加载数据集并进行分区
dataset = data_partition(args.dataset)
[user_train, user_valid, user_test, usernum, itemnum] = dataset
查看分区数据集情况为:可见ml-1m.txt是用户交互密集型数据。
3、调参实验结果对比
原文对于 SASRec 默认版本中的架构,我们使用两个自注意力块(b = 2), 优化器是 Adam 优化器,学习率设置为 0.001,批量大小为 128。由于稀疏性,MovieLens-1m 中关闭神经元的 dropout 率为 0.2,其他三个数据集为 0.5。 MovieLens-1m 的最大序列长度 n 设置为 200,其他三个数据集设置为 50,即大致与每个用户的平均操作数成正比。
默认参数为:batch_size=128,lr=0.001,num_blocks=2,num_heads=1,
hidden=50,maxlen=200,num_epochs=100,dropout_rate=0.2
avg_test_ndcg@10=0.5522, avg_test_hr@10=0.7957, avg_time=23.58
3.1.1、只改变num_blocks=3:
avg_test_ndcg@10=0.5501, avg_test_hr@10=0.7933, avg_time=33.04
3.1.2、只改变num_blocks=1:
avg_test_ndcg@10=0.5536, avg_test_hr@10=0.7934, avg_time=15.13
3.1.3、只改变num_blocks=4:
avg_test_ndcg@10=0.5514, avg_test_hr@10=0.7935, avg_time=38.43
num_blocks=1,avg_test_ndcg@10=0.5536, avg_test_hr@10=0.7934, avg_time=15.13
num_blocks=2,avg_test_ndcg@10=0.5522, avg_test_hr@10=0.7957, avg_time=23.58
num_blocks=3,avg_test_ndcg@10=0.5501, avg_test_hr@10=0.7933, avg_time=33.04
num_blocks=4,avg_test_ndcg@10=0.5514, avg_test_hr@10=0.7935, avg_time=38.43
结论一:对于num_blocks,同时考虑速度和精度,最终选择默认的num_blocks=2。
3.2.1、只改变lr=0.01
avg_test_ndcg@10=0.5103, avg_test_hr@10=0.7578, avg_time=21.58,性能下降严重
3.2.2、只改变lr=0.0001
avg_test_ndcg@10=0.3569, avg_test_hr@10=0.6023, avg_time=21.68,性能下降更严重
lr=0.01,avg_test_ndcg@10=0.5103, avg_test_hr@10=0.7578, avg_time=21.58
lr=0.001,avg_test_ndcg@10=0.5522, avg_test_hr@10=0.7957, avg_time=23.58
lr=0.0001,avg_test_ndcg@10=0.3569, avg_test_hr@10=0.6023, avg_time=21.68
结论二:对于lr,最终选择默认的lr=0.001。
3.3.1、只改变hidden_num=60
avg_test_ndcg@10=0.5635, avg_test_hr@10=0.8028, avg_time=19.24
3.3.2、只改变hidden_num=40
avg_test_ndcg@10=0.5432, avg_test_hr@10=0.7894, avg_time=16.53
hidden_num=40,avg_test_ndcg@10=0.5432, avg_test_hr@10=0.7894, avg_time=16.53
hidden_num=50,avg_test_ndcg@10=0.5522, avg_test_hr@10=0.7957, avg_time=23.58
hidden_num=60,avg_test_ndcg@10=0.5635, avg_test_hr@10=0.8028, avg_time=19.24
结论三:对于hidden_num,最终选择hidden_num=60。
3.4.1、只改变num_heads=2
avg_test_ndcg@10=0.5529, avg_test_hr@10=0.7960, avg_time=25.54,效果略有提升
3.4.2、改变num_heads=3
avg_test_ndcg@10=0.5655, avg_test_hr@10=0.8023, avg_time=25.75,效果进一步提升
3.4.3、改变num_heads=4
avg_test_ndcg@10=0.5619, avg_test_hr@10=0.8002, avg_time=28.65
num_heads=1,avg_test_ndcg@10=0.5522, avg_test_hr@10=0.7957, avg_time=23.58
num_heads=2,avg_test_ndcg@10=0.5529, avg_test_hr@10=0.7960, avg_time=25.54
num_heads=3,avg_test_ndcg@10=0.5655, avg_test_hr@10=0.8023, avg_time=25.75
num_heads=4,avg_test_ndcg@10=0.5619, avg_test_hr@10=0.8002, avg_time=28.65
结论四:对于num_heads,最终选择num_heads=3。
4、最终参数
batch_size=128,lr=0.001,num_blocks=2,num_heads=3,
hidden=60,maxlen=200,num_epochs=100,dropout_rate=0.2
最终性能:test_ndcg@10=0.592, test_hr@10=0.817, time=26.2s/10epoch,RX1650
原文性能:test_ndcg@10=0.591, test_hr@10=0.825, time=1.7s/epoch,RTX1080ti
5、代码修改
源代码在计算指标时,只计算HR@10和NDCG@10


现在我将代码进行修改,使其能够计算HR@K,NDCG@K(K=1,2,5,10)


输出结果为:
6、对比学习
此外,原文没有提到的一项技术叫做“对比学习”,但是在代码实现中却用到了,这个技巧很重要,对模型最终的性能影响非常大。于是我去查了一下什么叫做“对比学习”。
对比学习:通过生成正样本(用户交互序列)和负样本(替换部分项目生成的序列或者是随机生成用户并未交互的序列)来训练模型,使其能够更好地区分用户可能感兴趣的项目和不太可能感兴趣的项目。
对比学习的核心思想是让模型学会区分哪些项是相似的(正样本),哪些是不相关的(负样本)。在推荐系统的上下文中,这通常意味着需要两种数据样本:
正样本对:由来自同一用户的项目组成,这些项目被认为是用户可能感兴趣的,标签定义为1。
负样本对:由来自不同用户的项目组成,或者是一个用户不太可能感兴趣的项目,标签定义为0。
官方代码本身就已经实现了采用对比学习增强模型性能,那么我们来看看如果不使用对比学习,模型的效果到底如何。
6.1、数据预处理
使用的数据集不再是官方的ml-1m.txt(用户观看电影的序列数据集),而是使用的MOOCCube数据集,该数据集都是json格式,里面是学生观看课程的序列数据集,不了解的可以去搜一下这个数据集。
提取出每位用户的课程观看序列(用户和课程编号从 1 编号)
经过提取,得到user_numbered.json和course_numbered.json文件
统计出用户和课程的数量
- 课程编号完成,共编号了 706 门课程。
- 用户编号完成,共编号了 199199 个用户。
剔除学习课程小于 10 门的用户,统计出用户和课程的数量、平均每个用户观看的课程数、平均每个课程的被观看数
总用户数: 6548.00;总课程观看数: 97812.00
平均每个用户观看的课程数: 14.94;平均每个课程的被观看数: 142.17。如下图:
按照 SASRec 中的方法划分训练集、测试集和验证集
将json文件转为txt文件,得到user-course_order.txt,共97812行。如下图:
划分数据集:
6.2、未引入对比学习时
只有正向交互序列,通过前t-1个时间段的交互序列,预测第t个交互序列。部分代码如下所示:
其结果为:avg_test_ndcg@10=0.338, avg_test_hr@10=0.5933, avg_time=22.17s/10epoch
6.2、引入对比学习后
6.2.1、负样本序列生成办法之一(官方实现):
先定义一个项目id生成函数random_generate_neg_number(l, r, s):
确保该项目id不在用户已交互过的项目id集合中。
然后在采样器sample中接受用户id,生成该用户未交互过的序列作为负向序列
最后将正负向序列输入到模型中,定义正样本序列标签全为1,负样本序列全为0,累加正负向损失:
其结果为:avg_test_ndcg@10=0.4773, avg_test_hr@10=0.7467, avg_time=23.36s/10epoch
对比原先结果:avg_test_ndcg@10=0.338,avg_test_hr@10=0.5933,avg_time=22.17s/10epoch,提升巨大。
6.3.2、负样本序列生成办法之二(自己实现)
替换正例序列中的一部分,令其作为负例序列。核心代码如下:
其正负样例生成情况为:
与数据集中的情况一致:
其结果为:avg_test_ndcg@10=0.2606, avg_test_hr@10=0.5483, avg_time=23.35s/10epoch,RX1650
对比6.2的结果:avg_test_ndcg@10=0.338,avg_test_hr@10=0.5933,avg_time=22.17s/10epoch效果反而下降
原因:替换比例太低,导致正负样例相似度过高,调整替换比例为0.5,再次尝试。
其结果为:avg_test_ndcg@10=0.3994,avg_test_hr@10=0.7096,avg_time=25.93s/10epoch
对比6.2的结果:avg_test_ndcg@10=0.338,avg_test_hr@10=0.5933,avg_time=22.17s/10epoch效果提升,但还是不如6.2.1的负样本生成方法,再次增大替换比例,调整为0.8。
其结果为:
avg_test_ndcg@10=0.4489,avg_test_hr@10=0.7403,avg_time=29.35s/10epoch
对比6.2的结果:
avg_test_ndcg@10=0.338,avg_test_hr@10=0.5933,avg_time=22.17s/10epoch效果提升
结论:引入对比学习后,模型精度提升明显,且尝试了两种负样例生成方法
根据实验结果,6.3.1的方法提升最为明显
以上就是我对于SASRec这篇论文的代码复现以及结果总结了,对于这篇论文有什么不懂的,可以看我上一篇笔记有介绍这篇论文:学习笔记——SASRec 基于自注意力的序列推荐-CSDN博客
完整代码在正在整理中.........