第3章:图的表示与输入特征

聊MPNN,第一件事就是把图给“喂”进去。你想想看,图不是一张图片,也不是一段文本。它是一堆节点和一堆边,乱糟糟的。怎么把这个结构变成神经网络能懂的数字?

我个人习惯把这部分的思路捋成三步:节点、边、邻接关系。说白了,就是把现实世界里的实体和他们之间的关系,量化成一个矩阵、几个向量。

3.1 邻接矩阵——图的骨架

邻接矩阵是图论里的老古董了。一个N个节点的图,邻接矩阵就是一个N×N的方阵。如果节点i和节点j之间有边相连,A[i][j]就是非零值,通常填1;没边就填0。

但这里有个坑。我在做分子性质预测时,发现很多人直接把邻接矩阵扔进网络,结果模型训不出来。为什么?因为大部分邻接矩阵是稀疏的,99%的元素都是0。全连接层的参数量直接爆炸。

所以,我建议你把这东西当作“结构容器”,而不是输入层。真正的训练要用稀疏表示,或者用索引列表。

3.2 节点特征——给每个点赋予意义

邻接矩阵说了谁跟谁连着,但没告诉模型节点本身是什么。这就需要一个节点特征矩阵。

节点特征矩阵的形状是[N, F_n]。N是节点数,F_n是每个节点的特征维度。

举个例子,在社交网络里,每个用户的特征可以是:年龄、性别、注册天数、历史发帖数。在分子图里,每个原子的特征可以是:原子序数、价电子数、是不是在环上。

我的个人经验:初始特征千万不要拍脑袋选。可以先用PCA或autoencoder试一下,看看哪些特征是冗余的。我曾经在一个项目里塞了50维特征,最后发现其中30维都一个样,白费了算力。
领域 节点特征示例 维度参考
分子图 原子类型、度、手性、电荷 10~30
社交网络 年龄、兴趣编码、活跃度 8~50
交通路网 坐标、车流量、限速值 4~15

3.3 边特征——关系也得说话

光有节点特征,还不够。边代表的关系往往携带重要信息。你先看看,你家地图上的路,每条路都有距离、限速。分子里的化学键,有单键、双键、芳香键。

这些都得用边特征矩阵表示。如果是固定数量的边,可以用[E, F_e]。E是边数,F_e是每条边的特征维度。

但有个问题——图神经网络消息传的时候,每条边都带着一个向量,计算量直线上升。我见过不少人把边特征塞进去之后,显存直接炸了。

避坑指南:边特征不是越多越好。我曾经在一个推荐系统项目里,把用户-物品的交互时间、评分、位置、设备类型全塞进边特征,结果模型收敛极慢。后来发现“位置”这个特征噪声很大,去掉之后不仅快而且准。

如果你用PyTorch Geometric或DGL,代码里很常见的就是这样组织输入:

# x: 节点特征矩阵 [N, F_n]
# edge_index: 邻接关系的索引 [2, E](COO格式)
# edge_attr: 边特征矩阵 [E, F_e]

data = Data(x=x, edge_index=edge_index, edge_attr=edge_attr)

你看到了吗?edge_index就是邻接矩阵的稀疏表示。它只存有边的位置,不存那些全是0的地方。省内存,也省计算。

3.4 特征工程与归一化

图的输入特征也有归一化这一说。不是必须的,但我推荐做。

节点特征如果数值差异太大,比如有的特征是0到1,有的特征直接是几万,那梯度更新就会很拧巴。我习惯的做法:

  • 数值型特征:用Z-score归一化,或者缩放到[0,1]。
  • 类别型特征:用One-hot或可学习的Embedding。
  • 度特征:有时候把节点的度直接拼进去也有效,因为它反映了局部拓扑的稠密程度。

另外,边特征里的距离、时间等连续值,最好经过log变换或者分桶处理。我试过直接把路网距离原值扔进去,模型学得很差。log一下就好了很多。

3.5 特征融合策略

在一些模型里,你会看到节点特征和边特征在消息函数里拼接或者相加。

我个人习惯用一个GCN-like的层做早期融合:

# 例子:拼接节点特征和边特征
msg = W_src * h_src + W_edge * e_ij
# 或者直接拼接
msg = W * torch.cat([h_src, e_ij], dim=-1)

哪种好?不能一概而论。我以前做过对比实验,在分子性质预测里,拼接往往比加法好。但在社交网络里加法反而泛化更强。所以建议你两个都试一试,别偷懒。

3.6 本章知识结构图

我把这一章的核心逻辑画成了一张图。你看一眼就明白了。

图的表示与输入特征结构 图输入(Graph Input) 邻接矩阵 N×N 稀疏/稠密 节点特征矩阵 [N, F_n] 数值/类别 边特征矩阵 [E, F_e] 键/距离 归一化/Embedding Z-score / One-hot Log变换/分桶 特征融合(拼接/相加) 消息传递函数输入

3.7 总结与常用API

好了,总结一下。图的输入特征有三件套:邻接矩阵描述结构,节点特征描述自身,边特征描述关系。

代码层面,我最常用的就是PyTorch Geometric的Data对象。你看这个构建示例:

from torch_geometric.data import Data

# 3个节点,2条边 (0-1, 1-2)
edge_index = torch.tensor([[0, 1],
                           [1, 2]], dtype=torch.long).t().contiguous()
x = torch.tensor([[0.5, 1.2],
                  [0.9, 3.4],
                  [1.1, 0.2]], dtype=torch.float)
edge_attr = torch.tensor([[0.8],
                          [0.3]], dtype=torch.float)

data = Data(x=x, edge_index=edge_index, edge_attr=edge_attr)
print(data)

这里有个小细节。邻接矩阵的索引用了COO格式,也就是说用两个数组分别存起始点和终止点。这样在图很大的时候,内存占用只是O(E)而不是O(N²)。

推荐做法:

  • 节点特征一定要做归一化,尤其是多模态场景。
  • 边特征如果缺失,试试用0向量或者平均向量填充,别直接扔掉。
  • 邻接矩阵始终用稀疏格式,别为了好看用稠密矩阵。

嗯,这一章的内容就到这。接下来你就可以去设计你自己的输入特征了。记得多试试不同组合,图神经网络很吃特征质量。