第3章:图的表示与输入特征
聊MPNN,第一件事就是把图给“喂”进去。你想想看,图不是一张图片,也不是一段文本。它是一堆节点和一堆边,乱糟糟的。怎么把这个结构变成神经网络能懂的数字?
我个人习惯把这部分的思路捋成三步:节点、边、邻接关系。说白了,就是把现实世界里的实体和他们之间的关系,量化成一个矩阵、几个向量。
3.1 邻接矩阵——图的骨架
邻接矩阵是图论里的老古董了。一个N个节点的图,邻接矩阵就是一个N×N的方阵。如果节点i和节点j之间有边相连,A[i][j]就是非零值,通常填1;没边就填0。
但这里有个坑。我在做分子性质预测时,发现很多人直接把邻接矩阵扔进网络,结果模型训不出来。为什么?因为大部分邻接矩阵是稀疏的,99%的元素都是0。全连接层的参数量直接爆炸。
所以,我建议你把这东西当作“结构容器”,而不是输入层。真正的训练要用稀疏表示,或者用索引列表。
3.2 节点特征——给每个点赋予意义
邻接矩阵说了谁跟谁连着,但没告诉模型节点本身是什么。这就需要一个节点特征矩阵。
节点特征矩阵的形状是[N, F_n]。N是节点数,F_n是每个节点的特征维度。
举个例子,在社交网络里,每个用户的特征可以是:年龄、性别、注册天数、历史发帖数。在分子图里,每个原子的特征可以是:原子序数、价电子数、是不是在环上。
| 领域 | 节点特征示例 | 维度参考 |
|---|---|---|
| 分子图 | 原子类型、度、手性、电荷 | 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 本章知识结构图
我把这一章的核心逻辑画成了一张图。你看一眼就明白了。
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向量或者平均向量填充,别直接扔掉。
- 邻接矩阵始终用稀疏格式,别为了好看用稠密矩阵。
嗯,这一章的内容就到这。接下来你就可以去设计你自己的输入特征了。记得多试试不同组合,图神经网络很吃特征质量。