CH 15 / SOURCE EXAMPLE
gdcn.py
ch15 特征工程/code/ch04_feature_interaction/gdcn.py
这个示例讲什么
│ ├── gdcn.py §15.5.9 GDCN:门控交叉 + 串联/并联结构
GDCN:门控交叉网络(第15章 §15.5.9) Gated Cross Layer: c_{l+1} = c_0 ⊙ (W^c_l c_l + b_l) ⊙ σ(W^g_l c_l) + c_l = Feature Crossing × Information Gate + 残差 - W^c 学交叉关系,W^g 学逐维门控,σ 把门控压到 (0,1) - 门控为 1 → 退化为 DCN-V2;门控学到的值可以直接看"哪一维交互被保留" 两种组合方式: GDCN-S(Stacked):c_0 → GCN → c_L → DNN GDCN-P(Parallel):c_0 → GCN 与 DNN 并行,输出拼接 运行:python3 ch04_feature_interaction/gdcn.py
上面来自章节 README 或源码自身的说明,不是自动验证的运行结果,也不能代替 PDF 的书中论断。讲解时请结合实际代码、输入输出与原书页码。
源码结构与阅读入口
1"""2GDCN:门控交叉网络(第15章 §15.5.9)34Gated Cross Layer:5 c_{l+1} = c_0 ⊙ (W^c_l c_l + b_l) ⊙ σ(W^g_l c_l) + c_l6 = Feature Crossing × Information Gate + 残差7- W^c 学交叉关系,W^g 学逐维门控,σ 把门控压到 (0,1)8- 门控为 1 → 退化为 DCN-V2;门控学到的值可以直接看"哪一维交互被保留"9两种组合方式:10 GDCN-S(Stacked):c_0 → GCN → c_L → DNN11 GDCN-P(Parallel):c_0 → GCN 与 DNN 并行,输出拼接1213运行:python3 ch04_feature_interaction/gdcn.py14"""1516from __future__ import annotations1718import pathlib19import sys2021import torch22import torch.nn as nn2324sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent))2526from _train_utils import ( # noqa: E40227 N_FIELDS, MLP, BaselineMLP, count_params, embedding_table, print_comparison,28 run_experiment, stack_embeddings,29)3031DIM = 432FLAT = N_FIELDS * DIM333435class GatedCrossLayer(nn.Module):36 def __init__(self, dim: int):37 super().__init__()38 self.wc = nn.Linear(dim, dim)39 self.wg = nn.Linear(dim, dim)40 self.b = nn.Parameter(torch.zeros(dim))4142 def forward(self, c0: torch.Tensor, cl: torch.Tensor):43 cross = c0 * (self.wc(cl) + self.b)44 gate = torch.sigmoid(self.wg(cl))45 return cross * gate + cl464748class GDCN(nn.Module):49 def __init__(self, mode: str = "S", dim: int = DIM, n_cross: int = 3,50 hidden=(32,), use_gate: bool = True):51 super().__init__()52 assert mode in {"S", "P"}53 self.mode = mode54 self.dim = dim55 self.emb = embedding_table(dim)56 self.cross = nn.ModuleList([GatedCrossLayer(FLAT) for _ in range(n_cross)])57 self.use_gate = use_gate58 if mode == "S":59 self.deep = MLP(FLAT, hidden, out_dim=None, dropout=0.2)60 self.head = nn.Linear(hidden[-1], 1)61 else:62 self.deep = MLP(FLAT, hidden, out_dim=None, dropout=0.2)63 self.head = nn.Linear(FLAT + hidden[-1], 1)6465 def gated_cross(self, c0: torch.Tensor) -> torch.Tensor:66 c = c067 for layer in self.cross:68 if self.use_gate:69 c = layer(c0, c)70 else: # 去掉门控即 DCN-V2 的交叉层71 c = c0 * (layer.wc(c) + layer.b) + c72 return c7374 def forward(self, x):75 c0 = stack_embeddings(self.emb, x).flatten(1)76 if self.mode == "S":77 return self.head(self.deep(self.gated_cross(c0))).squeeze(-1)78 return self.head(torch.cat([self.gated_cross(c0), self.deep(c0)], dim=1)).squeeze(-1)798081def main() -> None:82 print("GDCN:c_{l+1} = c0 ⊙ (W^c c_l + b) ⊙ σ(W^g c_l) + c_l(§15.5.9)\n")83 _, a_s, _ = run_experiment(lambda: GDCN(mode="S"), "GDCN-S", steps=600)84 _, a_p, _ = run_experiment(lambda: GDCN(mode="P"), "GDCN-P", steps=600)85 _, a_nogate, _ = run_experiment(86 lambda: GDCN(mode="S", use_gate=False), "GDCN-S 无门控", steps=600)87 _, a_base, _ = run_experiment(BaselineMLP, "Baseline", steps=600)8889 print_comparison([90 ("Baseline MLP", a_base, count_params(BaselineMLP())),91 ("GDCN-S(串联)", a_s, count_params(GDCN(mode="S"))),92 ("GDCN-P(并联)", a_p, count_params(GDCN(mode="P"))),93 ("GDCN-S 无门控(≈DCN-V2)", a_nogate, count_params(GDCN(mode="S", use_gate=False))),94 ])9596 # 看门控值:每一维交叉信息被保留了多少97 torch.manual_seed(0)98 model = GDCN(mode="S")99 from _train_utils import make_dataset, to_tensor100 Xtr, ytr = make_dataset(4_000, seed=1)101 from _train_utils import fit102 Xtr, ytr = to_tensor(Xtr, ytr)103 fit(model, Xtr, ytr, Xtr[:2000], ytr[:2000], steps=200, name="")104 with torch.no_grad():105 inner = Xtr[:256]106 c = stack_embeddings(model.emb, inner).flatten(1)107 gates = []108 for layer in model.cross:109 gates.append(torch.sigmoid(layer.wg(c)).mean(0))110 c = layer(c0=stack_embeddings(model.emb, inner).flatten(1), cl=c)111 print("\n各层 Gate 平均值(取 256 个样本、40 维求均值):")112 for l, g in enumerate(gates):113 print(f" layer {l}: mean={g.mean():.3f} min={g.min():.3f} max={g.max():.3f}")114 print("\nGate 不是常数而是随样本变化:同一维交叉信息在有的样本里被放大、")115 print("在另一些样本里被抑制,这就是正文所说的'实例级筛选'(§15.5.9)。")116117118if __name__ == "__main__":119 main()如何用于学习或编写新代码
- 先看章节 README 的依赖和运行成本,定位这个例子的输入、输出及关键函数。
- 在原书 PDF 中找到对应概念的具体页;代码演示与书中论断分开标注。
- 只在依赖和耗时可控时运行单个示例,修改一个参数并记录真实输出与失败情形。
- 参考实现新代码时说明复用、改动、测试与尚未完成的生产依赖;教学示例并非生产系统。