消息传递神经网络:参考答案#
说明#
以下答案与正文10道题逐题对应。前四题给出主要推导与计算;第 5--7 题给出可以运行的程序和检查方法;第 8--10 题说明比较时需要保持哪些条件相同,以及实验结果能够说明什么、不能说明什么。
加入自环后,第一层为
\[\boldsymbol{h}^{[1]} =\left(\frac{1+2}{2},\frac{1+2+4}{3},\frac{2+4}{2}\right) =\left(\frac32,\frac73,3\right).\]第二层继续使用同一邻域:
\[\boldsymbol{h}^{[2]} =\left( \frac{\frac32+\frac73}{2}, \frac{\frac32+\frac73+3}{3}, \frac{\frac73+3}{2} \right) =\left(\frac{23}{12},\frac{41}{18},\frac83\right).\]两层后端点可以接收另一端点的二跳信息,中间节点的二跳集合没有新增节点。感受野描述可能依赖的最远距离,不表示每个节点都恰好存在该距离的邻居。
令 \(\pi\) 为节点置换。对新编号节点 \(\pi(v)\),其邻居集合正是 \(\{\pi(u):u\in\mathcal N(v)\}\)。共享消息函数使对应边消息只被重新编号;聚合器不依赖枚举顺序,因此聚合结果满足 \(\boldsymbol{m}'_{\pi(v)}=\boldsymbol{m}_v\),共享更新函数进一步给出 \(\boldsymbol{h}'_{\pi(v)}=\boldsymbol{h}_v\)。矩阵写法即
\[f(\boldsymbol{P}\cdot\bA\cdot\boldsymbol{P}\trans, \boldsymbol{P}\cdot\bX) =\boldsymbol{P}\cdot f(\bA,\bX).\]sum 与 mean 对节点行的排列不敏感,所以 \(\operatorname{READOUT}(\boldsymbol{P}\cdot\boldsymbol{H})=\operatorname{READOUT}(\boldsymbol{H})\)。证明要求图结构、特征和边特征同步置换;只打乱其中一项会破坏对应关系。
基例 \(L=0\) 时 \(\boldsymbol{h}_v^{[0]}\) 只依赖 \(v\)。假设第 \(L-1\) 层每个邻居 \(u\) 的表示只依赖其 \(L-1\) 跳邻域;第 \(L\) 层读取 \(v\) 的一跳邻居表示,所有依赖节点到 \(v\) 的距离至多为 \(1+(L-1)=L\),归纳完成。
“最多”是计算图的依赖上界。聚合器可能发生碰撞、权重可能为 0、非线性可能压缩信息,固定宽度也可能造成过度挤压,所以拥有 \(L\) 跳感受野并不保证能够重构或区分所有 \(L\) 跳子图。
计算表为
\[\begin{split}\begin{array}{c|ccc} \text{多重集合}&\mathrm{sum}&\mathrm{mean}&\mathrm{max}\\ \hline \{1\}&1&1&1\\ \{1,1\}&2&1&1\\ \{1,3\}&4&2&3\\ \{2,2\}&4&2&2\\ \{0,2\}&2&1&2 \end{array}.\end{split}\]mean 无法区分 \(\{1\}\) 与 \(\{1,1\}\),丢失重复次数;max 也无法区分这两个集合,并忽略所有非最大元素。sum 能区分这对集合,却无法区分 \(\{1,3\}\) 与 \(\{2,2\}\),因此求和也不是对任意实数多重集合都一一对应;配合足够表达力的映射时才可能增强区分能力。
一个不构造稠密邻接矩阵的 NumPy 骨架如下:
import numpy as np def edge_mean(edge_index, x, num_nodes=None, add_self_loops=True, edge_weight=None): edge_index = np.asarray(edge_index, dtype=np.int64) x = np.asarray(x, dtype=float) n = len(x) if num_nodes is None else int(num_nodes) if edge_index.ndim != 2 or edge_index.shape[0] != 2 or len(x) != n: raise ValueError("edge_index 或 x 维度错误") send, receive = edge_index if np.any(send < 0) or np.any(send >= n) or np.any(receive < 0) or np.any(receive >= n): raise IndexError("边端点越界") weight = (np.ones(len(send)) if edge_weight is None else np.asarray(edge_weight, dtype=float)) if weight.shape != (len(send),): raise ValueError("边权维度错误") if add_self_loops: nodes = np.arange(n) send = np.concatenate([send, nodes]) receive = np.concatenate([receive, nodes]) weight = np.concatenate([weight, np.ones(n)]) out = np.zeros((n, x.shape[1]), dtype=float) degree = np.zeros(n, dtype=float) np.add.at(out, receive, weight[:, None] * x[send]) np.add.at(degree, receive, weight) if np.any(degree == 0): raise ValueError("存在零度接收节点") return out / degree[:, None], degree
重复边会重复贡献;若希望先合并重复边,应把规则写入接口。负边权会使“度”不再是普通计数,是否允许需由模型定义。
对每个图先用手算确认边方向,再比较三种实现,例如
assert_allclose(dense, sparse)。置换测试构造 \(\bA'=\boldsymbol{P}\cdot\bA\cdot\boldsymbol{P}\trans\)、\(\bX'=\boldsymbol{P}\cdot\bX\),检查 \(\boldsymbol{H}'=\boldsymbol{P}\cdot\boldsymbol{H}\),并比较 readout。只有一条边 \(1\to2\) 且两节点特征不同的例子能暴露端点写反;孤立节点验证自环策略;重复边验证计数;非法端点应报错。浮点比较使用明确容差,整数度数应完全一致。层接口可写为
forward(x, edge_index, edge_attr=None, reduce="mean", residual=False)。所有边消息都从同一个旧x读取并写入新缓冲区;max 对空邻域要定义自环或显式初值。批量多图可附加batch编号并断言每条边两端属于同一图。测试改变节点和边枚举顺序、反转有向边、复制边以及关闭残差;对标量损失执行后向传播,断言每个参数梯度存在且有限。自动微分 只能核验实现的导数,不能替代边方向和聚合语义测试。可让节点自身特征相同,而标签由邻居中特定特征是否出现决定;这样 MLP 无法使用图结构信息,MPNN 则可以尝试学习邻居信息。一次性确定数据划分,两种模型使用相同的随机种子、相近参数量、更新次数和每次更新的目标节点数,并使用同一个验证集和相同标准选择模型。报告测试准确率或 macro-F1、训练时间、固定节点批量预测时间、峰值内存、参数量和消息函数运行次数。
若 MPNN 优于 MLP,只能说明所给结构对该任务有用;若使用传导划分,还应明确预测时可见哪些测试节点特征和边。不能通过测试标签构图或选择超参数。
三种实现接收完全相同的边、多重边规则、特征、数据类型和设备;固定种子、预热和重复次数。先逐项比较输出,再在多个 \(n\) 与密度下记录固定图批量时间、峰值内存和实际处理边数。循环版便于核验但解释器开销大;稠密版成本和内存随 \(n^2\) 增长;稀疏 scatter 通常适合 \(m\ll n^2\),但在很小或很密的图上未必最快。计时结论只适用于所用库、设备和规模。
各组使用相同的数据划分、随机种子、隐藏宽度、更新次数、批量处理方法与评价程序,并使用同一个验证集和相同标准选择模型。报告测试指标、训练时间、固定批量预测时间、峰值内存、消息函数运行次数、节点表示各维方差和不同节点表示的平均余弦相似度。比较有无自环时只改变自环设置,比较有无残差连接时只改变残差连接设置,并分别根据结果分析它们可能带来的影响。
深度增加会扩大感受野和计算量;若表示方差下降、相似度上升且性能恶化,可作为过度平滑的证据,但不是唯一解释。sum 的尺度受节点度影响,mean 丢失计数信息;不同任务可能得到不同排序,不能从单个数据集推出普遍结论。