目录

深化用户的兴趣表示

MIND (Multi-Interest Network with Dynamic Routing)模型

SDM

代码练习:MIND

B2I类

MIND类

运行结果

生成式召回方法

SASRec

代码

FFN

单个 Transformer 编码器块

模型主体

运行结果

结果分析

HSTU(提升特征输入侧)

TIGER(提升特征输出侧)


本文为funrec指导学习笔记,并整合网络资料,个人理解等完成。

序列召回就是要利用用户行为的时间顺序信息来进行推荐。它的基本想法是:用户的当前兴趣不仅取决于他过去喜欢什么,还取决于他最近在做什么,以及这些行为的顺序。

深化用户的兴趣表示

改进用户的兴趣表示方法

MIND (Multi-Interest Network with Dynamic Routing)模型

不用一个向量来概括用户的复杂性,而应该用一组向量,其中每个向量代表一个独立的兴趣维度。
MIND 模型在常规 Embedding 层基础上,核心新增两个组件:
- 多兴趣提取层:解决用户兴趣多样性问题,通过B2I(Behavior to Interest)动态路由算法,将用户历史行为(行为胶囊)聚合为多个兴趣向量(兴趣胶囊)

  1.     使用共享变换矩阵:所有行为胶囊共用一个双线性映射矩阵,确保兴趣向量处于同一表示空间,便于后续计算。
  2.     动态兴趣数量:根据用户历史行为数量动态调整兴趣数量,行为少的用户用少量兴趣节省资源,活跃用户用多兴趣更精准表示。

- Label-aware 注意力层:应用于训练阶段,从用户的多个兴趣向量中,筛选出与目标商品(标签)最相关的兴趣向量,为训练提供明确指引

  1.     注意力集中度超参数p:p值越大,注意力越集中于与目标商品最相似的兴趣向量;当p=0时,所有兴趣向量的权重相同
  2.     使用 sampled softmax 损失函数:针对 “正样本 + 采样出的负样本”做softmax归一化

SDM

融合长短期兴趣,捕捉动态变化
SDM模型 的核心思想是分别建模用户的短期即时兴趣和长期稳定偏好,然后智能地融合它们。

左下角借助LSTM捕捉短期兴趣,左上角讲长期兴趣按照特征分为多个子集,最终利用类似LSTM门控机制的原理融合二者

代码练习:MIND

B2I类

class B2I(nn.Module):
    def __init__(self, embedding_dim, num_behaviors):
        super(B2I, self).__init__()
        self.embedding = nn.Embedding(num_behaviors, embedding_dim)
        self.linear = nn.Linear(embedding_dim, embedding_dim)

    def forward(self, behaviors):
        # 获取行为的嵌入表示
        behaviors_embedding = self.embedding(behaviors)
        # 进行双线映射
        return self.linear(behaviors_embedding)

MIND类

class MIND(nn.Module):
    def __init__(self, num_behaviors, embedding_dim):
        super(MIND, self).__init__()
        self.b2i = B2I(embedding_dim, num_behaviors)
        # 用于注意力机制
        self.linear = nn.Linear(embedding_dim, 1)

    def forward(self, behaviors):
        # 获取多个兴趣向量
        interest_vectors = self.b2i(behaviors)
        # 计算注意力权重
        weights = F.softmax(self.linear(interest_vectors), dim=0)
        # 加权求和获得用户最终的兴趣表示
        user_interest_representation = torch.sum(weights * interest_vectors, dim=0)
        return user_interest_representation

运行结果

生成式召回方法

把推荐看作序列预测问题,直接对序列中物品与物品之间的动态依赖关系进行建模,关注“用户接下来会做什么”而非“用户是怎样的人”

SASRec

启发于Transformer,将用户的行为序列视为一个句子来处理,主要包含自注意力层和前馈网络层(增强非线性变换的能力)

代码

用ai生成了简单的例子学习。

FFN
class PointWiseFeedForward(nn.Module):
    """
    点向的前馈网络 (FFN),增强非线性变换的能力。
    在 SASRec 中,通常是 线性 -> 激活 -> 线性 的结构。
    """
    def __init__(self, hidden_size, dropout_rate):
        super(PointWiseFeedForward, self).__init__()
        self.conv1 = nn.Conv1d(hidden_size, hidden_size, kernel_size=1)
        self.dropout = nn.Dropout(dropout_rate)
        self.relu = nn.ReLU()
        self.conv2 = nn.Conv1d(hidden_size, hidden_size, kernel_size=1)

    def forward(self, inputs):
        # inputs 形状: (batch_size, seq_len, hidden_size)
        # Conv1d 需要 (N, C, L) 形状,所以需要转置
        outputs = self.conv1(inputs.transpose(1, 2))
        outputs = self.relu(outputs)
        outputs = self.dropout(outputs)
        outputs = self.conv2(outputs).transpose(1, 2)
        # outputs 形状: (batch_size, seq_len, hidden_size)
        return outputs
