首页 / 资讯中心 / 文章详情

URFormer:关系词表与专家路由实现异构图零样本迁移

URFormer:关系词表与专家路由实现异构图零样本迁移 ★ FEATURED ARTICLE
做异构图模型的同学大概都有过这种体验业务方甩过来一张新图schema 里躺着二十几种节点类型、上百种边语义你手里的模型只认得训练时见过的那几种关系于是又是一轮重写特征、重训 embedding、重调参。一个模型任意关系类型这句话之所以让人眼前一亮就是因为它戳中了这个反复返工的痛点。我这里要拆的这套框架内部代号叫 URFormer统一关系建模框架核心主张是把多张异构信息网络并成一张全局异构图用关系类型词表把每条元关系编码成可组合的向量再用一组按关系类型路由的轻量专家来承载不同关系的语义差异。模型预训练一次下游遇到没见过的关系类型时不需要重新训练主干也能直接干活。这篇文章适合三类人手上异构关系超过二十种、已经被 schema 演进折磨过的算法工程想搞清楚零样本迁移到新关系类型具体怎么落地、而不是只看个指标的读者以及准备自己复现一版做对比实验的研究者。下面我会把设计取舍、机制细节、可抄的训练配置和踩过的坑一次讲完。1. 为什么一个模型任意关系类型这句话值得较真先把问题摆到台面上。现实里的图数据几乎不存在节点只有一种、边只有一种的情况。电商场景里用户、商品、店铺、品类、品牌、优惠券互相连接边有浏览、加购、下单、退款、收藏风控场景里设备、账号、手机号、银行卡、收货地址交织边有登录、绑定、转账内容场景里作者、文章、话题、评论互相引用。这些图本质上都是异构信息网络Heterogeneous Information NetworkHIN它们的骨架由节点类型和关系类型共同定义而不是由节点本身定义。过去几年主流做法是一场景一模型给每种关系单独设一套 embedding 或者一条独立的注意力通道训练时把见过的关系类型都塞进去。短期指标确实好看长期维护成本高得离谱。最直接的后果是模型参数量随关系类型数量线性增长而真正高频的关系只有那么七八种剩下几十种长尾关系每种可能只有几千条边单独学一套参数根本学不动。更麻烦的是业务侧不会等你新品类、新互动行为、新设备指纹每个月都在加模型每次都要重训一遍。1.1 先把关系类型这个概念掰开很多人嘴上说关系类型实际上心里想的是边的名字。这两者不是一回事。在这套框架里一条关系类型被定义成一个三元组源节点类型、边语义、目标节点类型学术上常叫元关系meta-relation。举个具体的例子用户, 购买, 商品和用户, 点击, 商品是两条不同的关系类型用户, 购买, 商品和店铺, 售出, 商品也是两条不同的关系类型尽管边语义有交集。这个定义看着朴素但它直接决定了问题的规模。假设一张图里有 |A| 10 种节点类型|R| 12 种边语义那么理论上可能出现的关系类型数量是 10 × 12 × 10 1200 种而实际在数据里出现的可能只有一百多种。也就是说关系类型的空间是组合爆炸的但真实观测到的只是其中稀疏的一小片。这个空间大、观测稀的结构恰恰是任意关系类型能靠组合泛化做到的物理基础——如果每个关系类型都是完全独立的新事物那零样本迁移根本无从谈起。我把这个观察总结成一句话关系类型不是原子是可以拆的。拆成源类型、边语义、目标类型三段之后没见过的组合就能由见过的零件拼出来。整套方案的设计几乎都是围绕这句话展开的后面讲 tokenizer 的时候你会看到它是怎么落到实现上的。1.2 关系类型爆炸带来的三个真实麻烦第一个麻烦是长尾关系的表征学不动。单独给一条关系类型配一套参数样本量少的那些关系就永远停留在随机初始化附近。我实测过一个场景最高频的关系有 800 万条边最冷门的只有 300 条边。如果用独立参数冷门关系的召回率基本等于没训。而这类冷门关系往往业务价值不低——比如退款关系的样本天然就比下单少但它对风控判断的价值更高。第二个麻烦是schema 演进导致模型失效。业务新增一种互动行为比如收藏到合集这条关系的节点类型组合是用户, 收藏到, 合集训练集里从来没有过。旧模型的常规处理是把它当成未知边类型丢掉或者退回同构 GNN 用节点特征硬扛。丢掉信息是浪费硬扛则意味着模型对这个信号完全无感。这两种处理在实际业务里都很难交代。第三个麻烦是跨域迁移时语义对不上。A 业务里是买家, 下单, 货品B 业务里是用户, 购买, 商品人类一眼就知道是一回事但模型看到的是两个不同的符号 ID无法共享任何知识。这意味着每开一个新业务线就等于从零开始攒数据、训模型。这套框架想解决的本质上是让模型学到关系语义这件事本身而不是死记硬背关系类型的编号。1.3 这套思路适合谁、不适合谁说清楚适用边界比吹效果更重要。适合的场景有四个特征关系类型数量多超过 20 种、长尾关系占比高、schema 会持续演进、存在跨域迁移需求。推荐系统、风控图谱、内容理解、学术图谱这类场景基本都符合。不适合的场景也很明确。如果你只有一张同构图节点一种、边一种那这套东西的复杂度纯属浪费直接上常规 GNN 更省事。如果你的图极其巨大但只跑少数几种固定关系且对在线延迟卡到毫秒级那么全局异构图带来的采样开销和路由开销可能不划算。还有一种情况要谨慎如果你的关系类型之间语义差异极大且每种关系的样本都很充足那共享专家带来的收益会被稀释独立建模反而更直接。我个人的判断标准是——先看长尾关系的边数占比如果低于 5%收益会很有限如果在 15% 以上值得试。2. 架构选型为什么是全局异构图 关系词表 专家混合这套方案由三个部件咬合而成把所有任务图并成一张全局异构图、用一个关系类型 tokenizer 把元关系编码成可组合向量、用一组按关系路由的轻量专家承载语义差异。这三件事单独拿出来都不新鲜新鲜的是它们的组合方式。我在下面逐个说清楚为什么这么选以及为什么不选另外几条看起来更直接的路。2.1 把多张图并成一张全局异构图的三个好处最直觉的做法是每张子图单独训一个模型最后做集成。这个路子在小规模下能跑但它有三个致命问题参数量翻倍、跨图知识无法共享、推理时要维护一堆模型。合并成一张全局异构图之后这三个问题一次性解决。第一个好处是节点表征可以跨场景共享。同一个用户在推荐图里是买家在内容图里是读者在风控图里是账号主体。如果分开建模这三份表征互不相通合并之后它们是同一个节点的一个 token只是通过不同的关系类型连出去。这一点在冷启动场景下价值明显——新用户在主站没有行为但在另一个场景里有历史合并图能把这个信号带过来。第二个好处是关系类型词表只需要维护一份。所有场景的元关系都进同一张词表词表规模可控实测一百多个元关系就够覆盖大部分业务而且新增关系只需要往词表里加几个组合不需要改模型结构。第三个好处是采样可以在一个 batch 里混着来。训练时从一个全局图里按元关系分层采样一个 batch 里可能同时有推荐样本、内容样本、风控样本。这种混合采样自带正则效果我实测下来比单场景训练更不容易过拟合尤其是在小场景上。代价当然也有。全局图的邻居采样逻辑要写得足够稳否则一个场景的超级节点会把 batch 撑爆另外全局图的边表规模通常比单场景大一到两个数量级对存储和加载有要求。我的处理办法是节点特征存成按类型分片的稀疏矩阵边表按元关系分文件加载时按需 mmap不要一次性全塞进内存。2.2 关系类型 tokenizer把一条关系拆成三段可学习的向量这是整套方案里我认为最关键的一个设计。它的做法很朴素一条元关系 (src_type, edge_type, dst_type) 不去学一个整体 ID 的 embedding而是拆成三段分别查表再拼起来过一个小的投影层。为什么这么做因为整体 ID 的 embedding 在面对新关系类型时只能随机初始化而拆成三段之后新组合的每一段大概率都在训练集里出现过。举例用户, 收藏到, 合集这条关系在训练集里完全没出现过但用户作为源类型出现过无数次收藏到这个边语义可能在别的组合里出现过比如用户, 收藏到, 商品合集作为目标类型也可能出现过比如文章, 属于, 合集。三段都有已知信息拼出来的向量就不是随机的模型能做出合理推断。具体实现上三段各有一个 embedding 表维度设为 d/3 或者各 d/2 再加投影都行我用的是各 128 维、拼接后过一层 256 维的线性投影加 LayerNorm。投影层的存在很必要因为三段拼接后的分布和单段 embedding 差别很大不归一化的话后面路由会抖。另外我建议给每一段额外乘一个可学习的重要性权重三个标量softmax 归一化这样模型能自己调节边语义更重要还是节点类型更重要。在我的实验里边语义的权重普遍偏高符合直觉。注意三段 embedding 的表要分开建不要把 src_type 和 dst_type 塞进同一张表。虽然它们都是节点类型但扮演的角色不同共享表会让用户作为源和用户作为目标混成一个向量实测掉点 1 到 2 个百分点的 NDCG。2.3 MoT 专家不是给每种关系训一个模型而是让关系挑专家MoT 是 Mixture of Thought Experts 的缩写本质上是一种稀疏专家混合。它的结构是主干 Transformer 的所有参数共享只在 FFN 层注入若干组低秩适配器LoRA 或 adapter每组称为一个专家前向时根据当前 token 所属的关系类型向量路由网络选 top-k 个专家加权求和输出。这个设计解决了差异要建模、参数又不能爆炸的矛盾。关系类型之间确实存在语义差异——浏览和下单对用户兴趣的表达强度完全不同用同一套 FFN 处理会互相干扰。但差异又不是完全正交的点击和收藏都是弱反馈共享一套参数完全合理。让路由网络自己学哪些关系该共享、哪些该分开比人工划分关系簇靠谱得多。为什么用 LoRA 而不是给每个专家配完整的 FFN算笔账就清楚了。假设隐藏维度 256、FFN 中间层 1024一个完整 FFN 的参数量是 256×1024×2 ≈ 52 万。8 个专家就是 420 万乘上 4 层是 1680 万还只算了 FFN。改成 LoRArank8单个专家单层的参数量是 8×(2561024) ≈ 1 万8 个专家 4 层总共才 33 万左右差了 50 倍。而且 LoRA 的初始化为零矩阵训练初期等价于没有专家不会破坏主干已经学好的表征这对增量上线新关系类型特别友好。路由网络的设计也有讲究。输入是关系类型向量tokenizer 的输出加一个可学习的全局偏置输出是专家数量维的 logits取 top-2 后做 softmax 加权。我试过 top-1训练不稳定因为路由一旦早期选错就再难纠正top-3 以上收益不明显但显存上去了。top-2 是性价比最高的点。另外必须加负载均衡损失否则路由会坍缩到少数几个专家上这个坑我在第 5 节会细讲。2.4 四条路线的横向对比为了让你直观感受取舍我把常见的四种做法摆在一起比一下。这张表是我自己在选型时整理的数值来自同一个小规模基准约 120 万元关系边、26 种元关系、留出 5 种元关系做零样本测试。方案参数随关系类型增长未见关系类型的支持方式零样本 NDCG10显存占用相对值主要问题独立 embedding 的 GNN 基线线性增长无直接丢弃0.211.0长尾关系学不动分层注意力类型级 节点级近似线性退回类型无关注意力0.291.4schema 一变就退化单塔 Transformer关系只做偏置常数有偏置但无专家区分0.331.1语义差异大的关系互相干扰全局图 关系词表 MoT常数组合泛化 路由推断0.411.6采样与路由调参成本高看这张表要注意最后一行领先的 0.08 不是在所有场景都能复现的。它的优势集中在两类样本上一是训练时完全没出现过的元关系二是长尾元关系边数少于 5000 的那些。在高频关系上它和单塔 Transformer 的差距在噪声范围内。所以如果你的业务以头部关系为主别指望这套东西能带来明显提升。3. 核心机制拆解一层 URFormer 里到底发生了什么前面讲的是为什么这么设计这一节讲具体怎么算。我会沿着数据流走一遍输入侧的张量组织、注意力里的关系感知、专家路由的前向、预训练目标的设计最后给一组参数量和显存的手算过程。这一节偏工程实现如果你只想跑起来可以直接跳到第 4 节但我建议还是看一眼后面排查问题时这些细节会救你。3.1 输入侧到底喂了哪三个张量一条训练样本不是一个节点而是一个以目标节点为中心的 k-hop 子图。我采样的是 2-hop第一跳每类关系最多采 16 个邻居第二跳每类关系最多采 8 个总量上限 512 个节点。为什么按关系类型分层采样而不是全局随机因为异构图的度分布极度不均全局随机采样会让某些关系在子图里彻底消失等于这个样本白采。喂进模型的张量有三个。第一个是节点特征矩阵 X形状是 [B, N, d_in]B 是 batch 里的样本数N 是子图节点数上限不足补零配 maskd_in 是原始特征维度异构节点的特征维度不一致时先在输入侧做投影对齐到 256 维。第二个是节点类型 id形状 [B, N]用来查类型的 embedding 并加到节点表征上。第三个是边的关系类型 id形状 [B, N, N] 或者稀疏形式 [B, E]E 是子图内的边数。第三个张量是这套方案和常规 GNN 最大的区别所在。常规做法是把边类型当成注意力偏置类似相对位置编码这里更进一步——每条边的关系类型 id 会被查成完整的 256 维向量作为该条边的关系 token参与到后续计算里。也就是说一条边的语义不是加在注意力分数上的一个标量偏置而是有自己完整表征的对象。这个设计让关系类型能承载更复杂的语义也为后面的专家路由提供了输入。提示边的关系类型 id 一定要用元关系 id三元组整体不要只用边语义 id。我第一版偷懒只用了边语义结果用户, 购买, 商品和店铺, 购买, 商品被当成同一种关系退货率预测的指标掉了 4 个百分点。三元组整体编码才能区分方向。3.2 注意力计算先关系内、再关系间注意力层的计算分两步走这个分层结构是保证关系感知不失效的关键。第一步是关系内注意力也就是只在同一条元关系的邻居之间做 softmax。具体地对于目标节点 i 和它通过元关系 r 连出去的邻居 j注意力分数是 query 与 key 的点积加上一个关系偏置项偏置项来自 tokenizer 输出的关系向量过一个标量投影。softmax 在这条关系的邻居集合内归一化。这么做的好处是不同关系的邻居数量差异巨大有的关系几十个邻居有的只有一个如果放在一起 softmax邻居多的关系会压倒邻居少的关系。第二步是关系间聚合把每条关系算出来的聚合结果当成一个新的 token在关系维度上再跑一次注意力或者加权求和。权重由关系向量和当前节点表征共同决定相当于让模型自己判断对这个节点来说哪条关系更该被关注。这一步的计算量很小因为一个节点的关系类型通常不超过 10 条。实现上有个细节要注意fp16 下做两步 softmax 容易数值溢出尤其是在第二步关系数很少的时候。我的处理是第一步的 attention logits 减最大值标准操作第二步改用 fp32 计算虽然会慢一点点但避免了 loss 突然变 NaN。这个坑我在第 5 节还会提到。3.3 专家路由的前向过程节点表征经过注意力层之后进入 FFN这里就是 MoT 生效的地方。具体流程是拿当前 token 所属的元关系向量如果是一个聚合后的节点 token就取它所有入边关系的加权平均送进路由网络得到专家 logits取 top-2softmax 归一化得到权重然后分别过两个专家输出按权重加权求和。路由网络本身很轻就是一层 256→专家数的线性层参数量可以忽略。它输入的除了关系向量我还额外拼了一个层标识embedding因为不同层关注的粒度不同——浅层更关注局部结构深层更关注语义。实测加这一项在长尾关系上有小幅提升。训练时这个路由是可微的softmax 加权而非硬选推理时可以走硬选加速。有一点要强调专家是加在 FFN 上的不是替换 FFN。也就是输出 FFN(x) Σ w_k · LoRA_k(x)其中 LoRA_k 的 B 矩阵初始化为零。这样初始状态下模型行为和没有专家的 Transformer 完全一致训练是平滑演进的。如果直接把 FFN 替换成专家初始化那一刻分布就偏了收敛会慢很多。3.4 预训练目标图本身给的监督信号这套方案能预训练一次到处用靠的是自监督任务设计。用的是最朴素的一个给定源节点和关系类型预测目标节点的类型。比如从用户 u 通过购买关系连到了 v模型要预测 v 属于商品这个节点类型。这个任务不需要任何人工标注只要有图结构就能采样出无穷多的训练样本。为什么选这个任务而不是链接预测因为链接预测需要负采样而异构图的负采样极难做对——随机采一个节点作为负例大概率是类型不对模型直接靠类型判断就能拿高分学不到结构信息。节点类型预测没有这个问题它的难度天然来自于不同关系类型下的邻居分布差异。训练时会对目标节点的类型做标签平滑label smoothing 0.1一方面是防止过拟合另一方面是因为有些节点的类型本身就有歧义比如店铺和品牌在部分数据里混用。我实测标签平滑在这个任务上带来了 1.5 个点的 Macro-F1 提升。3.5 参数量与显存的手算过程为了让你有直观感受我按自己的实际配置算一遍。隐藏维度 2564 层 Transformer专家数 8top-2 路由LoRA rank 8FFN 中间层 1024。主干参数大致是每层的多头注意力4 头256 维约 26 万FFN 约 52 万两项合计 78 万4 层 312 万。节点类型 embedding 表和关系 tokenizer 的三张表加起来约 20 万26 种元关系对应的类型数量不多。LoRA 专家 33 万。总计约 370 万参数模型权重 fp16 下不到 8 MB。这个体量非常小意味着你可以把它部署在很边缘的机器上甚至 CPU 推理都能接受。显存的大头在激活值而不是参数。一个 batch 32 个样本、每个样本子图上限 512 个节点、256 维 fp16单层激活约 32×512×256×2 字节 ≈ 8.4 MB乘上中间变量数量约 6 倍和层数4 层但要存反向传播的中间值实际约 2 倍后大概 400 MB。加上注意力矩阵 32×512×512×2 字节 ≈ 16.8 MB如果不用稀疏掩码会更多总共 1 GB 以内能跑起来。我用单卡 24 GB 训练batch 开到 128 也没有 OOM实际瓶颈在数据加载而不在显存。这个账算下来得到的结论是这套方案的工程门槛比看起来低得多。它不是一个必须大算力才能玩的东西小团队完全跑得起。真正的成本在数据侧的 schema 整理和采样逻辑上而不是模型本身。4. 复现路径从 schema 定义到零样本评测这一节是可直接抄的部分。我按实际动手顺序写先定义 schema 和数据集再给采样和 tokenizer 的关键代码然后是训练配置最后是怎么设计零样本评测协议。每一步我都标注了容易出错的点和我的实际数值。4.1 第一步把 schema 定义清楚schema 定义是整个复现过程里最容易被跳过、但影响最大的一步。我用一个 Python 字典来描述节点类型、边语义、元关系三段分明。关键点是元关系不要手写从节点类型和边语义自动展开——因为后面采样和 tokenizer 都要按元关系 id 索引手写容易漏。# schema 定义节点类型、边语义、自动展开的元关系 NODE_TYPES [user, item, shop, category, brand, coupon] EDGE_SEMANTICS [click, favorite, cart, buy, refund, belong_to] # 每条边语义允许的 (src_type, dst_type) 组合不写全只写业务上真实存在的 ALLOWED { click: [(user, item), (user, shop)], favorite: [(user, item)], cart: [(user, item)], buy: [(user, item), (shop, item)], refund: [(user, item)], belong_to:[(item, category), (item, brand), (coupon, shop)], } META_RELATIONS [] for sem, pairs in ALLOWED.items(): for src, dst in pairs: META_RELATIONS.append((src, sem, dst)) META2ID {mr: i for i, mr in enumerate(META_RELATIONS)} print(len(META_RELATIONS)) # 本示例展开后是 11 条元关系这里有个容易踩的坑ALLOWED里只写真实存在的组合不要图省事用节点类型两两笛卡尔积。原因是你如果在 schema 里声明了 1200 种元关系但实际只有 11 种有边那么 tokenizer 的三张表会有大量永远不被激活的行训练时它们拿到的是纯噪声梯度会干扰路由网络的学习。宁可少声明用到再加。另外一个建议是给每个元关系加一个频次统计训练时对极低频元关系比如边数少于 100做下采样或者干脆不进训练集留给零样本评测用。这一步我在 4.4 会展开。4.2 第二步子图采样与 tokenizer 实现采样逻辑我写成按元关系分层的形式。核心思路是给定一个目标节点的元关系集合对每种元关系独立采样固定数量的邻居然后拼成一个子图。这样能保证子图里的关系类型分布相对均衡不会被超级节点主导。import random def sample_subgraph(seed_node, adj_by_meta, meta_ids, fanout(16, 8), max_nodes512): adj_by_meta: {(meta_id, node_id): [neighbor_ids]}按元关系分桶存储 seen {seed_node} frontier [seed_node] node_list, meta_list, src_idx, dst_idx [], [], [], [] for hop, k in enumerate(fanout): next_frontier [] for u in frontier: for mid in meta_ids: nbrs adj_by_meta.get((mid, u), []) if not nbrs: continue # 采样 k 个邻居如果邻居数远大于 k 则不重复采样 picked nbrs if len(nbrs) k else random.sample(nbrs, k) for v in picked: if len(node_list) max_nodes: break if v not in seen: seen.add(v) next_frontier.append(v) node_list.append(v) src_idx.append(u) dst_idx.append(v) meta_list.append(mid) frontier next_frontier[:64] # 控制二跳的入口数量防止爆炸 if not frontier: break return node_list, meta_list, src_idx, dst_idx这段代码里frontier next_frontier[:64]那一行是我调了两天才加的。不加的话二跳入口可能上千个采样耗时直接失控而且采出来的节点 90% 都在同一个超级节点的邻域里信息量极低。截断到 64 个入口之后采样耗时从每 batch 1.2 秒降到 0.2 秒指标基本没变。tokenizer 的实现就是三段查表拼接加投影import torch import torch.nn as nn class RelationTokenizer(nn.Module): def __init__(self, num_src, num_sem, num_dst, dim128, out_dim256): super().__init__() self.emb_src nn.Embedding(num_src, dim) self.emb_sem nn.Embedding(num_sem, dim) self.emb_dst nn.Embedding(num_dst, dim) self.proj nn.Linear(dim * 3, out_dim) self.norm nn.LayerNorm(out_dim) # 三段可学习重要性权重 self.weights nn.Parameter(torch.ones(3)) def forward(self, src_ids, sem_ids, dst_ids): w torch.softmax(self.weights, dim0) e torch.cat([ self.emb_src(src_ids) * w[0], self.emb_sem(sem_ids) * w[1], self.emb_dst(dst_ids) * w[2], ], dim-1) return self.norm(self.proj(e))注意self.weights那个乘法的位置。我一开始是在拼接之后做加权求和效果反而变差了因为拼接后的三维度之间本来就有量纲差异加权求和会放大波动。改成先乘再加投影训练曲线明显更稳。这个小改动带来的提升在长尾关系上有 1 个多百分点。4.3 第三步训练配置与超参训练配置我整理成了一张表这些都是我在小规模基准上试出来比较稳的值。规模更大的时候需要按比例调整尤其是学习率和 batch size。配置项取值说明优化器AdamWβ2 用 0.95 而不是 0.999图类任务上更稳主干学习率2e-4预训练时用 1e-4 也可以预训练更长就调小专家LoRA学习率1e-3专家的量级小学习率可以放大 5 倍路由学习率5e-4介于两者之间太大容易坍缩warmup总步数的 5%路由网络特别需要 warmupbatch size64显存允许就往上加128 也跑得动梯度裁剪1.0必开专家梯度偶尔会尖峰dropout0.1子图内部节点特征上 0.1 就够负载均衡损失系数0.01关键参数第 5 节细说预训练轮数15 到 20用验证集节点类型 Macro-F1 早停启动命令很朴素用标准的分布式训练就行python -m torch.distributed.run --nproc_per_node2 train_pretrain.py \ --schema config/schema_demo.json \ --graph_root /data/global_hin \ --hidden 256 --layers 4 --heads 4 \ --num_experts 8 --topk 2 --lora_rank 8 \ --batch 64 --lr_main 2e-4 --lr_expert 1e-3 \ --balance_coef 0.01 --epochs 20主干和专家的分离学习率必须分开设这是我在复现时最容易忽略的一点。如果共用同一个学习率要么主干学得不够要么专家过拟合到少数几个高频关系上。我用的是 parameter group 分别指定代码里按参数名前缀匹配即可。4.4 第四步零样本评测协议怎么设计这一节可能是最容易被做错的地方。很多复现的指标虚高问题都出在评测协议上。我的做法是按元关系整体留出而不是按样本随机留出。具体地从 26 条元关系里挑 5 条这 5 条的所有边在训练阶段完全不出现测试时只在这些边对应的样本上评。挑哪 5 条也有讲究不能随机挑。我按两个维度挑一是要有 2 条是组合新但零件旧的三段里每一段都在训练集出现过二是要有 1 到 2 条是零件也新的至少有一段完全没出现过。这两种情况的难度差很远混在一起报指标会误导。实际数值上我实测的结果大致是这样组合新的元关系上零样本的节点类型预测 Macro-F1 能到完全监督版本的 82% 左右零件也新的那种只有 63% 左右基本就是靠类型 embedding 的初始化和路由先验在撑。这个落差需要在方案宣传时如实说明——任意关系类型指的是架构上支持不是指标上无损。还有一个细节零样本评测时新元关系的 embedding 是随机初始化的。如果你能拿到新关系的少量边比如 50 条做几步微调只训 tokenizer 和门控冻住主干指标能拉回到完全监督的 90% 以上。我把这个叫热启动实际落地时这个设置更现实——纯零样本更多是研究指标业务上一般多少能拿到一点样本。4.5 实测的吞吐与成本最后给一组实际数字方便你判断资源需求。在 120 万元关系边、平均子图 180 个节点的配置下双卡 24 GB 训练一个 epoch 大约 22 分钟20 个 epoch 跑完约 7.5 小时。推理侧单卡 batch 256 的 QPS 约 1400fp16如果开启专家硬选和 fp32 只在关系间聚合那一步用QPS 能提到 1800 左右。这套数字意味着一次完整实验的算力成本是很低的。真正的时间成本在数据侧——把业务图整理成统一的 HIN 格式、给每条边打上正确的元关系 id、处理脏数据里的类型不一致这部分我花了整个项目六成的时间。如果你准备动手建议先把 schema 和数据清洗做扎实模型侧反而是最快的一步。5. 实战问题排查几个把我卡住过的坑理论和代码都写完了实际跑起来才是见真章的时候。这一节我把遇到过的问题、定位思路和解决方式整理出来其中前两个是这套方案独有的和专家路由有关后面几个是异构图模型的通病但在这个框架下表现有差异。5.1 专家路由坍缩8 个专家最后只有 2 个在干活这是最常见也最隐蔽的问题。表现是训练 loss 正常下降验证指标也不算差但你去看路由的专家使用统计会发现 8 个专家里有 5 个的调用次数接近于零所有 token 都挤在两三个专家上。这个时候模型退化成了一个参数量更大的普通 Transformer专家机制形同虚设。原因是路由网络的正反馈循环某个专家早期因为随机初始化恰好表现稍好拿到的梯度就更多表现更好然后被选得更多。这个马太效应在训练初期几轮内就会固化。解决办法有三层。第一层是必加的负载均衡损失形式是专家使用频率的方差项系数 0.01。这个系数很敏感0.001 基本没效果0.1 会压得路由无法区分关系类型指标反而掉。第二层是路由 logits 加噪声训练时给 logits 加一点高斯噪声标准差 0.1推理时去掉这个技巧能显著缓解早期固化。第三层是监控我会在训练日志里每 100 步打印一次专家使用分布一旦发现某个专家连续 500 步零调用就说明均衡没做好得回去调系数。注意不要用硬性轮转强制轮流选专家来解决坍缩。我试过训练 loss 能降但验证指标明显变差因为强行让不合适的专家处理不熟悉的关系类型等于给模型注入了错误先验。均衡损失加噪声是更温和也更有效的路子。5.2 新关系类型上线后指标反而下降这个问题比路由坍缩更让人抓狂明明是为了支持新关系结果加进去之后老关系的指标也掉了。我遇到过两次原因不同。第一次是词表污染。新加的元关系在 tokenizer 的三段里引入了新的节点类型而这些新类型的 embedding 是随机初始化的训练时它们参与到了共享的投影层和路由网络里把原本稳定的分布带偏了。解决办法是给新加入的类型 embedding 加一个小的初始化方差比如 0.02 而不是默认的 1.0并且在头几百步冻结投影层只训新表。第二次是采样预算被挤占。全局图的子图节点上限是固定的我设的 512新增关系之后同样的预算要分给更多关系导致每条关系分到的邻居数变少老关系的子图信息变稀疏了。这个问题的定位方法很直接对比加关系前后每个样本的平均子图节点数和每种关系的平均邻居数一看就明白。解决办法是把上限提到 768或者在采样时按关系类型设优先级高频关系保持原配额。5.3 一份可以贴在显示器上的排查表我把常见现象和对应原因整理成表方便你按症状查。现象最可能的原因排查动作处理方式loss 突然变 NaN关系间聚合用了 fp16打印该步的 logits 范围关系间 softmax 强制 fp32验证指标卡在基线不动专家全坍缩或全不用看专家调用分布调均衡系数到 0.01加路由噪声长尾关系指标极差采样时被高频关系挤掉统计每种关系的平均邻居数按关系分层采样设最小配额零样本新关系几乎随机三段里有新零件检查新元关系三段是否都见过加少量样本热启动微调训练快但推理慢路由走了 soft 加权profile 路由耗时占比推理改硬选 top-2加新关系后老关系掉点子图预算被摊薄对比加前后的子图规模提上限或设关系优先级指标波动大重跑差异明显采样随机性太强固定种子重跑三次固定采样种子 增大 batch这张表里我觉得最值得记的是固定种子重跑三次这一条。图模型的方差本来就大很多所谓的改进其实是随机波动。我现在的习惯是任何改动都要跑三个种子取均值差异小于 0.5 个百分点的一律当噪声处理不写进结论。5.4 几条不太会写在文档里的心得第一条先把单关系跑通再上全局图。我一开始就直奔全局异构图的实现结果采样、类型对齐、路由三处问题同时出现根本没法定位。后来退回单场景只有 6 种元关系把整条链路跑顺再逐步加场景问题就一个一个浮出来了。这个顺序看起来慢实际快得多。第二条关系类型词表要留扩展位。embedding 表可以预留 20% 的空行方便后续加新类型时不用改 checkpoint 结构。我第一版表大小写死了后来加关系只能重训白扔了两天。第三条别迷信 top-k 路由的 k 值。很多人觉得专家多了 k 就该大其实不然。我的实验里专家数从 4 加到 8 时top-2 一直是最优加到 16 个专家时 top-2 仍然够用因为路由本来就是在做稀疏选择k 大了反而破坏了稀疏性带来的正则效果。第四条评估时一定要分关系类型看指标不能只看总体均值。总体均值会被高频关系主导把长尾关系的恶化完全掩盖。我习惯在评估脚本里输出一张按元关系分组的指标表每次看这张表比看总均值有用十倍。我个人在实际操作中的体会是这类统一建模方案的真正价值不在于论文里那个漂亮的平均指标而在于它把支持新关系类型从一次工程改造变成了一次配置改动。这个转变对长期维护的意义远大于单点指标的提升。至于后续还能怎么扩我目前在试两个方向一是把关系类型的层级结构比如购买是互动的子类编码进 tokenizer二是让专家在层与层之间共享而不只是层内独立前者看起来更有戏。
阅读完成 · 觉得有帮助?
咨询建站