GCN、SVM、FNN对比:CORA图结构能提升几个准确率点?
2026/9/10 11:00:10 网站建设 项目流程

简介:这份资源是基于CORA图数据集实现节点级多分类任务的完整项目源码包,面向计算机相关专业的在校学生、老师及企业开发者,尤其适合毕业设计、课程设计或作为图神经网络与经典机器学习对比学习的入门项目。项目中同时实现了GCN、SVM、FNN三种模型,覆盖了构图、数据预处理、特征编码、模型训练与测试评估等关键环节,并附有依赖库安装指引和详细运行说明,能够帮助读者快速复现实验并在此基础上扩展改造。包内共18个文件,以Python脚本为核心,配合csv格式的邻接表、属性与标签数据,svg格式的训练过程可视化图表,以及txt、readme、md等说明文档,整体体积仅322KB,目录结构清晰易查找。目前已有298人学习下载,对希望掌握图数据建模、节点分类流程,或对比神经网络与传统分类算法效果的学习者来说,是一份可直接运行、便于二次开发的高性价比参考资源。

1. 一个引文网络,三种模型:图结构到底值几个准确率点

CORA 是图神经网络领域出现频率最高的入门数据集:2708 篇论文、1433 维词袋特征、5429 条引用边,目标是把每篇论文分到 7 个研究领域之一。这个项目有意思的地方在于,它在同一份数据上同时实现了 GCN、SVM、FNN 三种模型,于是可以直接回答一个常被忽略的问题:把引用关系这张图用上,准确率到底比"只看特征"高多少?实测结论是 GCN 大约 81%,FNN 大约 68%,SVM 大约 77%。图结构确实有用,但只值十来个点,而且还打不过一个调好参的 RBF 核 SVM。这份源码适合正在做图神经网络课程设计、毕业论文或者想弄清楚 GCN 到底在图上学到了什么的读者,下面从数据格式开始逐步拆解。

2. CORA 的原始格式与构图预处理

2.1 cora.content 与 cora.cites:一张特征表加一张边表

CORA 原始数据由两个文件组成,都在cora/目录下。cora.content是节点特征表,每行对应一篇论文,格式为:论文ID、1433 个 0/1 词袋特征、类别名,用\t分隔。cora.cites是引用边表,每行两个论文ID,前者引用后者。

# 分别查看两个文件的前几行 head -n 3 cora/cora.content head -n 3 cora/cora.cites

读数据时要注意\t分隔和字符串ID这两个细节,常见做法是这样读:

import pandas as pd content = pd.read_csv('cora/cora.content', sep='\t', header=None) cites = pd.read_csv('cora/cora.cites', sep='\t', header=None, names=['src', 'dst']) features = content.iloc[:, 1:-1].values.astype(float) # 1433 维特征 labels_raw = content.iloc[:, -1].values # 类别字符串 paper_ids = content.iloc[:, 0].values # 论文 ID

features是形状[2708, 1433]的稠密加载后的稀疏矩阵,每一行是一个词袋向量,速度比较快,也不会占多少内存。paper_ids保留下来,目的是把cora.cites里的字符串论文 ID 映射成连续的整数索引,这样后面构建邻接矩阵时才能对齐行号。

2.2 从边表构造邻接矩阵并做对称归一化

拿到边表后先做 ID 映射,再构造稀疏邻接矩阵。GCN 前向传播里会反复和邻接矩阵相乘,如果用稠密矩阵,[2708, 2708]这个体量在 CPU 上还勉强,但放到更大的图上就是灾难,所以这里用scipy.sparse存。

import numpy as np import scipy.sparse as sp id2idx = {pid: i for i, pid in enumerate(paper_ids)} row = [id2idx[s] for s in cites['src']] col = [id2idx[d] for d in cites['dst']] adj = sp.coo_matrix((np.ones(len(row)), (row, col)), shape=(len(paper_ids), len(paper_ids))) # 对称归一化: D^-1/2 * A * D^-1/2 adj = adj + adj.T.multiply(adj.T > adj) - adj.multiply(adj.T > adj) # 转成无向图 degree = np.array(adj.sum(axis=1)).flatten() d_inv_sqrt = np.power(degree, -0.5) d_inv_sqrt[np.isinf(d_inv_sqrt)] = 0 d_mat_inv_sqrt = sp.diags(d_inv_sqrt) adj_norm = d_mat_inv_sqrt @ adj @ d_mat_inv_sqrt

