CH 15 / SOURCE EXAMPLE

can.py

ch15 特征工程/code/ch04_feature_interaction/can.py

这个示例讲什么

│ ├── can.py §15.5.7 CAN:Co-Action Unit + Multi-order

CAN:Co-Action Network,由特征生成参数(第15章 §15.5.7) Co-Action Unit 把一对特征分成两个角色: Induction Side(参数生成侧):P_induction 被 reshape 成 Micro-MLP 的 W 与 b Feed Side(输入侧) :P_feed 作为 Micro-MLP 的输入 h_0 = P_feed h_i = σ(w_{i-1} h_{i-1} + b_{i-1}) F(u, m) = [h_1 ‖ h_2 ‖ … ‖ h_L] Multi-order Enhancement:把 (P_feed)^c(c=1..C)分别送进同一个 Micro-MLP 再求和, 用 tanh 约束高阶项的数值范围(正文 §15.5.7)。 参数量对

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

源码结构与阅读入口

  1. CoActionUnit · 第 41 行一个 Micro-MLP:每层都是 d×d 的 Weight 加 d 维 Bias,参数由 Induction 侧生成。
  2. CAN · 第 77 行Induction 侧:用户 ID(词表大、组合多,适合用参数生成)
  3. main · 第 109 行

完整源码 · 139 行

