CH 10 / SOURCE EXAMPLE
mind.py
ch10 召回模型/code/ch06_multi_interest/mind.py
这个示例讲什么
│ ├── mind.py MIND:胶囊网络动态路由 + Label-aware Attention
MIND 多兴趣召回(第10章 §10.5.1) 论文:Li et al., "Multi-Interest Network with Dynamic Routing for Recommendation at Tmall", CIKM 2019 核心思想: - 用胶囊网络(Capsule Network)的动态路由机制,从用户行为序列中抽取多个兴趣向量 - 每个兴趣胶囊对应用户某一方面的偏好 - 训练时用 Label-aware Attention 机制,根据目标物品选择最相关的兴趣向量 本实现:PyTorch 实现,动态路由 + Label-aware Attention + Sampled Softmax loss。
上面来自章节 README 或源码自身的说明,不是自动验证的运行结果,也不能代替 PDF 的书中论断。讲解时请结合实际代码、输入输出与原书页码。
源码结构与阅读入口
_squash· 第 24 行Capsule squash 函数:将向量压缩到单位球内,保持方向。v: [..., d]MIND· 第 32 行简化版 MIND 多兴趣模型。
1"""2MIND 多兴趣召回(第10章 §10.5.1)34论文:Li et al., "Multi-Interest Network with Dynamic Routing for Recommendation at Tmall", CIKM 201956核心思想:7- 用胶囊网络(Capsule Network)的动态路由机制,从用户行为序列中抽取多个兴趣向量8- 每个兴趣胶囊对应用户某一方面的偏好9- 训练时用 Label-aware Attention 机制,根据目标物品选择最相关的兴趣向量1011本实现:PyTorch 实现,动态路由 + Label-aware Attention + Sampled Softmax loss。12"""1314import math15import random16import collections1718import torch19import torch.nn as nn20import torch.nn.functional as F21import torch.optim as optim222324def _squash(v: torch.Tensor) -> torch.Tensor:25 """Capsule squash 函数:将向量压缩到单位球内,保持方向。v: [..., d]"""26 norm_sq = (v * v).sum(dim=-1, keepdim=True)27 norm = torch.sqrt(norm_sq + 1e-9)28 scale = norm_sq / (1.0 + norm_sq) / norm29 return v * scale303132class MIND(nn.Module):33 """34 简化版 MIND 多兴趣模型。3536 组件:37 item_emb: 物品 Embedding 表38 动态路由: 3 次迭代,b_ij += dot(interest_k, item_emb),softmax 得路由权重39 label_aware_attention: w_k = softmax(pow(dot(interest_k, target), p))40 Sampled Softmax loss 训练41 """4243 def __init__(44 self,45 n_items: int,46 emb_dim: int = 32,47 n_interests: int = 4,48 routing_iters: int = 3,49 lr: float = 0.01,50 reg: float = 1e-4,51 ):52 super().__init__()53 self.n_items = n_items54 self.emb_dim = emb_dim55 self.n_interests = n_interests56 self.routing_iters = routing_iters57 self.reg = reg5859 self.item_emb = nn.Embedding(n_items, emb_dim)60 nn.init.uniform_(self.item_emb.weight, -math.sqrt(1.0 / emb_dim), math.sqrt(1.0 / emb_dim))6162 self.optimizer = optim.Adam(self.parameters(), lr=lr, weight_decay=reg)6364 def _dynamic_routing(self, seq_embs: torch.Tensor) -> torch.Tensor:65 """66 EM 式动态路由,输入行为序列 Embedding,输出 n_interests 个兴趣向量。6768 Args:69 seq_embs: [T, d] 行为序列 Embedding7071 Returns:72 interests: [K, d]73 """74 T, d = seq_embs.shape75 K = self.n_interests7677 b = torch.zeros(T, K, device=seq_embs.device) # 路由 logit7879 interests = torch.zeros(K, d, device=seq_embs.device)80 for _ in range(self.routing_iters):81 c = F.softmax(b, dim=1) # [T, K]82 # 加权聚合:interests[k] = sum_t c[t][k] * seq_embs[t]83 interests = torch.einsum('tk,td->kd', c, seq_embs) # [K, d]84 interests = _squash(interests) # 胶囊 squash85 # 更新路由 logit86 b = b + torch.einsum('td,kd->tk', seq_embs, interests) # [T, K]8788 return interests8990 def encode_user(self, seq: list) -> torch.Tensor:91 """给定用户行为序列(id 列表),返回 K 个兴趣向量 [K, d]。"""92 if not seq:93 return torch.zeros(self.n_interests, self.emb_dim)94 seq_t = torch.tensor(seq, dtype=torch.long)95 with torch.no_grad():96 seq_embs = self.item_emb(seq_t) # [T, d]97 return self._dynamic_routing(seq_embs)9899 def _label_aware_attention(100 self, interests: torch.Tensor, target_emb: torch.Tensor, pow_p: float = 2.0101 ) -> torch.Tensor:102 """103 Label-aware Attention:对多个兴趣向量加权求和,权重由目标物品相似度决定。104105 attention_weight[k] = softmax(pow(<interest_k, target_emb>, pow_p))106 p=1 时退化为普通 softmax。107 """108 scores = (interests * target_emb.unsqueeze(0)).sum(dim=-1) # [K]109 weights = F.softmax(scores.pow(pow_p), dim=0) # [K]110 fused = (weights.unsqueeze(-1) * interests).sum(dim=0) # [d]111 return fused112113 def forward(self, seq_ids: torch.Tensor, pos_id: torch.Tensor, neg_id: torch.Tensor):114 """BPR 训练步骤:用 label-aware attention 融合兴趣后打分,返回 loss。"""115 seq_embs = self.item_emb(seq_ids) # [T, d]116 pos_emb = self.item_emb(pos_id) # [d]117 neg_emb = self.item_emb(neg_id) # [d]118119 interests = self._dynamic_routing(seq_embs) # [K, d]120 user_emb = self._label_aware_attention(interests, pos_emb) # [d]121122 s_pos = F.cosine_similarity(user_emb.unsqueeze(0), pos_emb.unsqueeze(0))123 s_neg = F.cosine_similarity(user_emb.unsqueeze(0), neg_emb.unsqueeze(0))124 loss = -F.logsigmoid(s_pos - s_neg).mean()125 return loss126127 def train_model(128 self,129 user_seqs: dict,130 epochs: int = 10,131 neg_k: int = 1,132 ) -> None:133 user_pos = collections.defaultdict(set)134 for uid, seq in user_seqs.items():135 user_pos[uid].update(seq)136137 samples = []138 for uid, seq in user_seqs.items():139 if len(seq) < 2:140 continue141 for t in range(1, len(seq)):142 samples.append((uid, seq[:t], seq[t]))143144 self.train()145 for epoch in range(epochs):146 random.shuffle(samples)147 total_loss = 0.0148 for uid, context, pos_id in samples:149 for _ in range(neg_k):150 neg_id = random.randint(0, self.n_items - 1)151 while neg_id in user_pos[uid]:152 neg_id = random.randint(0, self.n_items - 1)153154 seq_t = torch.tensor(context, dtype=torch.long)155 pos_t = torch.tensor(pos_id, dtype=torch.long)156 neg_t = torch.tensor(neg_id, dtype=torch.long)157158 self.optimizer.zero_grad()159 loss = self(seq_t, pos_t, neg_t)160 loss.backward()161 self.optimizer.step()162 total_loss += loss.item()163164 if epoch % 2 == 0:165 print(f" epoch {epoch:3d} avg_loss={total_loss / max(len(samples), 1):.4f}")166167 def recommend(168 self, seq: list, top_k: int = 10, exclude: set = None169 ) -> list:170 """171 线上推荐:对每个兴趣向量做 ANN 检索,取各兴趣的 Top-K 物品合并后重排。172 """173 exclude = exclude or set(seq)174 self.eval()175 with torch.no_grad():176 interests = self.encode_user(seq) # [K, d]177 all_embs = self.item_emb.weight # [n_items, d]178 # 对每个兴趣计算余弦相似度179 interests_norm = F.normalize(interests, dim=-1) # [K, d]180 all_norm = F.normalize(all_embs, dim=-1) # [n_items, d]181 sims = torch.mm(interests_norm, all_norm.t()) # [K, n_items]182 max_sims, _ = sims.max(dim=0) # [n_items]183184 score_dict = {}185 for iid in range(self.n_items):186 if iid not in exclude:187 score_dict[iid] = max_sims[iid].item()188189 ranked = sorted(score_dict.items(), key=lambda x: x[1], reverse=True)190 return ranked[:top_k]191192193# ── 快速验证 ──────────────────────────────────────────────────────────────────194if __name__ == "__main__":195 random.seed(42)196 torch.manual_seed(42)197198 n_items = 20199 user_seqs = {200 u: random.sample(range(n_items), k=random.randint(5, 10))201 for u in range(8)202 }203204 model = MIND(n_items, emb_dim=16, n_interests=3, routing_iters=3, lr=0.05)205 print("MIND 多兴趣模型训练:")206 model.train_model(user_seqs, epochs=6)207208 seq = user_seqs[0]209 recs = model.recommend(seq, top_k=5)210 print(f"\n用户序列 {seq} 的 Top-5 推荐:")211 for iid, s in recs:212 print(f" item {iid}: {s:.4f}")213214 # 可视化多兴趣向量(查看是否分散)215 interests = model.encode_user(seq)216 print(f"\n{len(interests)} 个兴趣向量与 item 的相似度分布:")217 model.eval()218 with torch.no_grad():219 all_embs = model.item_emb.weight220 interests_norm = F.normalize(interests, dim=-1)221 all_norm = F.normalize(all_embs, dim=-1)222 sims = torch.mm(interests_norm, all_norm.t()) # [K, n_items]223 for k in range(len(interests)):224 norm_val = interests[k].norm().item()225 max_sim = sims[k].max().item()226 mean_sim = sims[k].mean().item()227 print(f" interest {k}: norm={norm_val:.4f} max_sim={max_sim:.4f} mean_sim={mean_sim:.4f}")如何用于学习或编写新代码
- 先看章节 README 的依赖和运行成本,定位这个例子的输入、输出及关键函数。
- 在原书 PDF 中找到对应概念的具体页;代码演示与书中论断分开标注。
- 只在依赖和耗时可控时运行单个示例,修改一个参数并记录真实输出与失败情形。
- 参考实现新代码时说明复用、改动、测试与尚未完成的生产依赖;教学示例并非生产系统。