CH 12 / SOURCE EXAMPLE
wide_deep.py
ch12 精排模型/code/ch02_deep_ranking/wide_deep.py
这个示例讲什么
│ ├── wide_deep.py Wide&Deep:广义线性 + Embedding MLP
Wide & Deep 精排模型(第12章 §12.4.1.1) Wide 分支(广义线性模型 + 显式交叉特征)与 Deep 分支(Embedding + MLP) 并行计算,得分相加后统一过 Sigmoid: z_wide(x) = w_wideᵀ [x; φ(x)] + b_wide z_deep(x) = w_deepᵀ h^(L) + b_deep p̂ = σ(z_wide + z_deep) 两个分支共享同一个交叉熵损失联合训练(而非分别训练后再集成)。 本书依据:《第十二章-精排模型.pdf》§12.4.1.1,PDF 第 8–12 页。 依赖:PyTorch。运行:python3 wide_deep.py
上面来自章节 README 或源码自身的说明,不是自动验证的运行结果,也不能代替 PDF 的书中论断。讲解时请结合实际代码、输入输出与原书页码。
源码结构与阅读入口
_auc· 第 23 行WideAndDeep· 第 34 行Wide 分支:原始稀疏特征 + 手工交叉特征 → 线性make_data· 第 108 行4 个类别字段(如 用户年龄段/物品类别/上下文/设备),
1"""2Wide & Deep 精排模型(第12章 §12.4.1.1)34Wide 分支(广义线性模型 + 显式交叉特征)与 Deep 分支(Embedding + MLP)5并行计算,得分相加后统一过 Sigmoid:67 z_wide(x) = w_wideᵀ [x; φ(x)] + b_wide8 z_deep(x) = w_deepᵀ h^(L) + b_deep9 p̂ = σ(z_wide + z_deep)1011两个分支共享同一个交叉熵损失联合训练(而非分别训练后再集成)。1213本书依据:《第十二章-精排模型.pdf》§12.4.1.1,PDF 第 8–12 页。14依赖:PyTorch。运行:python3 wide_deep.py15"""1617import random18import torch19import torch.nn as nn20import torch.optim as optim212223def _auc(scores, labels) -> float:24 pos = [s for s, y in zip(scores, labels) if y == 1]25 neg = [s for s, y in zip(scores, labels) if y == 0]26 if not pos or not neg:27 return 0.528 return sum(1.0 if p > n else 0.5 if p == n else 0.029 for p in pos for n in neg) / (len(pos) * len(neg))303132# ── Wide & Deep 模型 ──────────────────────────────────────────────────────────3334class WideAndDeep(nn.Module):35 """36 Wide 分支:原始稀疏特征 + 手工交叉特征 → 线性37 Deep 分支:类别特征 Embedding + 数值特征 → 多层 MLP3839 正文说明(PDF 第 8–12 页):40 - Wide 分支侧重「记忆」已知的特征组合;41 - Deep 分支通过 Embedding + 非线性变换侧重「泛化」;42 - 两分支共享损失联合训练。43 """4445 def __init__(self, n_fields: int, vocab_size: int, emb_dim: int = 8,46 n_dense: int = 2, hidden: int = 32, cross_pairs=None):47 super().__init__()48 self.n_fields = n_fields49 self.vocab_size = vocab_size50 self.cross_pairs = cross_pairs or [(0, 1), (0, 2), (1, 3)]5152 # Deep 分支:每个字段一个 Embedding 表 + 数值特征拼接53 self.embs = nn.ModuleList([54 nn.Embedding(vocab_size, emb_dim) for _ in range(n_fields)])55 deep_in = n_fields * emb_dim + n_dense56 self.deep = nn.Sequential(57 nn.Linear(deep_in, hidden), nn.ReLU(),58 nn.Linear(hidden, hidden // 2), nn.ReLU(),59 nn.Linear(hidden // 2, 1),60 )6162 # Wide 分支:原始特征的 one-hot 展平 + 显式交叉特征63 # 正文 φ_k(x)=Π x_i^{c_ki}:对离散字段即两个 one-hot 的 Hadamard 积64 # (等价于联合取值的指示向量,维度 vocab²)65 wide_in = n_fields * vocab_size + len(self.cross_pairs) * vocab_size ** 266 self.wide = nn.Linear(wide_in, 1)6768 def _deep_forward(self, feat_ids, dense):69 embs = [emb(feat_ids[:, i]) for i, emb in enumerate(self.embs)]70 h = torch.cat(embs + [dense], dim=1)71 return self.deep(h).squeeze(-1)7273 def _wide_forward(self, feat_ids):74 onehots = [nn.functional.one_hot(feat_ids[:, i], self.vocab_size).float()75 for i in range(self.n_fields)]76 # 交叉特征:两个字段 one-hot 的外积展平后只保留联合激活位置77 crosses = [(onehots[i].unsqueeze(2) * onehots[j].unsqueeze(1)).flatten(1)78 for i, j in self.cross_pairs]79 x = torch.cat(onehots + crosses, dim=1)80 return self.wide(x).squeeze(-1)8182 def forward(self, feat_ids: torch.Tensor,83 dense: torch.Tensor) -> torch.Tensor:84 return self._wide_forward(feat_ids) + self._deep_forward(feat_ids, dense)8586 def predict_proba(self, feat_ids, dense) -> torch.Tensor:87 with torch.no_grad():88 return torch.sigmoid(self.forward(feat_ids, dense))8990 def fit(self, feat_ids, dense, y, epochs: int = 40, lr: float = 1e-2):91 opt = optim.Adam(self.parameters(), lr=lr)92 for ep in range(epochs):93 opt.zero_grad()94 loss = nn.functional.binary_cross_entropy_with_logits(95 self.forward(feat_ids, dense), y.float())96 loss.backward()97 opt.step()98 if (ep + 1) % 10 == 0:99 print(f" epoch {ep+1:3d} loss={loss.item():.4f}")100101 def rank(self, feat_ids, dense, top_k) -> list[tuple[int, float]]:102 scores = self.predict_proba(feat_ids, dense).tolist()103 return sorted(enumerate(scores), key=lambda t: -t[1])[:top_k]104105106# ── 合成数据 ──────────────────────────────────────────────────────────────────107108def make_data(n: int = 500, n_fields: int = 4, vocab: int = 6,109 n_dense: int = 2, seed: int = 42):110 """111 4 个类别字段(如 用户年龄段/物品类别/上下文/设备),112 2 个数值特征(如 历史 CTR、物品时长)。113 标签依赖主效应 + 字段 0×1 的交叉效应。114 """115 torch.manual_seed(seed)116 random.seed(seed)117 ids = torch.randint(0, vocab, (n, n_fields), dtype=torch.long)118 dense = torch.randn(n, n_dense)119120 logit = (0.5 * ids[:, 0].float() + 0.4 * ids[:, 1].float()121 - 0.3 * ids[:, 2].float()122 + 0.8 * (ids[:, 0] == ids[:, 1]).float() # 交叉效应123 + 0.5 * dense[:, 0] - 0.3 * dense[:, 1] - 2.0)124 y = (torch.sigmoid(logit + 0.3 * torch.randn(n)) > 0.5).long()125 return ids, dense, y126127128# ── 主演示 ────────────────────────────────────────────────────────────────────129130if __name__ == "__main__":131 random.seed(42)132 torch.manual_seed(42)133134 ids, dense, y = make_data(500)135 split = 380136 ids_tr, d_tr, y_tr = ids[:split], dense[:split], y[:split]137 ids_te, d_te, y_te = ids[split:], dense[split:], y[split:]138139 print("=== Wide & Deep 训练 ===")140 model = WideAndDeep(n_fields=4, vocab_size=6, emb_dim=8, n_dense=2,141 cross_pairs=[(0, 1), (1, 2)])142 model.fit(ids_tr, d_tr, y_tr, epochs=40)143144 p_te = model.predict_proba(ids_te, d_te).tolist()145 auc_te = _auc(p_te, y_te.tolist())146 print(f"\n测试集 AUC={auc_te:.4f}")147148 # ── 分支贡献分析 ──────────────────────────────────────────────────────────149 with torch.no_grad():150 z_wide = model._wide_forward(ids_te[:20])151 z_deep = model._deep_forward(ids_te[:20], d_te[:20])152 print(f"\n两分支得分量级(前 20 样本平均绝对值):")153 print(f" |z_wide|={z_wide.abs().mean().item():.4f} "154 f"|z_deep|={z_deep.abs().mean().item():.4f}")155156 # ── 精排推理演示 ──────────────────────────────────────────────────────────157 top5 = model.rank(ids_te[:20], d_te[:20], top_k=5)158 print("\n精排 Top-5(候选集 20 条):")159 for rank_i, (idx, score) in enumerate(top5, 1):160 print(f" #{rank_i} item={idx:2d} score={score:.4f} label={y_te[idx].item()}")161162 # ── 交叉特征贡献演示 ──────────────────────────────────────────────────────163 # 正文指出:Wide 分支通过 φ_k(x)=Π x_i^{c_ki} 记忆显式组合。164 # 这里观察交叉字段 0×1 同时取 2 时的得分差异。165 print("\nWide 分支交叉特征示例:")166 print(" 字段 0 与字段 1 同时取值 2 时,交叉特征 (x0==x1) 激活,")167 print(" 对应参数可获得直接的正向权重——这是 Wide 分支「记忆」能力来源。")如何用于学习或编写新代码
- 先看章节 README 的依赖和运行成本,定位这个例子的输入、输出及关键函数。
- 在原书 PDF 中找到对应概念的具体页;代码演示与书中论断分开标注。
- 只在依赖和耗时可控时运行单个示例,修改一个参数并记录真实输出与失败情形。
- 参考实现新代码时说明复用、改动、测试与尚未完成的生产依赖;教学示例并非生产系统。