第2章 图神经网络基础:图的概念、节点、边、邻接矩阵

好,咱们正式开始啃图神经网络。这一章我得先把图的基础概念给你掰开揉碎讲清楚。你可能会想,不就是图吗?计算机里到处都是图。但图神经网络里的图,和我们平时说的社交网络图,还真有点不一样。

我个人习惯先把“图”这个东西抽象成数学结构来看。你想想看,一个分子不就是一张天然图吗?原子是节点,化学键是边。QSAR任务本质上就是在这样的图上做预测。所以,搞明白图的底层逻辑,后面学GNN才不费劲。

从一碗面说起:图到底是什么?

我看过不少教程,一上来就扔公式。这其实不太友好。让我先从一个特别直观的例子切入:一碗拉面。

  • 节点:面条里每一根面条的端点,或者面碗里的配菜(牛肉、香菜、辣椒)。
  • 边:面条本身的长度,连接两个端点的就是一根完整的面条。它们之间的关系是“相连”。
  • 图:整碗面的结构——哪根面条连了哪根,哪块牛肉挨着哪个辣椒。

在化学里,分子就是一碗结构清晰的“面”。

正式点说,一个图用数学语言可以这么写:

G = (V, E)

V 是节点集合:v₁, v₂, v₃, ..., vₙ
E 是边集合:e₁, e₂, e₃, ..., eₘ

每个节点都有它的属性。边的属性也很关键,比如键的强弱。

节点(Vertex / Node)——分子里的每个原子

节点是图的最小单元。在我接触的QSAR项目里,节点通常对应分子中的原子。我见过把整个官能团当做一个节点的情况,嗯,那属于进阶玩法,初期咱们还是按原子来。

节点的核心信息都在它的特征向量里。我给你看看我曾经处理过的分子节点特征表:

特征类别 编码方式 举例(C原子)
原子类型 One-hot编码 C: [0,0,1,0,...]
价电子数 标量 4
杂化方式 One-hot编码 sp³: [1,0,0]
是否在环中 二值 1 或 0
电荷 标量 0

这里有个坑我得提醒你:不同特征的尺度差异很大。价电子数是4,电荷可能是-1到1。如果不做归一化,模型会直接把大数特征的节点当成“更重要”的节点。我曾经犯过这种错,训练出来的模型只认原子类型不看电荷,后来重训了一轮。所以,特征预处理千万别省。

边(Edge)——原子间的纽带

边定义了两个节点之间的关系。在分子图里,边就是化学键。但边的类型比你想的要多一些。

我习惯把边分成三种:

  • 有向边:箭头从A指向B,关系是单向的。比如反应物到产物。
  • 无向边:两个节点互相连接,不分方向。分子里的共价键就是无向的。
  • 带权边:边上有权重值。在分子图里,权重可以表示键能或键级。

咱们GNN里处理的分子图,绝大多数是无向带权图。

边的特征也需要编码。举个键特征的例子:

键类型 = {单键:0, 双键:1, 三键:2, 芳香键:3}
键是否共轭 = {是:1, 否:0}
键是否在环内 = {是:1, 否:0}

这些特征组合起来,就构成了每条边的描述。

邻接矩阵——藏在数据里的“地图”

这是我最想让你搞懂的部分。邻接矩阵是图的数学化身。

它是一个N×N的方阵(N是节点数)。如果节点i和节点j之间有边,那么A[i][j] = 1(或权重值);没边就是0。

我画个简单的图帮你理解。假设有这么个小分子:

  • 4个原子(节点编号0, 1, 2, 3)
  • 3条键:0-1, 0-2, 1-3

对应的邻接矩阵如下:

    0  1  2  3
0: [0, 1, 1, 0]
1: [1, 0, 0, 1]
2: [1, 0, 0, 0]
3: [0, 1, 0, 0]

这个矩阵有几个重要性质:

  1. 对称性:无向图的邻接矩阵沿对角线对称。A[0][1]和A[1][0]都是1。
  2. 对角线是0:自己不和自已相连(除非有自环)。
  3. 稀疏性:分子图的邻接矩阵极其稀疏。一个分子几十个原子,矩阵里大部分是0。

关键点:GNN的核心操作——消息传递,本质上就是在邻接矩阵指导下,让每个节点去“看”它的邻居节点。没有邻接矩阵,图就没有结构信息,模型就没法知道谁和谁有关系。

图的存储与读取——从SMILES到矩阵

在QSAR里,我们拿到的数据通常是SMILES字符串。比如:

SMILES: CCO(乙醇)

要把SMILES变成邻接矩阵和节点特征矩阵,我们需要做几步处理。我常用的工具是RDKit,它可以把分子转成图对象:

from rdkit import Chem
from rdkit.Chem import rdmolops

mol = Chem.MolFromSmiles("CCO")

# 获取邻接矩阵(N x N,无向)
adj_matrix = rdmolops.GetAdjacencyMatrix(mol)

# 输出结果(3个原子)
# [[0, 1, 0],
#  [1, 0, 1],
#  [0, 1, 0]]

注意看,这个结果和分子结构完全吻合:C-C相连,C-O相连,C和O之间隔着C。

我建议你本地跑一下这段代码。自己动手把SMILES转成矩阵,那种感觉和看懂理论完全不一样。

个人建议:初学阶段不要用复杂度太高的分子练手,像苯环或长碳链这种。先用简单的小分子(比如乙醇、甲烷、水),它们的邻接矩阵小,你还能手动验证一遍,不容易出错。这样做一次,之后面对大分子心里就有底了。

本章核心逻辑框架

让我把本章的核心概念串成一张图,这样你看完就能把知识点挂起来:

图神经网络基础 - 核心概念结构 图 G = (V, E) 节点集 V(原子) 边集 E(化学键) 节点特征向量 (原子类型、杂化方式、电荷…) 边特征 / 权重 (键类型、键级、共轭状态…) 邻接矩阵 A (N×N) 存储节点间的连接关系 消息传递:节点 A ← 聚合 ← 邻居节点 + 边属性

这张图把整章的逻辑串起来了:图由节点和边构成,各自携带特征,邻接矩阵负责记录连接关系,最后所有的结构信息都汇入消息传递机制——这正是GNN做QSAR预测的根基。

避坑与实战提醒

讲完概念,我分享几个真实项目中踩过的坑,希望能帮你省点时间。

避坑1:对称矩阵别忘保持

我曾经在图构建时把无向图的(A[i][j])设了但(A[j][i])没设,导致模型学到的是有向信息。分子结构是无向的,矩阵必须对称。每次构建后做个简单验证:assert (adj == adj.T).all()。

避坑2:节点特征要对齐矩阵索引

节点0的特征必须对应邻接矩阵第0行。我在做官能团映射时,有一次给节点重新排序后忘了调整矩阵,特征和连接关系错位了。结果模型跑了30轮才收敛,后来发现是数据对齐出了问题。正确做法:给每个节点分配一个永久ID,矩阵和特征表都用这个ID索引。

建议步骤:处理分子数据时,我习惯按这个顺序来:

  1. 读SMILES → 生成RDKit分子对象
  2. 获取原子数N → 初始化N×N邻接矩阵
  3. 遍历所有键,填充矩阵(记得填两个方向)
  4. 提取每个原子的特征,放入N×d的节点特征矩阵
  5. 验证矩阵对称性和索引一致性

这个流程每一步都可以用代码检查,不会等到训练时才发现数据有问题。

好了,基础概念就讲到这里。图、节点、边、邻接矩阵——这四个东西弄明白了,你就算摸到了GNN的门。下个知识点我带你看看这些结构怎么传递给模型去做学习。