1"""2CAN:Co-Action Network,由特征生成参数(第15章 §15.5.7)34Co-Action Unit 把一对特征分成两个角色:5    Induction Side(参数生成侧):P_induction 被 reshape 成 Micro-MLP 的 W 与 b6    Feed Side(输入侧)        :P_feed 作为 Micro-MLP 的输入7        h_0 = P_feed8        h_i = σ(w_{i-1} h_{i-1} + b_{i-1})9        F(u, m) = [h_1 ‖ h_2 ‖ … ‖ h_L]1011Multi-order Enhancement:把 (P_feed)^c(c=1..C)分别送进同一个 Micro-MLP 再求和,12用 tanh 约束高阶项的数值范围(正文 §15.5.7)。1314参数量对比是本节的重点:15    笛卡尔积交互 Embedding:O(N²D)16    CAN:O(N(D' + D))17本脚本用一个 400×40 的 induction 参数表,与 400×200 的 pair 表直接对照。1819运行:python3 ch04_feature_interaction/can.py20"""2122from __future__ import annotations2324import pathlib25import sys2627import torch28import torch.nn as nn2930sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent))3132from _train_utils import (  # noqa: E40233    FIELD_NAMES, FIELD_SIZES, N_FIELDS, MLP, BaselineMLP, count_params, embedding_table,34    print_comparison, run_experiment, stack_embeddings,35)3637DIM = 438INDUCTION_LAYERS = 2394041class CoActionUnit(nn.Module):42    """43    一个 Micro-MLP:每层都是 d×d 的 Weight 加 d 维 Bias,参数由 Induction 侧生成。44    multi_order=True 时,Feed 侧额外提供 P² 作为第二阶输入。45    """4647    def __init__(self, dim: int, n_layers: int = INDUCTION_LAYERS, multi_order: bool = False):48        super().__init__()49        self.dim = dim50        self.n_layers = n_layers51        self.multi_order = multi_order52        self.param_per_layer = dim * dim + dim53        self.param_dim = n_layers * self.param_per_layer54        self.n_orders = 2 if multi_order else 15556    def forward(self, p_induction: torch.Tensor, p_feed: torch.Tensor) -> torch.Tensor:57        # p_induction: [B, param_dim],p_feed: [B, d]58        B = p_feed.shape[0]59        d = self.dim60        outs = []61        for c in range(1, self.n_orders + 1):62            # (P_feed)^c,用 tanh 做有界约束(§15.5.7 Multi-order Enhancement)63            h = torch.tanh(p_feed) if c == 1 else torch.tanh(p_feed ** c)64            layer_outs = []65            offset = 066            for _ in range(self.n_layers):67                w = p_induction[:, offset:offset + d * d].view(B, d, d)68                offset += d * d69                b = p_induction[:, offset:offset + d].view(B, d)70                offset += d71                h = torch.tanh(torch.bmm(h.unsqueeze(1), w).squeeze(1) + b)72                layer_outs.append(h)              # 每层输出都要保留73            outs.append(torch.cat(layer_outs, dim=1))   # Concat(h_1..h_L)74        return torch.stack(outs, dim=0).sum(0)   # Σ_c H(P_ind, (P_feed)^c)757677class CAN(nn.Module):78    """79    Induction 侧:用户 ID(词表大、组合多,适合用参数生成)80    Feed 侧    :物品 ID81    正文 §15.5.7 提到可以反过来选,取决于两侧规模;这里把选择写在构造参数里。82    """8384    def __init__(self, dim: int = DIM, multi_order: bool = False,85                 induction_field: str = "user", feed_field: str = "item"):86        super().__init__()87        self.emb = embedding_table(dim)88        self.induction_field = induction_field89        self.feed_field = feed_field90        self.unit = CoActionUnit(dim, INDUCTION_LAYERS, multi_order)91        # 只为 induction 侧的特征建一张"参数表",而不是为每个 pair 建 Embedding92        self.induction_table = nn.Embedding(FIELD_SIZES[induction_field], self.unit.param_dim)93        # 初始化偏大一些:权重太小会让 Micro-MLP 输出接近常数、梯度消失(实测94        # std=0.02 时 AUC 掉约 0.006,std=0.5 才与 Baseline 持平)。95        nn.init.normal_(self.induction_table.weight, std=0.5)96        inter_dim = self.unit.n_layers * dim97        self.mlp = MLP(dim * N_FIELDS + inter_dim, (32,), out_dim=1, dropout=0.2)9899    def forward(self, x):100        E = stack_embeddings(self.emb, x)                    # [B, m, d]101        i_idx = FIELD_NAMES.index(self.induction_field)102        f_idx = FIELD_NAMES.index(self.feed_field)103        p_ind = self.induction_table(x[:, i_idx])            # Parameter Lookup104        p_feed = E[:, f_idx]105        inter = self.unit(p_ind, p_feed)106        return self.mlp(torch.cat([E.flatten(1), inter], dim=1)).squeeze(-1)107108109def main() -> None:110    print("CAN:Induction Side 生成 Micro-MLP 参数,Feed Side 作为输入(§15.5.7)\n")111    m = N_FIELDS112    d = DIM113    n_items = FIELD_SIZES["item"]114    n_users = FIELD_SIZES["user"]115116    print("参数量对照(用户侧 × 物品侧):")117    print(f"  笛卡尔积交互 Embedding : {n_users} × {n_items} × {d} = "118          f"{n_users * n_items * d:,}")119    print(f"  CAN(induction 参数表): {n_users} × {CoActionUnit(d).param_dim} = "120          f"{n_users * CoActionUnit(d).param_dim:,}"121          f"(相当于前者的 1/{n_items})")122    print(f"  另一侧物品仍走常规 Embedding:{n_items} × {d} = {n_items * d:,}\n")123124    _, a_plain, _ = run_experiment(lambda: CAN(multi_order=False), "CAN", steps=600)125    _, a_multi, _ = run_experiment(lambda: CAN(multi_order=True), "CAN+Multi-order", steps=600)126    _, a_base, _ = run_experiment(BaselineMLP, "Baseline", steps=600)127128    print_comparison([129        ("Baseline MLP", a_base, count_params(BaselineMLP())),130        ("CAN", a_plain, count_params(CAN())),131        ("CAN + Multi-order", a_multi, count_params(CAN(multi_order=True))),132    ])133    print("\nCAN 的核心不是'多加一层网络',而是把过去固定的交互函数换成由一侧特征")134    print("现场生成的 Micro-MLP:不同 user 对应不同的交互函数(§15.5.7)。")135    print("micro-MLP 只有 1 层、无 bias、无激活时,它退化为内积,也就是 FM。")136137138if __name__ == "__main__":139    main()

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

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