引言
图神经网络(Graph Neural Network,GNN)是一类专门处理图结构数据的深度学习模型。与传统神经网络处理规则网格数据(如图像、文本序列)不同,GNN能够直接对任意拓扑结构的图进行建模,捕捉节点之间的复杂关系。GNN在社交网络分析、分子性质预测、推荐系统、知识图谱推理等领域展现了卓越性能。本文将用Java实现GNN的核心机制——消息传递神经网络(Message Passing Neural Network,MPNN),并在一个简单的节点分类任务上展示其图结构学习能力。
算法原理
图的基本表示
图由节点集合 $V$ 和边集合 $E$ 组成。每个节点 $v$ 有特征向量 $x_v$,邻接矩阵 $A$ 描述节点间的连接关系。
消息传递框架
GNN通过迭代的消息传递更新节点表示:
消息聚合:每个节点收集邻居信息
$$m_v^{(l)} = \text{AGGREGATE}^{(l)}\left(\left{h_u^{(l-1)} : u \in \mathcal{N}(v)\right}\right)$$
节点更新:结合自身状态和聚合消息更新表示
$$h_v^{(l)} = \text{UPDATE}^{(l)}\left(h_v^{(l-1)}, m_v^{(l)}\right)$$
图卷积网络(GCN)
GCN是GNN的经典变体,其更新规则为:
$$H^{(l)} = \sigma\left(\tilde{D}^{-\frac{1}{2}}\tilde{A}\tilde{D}^{-\frac{1}{2}}H^{(l-1)}W^{(l)}\right)$$
其中 $\tilde{A} = A + I$ 是加自环的邻接矩阵,$\tilde{D}$ 为度矩阵,$W^{(l)}$ 为可学习参数。
Java实现
1. 图数据结构
import java.util.*;
/**
* 图数据结构:支持带特征节点和边的无向图
*/
public class Graph {
private final int numNodes;
private final List<Set<Integer>> adjacencyList;
private final double[][] nodeFeatures; // [numNodes, featureDim]
private final int[] labels; // 节点标签(用于分类任务)
public Graph(int numNodes, int featureDim) {
this.numNodes = numNodes;
this.adjacencyList = new ArrayList<>();
for (int i = 0; i < numNodes; i++) {
adjacencyList.add(new HashSet<>());
}
this.nodeFeatures = new double[numNodes][featureDim];
this.labels = new int[numNodes];
Arrays.fill(this.labels, -1); // -1表示无标签
}
/** 添加无向边 */
public void addEdge(int u, int v) {
if (u < 0 || u >= numNodes || v < 0 || v >= numNodes) {
throw new IllegalArgumentException("节点索引越界");
}
adjacencyList.get(u).add(v);
adjacencyList.get(v).add(u);
}
/** 设置节点特征 */
public void setNodeFeature(int node, double[] features) {
System.arraycopy(features, 0, nodeFeatures[node], 0, features.length);
}
/** 设置节点标签 */
public void setLabel(int node, int label) {
labels[node] = label;
}
/** 获取节点的邻居列表 */
public int[] getNeighbors(int node) {
Set<Integer> neighbors = adjacencyList.get(node);
int[] result = new int[neighbors.size()];
int idx = 0;
for (int n : neighbors) result[idx++] = n;
return result;
}
/** 获取节点度数(含自环) */
public int getDegree(int node) {
return adjacencyList.get(node).size() + 1; // +1 for self-loop
}
public int getNumNodes() { return numNodes; }
public double[][] getNodeFeatures() { return nodeFeatures; }
public int[] getLabels() { return labels; }
/** 打印图结构 */
public void printStructure() {
System.out.println("图结构信息:");
System.out.println(" 节点数: " + numNodes);
int edgeCount = 0;
for (Set<Integer> neighbors : adjacencyList) {
edgeCount += neighbors.size();
}
System.out.println(" 边数: " + (edgeCount / 2));
System.out.println(" 特征维度: " + nodeFeatures[0].length);
}
}
2. 图卷积层
import java.util.Random;
/**
* 图卷积层(GCN Layer):实现归一化邻接矩阵的消息传递
*/
public class GraphConvolutionLayer {
private final int inputDim;
private final int outputDim;
private final double[][] weights; // [inputDim, outputDim]
private final double[] bias; // [outputDim]
public GraphConvolutionLayer(int inputDim, int outputDim, long seed) {
this.inputDim = inputDim;
this.outputDim = outputDim;
Random rand = new Random(seed);
// Xavier初始化
this.weights = new double[inputDim][outputDim];
double scale = Math.sqrt(2.0 / (inputDim + outputDim));
for (int i = 0; i < inputDim; i++) {
for (int j = 0; j < outputDim; j++) {
weights[i][j] = rand.nextGaussian() * scale;
}
}
this.bias = new double[outputDim];
}
/** ReLU激活函数 */
private double relu(double x) {
return Math.max(0, x);
}
/**
* 图卷积前向传播
* @param graph 输入图
* @param nodeEmbeddings 当前节点嵌入 [numNodes, inputDim]
* @return 更新后的节点嵌入 [numNodes, outputDim]
*/
public double[][] forward(Graph graph, double[][] nodeEmbeddings) {
int numNodes = graph.getNumNodes();
double[][] newEmbeddings = new double[numNodes][outputDim];
// 对每个节点执行图卷积
for (int v = 0; v < numNodes; v++) {
// 1. 聚合邻居特征(含自环)
double[] aggregated = new double[inputDim];
int[] neighbors = graph.getNeighbors(v);
// 归一化因子:1/sqrt(deg(v) * deg(u))
double degV = graph.getDegree(v);
// 加入自身特征
double selfNorm = 1.0 / Math.sqrt(degV * degV);
for (int j = 0; j < inputDim; j++) {
aggregated[j] += selfNorm * nodeEmbeddings[v][j];
}
// 加入邻居特征
for (int u : neighbors) {
double degU = graph.getDegree(u);
double norm = 1.0 / Math.sqrt(degV * degU);
for (int j = 0; j < inputDim; j++) {
aggregated[j] += norm * nodeEmbeddings[u][j];
}
}
// 2. 线性变换 + 偏置 + 激活
for (int j = 0; j < outputDim; j++) {
double sum = bias[j];
for (int k = 0; k < inputDim; k++) {
sum += aggregated[k] * weights[k][j];
}
newEmbeddings[v][j] = relu(sum);
}
}
return newEmbeddings;
}
}
3. GNN分类器
/**
* 图神经网络节点分类器
*/
public class GNNClassifier {
private final GraphConvolutionLayer gcn1;
private final GraphConvolutionLayer gcn2;
private final int numClasses;
private final double learningRate;
// 输出层权重
private final double[][] outputWeights;
private final double[] outputBias;
public GNNClassifier(int featureDim, int hiddenDim, int numClasses,
double learningRate, long seed) {
this.gcn1 = new GraphConvolutionLayer(featureDim, hiddenDim, seed);
this.gcn2 = new GraphConvolutionLayer(hiddenDim, hiddenDim, seed + 1);
this.numClasses = numClasses;
this.learningRate = learningRate;
Random rand = new Random(seed + 2);
this.outputWeights = new double[hiddenDim][numClasses];
double scale = Math.sqrt(2.0 / (hiddenDim + numClasses));
for (int i = 0; i < hiddenDim; i++) {
for (int j = 0; j < numClasses; j++) {
outputWeights[i][j] = rand.nextGaussian() * scale;
}
}
this.outputBias = new double[numClasses];
}
/**
* 前向传播:计算所有节点的类别概率
* @return 每个节点的类别概率分布 [numNodes, numClasses]
*/
public double[][] forward(Graph graph) {
// 第一层GCN
double[][] h1 = gcn1.forward(graph, graph.getNodeFeatures());
// 第二层GCN
double[][] h2 = gcn2.forward(graph, h1);
// 输出层:线性变换
int numNodes = graph.getNumNodes();
double[][] logits = new double[numNodes][numClasses];
for (int v = 0; v < numNodes; v++) {
for (int c = 0; c < numClasses; c++) {
double sum = outputBias[c];
for (int j = 0; j < h2[0].length; j++) {
sum += h2[v][j] * outputWeights[j][c];
}
logits[v][c] = sum;
}
}
// Softmax归一化
return softmax(logits);
}
/** Softmax函数 */
private double[][] softmax(double[][] logits) {
double[][] probs = new double[logits.length][logits[0].length];
for (int i = 0; i < logits.length; i++) {
double max = Double.NEGATIVE_INFINITY;
for (double v : logits[i]) max = Math.max(max, v);
double sum = 0;
for (int j = 0; j < logits[0].length; j++) {
probs[i][j] = Math.exp(logits[i][j] - max);
sum += probs[i][j];
}
for (int j = 0; j < logits[0].length; j++) {
probs[i][j] /= sum;
}
}
return probs;
}
/** 预测节点类别 */
public int[] predict(Graph graph) {
double[][] probs = forward(graph);
int[] predictions = new int[probs.length];
for (int i = 0; i < probs.length; i++) {
int bestClass = 0;
for (int j = 1; j < numClasses; j++) {
if (probs[i][j] > probs[i][bestClass]) bestClass = j;
}
predictions[i] = bestClass;
}
return predictions;
}
/** 计算准确率 */
public double accuracy(Graph graph, boolean[] mask) {
int[] preds = predict(graph);
int[] labels = graph.getLabels();
int correct = 0, total = 0;
for (int i = 0; i < mask.length; i++) {
if (mask[i]) {
total++;
if (preds[i] == labels[i]) correct++;
}
}
return total == 0 ? 0 : (double) correct / total;
}
/**
* 简化训练:使用标签传播思想更新输出层
* 完整版需要图上的反向传播,此处为教学简化
*/
public void train(Graph graph, boolean[] trainMask, int epochs) {
System.out.println("开始训练GNN节点分类器...");
System.out.println("图卷积层1: " + gcn1 + " -> " + gcn2);
System.out.println("输出类别数: " + numClasses);
System.out.println("训练节点数: " + countTrue(trainMask));
System.out.println();
for (int epoch = 0; epoch < epochs; epoch++) {
double[][] probs = forward(graph);
int[] labels = graph.getLabels();
// 简化训练:根据预测误差调整输出层
// 仅对训练节点计算梯度
for (int v = 0; v < graph.getNumNodes(); v++) {
if (!trainMask[v] || labels[v] < 0) continue;
for (int c = 0; c < numClasses; c++) {
double target = (c == labels[v]) ? 1.0 : 0.0;
double error = probs[v][c] - target;
// 更新输出层偏置
outputBias[c] -= learningRate * error;
}
}
if (epoch % 50 == 0) {
double trainAcc = accuracy(graph, trainMask);
System.out.printf("Epoch %3d | 训练准确率: %.2f%%%n", epoch, trainAcc * 100);
}
}
}
private int countTrue(boolean[] arr) {
int count = 0;
for (boolean b : arr) if (b) count++;
return count;
}
}
4. 数据集构建与主程序
import java.util.Random;
/**
* GNN节点分类演示:在合成图上进行社区发现
*/
public class GNNDemo {
public static void main(String[] args) {
System.out.println("=== 图神经网络GNN节点分类演示 ===");
System.out.println("任务:在具有社区结构的图中预测节点所属社区");
System.out.println();
// 构建合成图:两个社区通过少量边连接
int numNodes = 50;
int featureDim = 4;
int numClasses = 2;
Graph graph = buildCommunityGraph(numNodes, featureDim, 42);
graph.printStructure();
System.out.println();
// 划分训练集和测试集
boolean[] trainMask = new boolean[numNodes];
Random rand = new Random(123);
for (int i = 0; i < numNodes; i++) {
trainMask[i] = rand.nextDouble() < 0.6; // 60%节点用于训练
}
int trainCount = 0;
for (boolean b : trainMask) if (b) trainCount++;
System.out.println("训练节点: " + trainCount + ", 测试节点: " + (numNodes - trainCount));
System.out.println();
// 创建并训练GNN
GNNClassifier gnn = new GNNClassifier(
featureDim, // 输入特征维度
16, // 隐藏层维度
numClasses, // 输出类别数
0.05, // 学习率
456 // 随机种子
);
gnn.train(graph, trainMask, 500);
// 评估
System.out.println("\n=== 最终评估 ===");
double trainAcc = gnn.accuracy(graph, trainMask);
boolean[] testMask = new boolean[numNodes];
for (int i = 0; i < numNodes; i++) testMask[i] = !trainMask[i];
double testAcc = gnn.accuracy(graph, testMask);
System.out.printf("训练集准确率: %.2f%%%n", trainAcc * 100);
System.out.printf("测试集准确率: %.2f%%%n", testAcc * 100);
// 显示部分节点的预测结果
System.out.println("\n节点预测详情(前15个节点):");
System.out.printf("%-6s %-8s %-10s %-10s%n", "节点", "真实标签", "预测标签", "是否正确");
System.out.println("-".repeat(40));
int[] predictions = gnn.predict(graph);
int[] labels = graph.getLabels();
for (int i = 0; i < Math.min(15, numNodes); i++) {
boolean correct = predictions[i] == labels[i];
System.out.printf("%-6d %-8d %-10d %-10s%n",
i, labels[i], predictions[i], correct ? "✓" : "✗");
}
// 分析图卷积的效果
System.out.println("\n=== 图卷积效果分析 ===");
analyzeGraphConvolutionEffect(graph);
System.out.println("\nGNN通过消息传递机制聚合邻居信息,");
System.out.println("使相连节点获得相似的嵌入表示,");
System.out.println("从而实现基于图结构的半监督分类。");
}
/**
* 构建具有社区结构的合成图
* 社区0:节点0-24,社区1:节点25-49
*/
private static Graph buildCommunityGraph(int numNodes, int featureDim, long seed) {
Random rand = new Random(seed);
Graph graph = new Graph(numNodes, featureDim);
int half = numNodes / 2;
// 设置节点特征和标签
for (int i = 0; i < numNodes; i++) {
int community = (i < half) ? 0 : 1;
graph.setLabel(i, community);
// 社区内节点特征相似
double[] features = new double[featureDim];
for (int j = 0; j < featureDim; j++) {
features[j] = rand.nextGaussian() + community * 2.0;
}
graph.setNodeFeature(i, features);
}
// 在社区内添加密集连接
double intraProb = 0.3;
for (int i = 0; i < half; i++) {
for (int j = i + 1; j < half; j++) {
if (rand.nextDouble() < intraProb) graph.addEdge(i, j);
}
}
for (int i = half; i < numNodes; i++) {
for (int j = i + 1; j < numNodes; j++) {
if (rand.nextDouble() < intraProb) graph.addEdge(i, j);
}
}
// 在社区间添加少量连接
double interProb = 0.05;
for (int i = 0; i < half; i++) {
for (int j = half; j < numNodes; j++) {
if (rand.nextDouble() < interProb) graph.addEdge(i, j);
}
}
return graph;
}
/** 分析图卷积对邻居特征的聚合效果 */
private static void analyzeGraphConvolutionEffect(Graph graph) {
double[][] features = graph.getNodeFeatures();
int[] labels = graph.getLabels();
// 计算社区内和社区间的平均特征距离
double intraDist = 0, interDist = 0;
int intraCount = 0, interCount = 0;
for (int i = 0; i < graph.getNumNodes(); i++) {
for (int j = i + 1; j < graph.getNumNodes(); j++) {
double dist = euclideanDistance(features[i], features[j]);
if (labels[i] == labels[j]) {
intraDist += dist;
intraCount++;
} else {
interDist += dist;
interCount++;
}
}
}
System.out.printf("社区内平均特征距离: %.4f%n", intraDist / intraCount);
System.out.printf("社区间平均特征距离: %.4f%n", interDist / interCount);
System.out.println("GNN通过聚合邻居特征,使同类节点嵌入更加接近。");
}
private static double euclideanDistance(double[] a, double[] b) {
double sum = 0;
for (int i = 0; i < a.length; i++) {
double diff = a[i] - b[i];
sum += diff * diff;
}
return Math.sqrt(sum);
}
}
复杂度分析
| 操作 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 单层GCN前向 | O( | E |
| L层GCN前向 | O(L × | E |
| 节点分类 | O( | V |
其中 $|V|$ 为节点数,$|E|$ 为边数。GCN的高效性来源于利用稀疏邻接矩阵,避免了对稠密矩阵的操作。
总结
本文实现了图神经网络的核心机制,要点包括:
- 消息传递是GNN的核心范式:每个节点聚合邻居信息并更新自身表示,通过多层传播捕获多跳邻居关系
- 图卷积通过对邻接矩阵进行归一化($\tilde{D}^{-1/2}\tilde{A}\tilde{D}^{-1/2}$),平衡高度数节点的影响,防止数值不稳定
- 半监督学习使GNN能够利用少量标注节点和大量未标注节点,通过图结构传播标签信息
- Java实现完整展示了从图数据结构到消息传递再到节点分类的完整流程
GNN将深度学习从规则网格数据拓展到任意图结构,是处理关系数据的首选框架。本文的简化实现保留了MPNN的核心思想,实际应用中PyTorch Geometric和DGL等库提供了更高效的稀疏矩阵运算支持。