SASRec 论文精读

前言

这两天完成了之前文本分类的任务,做了一周多,也跑出了比较好的结果。接下来要开始推荐系统相关的工作,之前从没有接触过,可以说是从零开始。Mentor 给我推荐了 SASRec 这篇文章,然后说后面的推荐系统构建以这个为基线,于是花了一个下午通读了论文,并且复现了代码

论文标题:Self-Attentive Sequential Recommendation 论文链接 论文原始代码(TF 实现) 论文代码(Pytorch 实现)

论文简介

alt text

这篇文章是 2018 年的,最大的贡献应该就是首次把 self-attention 给引入到了序列推荐领域。然后用的很多技术手段实现细节都是和 《Attention Is All You Need》相同的,把如果之前看过那篇现在再来看这篇就会很好理解。

这篇文章在推荐系统中的细分应该算是基于序列的推荐,其使用的数据集中只有 user id 和 item id 这两列,就是基于用户交互的物品序列(按照时间戳排序)去预测用户下一个可能喜欢的物品。SASRec 既可以做召回也可以做排序

复现过程记录

我是看的 pytorch 版本的实现,文件很简单,就三个,分别是main.pymodel.pyutils.py,先去瞅了一眼模型架构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
class SASRec(torch.nn.Module):
def __init__(self, user_num, item_num, args):
super(SASRec, self).__init__()

self.user_num = user_num
self.item_num = item_num
self.dev = args.device
self.norm_first = args.norm_first

# TODO: loss += args.l2_emb for regularizing embedding vectors during training
# https://stackoverflow.com/questions/42704283/adding-l1-l2-regularization-in-pytorch
self.item_emb = torch.nn.Embedding(self.item_num+1, args.hidden_units, padding_idx=0)
self.pos_emb = torch.nn.Embedding(args.maxlen+1, args.hidden_units, padding_idx=0)
self.emb_dropout = torch.nn.Dropout(p=args.dropout_rate)

self.attention_layernorms = torch.nn.ModuleList() # to be Q for self-attention
self.attention_layers = torch.nn.ModuleList()
self.forward_layernorms = torch.nn.ModuleList()
self.forward_layers = torch.nn.ModuleList()

self.last_layernorm = torch.nn.LayerNorm(args.hidden_units, eps=1e-8)

for _ in range(args.num_blocks):
new_attn_layernorm = torch.nn.LayerNorm(args.hidden_units, eps=1e-8)
self.attention_layernorms.append(new_attn_layernorm)

new_attn_layer = torch.nn.MultiheadAttention(args.hidden_units,
args.num_heads,
args.dropout_rate) # batch_first=False
self.attention_layers.append(new_attn_layer)

new_fwd_layernorm = torch.nn.LayerNorm(args.hidden_units, eps=1e-8)
self.forward_layernorms.append(new_fwd_layernorm)

new_fwd_layer = PointWiseFeedForward(args.hidden_units, args.dropout_rate)
self.forward_layers.append(new_fwd_layer)

可以看到里面实现了 attention 部分,其他比较值得关注的就是 embedding 部分,在论文 3.A 中有描述到整个 embedding 是由 item embedding 和 position embedding 结合起来的,并且这里的位置编码是用了可学习的,为什么不用 原始 transformer 里那种固定的,论文里有原话: > We also tried the fixed position embedding as used in [3], but found that this led to worse performance in our case.

因为实验发现效果不好

预测层

1
2
3
4
5
6
7
8
9
10
11
12
def predict(self, user_ids, log_seqs, item_indices):
log_feats = self.log2feats(log_seqs)

final_feat = log_feats[:, -1, :]

item_embs = self.item_emb(torch.LongTensor(item_indices).to(self.dev)) # (U, I, C)

logits = item_embs.matmul(final_feat.unsqueeze(-1)).squeeze(-1)

# preds = self.pos_sigmoid(logits) # rank same item list for different users

return logits # preds # (U, I)
  • log_seqs 是用户的历史交互序列(如 [item1, item2, …, itemT]),通常是一个形状为 (batch_size, seq_len) 的张量。
  • self.log2feats 是模型的前向传播部分,包括:
    • Embedding 层:将物品 ID 映射为稠密向量(item_emb)。
    • Transformer 编码器:对序列进行自注意力计算,得到每个时间步的特征表示。
  • log_feats 的形状通常是 (batch_size, seq_len, hidden_units),即每个时间步的特征表示。
  • final_feat 是 log_feats 最后一个时间步的特征,形状为 (batch_size, hidden_units)
    • 这和 nlp 中进行文本分类的逻辑是一样的,在 NLP 中,许多模型(如 BERT、Transformer)会取 [CLS] token 或最后一个时间步的 hidden state 作为整个序列的表示,而不是所有 token 的平均
    • SASRec 类似地假设最后一个交互的物品已经隐含了用户当前兴趣的关键信息(因为 Transformer 的自注意力机制会聚合整个序列的信息)。
  • logits 的计算详解:
    • final_feat.unsqueeze(-1),把 final_feat 变成 (batch_size, hidden_units, 1) item
正在加载今日诗词....
欢迎关注我的其它发布渠道