本节摘要:异构图允许节点与边携带类型,关系图卷积按边类型分路聚合再求和,元路径视角把跨类型的间接关系串成语义通道;动态图分离散快照与连续事件两支,时间编码让模型感知边的时序位置。本节给出异构数据对象的组织方式、关系分路的代码实现与动态图的建模选型。
第一桩悬案来自内容平台:图里有用户、视频、标签、创作者四类节点,关注、发布、打标签、观看四类边——单类型的经典模型在这张图上等于让只会一种语言的侦探审多方言证词,逐类翻译又丢失跨类线索。第二桩悬案紧随其后:这张图还是活的,新边每分钟都在生长,上周的关系网与今天的关系网已经不同——静态快照式的建模等于拿旧照片破新案。两桩悬案合成本节的双重主题:异构改造让模型听懂多方言,动态改造让模型看清时间线。它们是第四章选编里"多类型边处理"与"图是活的"两条坑的正解,也是工业现场最常见的升级需求。

异构图的建模核心思想是"按类型分路,在节点处会师"。关系图卷积给出最清晰的实现:对每类边维护一套独立的变换参数,聚合时同类型的邻居走同一条路,各路结果在目标节点处求和融合。这样每种关系都学到了自己的"问询方式"——关注关系问出的是社交影响,观看关系问出的是内容偏好,两者在用户节点汇合,画像自然比混在一起问全面。类型带来的参数量问题(边类型过多时参数爆炸)有常规解法:基分解(把每类关系的参数写成共享基向量的组合)或按语义聚类合并类型。
元路径视角补充了间接关系的语义通道:用户到视频到标签再到视频的路径,语义上是"看过带同样标签内容的人还看什么"——把元路径上的游走结果当作额外的聚合通道(异构注意力网络的思路),模型就能显式利用这类多跳语义,而不必指望消息传递层数把它"顺带"学出来。
# 关系分路聚合的最小实现:每类边独立变换,节点处求和 import torch import torch.nn.functional as F n_users, n_videos, n_tags = 6, 5, 4 d = 8 x_user = torch.randn(n_users, d) x_video = torch.randn(n_videos, d) # 两类边:用户-观看->视频;视频-被打标签(此处仅示意观看路) watch_edges = torch.tensor([[0, 1, 1, 2, 3, 4, 5], [0, 0, 1, 1, 2, 3, 4]]) # (用户, 视频) 对 W_watch = torch.nn.Linear(d, d, bias=False) # 观看关系的专属问询 W_self = torch.nn.Linear(d, d, bias=False) # 自身通道 # 分路:把源节点特征经"观看路"变换,散射到目标节点求和 msg = W_watch(x_user[watch_edges[0]]) # 每条边的消息 agg = torch.zeros(n_videos, d) agg.index_add_(0, watch_edges[1], msg) # 按目标节点汇聚(置换不变) h_video = F.relu(W_self(x_video) + agg) # 会师:自身+观看路证据 print("视频节点新表示:", h_video.shape) # 再加"标签路""发布路",各路 index_add 后求和——异构建模的骨架就是如此直白
PyG 的 HeteroData 对象把这套结构工程化:不同类型的节点特征、不同类型的边索引分仓存放,层定义里给每种关系指定卷积实例,前向时框架自动路由。异构注意力进一步给每条路的每个邻居配上可学习权重,路内路间都实现"重点盯防"。
# PyG 异构数据对象与分路层的组织方式(示意骨架) from torch_geometric.data import HeteroData import torch hetero = HeteroData() hetero["user"].x = torch.randn(6, 8) hetero["video"].x = torch.randn(5, 8) hetero["tag"].x = torch.randn(4, 8) hetero["user", "watches", "video"].edge_index = watch_edges # ...其余关系照此填仓 from torch_geometric.nn import HeteroConv, GCNConv conv = HeteroConv( {("user", "watches", "video"): GCNConv(8, 8), # ("user","follows","user"): GCNConv(8,8), # ("video","has","tag"): GCNConv(8,8), }, aggr="sum") # 各路输出在节点处求和会师 out = conv({"user": hetero["user"].x, "video": hetero["video"].x, "tag": hetero["tag"].x}, hetero.edge_index_dict) print("异构前向输出类型:", {k: tuple(v.shape) for k, v in out.items()})
动态图的建模选型先看数据形态。快照式适合按天然时段组织的数据(每日交易图、每周社交图):每个时段一张静态图,分别做消息传递,节点表示跨时段传递(常用循环单元或注意力汇总历史),时空块结构在第四章交通预测的骨架里已经见过。连续式适合事件流数据(每条边带精确时间戳):模型按事件到达顺序更新节点记忆,时间编码(把时间差映射为可学习的正弦或指数特征,拼进消息)让"刚发生"与"很早发生"的边产生不同的影响——这是时序图网络一系的核心设计,其节点记忆模块本质是"给每个嫌疑人维护一份随事件更新的滚动档案"。选型经验:时段边界清晰、离线分析为主用快照式;事件密集、在线更新为主用连续式;两者也可混用(快照做粗粒度、连续做细粒度修正)。
# 时间编码:把时间差变成特征向量(连续式动态图的关键部件) import torch def time_encoding(dt: torch.Tensor, d=8): """dt: 事件距当前的时间差(越大越久远)""" freqs = torch.arange(1, d + 1) * 0.5 phase = dt.unsqueeze(-1) * torch.log(1 + freqs) # 频率递增的相位 return torch.cat([phase.sin(), phase.cos()], dim=-1) # [批, 2d] dts = torch.tensor([0.5, 6.0, 72.0, 720.0]) # 半小时前到一月前 enc = time_encoding(dts) print("时间编码形状:", enc.shape) print("相邻两事件编码的距离:", [round(float(torch.norm(enc[i] - enc[i+1])), 3) for i in range(3)]) # 时间差越大,编码距离整体越大且增速放缓——模型由此感知"新鲜度", # 新边的证词权重高,陈年旧边的证据自动衰减
下一桩悬案与模型能力无关、与办案资格有关:监管方要求模型的结论附带理由。可解释性改造登场。