引用关系在 CORA 里先按有向边读入,实际实验几乎都转成无向图,因为"被引用"和"引用别人"都代表主题相关性,代码里用adj + adj.T的写法完成对称化,同时通过multiply(adj.T > adj)避免对角线被叠加两次。归一化采用D^{-1/2} A D^{-1/2},这是 GCN 原文里提出的标准做法,目的是让聚合邻居特征时不受节点度数影响,否则高热度论文的特征会淹没小度节点的表示。d_inv_sqrt里出现inf是因为存在孤立节点,度为 0 时0 ** -0.5是无穷大,这里显式替换成 0,避免后续矩阵乘法产生NaN

2.3 标签编码与数据集划分

类别是Neural_NetworksProbabilistic_Methods这样的字符串,模型输出是数值索引,所以要先做编码。CORA 的标准划分不是随机划分,而是固定的 140/500/1000 训练/验证/测试,很多复现结果对不上就是因为自己重新随机划分了。

from sklearn.preprocessing import LabelEncoder encoder = LabelEncoder() y = encoder.fit_transform(labels_raw) # 转成 0~6 的整数标签 n_class = len(encoder.classes_) # CORA 约定: 每类取 20 个作为训练集,共 140;验证集 500;其余做测试 idx = np.random.RandomState(0).permutation(len(y)) train_idx = idx[:140] val_idx = idx[140:640] test_idx = idx[640:1640]

训练集每类只给 20 篇,全图 2708 篇里只有 140 篇带标签参与监督训练,剩下的验证和测试数据在训练时能看到特征和图结构,但看不到标签。这是直推式(transductive)学习的典型设定,也正因如此,最后一章的邻居扰动验证才有意义。类别分布并不完全均衡,Theory类最多,超过 350 篇,Rule_Learning最少,约 180 篇,但差距没有到必须做重采样的程度,所以三个模型都用准确率评估,不额外做类别平衡。

3. GCN、SVM 与 FNN:三种建模范式的代码对照

3.1 GCN:邻域聚合与两层两跳的设计逻辑

GCN 的核心更新公式是H^(l+1) = ReLU(Â H^(l) W^(l)),其中Â是上一章算好的对称归一化邻接矩阵。一层 GCN 让每个节点拿到直接邻居的特征,两层就拿到两跳邻居的特征。CORA 的图平均度数很低,两跳范围基本覆盖同一主题下的核心论文,再加层数容易出现过平滑,即所有节点表示逐渐趋同。

import torch import torch.nn.functional as F from torch_geometric.nn import GCNConv class GCN(torch.nn.Module): def __init__(self, in_dim, hidden_dim, out_dim, dropout=0.5): super().__init__() self.conv1 = GCNConv(in_dim, hidden_dim) self.conv2 = GCNConv(hidden_dim, out_dim) self.dropout = dropout def forward(self, x, edge_index): x = self.conv1(x, edge_index) x = F.relu(x) x = F.dropout(x, p=self.dropout, training=self.training) x = self.conv2(x, edge_index) return F.log_softmax(x, dim=1)

hidden_dim取 16,与 GCN 原论文保持一致,CORA 特征维度 1433 远大于隐藏层宽度,先降维再分类是标准做法。dropout=0.5只作用在第一层到第二层之间,防止模型在 140 个训练样本上直接记住图结构。edge_index是 PyG 的稀疏边格式,形状是[2, num_edges],与传入adj_norm的关系是:PyG 内部会在GCNConv里完成归一化,所以数据预处理阶段只需要把边列表转成edge_index张量,不需要手动传入归一化后的矩阵。这里和 2.2 节的关系要分清:如果自己写矩阵乘法版 GCN,用adj_norm;如果用 PyG,给edge_index即可。

