CH 11 / SOURCE EXAMPLE

tdssm.py

ch11 粗排模型/code/ch01_two_tower/tdssm.py

这个示例讲什么

│ ├── tdssm.py TDSSM:静态 MLP(长期)+ LSTM(短期)

TDSSM 时序双塔粗排模型(第11章 §11.1.2) 核心扩展(相对 DSSM): - 用户塔分为静态兴趣(MLP)和动态兴趣(GRU 序列编码)两部分 - 两者融合后与物品 Embedding 做余弦相似度 - 体现长期兴趣 + 短期兴趣解耦建模的思想 本实现:静态 MLP(长期兴趣)+ nn.GRU(短期兴趣),PyTorch autograd。

上面来自章节 README 或源码自身的说明,不是自动验证的运行结果,也不能代替 PDF 的书中论断。讲解时请结合实际代码、输入输出与原书页码。

源码结构与阅读入口

  1. TDSSM · 第 23 行时序 DSSM(TDSSM)。
  2. _make_batch · 第 133 行
  3. train · 第 159 行

完整源码 · 211 行

1"""2TDSSM 时序双塔粗排模型(第11章 §11.1.2)34核心扩展(相对 DSSM):5- 用户塔分为静态兴趣(MLP)和动态兴趣(GRU 序列编码)两部分6- 两者融合后与物品 Embedding 做余弦相似度7- 体现长期兴趣 + 短期兴趣解耦建模的思想89本实现:静态 MLP(长期兴趣)+ nn.GRU(短期兴趣),PyTorch autograd。10"""1112import math13import random14import collections15from typing import List, Tuple, Dict1617import torch18import torch.nn as nn19import torch.nn.functional as F20import torch.optim as optim212223class TDSSM(nn.Module):24    """25    时序 DSSM(TDSSM)。2627    用户塔 = 静态 MLP(长期兴趣)+ GRU(短期兴趣)→ element-wise 相加融合28    物品塔 = MLP29    融合方式:element-wise 相加(可换成 concat 或加权乘)30    """3132    def __init__(33        self,34        n_users: int,35        n_items: int,36        emb_dim: int = 32,37        static_hidden: int = 32,38        lstm_hidden: int = 32,39        output_dim: int = 32,40        temperature: float = 0.1,41    ):42        super().__init__()43        self.n_items = n_items44        self.output_dim = output_dim45        self.temperature = temperature4647        self.user_static_emb = nn.Embedding(n_users, emb_dim)48        self.item_emb = nn.Embedding(n_items, emb_dim)4950        # 静态用户塔(长期兴趣 MLP)51        self.static_mlp = nn.Sequential(52            nn.Linear(emb_dim, static_hidden),53            nn.ReLU(),54            nn.Linear(static_hidden, output_dim),55        )5657        # 短期兴趣:GRU 编码行为序列58        self.gru = nn.GRU(emb_dim, lstm_hidden, batch_first=True)59        self.gru_proj = nn.Linear(lstm_hidden, output_dim)6061        # 物品塔 MLP62        self.item_mlp = nn.Sequential(63            nn.Linear(emb_dim, output_dim),64        )6566        # 初始化 Embedding67        scale = math.sqrt(1.0 / emb_dim)68        nn.init.uniform_(self.user_static_emb.weight, -scale, scale)69        nn.init.uniform_(self.item_emb.weight, -scale, scale)7071    def encode_user(self, uid: torch.Tensor, seq_ids: torch.Tensor, seq_len: int) -> torch.Tensor:72        """73        Args:74            uid:     [B] 用户 ID(用于静态特征)75            seq_ids: [B, T] 用户近期行为序列(物品 ID),不足 T 的补 076            seq_len: 实际最大序列长度(用于 packed / mean pooling)77        Returns:78            user_emb: [B, output_dim]79        """80        # 静态兴趣81        e_static = self.static_mlp(self.user_static_emb(uid))  # [B, D]8283        # 短期兴趣(GRU)84        if seq_len > 0:85            seq_emb = self.item_emb(seq_ids)          # [B, T, emb_dim]86            _, h_n = self.gru(seq_emb)                 # h_n: [1, B, lstm_hidden]87            e_temp = self.gru_proj(h_n.squeeze(0))     # [B, output_dim]88        else:89            e_temp = torch.zeros(uid.size(0), self.output_dim)9091        # 融合(element-wise 相加)92        return e_static + e_temp9394    def encode_item(self, iid: torch.Tensor) -> torch.Tensor:95        return self.item_mlp(self.item_emb(iid))  # [B, output_dim] or [N, output_dim]9697    def forward(98        self,99        uid: torch.Tensor,100        seq_ids: torch.Tensor,101        pos_iid: torch.Tensor,102        neg_iid: torch.Tensor,103    ) -> torch.Tensor:104        """BPR loss: − log σ(s_pos − s_neg)"""105        ue = self.encode_user(uid, seq_ids, seq_ids.size(1))  # [B, D]106        pe = self.encode_item(pos_iid)                         # [B, D]107        ne = self.encode_item(neg_iid)                         # [B, D]108        s_pos = F.cosine_similarity(ue, pe, dim=1)109        s_neg = F.cosine_similarity(ue, ne, dim=1)110        loss = -F.logsigmoid(s_pos - s_neg).mean()111        return loss112113    def pre_rank(114        self, uid: int, seq: List[int], candidates: List[int], top_k: int = 20115    ) -> List[Tuple[int, float]]:116        """保留接口:pre_rank(uid, seq, candidates, top_k) -> List[Tuple[int, float]]"""117        with torch.no_grad():118            u = torch.tensor([uid])119            T = len(seq)120            if T > 0:121                seq_t = torch.tensor([seq]).long()  # [1, T]122            else:123                seq_t = torch.zeros(1, 1, dtype=torch.long)124            ue = self.encode_user(u, seq_t, T)  # [1, D]125            cand_t = torch.tensor(candidates)126            ie = self.encode_item(cand_t)         # [N, D]127            sims = F.cosine_similarity(ue, ie, dim=1)128            scores = [(candidates[j], sims[j].item()) for j in range(len(candidates))]129        scores.sort(key=lambda x: x[1], reverse=True)130        return scores[:top_k]131132133def _make_batch(134    samples: List[Tuple[int, List[int], int]],135    n_items: int,136    user_pos: Dict[int, set],137    max_seq_len: int = 20,138) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]:139    uids, seqs, pos_ids, neg_ids = [], [], [], []140    for uid, context, pos_id in samples:141        neg_id = random.randint(0, n_items - 1)142        while neg_id in user_pos[uid]:143            neg_id = random.randint(0, n_items - 1)144        uids.append(uid)145        pos_ids.append(pos_id)146        neg_ids.append(neg_id)147        # pad / truncate sequence148        ctx = context[-max_seq_len:]149        pad = [0] * (max_seq_len - len(ctx))150        seqs.append(pad + ctx)151    return (152        torch.tensor(uids),153        torch.tensor(seqs, dtype=torch.long),154        torch.tensor(pos_ids),155        torch.tensor(neg_ids),156    )157158159def train(160    model: TDSSM,161    user_seqs: Dict[int, List[int]],162    epochs: int = 10,163    lr: float = 0.01,164) -> None:165    optimizer = optim.Adam(model.parameters(), lr=lr)166    user_pos: Dict[int, set] = collections.defaultdict(set)167    for uid, seq in user_seqs.items():168        for i in seq:169            user_pos[uid].add(i)170171    samples = []172    for uid, seq in user_seqs.items():173        if len(seq) < 2:174            continue175        for t in range(1, len(seq)):176            samples.append((uid, seq[:t], seq[t]))177178    for epoch in range(epochs):179        random.shuffle(samples)180        total_loss = 0.0181        uids_t, seqs_t, pos_t, neg_t = _make_batch(182            samples, model.n_items, user_pos183        )184        optimizer.zero_grad()185        loss = model(uids_t, seqs_t, pos_t, neg_t)186        loss.backward()187        optimizer.step()188        total_loss = loss.item()189        if epoch % 2 == 0:190            print(f"  epoch {epoch:3d}  avg_loss={total_loss:.4f}")191192193# ── 快速验证 ──────────────────────────────────────────────────────────────────194if __name__ == "__main__":195    random.seed(42)196    torch.manual_seed(42)197198    n_users, n_items = 8, 20199    user_seqs = {u: random.sample(range(n_items), k=random.randint(5, 10)) for u in range(n_users)}200201    model = TDSSM(n_users, n_items, emb_dim=16, static_hidden=16, lstm_hidden=16, output_dim=16)202    print("TDSSM 训练:")203    train(model, user_seqs, epochs=6, lr=0.05)204205    uid = 0206    seq = user_seqs[uid]207    candidates = [i for i in range(n_items) if i not in set(seq)]208    result = model.pre_rank(uid, seq[-5:], candidates, top_k=5)209    print(f"\n用户 {uid} 粗排 Top-5(最近 5 个行为作为短期序列):")210    for iid, s in result:211        print(f"  item {iid}: {s:.4f}")

如何用于学习或编写新代码

  1. 先看章节 README 的依赖和运行成本,定位这个例子的输入、输出及关键函数。
  2. 在原书 PDF 中找到对应概念的具体页;代码演示与书中论断分开标注。
  3. 只在依赖和耗时可控时运行单个示例,修改一个参数并记录真实输出与失败情形。
  4. 参考实现新代码时说明复用、改动、测试与尚未完成的生产依赖;教学示例并非生产系统。