做图神经网络这几年,尤其是异质图方向,我翻论文时有个习惯:先看有没有源码,再看它是不是只在干净数据集上刷点。HGNN-AC这篇发表于2021年Web会议上(题目是《Heterogeneous Graph Neural Network via Attribute Completion》)的文章,之所以值得单独写一篇解读,是因为它精准命中了我实际项目里反复遇到的痛点——真实场景中的图数据,节点属性从来不完整。它做的事情可以用一句话概括:在异质图神经网络的消息传递过程中,把“属性缺失”当一等公民来处理,而不是在预处理阶段用零值凑合。如果你正在做图上的节点分类、用户画像补全、推荐系统冷启动,或者单纯想搞明白元学习怎么真正落进图任务,这篇文章值得花半小时读完。我下面把我的解读、复现心得和踩坑记录一起放出来。
1. 问题从哪来:异质图神经网络的“属性完整”假设
1.1 异质图为什么不能直接套 GCN
先对齐一下基础概念。异质图指的是节点类型和边关系类型都超过一种的图。学术网络是典型例子:作者、论文、会议是三种节点,论文和作者之间是“写作/被写”,论文和会议之间是“发表/收录”,作者之间可能还有“合作”关系。放在推荐场景里,用户、商品、类目、品牌都是不同类型节点,边关系也五花八门。传统 GCN 这类同质模型之所以在这里容易翻车,是因为它默认所有节点共享一个特征空间、一种邻居语义。但异质图里不同类型的节点特征向量含义完全不同,直接拼在一起做邻居聚合,等于把苹果和橙子混在一起榨汁,信息互相干扰。
这就是异质图神经网络存在的意义。HAN、HGT 这些模型通过类型级别的注意力机制,先对不同关系类型下的邻居做加权聚合,再对节点类型的差异做融合,让每种关系只影响它该影响的维度。从结果看,这类模型在学术网络、商品推荐网络上的节点分类和链路预测确实比同质模型强得多。但几乎所有异质图神经网络在推导公式时,都默认输入属性矩阵是完整的。
1.2 属性缺失在真实场景里有多普遍
我自己的经验是:现实里拿到的图数据,属性缺失才是常态。用户在社交平台注册时会跳过一堆选填项,论文在数据库里经常缺摘要和关键词,商品描述在不同渠道里一半是空的,甚至同一个用户在不同设备上留下的行为特征,对齐之后也会缺一大块。更麻烦的是,缺失不是随机的,它往往和节点本身的状态相关——活跃用户画像更完整,冷启动用户几乎什么都没有。这种非随机缺失会让简单的填充方法引入系统偏差。
放在异质图里,问题还会加倍。不同节点类型属性维度不同,同一批用户里有些人有全部属性,有些人只有行为序列没有静态画像。消息传递的时候,一个有缺失属性的节点会把它的“残缺表示”传给邻居;邻居再往上聚合,缺失信息就像滚雪球一样扩散。如果缺失率上了百分之三五十,注意力机制算出来的权重基本是噪声。
1.3 常规填充方案错在哪
常见的处理思路无非几种。零填充最简单,但对异质图来说等于强行告诉模型“这个维度上大家都是零点”,会引入假的相似性。均值填充稍微好一点,但会把不同节点的属性往全局分布上拉,抹平个体差异。用自编码器或者说矩阵补全做预处理,理论上更高级,但它是任务无关的——补全目标只是让重建误差最小,完全不管下游是要做分类还是做检索。还有一个更隐蔽的问题:这些方法把补全和下游任务拆成了两个独立阶段,补全时错杀的有用信息,下游任务永远找不回来。
HGNN-AC 的核心立场就是:属性补全这件事不能离线做、孤立做,它应该和下游任务联合优化。补全器需要知道下游分类器想要什么样的特征,而不是一味地还原“真实值”。
2. 核心思路:把属性补全变成下游任务的一部分
2.1 任务导向补全,而不是重建导向补全
先讲一个直觉上的区别。如果纯粹做属性重建,目标函数是让预测出来的缺失值和真实缺失值尽量接近。看起来没毛病,但仔细想会发现:分类器真正关心的是特征里的判别信息,不是完整信息。一个属性可能有几十个维度,其中三个维度对分类特别关键,其他维度对分类没有区分度。重建导向的模型会把所有维度的误差一视同仁地压,反而可能把关键维度的微弱信号磨平了。
HGNN-AC 的思路是“面向任务补全”,目标函数不是补全值和真实值的距离,而是“补全之后的属性能不能让下游分类器取得更好的效果”。这就相当于让生成器不是为了像原图,而是为了让判别器更好认。换句话说,补全器学到的是:什么样的补全会把分类器需要的信息保留下来,而不是机械地模仿完整数据分布。
2.2 元学习在这里解决什么
如果只是把补全器和分类器放在一起联合训练,面临的难点是:补全器要适应的不是某一种固定缺失模式,而是各种比例、各种位置的缺失。训练时如果只在一种掩码模式下训练,测试时遇到不同缺失比例,补全器马上失灵。元学习就是为这种“快速适应新任务”而设计的。
具体到这篇文章,作者借鉴了 MAML 的“支持集/查询集”和双层优化思想,但它没有用 MAML 那种按任务划分数据集的方式,而是按“缺失模式”划分任务:同一批图数据,随机采样出多种缺失掩码,每一种掩码构成一个元任务。补全器需要学会一套初始化参数,遇到新的缺失模式时,只需几步梯度更新就能快速调整到合适状态。分类器同理,也需要在支持集上做内层更新,再在查询集上评估,确保它在补全后的特征上足够稳定。
2.3 整体框架一看就懂
整个 HGNN-AC 框架很好理解:输入异质图和部分可见的属性矩阵,先由 Meta-Completer 补全属性,再由一个异质图编码器(论文里用的是 HAN 这类模型)做消息传递得到节点表示,最后由 Meta-Classifier 做节点分类。训练过程不是简单的“先补全再训练”,而是补全器和分类器互相咬合、交替更新。这里补全器和分类器的更新依赖是耦合的,补全器一变,分类器的输入特征就变,分类器的梯度反过来又指导补全器调整方向。想一想确实能感觉到,这个机制比“预处理阶段一次性填充”高明在哪儿。
3. 方法拆解:HGNN-AC 里两个元学习器怎么配合
3.1 符号定义与掩码机制
先定义清楚数学符号。异质图可以记成 G = (V, E, φv, φe),V 是节点集合,E 是边集合,φv 把每个节点映射到节点类型集合 Tv,φe 把每条边映射到关系类型集合 Te。节点属性矩阵是 X ∈ R^{n×d},n 是节点数,d 是属性维度。现实情况里 X 只有一部分是可见的,用掩码矩阵 M 表示:Mij = 1 代表第 i 个节点的第 j 维属性可见,Mij = 0 代表缺失。
训练的时候,模型会对完整的属性矩阵随机挖掉一部分,模拟缺失场景。要注意的是,掩码不仅会影响补全器看到的输入,也会影响损失函数的计算范围。补全器只知道可见位置的值,预测未可见位置的值;重建损失也只作用在缺失位置上,防止补全器作弊——如果它既看到了输入又拿同一个位置的输出算损失,那等于直接抄答案。
3.2 补全器 Meta-Completer 怎么工作
Meta-Completer 的输入是可见属性 X ⊙ M 和图的邻接/类型结构,输出是补全后的 X_hat。工程实现上,它通常是一个图神经网络编码器加一个映射层。编码器负责聚合邻居信息,因为缺失维度的推断不能只靠节点自身,要靠结构相似节点的属性来推理。例如一个用户没有填职业,但他的学术合作者大多是某方向研究者,那这个用户大概率也是相关方向从业者。这正是图结构相比传统表格数据的优势。
补全器的核心特殊性在于训练方式。它的参数 θg 不是直接只通过重建损失更新,而是要让“补全后的属性”在分类器上表现好。我理解论文里这个设计是把分类器当成补全器的“评委”,补全器生成的属性如果让评委很难判断,就会收到惩罚,从而调整生成策略。
3.3 分类器 Meta-Classifier 怎么工作
Meta-Classifier 本质上还是一个异质图神经网络分类器,通常接 HAN 或同类模型作为编码器。它对不同类型邻居计算注意力权重,把邻居信息加权聚合进当前节点表示,然后接一个线性分类层输出类别概率。它的参数 θc 更新分为内循环和外循环两部分:内循环在支持集上做常规梯度更新,得到一个临时的分类器;外循环在查询集上评估这个临时分类器,用评估结果反过来指导补全器的更新。
这个过程可以换成人话:先给分类器“喂”一小批数据让它快速学几步,然后拿这些数据之外的节点来考它,如果考砸了,说明问题出在补全器给的属性上,于是回头调整补全器。补全器为了让分类器在“没见过的节点”上也表现好,就必须学会生成更通用的属性,而不是死记硬背训练集特征。
3.4 双层优化训练流程与伪代码
我把训练流程整理成可执行的伪代码结构,方便理解参数更新顺序:
for epoch in range(max_epoch): # 1. 随机生成缺失掩码,模拟属性缺失 mask = random_mask(ratio=config.mask_ratio) x_visible = x * mask # 2. 补全器预测完整属性 x_hat = completer(x_visible, adj, mask) # 3. 异质图编码器生成节点嵌入 embeddings = heterogeneous_backbone(x_hat, adj) # 4. 内循环:分类器在支持集上快速更新 theta_c_temp = theta_c for step in range(inner_steps): loss_inner = cross_entropy(classifier(embeddings, support_idx), support_y) theta_c_temp = theta_c_temp - inner_lr * grad(loss_inner, theta_c_temp) # 5. 外循环:用更新后的临时分类器评估查询集 loss_meta = cross_entropy(classifier(embeddings, query_idx), query_y) # 注意:这一步的梯度要回传到补全器参数 theta_g theta_g = theta_g - meta_lr * grad(loss_meta, theta_g) # 6. 常规更新分类器参数 loss_cls = cross_entropy(classifier(embeddings, support_idx), support_y) theta_c = theta_c - cls_lr * grad(loss_cls, theta_c) # 7. 辅助重建损失:让补全值不要太离谱 loss_rec = mse_loss(x_hat[mask == 0], x[mask == 0]) theta_g = theta_g - rec_lr * grad(loss_rec, theta_g)实际代码里第 5 步很容易写错。loss_meta 对 θg 的梯度要穿过 θc_temp,而 θc_temp 又依赖分类器在嵌入上的梯度,所以整条计算图非常长。如果不小心在第 4 步之后 detach 了 embeddings,或者提前释放了计算图,补全器就再也收不到来自分类任务的信号,退化成普通重建模型。
4. 复现实操:跑通 HGNN-AC 需要注意的细节
4.1 掩码比例怎么设
论文在实验里通常会考察多个缺失比例,比如 0.1、0.3、0.5,测试时固定某一个比例来看模型表现。我在自己复现时遇到的问题很典型:训练时只用 0.3 的掩码比例,测试换成 0.5,效果立刻掉回普通 HAN 水平。原因不复杂,补全器只见过“挖掉三成”的缺失模式,没见过“挖掉一半”的模式,泛化自然无从谈起。
稳妥的做法是在训练阶段按区间随机采样掩码比例,比如每次迭代从 0.1 到 0.5 之间随机取一个比例。如果显存允许,还可以用 curriculum 方式:先低比例后高比例,让补全器逐步适应更严重的缺失。这个方法简单但效果立竿见影,我实测下来高缺失比例上的指标能稳定提升两到三个点。
4.2 内循环步数与学习率的搭配
内循环步数太少,分类器来不及“适应”这批数据,临时参数和最终参数相差无几,外循环的元学习信号就退化了;内循环步数太多,临时分类器会在支持集上过拟合,查询集上的 loss 噪声巨大,梯度不稳。我试过内循环 1 到 5 步,比较稳的是 2 步,再多就容易震荡。
学习率方面,内循环学习率要相对大,才能体现快速适应,但也不能大到发散,0.01 左右比较合适;外循环元学习率要小一个量级,0.001 起步。另外强烈建议加线性 warmup,否则前几个 epoch 的 meta loss 会跳得心慌。我们做 GNN 训练时习惯用 Adam,但双层优化的 meta 梯度路径更长,Adam 的默认参数不总可靠,可以考虑把 beta2 调低到 0.99 或者直接试 SGD Momentum。
4.3 损失权重和梯度路径
整个框架有两个目标:一个是通过查询集分类 loss 实现的元目标,一个是重建 loss。二者权重如果失衡,问题很明显。重建 loss 权重太大,补全器把所有心思放在还原“真实值”上,对分类器需求的关注下降;元目标权重太大,补全器可能生成非常极端的属性——分类器在支持集上表现极好,但补全属性已经脱离实际。我的经验是重建 loss 权重从 0.1 开始调,观察验证集上两个 loss 的下降曲线,找到平衡点。
梯度路径上要注意一个隐蔽的坑:如果你在第 4 步内循环更新时,对分类器参数 θc_temp 的更新不保留完整计算图,那第 5 步 loss_meta 关于 θg 的梯度会少掉一大段。PyTorch 里默认会保留计算图,但如果你在代码里手动 detach 了某个中间变量,或者为了省显存用了 no_grad 模式,补全器就会退化。我调试时遇到过一次补全器完全不更新的情况,最后发现就是 detach 位置不对。
4.4 显存优化与内存管理
双层优化的一个现实代价是显存消耗大。整个计算图包含补全器、异质图编码器、分类器内循环和外循环,显存很容易直接爆炸。我试过在单卡上跑 10 万节点规模的异质图,把 batch 切得很小才能跑起来。有几个缓解手段值得试:把异质图编码器的参数冻结一部分、梯度检查点、混合精度训练。如果还是爆显存,可以把内循环里的临时分类器简化——论文中使用的是简单线性分类头做内循环,这本身就已经非常省显存,不用额外堆 MLP 层。
# 使用梯度检查点示例 from torch.utils.checkpoint import checkpoint def custom_forward(x_hat, adj): return heterogeneous_backbone(x_hat, adj) embeddings = checkpoint(custom_forward, x_hat, adj)做推理时别忘了算的是 meta-test 场景下的补全,我们并不会知道真实缺失比例是多少,所以测试阶段可以直接用训练时平均掩码比例,或者更稳妥的做法是训练时采样多种比例,测试时对每个节点做多次随机掩码取平均。
5. 实验与消融:方法有效性怎么看
5.1 常用公开异质图基准
HGNN-AC 这类论文做节点分类实验时,我见到最多的是三类常规异质图数据:第一类是论文引用网络,节点类型包括论文、作者、会议/期刊;第二类是文献和主题相关的网络;第三类是电影评价网络,节点类型包括电影、导演、演员、标签。每个数据集划分训练、验证、测试集的方式有讲究,用固定索引的划分最公平。说实话这类数据规模不算大,但正因为小,模型对比时更聚焦,噪声也小。
5.2 主要对比结果
在缺失率较低的时候,HGNN-AC 的提升幅度不太明显,因为 HAN 加零填充也能勉强工作。但在缺失率到达 0.4 以上时,差距开始拉大。我复现时观察到的典型情况是:零填充和均值填充的基线在 0.5 以上缺失率时准确率会明显下滑,而 HGNN-AC 仍能保持接近完整属性的水平。这种“高缺失比例下不掉点”的能力,比低缺失比例下的高指标更值得关注,因为真实场景的缺失率往往比公开数据集的实验设置更恶劣。
5.3 消融实验拆解
消融实验通常验证四个东西的贡献:去掉元学习机制,把补全器和分类器变成普通联合训练;去掉补全器直接零填充;去掉重建 loss 只用分类 loss;以及换用单一的掩码比例替代多种掩码比例训练。每一组消融都会带来不同程度的性能回退。从我的理解看,最重要的组件其实是“元学习机制”,因为它保证了补全器在没见过的高缺失模式下依然能适应。但实际工程里,多掩码比例训练可能比元学习更容易落地,因为它几乎不增加训练复杂度,提升却很稳定。
6. 落地场景和个人体会
6.1 什么场景优先考虑 HGNN-AC
如果项目里已经有异质图数据,属性缺失率肉眼可见,HGNN-AC 的收益通常很大。推荐系统是典型场景:用户属性、商品属性各自缺失得很厉害,而用户和商品之间的交互图结构天然是异质的。冷启动场景也合适,新用户没有多少历史行为,只能靠图结构和已有属性推断。另外,知识图谱补全相关的任务也可以借鉴,只要是“属性缺失 + 图结构丰富”的组合,就适合任务导向的补全思路。
6.2 什么场景不建议用
属性本身就是极稀疏的数据,比如一个节点只有两三个可见属性,其余全是占位符,补全器很难学到有效模式。图结构太稀疏的场景也不建议。补全器需要邻居作为信息来源,邻居如果几乎不存在,它只能退化回均值填充。我遇到过一个内部图,平均每个节点只有 1.2 个有效邻居,跑 HGNN-AC 的效果和均值填充基本没有区别。这种情况下,外部知识库或者文本属性抽取可能是更值得投入的方向。
6.3 从论文里学到的通用方法论
抛开具体的模型结构,我最欣赏这篇文章的底层逻辑:缺失数据处理不应该和主任务解耦。这个原则不只适用于图神经网络,也适用于泛型机器学习。以后遇到特征缺失的问题,不妨别急着用中位数填充,先想想能不能设计一个可微的缺失处理模块,让主任务的梯度流回去指导填充策略。实际操作中,大体流程其实相当直接:先构造模拟缺失做训练,再把缺失处理模块和主任务联合训练,最后在真实缺失数据上验证。这套工作流我后来迁移到表格数据上,效果同样明显。
这篇论文给我最大的个人体会就是:别把数据预处理当“脏活外包”。你在前面偷的懒,后面模型都会还回来。属性补全本来就是在跟真实世界打交道,与其用静态填充假装问题不存在,不如让模型结合任务需求动态做出判断。打算入坑图属性补全的读者,我建议先把这套双层优化的后门走通,再考虑去魔改自己的补全器,毕竟,核心理念跑通了,剩下的细节不过都是锦上添花。