单个 Transformer 编码器块
class SASRecBlock(nn.Module):
    """
    单个 Transformer 编码器块,包含 Multi-Head Attention 和 FFN。
    """
    def __init__(self, hidden_size, num_heads, dropout_rate):
        super(SASRecBlock, self).__init__()
        # 自注意力层
        self.attn_layernorm = nn.LayerNorm(hidden_size, eps=1e-8)
        self.attn_layer = nn.MultiheadAttention(hidden_size, num_heads, dropout=dropout_rate, batch_first=True)
        self.attn_dropout = nn.Dropout(dropout_rate)

        # 前馈网络层
        self.ffn_layernorm = nn.LayerNorm(hidden_size, eps=1e-8)
        self.ffn_layer = PointWiseFeedForward(hidden_size, dropout_rate)
        self.ffn_dropout = nn.Dropout(dropout_rate)

    def forward(self, inputs, attention_mask):
        
        q_norm = self.attn_layernorm(inputs)
        
        seq_len = inputs.shape[1]
        mask_2d = attention_mask[0, :, :].float() * -1e9  # 0->0, 1->-inf
        
        # Q, K, V 都是 q_norm
        attn_outputs, _ = self.attn_layer(q_norm, q_norm, q_norm, attn_mask=mask_2d)
        
        # 残差连接 + Dropout
        outputs = inputs + self.attn_dropout(attn_outputs)

        # 2. 前馈网络部分
        # LayerNorm -> FFN -> Dropout -> Add(残差连接)
        outputs = self.ffn_layernorm(outputs) # inputs -> outputs
        
        ffn_outputs = self.ffn_layer(outputs)
        
        # 残差连接 + Dropout
        outputs = outputs + self.ffn_dropout(ffn_outputs)
        
        # outputs 形状: (batch_size, seq_len, hidden_size)
        return outputs
模型主体

负责嵌入、位置编码、堆叠 Block 和 Masking

class SASRec(nn.Module):
    def __init__(self, item_num, max_len, hidden_size, num_heads, num_blocks, dropout_rate):
        super(SASRec, self).__init__()
        
        self.item_num = item_num  # 物品总数 (包括PAD=0)
        self.max_len = max_len    # 最大序列长度
        self.hidden_size = hidden_size
        
        # 嵌入层
        # Item 嵌入 (item_num 包含了一个用于 padding 的 0)
        self.item_emb = nn.Embedding(item_num, hidden_size, padding_idx=0)
        # Positional 嵌入 (位置编码)
        self.pos_emb = nn.Embedding(max_len, hidden_size)
        self.emb_dropout = nn.Dropout(dropout_rate)

        # Transformer 编码器堆叠
        self.blocks = nn.ModuleList([
            SASRecBlock(hidden_size, num_heads, dropout_rate)
            for _ in range(num_blocks)
        ])
        
        self.final_layernorm = nn.LayerNorm(hidden_size, eps=1e-8)
        
    def forward(self, item_seq):
        """
        :param item_seq: 用户行为序列 (batch_size, seq_len)
        :return: 预测向量 (batch_size, seq_len, hidden_size)
        """

        item_vectors = self.item_emb(item_seq)

        positions = torch.arange(item_seq.shape[1], dtype=torch.long, device=item_seq.device)
        pos_vectors = self.pos_emb(positions)
        
        # 嵌入相加: 物品嵌入 + 位置嵌入
        outputs = item_vectors + pos_vectors
        outputs = self.emb_dropout(outputs)
        seq_len = item_seq.shape[1]
        causality_mask = torch.tril(torch.ones((seq_len, seq_len), dtype=torch.bool, device=item_seq.device)).unsqueeze(0)
        # 形状 (1, seq_len, seq_len), True 表示可见,False 表示不可见 (需要设置成 -inf)
        
        # item_seq == 0 表示 padding 部分 (需要一同 mask 掉,以免影响计算)
        padding_mask = (item_seq != 0).unsqueeze(1).repeat(1, seq_len, 1) 
        # padding_mask: (B, 1, L) -> (B, L, L), True 表示非 padding
        
        # 最终 Attention Mask: 既不能是未来物品,也不能是 padding 
        # (B, L, L) 形状,False 表示需要 mask
        attention_mask = causality_mask & padding_mask

        for block in self.blocks:
            # attention_mask: False 的位置在 MultiheadAttention 中会被忽略/设为 -inf
            outputs = block(outputs, attention_mask)

        outputs = self.final_layernorm(outputs)
        
        return outputs
        
    def predict(self, item_seq):
        hidden_states = self.forward(item_seq)

        return hidden_states[:, -1, :] # (batch_size, hidden_size)

运行结果

结果分析

这个向量是从 (4, 50, 64) 中提取出来的,代表了模型认为最能概括用户当前兴趣的总结向量。

  1. 在训练时, 模型用这个向量来计算损失,指导模型学习下一个应该推荐的物品是什么。

  2. 在召回时, 这个 向量就是用户的 “查询向量 (Query Vector)”。系统将它与所有物品的嵌入向量进行对比,找到相似度最高的物品,作为推荐结果。

HSTU(提升特征输入侧)

SASRec 仅聚焦于由物品ID构成的序列
HSTU 学习这个复杂的句子(融入了用户的属性,行为,点击等等),并预测下一个可能的事件。
- 特征处理:类别特征按时间 “拉平” 成事件序列,数值特征通过行为模式隐式建模。
- HSTU 单元:用统一模块替代传统推荐模型的多个异构组件,包含点向投影、点向聚合、点向转换三个子模块,分别处理特征提取、交互和转换。
- 替换 softmax:放弃传统 softmax 归一化,用点向聚合保留用户兴趣的 “绝对强度”,避免偏好信号被削弱。

TIGER(提升特征输出侧)

将推荐目标从 “匹配 ID” 升级为 “生成物品语义描述”,解决了输出内容低效的问题,通过语义 ID 重塑推荐终点
 

更多推荐