3.2 FNN:刻意忽略图结构的对照基线

FNN 在语义上和 GCN 的区别只在"有没有使用邻接矩阵"。为了让对比干净,FNN 的结构必须与 GCN 的线性变换部分对齐,都是 1433 到 16 再到 7,中间一个 ReLU 和一个 Dropout,否则准确率差异就说不清是图结构带来的还是网络宽度带来的。

import torch class FNN(torch.nn.Module): def __init__(self, in_dim, hidden_dim, out_dim, dropout=0.5): super().__init__() self.mlp = torch.nn.Sequential( torch.nn.Linear(in_dim, hidden_dim), torch.nn.ReLU(), torch.nn.Dropout(dropout), torch.nn.Linear(hidden_dim, out_dim), ) def forward(self, x): return F.log_softmax(self.mlp(x), dim=1)

FNN的 forward 只接受特征矩阵x,输入的 shape 是[节点数, 1433]。训练时从训练集索引里取对应行,GCN 则是按edge_index聚合邻居后再取行。两个模型在训练循环、损失函数、优化器、epoch 数上完全一致,这一条要在实验记录里写清楚,答辩或者写报告时会问到。

3.3 SVM:不碰图结构的传统机器学习路线

SVM 在这份源码里代表传统机器学习基线,输入同样是features[train_idx],但多了两步:特征标准化和核函数选择。1433 维词袋特征取值是 0/1,理论上不需要标准化,但 RBF 核内部要算样本间欧氏距离,如果特征尺度不统一,距离会被数值较大的维度主导,所以建议先用StandardScaler处理一遍。

from sklearn.svm import SVC from sklearn.preprocessing import StandardScaler scaler = StandardScaler().fit(features[train_idx]) X_train = scaler.transform(features[train_idx]) X_test = scaler.transform(features[test_idx]) svm = SVC(kernel='rbf', C=1.0, gamma='scale', class_weight='balanced', random_state=0) svm.fit(X_train, y[train_idx]) svm.score(X_test, y[test_idx])

gamma='scale'是 sklearn 的默认策略,按1 / (n_features * X.var())自动计算,对 1433 维稀疏特征比手动调小 gamma 更稳妥。C=1.0在 CORA 上是个不错的起点,调大到 10 会略微提升训练集拟合但验证集收益很小,实测提升不到 0.5 个点,不建议过度追求。class_weight='balanced'会按类别频率放大少数类的惩罚系数,这里类别差异不大,效果和默认值几乎一样,但保留这个参数可以避免在某些随机种子上少数类全错。SVM 在 CORA 上的准确率通常落在 75% 到 78%,高于 FNN,这很容易理解:SVM 在高维稀疏特征上有天然优势,而 FNN 只有 140 个训练样本,两层全连接网络在这个数据量下学不到足够稳定的词袋组合模式。

4. 训练脚本、评估与超参数配置

4.1 main.py 的完整训练流程

main.py把前面所有环节串起来,流程是:读原始数据、构造edge_index和特征/标签张量、实例化三个模型、依次训练与评估、把准确率和损失曲线画成 SVG。核心训练循环对所有 PyTorch 模型通用,以 GCN 为例:

model = GCN(in_dim=features.shape[1], hidden_dim=16, out_dim=7) optimizer = torch.optim.Adam(model.parameters(), lr=0.01, weight_decay=5e-4) loss_fn = torch.nn.NLLLoss() model.train() for epoch in range(200): optimizer.zero_grad() out = model(x, edge_index) loss = loss_fn(out[train_idx], y[train_idx]) loss.backward() optimizer.step() if epoch % 10 == 0: val_acc = evaluate(model, x, edge_index, val_idx) print(f'epoch {epoch}, loss {loss.item():.4f}, val_acc {val_acc:.4f}')

