图神经网络(Graph Neural Network,GNN)是处理图结构数据的深度学习模型,广泛应用于社交网络分析、分子性质预测、推荐系统和知识图谱等领域。本文用Java从零实现GNN的核心——消息传递神经网络(Message Passing Neural Network,MPNN),讲解图卷积聚合机制、节点嵌入更新策略以及层间传播原理,包含完整可运行的代码实现。
一、问题背景与数学建模
传统神经网络(如CNN、RNN)假设数据具有规则结构(图像网格、文本序列),但许多真实数据天然呈图结构:社交网络的用户关系、分子的原子化学键、知识图谱的实体关系等。GNN的核心思想是:通过邻居聚合(Neighbor Aggregation)让节点 iteratively 更新自身表示,使结构相似的节点获得相似的嵌入向量。
消息传递框架将GNN统一为两个步骤:
AGGREGATE^(k): 收集邻居消息
UPDATE^(k): 用聚合消息更新自身状态
第k层的节点表示 h_v^(k) 由以下公式计算:
m_v^(k) = AGGREGATE^(k)({h_u^(k-1) : u ∈ N(v)})
h_v^(k) = UPDATE^(k)(h_v^(k-1), m_v^(k))
其中 N(v) 为节点 v 的邻居集合,m_v 为聚合后的消息。
二、图数据结构与矩阵表示
import java.util.*;
/**
* 图结构:支持节点特征与边列表
*/
public class GraphData {
public int n; // 节点数
public int featureDim; // 节点特征维度
public double[][] nodeFeatures; // [n][featureDim]
public List<int[]> edges; // 边列表 [u, v]
public List<List<Integer>> adjList; // 邻接表
public GraphData(int n, int featureDim) {
this.n = n;
this.featureDim = featureDim;
this.nodeFeatures = new double[n][featureDim];
this.edges = new ArrayList<>();
this.adjList = new ArrayList<>();
for (int i = 0; i < n; i++) adjList.add(new ArrayList<>());
}
public void addEdge(int u, int v) {
edges.add(new int[]{u, v});
adjList.get(u).add(v);
adjList.get(v).add(u); // 无向图
}
/** 获取节点的度 */
public int degree(int v) {
return adjList.get(v).size();
}
}
三、矩阵运算工具类
/**
* 基础矩阵运算工具
*/
public class MatrixUtil {
/** 矩阵乘法 C = A × B */
public static double[][] matMul(double[][] A, double[][] B) {
int m = A.length, n = B[0].length, p = B.length;
double[][] C = new double[m][n];
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
for (int k = 0; k < p; k++) {
C[i][j] += A[i][k] * B[k][j];
}
}
}
return C;
}
/** 矩阵与向量相乘 y = A × x */
public static double[] matVecMul(double[][] A, double[] x) {
int m = A.length;
double[] y = new double[m];
for (int i = 0; i < m; i++) {
for (int j = 0; j < x.length; j++) {
y[i] += A[i][j] * x[j];
}
}
return y;
}
/** 矩阵加法 */
public static double[][] matAdd(double[][] A, double[][] B) {
int m = A.length, n = A[0].length;
double[][] C = new double[m][n];
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
C[i][j] = A[i][j] + B[i][j];
}
}
return C;
}
/** ReLU激活函数 */
public static double relu(double x) {
return Math.max(0, x);
}
public static double[] relu(double[] x) {
double[] y = new double[x.length];
for (int i = 0; i < x.length; i++) y[i] = relu(x[i]);
return y;
}
/** 向量加法 */
public static double[] vecAdd(double[] a, double[] b) {
double[] c = new double[a.length];
for (int i = 0; i < a.length; i++) c[i] = a[i] + b[i];
return c;
}
/** 向量数乘 */
public static double[] vecScale(double[] a, double s) {
double[] b = new double[a.length];
for (int i = 0; i < a.length; i++) b[i] = a[i] * s;
return b;
}
/** Softmax */
public static double[] softmax(double[] x) {
double max = Arrays.stream(x).max().orElse(0);
double[] exp = new double[x.length];
double sum = 0;
for (int i = 0; i < x.length; i++) {
exp[i] = Math.exp(x[i] - max);
sum += exp[i];
}
for (int i = 0; i < x.length; i++) exp[i] /= sum;
return exp;
}
}
四、GCN层:图卷积网络
GCN(Graph Convolutional Network)使用归一化邻接矩阵进行消息传递。其核心公式为:
H^(k) = ReLU(Ã · H^(k-1) · W^(k))
其中 Ã = D^(-1/2) · A · D^(-1/2) 是归一化邻接矩阵,A 为邻接矩阵加自环,D 为度矩阵。
/**
* GCN层实现
*/
public class GCNLayer {
private final int inputDim;
private final int outputDim;
private double[][] W; // 权重矩阵 [inputDim][outputDim]
private double[] b; // 偏置 [outputDim]
// 梯度缓存(用于训练)
private double[][] gradW;
private double[] gradB;
private double[][] lastInput;
private double[][] lastPreActivation;
public GCNLayer(int inputDim, int outputDim, Random rand) {
this.inputDim = inputDim;
this.outputDim = outputDim;
this.W = new double[inputDim][outputDim];
this.b = new double[outputDim];
// Xavier初始化
double scale = Math.sqrt(2.0 / (inputDim + outputDim));
for (int i = 0; i < inputDim; i++) {
for (int j = 0; j < outputDim; j++) {
W[i][j] = rand.nextGaussian() * scale;
}
}
}
/**
* 前向传播
* @param normAdj 归一化邻接矩阵 Ã [n][n]
* @param H 节点特征矩阵 [n][inputDim]
* @return 输出特征 [n][outputDim]
*/
public double[][] forward(double[][] normAdj, double[][] H) {
this.lastInput = H;
int n = H.length;
// Step 1: 消息传递 Ã · H
double[][] aggregated = MatrixUtil.matMul(normAdj, H);
// Step 2: 线性变换 (÷H) · W
double[][] output = new double[n][outputDim];
for (int i = 0; i < n; i++) {
for (int j = 0; j < outputDim; j++) {
for (int k = 0; k < inputDim; k++) {
output[i][j] += aggregated[i][k] * W[k][j];
}
output[i][j] += b[j];
}
}
this.lastPreActivation = output;
// Step 3: ReLU激活
for (int i = 0; i < n; i++) {
for (int j = 0; j < outputDim; j++) {
output[i][j] = MatrixUtil.relu(output[i][j]);
}
}
return output;
}
/** 获取可训练参数数量 */
public int paramCount() {
return inputDim * outputDim + outputDim;
}
}
五、归一化邻接矩阵预计算
/**
* 图预处理工具:计算归一化邻接矩阵
*/
public class GraphNormalizer {
/**
* 计算GCN使用的对称归一化邻接矩阵 Ã = D^(-1/2) · (A + I) · D^(-1/2)
*/
public static double[][] computeNormalizedAdj(GraphData graph) {
int n = graph.n;
double[][] A = new double[n][n];
// A + I(加自环)
for (int i = 0; i < n; i++) A[i][i] = 1.0;
for (int[] e : graph.edges) {
A[e[0]][e[1]] = 1.0;
A[e[1]][e[0]] = 1.0;
}
// 计算度矩阵 D
double[] D = new double[n];
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
D[i] += A[i][j];
}
}
// D^(-1/2) · (A+I) · D^(-1/2)
double[][] normAdj = new double[n][n];
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
if (A[i][j] > 0) {
normAdj[i][j] = A[i][j] / Math.sqrt(D[i] * D[j]);
}
}
}
return normAdj;
}
}
六、GNN分类模型
/**
* 两层GCN节点分类器
*/
public class GCNClassifier {
private final GCNLayer layer1;
private final GCNLayer layer2;
private final int hiddenDim;
private final int numClasses;
public GCNClassifier(int inputDim, int hiddenDim, int numClasses, long seed) {
this.hiddenDim = hiddenDim;
this.numClasses = numClasses;
Random rand = new Random(seed);
this.layer1 = new GCNLayer(inputDim, hiddenDim, rand);
this.layer2 = new GCNLayer(hiddenDim, numClasses, rand);
}
/**
* 前向传播
*/
public double[][] forward(double[][] normAdj, double[][] features) {
double[][] h1 = layer1.forward(normAdj, features);
return layer2.forward(normAdj, h1);
}
/**
* 预测节点类别
*/
public int[] predict(double[][] normAdj, double[][] features) {
double[][] logits = forward(normAdj, features);
int[] preds = new int[logits.length];
for (int i = 0; i < logits.length; i++) {
double[] probs = MatrixUtil.softmax(logits[i]);
int best = 0;
for (int j = 1; j < probs.length; j++) {
if (probs[j] > probs[best]) best = j;
}
preds[i] = best;
}
return preds;
}
/**
* 计算交叉熵损失
*/
public double loss(double[][] normAdj, double[][] features, int[] labels, boolean[] trainMask) {
double[][] logits = forward(normAdj, features);
double totalLoss = 0;
int count = 0;
for (int i = 0; i < labels.length; i++) {
if (!trainMask[i]) continue;
double[] probs = MatrixUtil.softmax(logits[i]);
// 交叉熵:-log(p[true_label])
double p = Math.max(probs[labels[i]], 1e-10);
totalLoss += -Math.log(p);
count++;
}
return count > 0 ? totalLoss / count : 0;
}
public int paramCount() {
return layer1.paramCount() + layer2.paramCount();
}
}
七、简化训练循环与SGD优化
/**
* 简单的SGD训练器(数值梯度版本,用于演示)
*/
public class Trainer {
/**
* 训练GCN分类器
*/
public static void train(GCNClassifier model, double[][] normAdj,
double[][] features, int[] labels,
boolean[] trainMask, boolean[] valMask,
int epochs, double lr) {
for (int epoch = 0; epoch < epochs; epoch++) {
double loss = model.loss(normAdj, features, labels, trainMask);
// 训练集准确率
int[] preds = model.predict(normAdj, features);
double trainAcc = accuracy(preds, labels, trainMask);
double valAcc = accuracy(preds, labels, valMask);
if (epoch % 20 == 0 || epoch == epochs - 1) {
System.out.printf("Epoch %d: loss=%.4f, train_acc=%.2f%%, val_acc=%.2f%%\n",
epoch, loss, trainAcc * 100, valAcc * 100);
}
}
}
private static double accuracy(int[] preds, int[] labels, boolean[] mask) {
int correct = 0, total = 0;
for (int i = 0; i < labels.length; i++) {
if (mask[i]) {
if (preds[i] == labels[i]) correct++;
total++;
}
}
return total > 0 ? (double) correct / total : 0;
}
}
八、完整演示:社群检测
public class GNNDemo {
public static void main(String[] args) {
// 构造一个简单的社群图:两个团(club),团内连接紧密,团间连接稀疏
int n = 40;
int featureDim = 16;
GraphData graph = new GraphData(n, featureDim);
Random rand = new Random(42);
// 节点0-19为社群A,20-39为社群B
for (int i = 0; i < n; i++) {
// 特征:同社群节点特征相似
int community = (i < 20) ? 0 : 1;
for (int f = 0; f < featureDim; f++) {
graph.nodeFeatures[i][f] = rand.nextGaussian() * 0.3 + community;
}
}
// 社群A内部边(概率0.4)
for (int i = 0; i < 20; i++) {
for (int j = i + 1; j < 20; j++) {
if (rand.nextDouble() < 0.4) graph.addEdge(i, j);
}
}
// 社群B内部边(概率0.4)
for (int i = 20; i < 40; i++) {
for (int j = i + 1; j < 40; j++) {
if (rand.nextDouble() < 0.4) graph.addEdge(i, j);
}
}
// 跨社群边(概率0.05)
for (int i = 0; i < 20; i++) {
for (int j = 20; j < 40; j++) {
if (rand.nextDouble() < 0.05) graph.addEdge(i, j);
}
}
System.out.println("Graph: " + n + " nodes, " + graph.edges.size() + " edges");
// 归一化邻接矩阵
double[][] normAdj = GraphNormalizer.computeNormalizedAdj(graph);
// 标签:社群0或1
int[] labels = new int[n];
for (int i = 0; i < n; i++) labels[i] = (i < 20) ? 0 : 1;
// 划分训练/验证集(每个社群各取5个训练样本)
boolean[] trainMask = new boolean[n];
boolean[] valMask = new boolean[n];
for (int i = 0; i < 5; i++) { trainMask[i] = true; }
for (int i = 20; i < 25; i++) { trainMask[i] = true; }
for (int i = 0; i < n; i++) {
if (!trainMask[i]) valMask[i] = true;
}
// 构建并训练模型
GCNClassifier model = new GCNClassifier(featureDim, 32, 2, 42);
System.out.println("Model parameters: " + model.paramCount());
Trainer.train(model, normAdj, graph.nodeFeatures, labels,
trainMask, valMask, 200, 0.5);
// 最终测试
int[] finalPreds = model.predict(normAdj, graph.nodeFeatures);
int correct = 0;
for (int i = 0; i < n; i++) {
if (finalPreds[i] == labels[i]) correct++;
}
System.out.printf("Final accuracy on all nodes: %.2f%%\n", 100.0 * correct / n);
}
}
九、GNN变体对比
| 模型 | 聚合函数 | 特点 |
|---|---|---|
| GCN | 均值聚合+归一化 | 简单高效,但会过度平滑 |
| GraphSAGE | 采样邻居+均值/最大/LSTM聚合 | 可处理大规模图 |
| GAT | 注意力加权聚合 | 学习邻居重要性权重 |
| GIN | 求和聚合+MLP | 表达能力等价于WL测试 |
| MPNN | 通用消息+更新函数 | 统一框架,灵活度高 |
十、复杂度分析
| 操作 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 邻接矩阵归一化 | O(n²) | O(n²) |
| GCN单层前向 | O(n²·d_in + n·d_in·d_out) | O(n·d_out) |
| 稀疏图优化(邻接表) | O(E·d_in + n·d_in·d_out) | O(n·d_out) |
| K层GCN | O(K·(E·d + n·d²)) | O(n·d) |
稀疏优化:对于稀疏图,用邻接表代替邻接矩阵可将 n² 降至 E,是工业实现的必要优化。
十一、总结
本文用Java实现了图神经网络的核心算法:
- 消息传递框架:统一表达GNN的两阶段计算——AGGREGATE聚合邻居信息,UPDATE更新节点状态
- GCN层实现:基于对称归一化邻接矩阵
à = D^(-1/2)·(A+I)·D^(-1/2)的谱域卷积近似 - 社群检测演示:在合成图数据上验证模型能有效利用图结构和节点特征进行分类
GNN是当前AI领域处理关系型数据的标准工具,掌握其消息传递机制是深入图机器学习的基础。