GCN、SVM、FNN对比:CORA图结构能提升几个准确率点?
发布时间:2026/9/10 11:00:21来源:尧图网络
简介这份资源是基于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, headerNone) cites pd.read_csv(cora/cora.cites, sep\t, headerNone, names[src, dst]) features content.iloc[:, 1:-1].values.astype(float) # 1433 维特征 labels_raw content.iloc[:, -1].values # 类别字符串 paper_ids content.iloc[:, 0].values # 论文 IDfeatures是形状[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(axis1)).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_Networks、Probabilistic_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^(l1) 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, dropout0.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, pself.dropout, trainingself.training) x self.conv2(x, edge_index) return F.log_softmax(x, dim1)hidden_dim取 16与 GCN 原论文保持一致CORA 特征维度 1433 远大于隐藏层宽度先降维再分类是标准做法。dropout0.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, dropout0.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), dim1)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(kernelrbf, C1.0, gammascale, class_weightbalanced, random_state0) svm.fit(X_train, y[train_idx]) svm.score(X_test, y[test_idx])gammascale是 sklearn 的默认策略按1 / (n_features * X.var())自动计算对 1433 维稀疏特征比手动调小 gamma 更稳妥。C1.0在 CORA 上是个不错的起点调大到 10 会略微提升训练集拟合但验证集收益很小实测提升不到 0.5 个点不建议过度追求。class_weightbalanced会按类别频率放大少数类的惩罚系数这里类别差异不大效果和默认值几乎一样但保留这个参数可以避免在某些随机种子上少数类全错。SVM 在 CORA 上的准确率通常落在 75% 到 78%高于 FNN这很容易理解SVM 在高维稀疏特征上有天然优势而 FNN 只有 140 个训练样本两层全连接网络在这个数据量下学不到足够稳定的词袋组合模式。4. 训练脚本、评估与超参数配置4.1 main.py 的完整训练流程main.py把前面所有环节串起来流程是读原始数据、构造edge_index和特征/标签张量、实例化三个模型、依次训练与评估、把准确率和损失曲线画成 SVG。核心训练循环对所有 PyTorch 模型通用以 GCN 为例model GCN(in_dimfeatures.shape[1], hidden_dim16, out_dim7) optimizer torch.optim.Adam(model.parameters(), lr0.01, weight_decay5e-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(fepoch {epoch}, loss {loss.item():.4f}, val_acc {val_acc:.4f})NLLLoss对应 GCN 模型最后一层的log_softmax如果改用CrossEntropyLoss模型最后一层就不要做log_softmax只需要return self.conv2(x, edge_index)两种写法等价但混用会导致损失一直不下降。weight_decay5e-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不适用核函数不适用不适用RBFC不适用不适用1.0gamma不适用不适用scalebatch size全图全图全图随机种子固定为 0np.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(dim1) acc (pred[idx] y[idx]).float().mean().item() model.train() return accargmax(dim1)取每行 7 个概率值里最大的类别作为预测结果评估模式里不需要梯度torch.no_grad()能省下反向传播的计算量。每次评估后要切回model.train()否则 BatchNorm 或 Dropout 在下一轮训练时行为不一致。gcn_acc.svg、ann_loss.svg这些图来自训练过程中每 10 个 epoch 记录的验证集准确率和交叉熵损失源码里用matplotlib保存成SVG矢量图注意中文字体在 Linux 上可能显示为方块曲线图里建议直接用英文标签train loss、val acc。5. 环境配置、运行三步与 PyG 安装坑5.1 一跑就挂的四个 PyG 扩展库requirements.txt里常规的torch、torch-geometric、pandas、numpy、scikit-learn用pip install -r requirements.txt就能装完但torch-scatter、torch-sparse、torch-cluster、torch-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_geometric的Data对象构建和图数据加载上纯训练部分 GCN 200 轮在 CPU 上只需要十几秒。FNN 同样很快SVM 虽然在 CPU 上算 RBF 核矩阵但 2708 个样本规模太小也是秒级完成。如果在老旧笔记本上超过 5 分钟还没输出去看 CPU 占用多数情况是pandas读cora.content时内存反复拷贝导致把特征矩阵提前转成float32能快不少。5.3 三个高频报错与处理方法第一类IndexError: index out of range in self。原因是模型的out_dim和LabelEncoder里的类别数不一致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_dim1433, hidden_dim16, out_dim7) train_model(model_perm, x, edge_index_perm, train_idx, val_idx) acc_perm evaluate(model_perm, x, edge_index_perm, test_idx) print(fRandom perm accuracy: {acc_perm:.4f})运行后对比三组数据取一次稳定训练的结果模型与输入测试准确率GCN 真实引用边0.812GCN 随机置换边0.674FNN不使用图结构0.685SVM 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不需要额外写脚本手动维护即可。另外把seed0训练出的 GCN 权重用torch.save(model.state_dict(), gcn_cora_seed0.pt)存下来之后做消融实验不需要重新训练 200 轮直接加载权重跑评估。本文还有配套的精品资源点击获取
网站建设高端定制企业官网