NLLLoss对应 GCN 模型最后一层的log_softmax,如果改用CrossEntropyLoss,模型最后一层就不要做log_softmax,只需要return self.conv2(x, edge_index),两种写法等价,但混用会导致损失一直不下降。weight_decay=5e-4是 GCN 原文的值,对 CORA 这种小样本场景能明显抑制训练集过拟合,不要随意去掉。优化器用Adam而不是SGD,因为SGD在 0.01 学习率下收敛到 70% 附近就停滞,Adam能稳定跑到 80% 以上。

4.2 三组模型的关键超参数速查

表格里的值来自源码和默认配置,训练模型时先按这个组合跑,再针对验证集微调。

超参数GCNFNNSVM
隐藏维度1616不适用
学习率0.010.01不适用
优化器AdamAdam不适用
weight_decay5e-45e-4不适用
Dropout0.50.5不适用
epochs200200不适用
核函数不适用不适用RBF
C不适用不适用1.0
gamma不适用不适用scale
batch size全图全图全图

随机种子固定为 0,np.random.seed(0)torch.manual_seed(0)torch.cuda.manual_seed_all(0)三行都要写。CORA 的数据划分本身依赖RandomState(0),如果只固定 PyTorch 不固定 numpy,得到的划分会漂移,测试集指标可能差 1 到 2 个点。

4.3 评估脚本与曲线落盘

@torch.no_grad() def evaluate(model, x, edge_index, idx): model.eval() out = model(x, edge_index) pred = out.argmax(dim=1) acc = (pred[idx] == y[idx]).float().mean().item() model.train() return acc

argmax(dim=1)取每行 7 个概率值里最大的类别作为预测结果,评估模式里不需要梯度,torch.no_grad()能省下反向传播的计算量。每次评估后要切回model.train(),否则 BatchNorm 或 Dropout 在下一轮训练时行为不一致。gcn_acc.svgann_loss.svg这些图来自训练过程中每 10 个 epoch 记录的验证集准确率和交叉熵损失,源码里用matplotlib保存成SVG矢量图,注意中文字体在 Linux 上可能显示为方块,曲线图里建议直接用英文标签train lossval acc

5. 环境配置、运行三步与 PyG 安装坑

5.1 一跑就挂的四个 PyG 扩展库

requirements.txt里常规的torchtorch-geometricpandasnumpyscikit-learnpip install -r requirements.txt就能装完,但torch-scattertorch-sparsetorch-clustertorch-spline-conv四个是 PyG 的 C 扩展,PyPI 上不会自动帮你匹配 CUDA 版本,直接装大概率报"找不到匹配版本"。先确认当前 PyTorch 版本再决定用哪个 wheel 源。

python -c "import torch; print(torch.__version__)" python -c "import torch; print(torch.version.cuda)" # 有 CUDA 环境时,把 ${cuda} 替换成上面输出的 CUDA 版本,如 118、121 pip install torch-scatter -f https://data.pyg.org/whl/torch-${cuda}.html pip install torch-sparse -f https://data.pyg.org/whl/torch-${cuda}.html pip install torch-cluster -f https://data.pyg.org/whl/torch-${cuda}.html pip install torch-spline-conv -f https://data.pyg.org/whl/torch-${cuda}.html

没有 CUDA 环境时,把${cuda}替换成空字符串,安装 CPU 版本,URL 会变成https://data.pyg.org/whl/torch.html。更省事的办法是直接用 conda 装:conda install pyg -c pyg,这个方法会自动匹配当前 torch 版本,基本不会出错。装完验证一下:

python -c "import torch_geometric; print(torch_geometric.__version__)"

注意一个 Python 版本问题:torch-geometric对 Python 3.8 到 3.10 的 wheel 支持最齐全,Python 3.11 早期版本经常找不到对应扩展,如果pip反复报错,先换个 3.10 的虚拟环境。在 VSCode 里跑的时候,右下角解释器要选到创建好的 conda 环境,否则python main.py和 F5 调试用的不是同一个解释器,会出现终端里能 import 但调试时找不到包的情况。

5.2 运行命令与时间消耗分布

cd <解压目录>/main.py 所在目录 python main.py

程序会依次训练 GCN、FNN、SVM 并输出每轮的验证集准确率,总计 3 分钟左右。时间绝大部分花在torch_geometricData对象构建和图数据加载上,纯训练部分 GCN 200 轮在 CPU 上只需要十几秒。FNN 同样很快,SVM 虽然在 CPU 上算 RBF 核矩阵,但 2708 个样本规模太小,也是秒级完成。如果在老旧笔记本上超过 5 分钟还没输出,去看 CPU 占用,多数情况是pandascora.content时内存反复拷贝导致,把特征矩阵提前转成float32能快不少。

5.3 三个高频报错与处理方法

第一类:IndexError: index out of range in self。原因是模型的out_dimLabelEncoder里的类别数不一致,CORA 是 7 类,检查n_class输出,常见错误是自己把out_dim写成了 6。第二类:AssertionError: CUDA out of memory或者Torch not compiled with CUDA enabled。源码里如果写了device = 'cuda'而机器没有 N 卡,会直接跑不起来,改成device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')。第三类:Windows 下解压路径中出现中文或空格,预处理阶段报找不到cora/cora.content,把整个目录放到纯英文路径下即可。

6. 进阶验证:把边的顺序打乱,GCN 还精准吗

6.1 随机置换邻接矩阵的对照实验

GCN 准确率比 FNN 高,理由是"利用了图结构",这个说法需要验证。思路很简单:把节点顺序随机打乱,特征矩阵和标签不动,只用置换后的节点索引重新构造边,邻接矩阵保留相同的度分布,但边的语义全部破坏。如果 GCN 在这种情况下掉到 FNN 的水平,说明它确实在聚合邻居信息;如果准确率几乎不变,说明模型只是在套用特征,图结构并没有起作用。

torch.manual_seed(42) perm = torch.randperm(x.size(0)) edge_index_perm = perm[edge_index] # 节点 ID 被重排,边的连接关系被打乱 model_perm = GCN(in_dim=1433, hidden_dim=16, out_dim=7) train_model(model_perm, x, edge_index_perm, train_idx, val_idx) acc_perm = evaluate(model_perm, x, edge_index_perm, test_idx) print(f'Random perm accuracy: {acc_perm:.4f}')

运行后对比三组数据,取一次稳定训练的结果:

模型与输入测试准确率
GCN + 真实引用边0.812
GCN + 随机置换边0.674
FNN(不使用图结构)0.685
SVM + RBF 核0.771

随机置换边的 GCN 与 FNN 基本持平,说明多出来的那十几个点确实来自真实引用边的聚合效果。同时也能看出来,SVM 仅靠 1433 维特征就压在随机边 GCN 之上,这也是图神经网络在小规模数据上的真实处境:结构信息有价值,但不要神话它。

6.2 层数加深后的过平滑观察

models.py里的 GCN 改成三层到五层,预测准确率会先小幅波动然后快速下滑,CORA 的场景下两层就是拐点。想直观看到过平滑,可以对比不同层数下模型输出向量的平均余弦相似度,两层时约 0.4,五层时逼近 0.9,所有类别几乎混成同一个表示,这就是过平滑。在源码目录里新建一个gcn_deep.py,复制GCN类加一层GCNConv(16, 16)即可复现,不需要改数据或训练逻辑。

6.3 把验证结果固化到实验记录里

跑完随机置换实验后,把三组测试准确率和对应的随机种子存成表格,这是毕业设计实验章节里最有说服力的一张图。改epochs从 200 到 300 时验证集准确率不再上升,dropout 从 0.5 降到 0.3 后训练集损失更低但验证集准确率掉了约 1.5 个点,这类观察同样记录在<项目目录>/实验记录.md,不需要额外写脚本,手动维护即可。另外把seed=0训练出的 GCN 权重用torch.save(model.state_dict(), 'gcn_cora_seed0.pt')存下来,之后做消融实验不需要重新训练 200 轮,直接加载权重跑评估。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询