十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

CORA节点分类实战:GCN、SVM与FNN三模型对比解析

CORA节点分类实战:GCN、SVM与FNN三模型对比解析 简介面向图神经网络入门者、毕业设计学生及相关方向开发者该项目以CORA引文数据集为载体完整实现节点级多分类流程。代码同步给出GCN、SVM与FNN三种模型从数据读取、构图、特征编码到模型训练、评估与对比均有清晰封装适合作为课程设计、毕业设计或初期实验的参考框架。包体整体约322KB共18个文件其中6个Python脚本覆盖模型构建、训练与测试3张csv数据表存放节点属性与标签4张svg曲线图展示损失和准确率变化另有依赖清单与运行说明辅助复现。目前已有298人浏览学习项目在测试环境下可正常运行针对torch-geometric相关扩展库安装失败的问题说明中提供了按CUDA版本或CPU环境替换安装源的方案能有效减少环境配置障碍。读者既可快速跑通CORA节点分类也可在此之上调整模型结构、替换传统分类器或扩展新实验适合教学演示和二次开发。1. 为什么拿CORA练手节点分类任务的第一块试验田CORA是图学习里最常被拉出来“试刀”的引文数据集。2708篇论文按关键词被表示成1433维0/1向量5429条引用关系构成图结构每篇论文归属7个类别。节点级分类就是在已知部分论文标签的情况下预测剩余论文的研究领域。你能在这个数据上同时体验三件事图卷积网络GCN怎样把邻居信息聚合进节点表示支持向量机SVM怎样只靠原始特征做分类前馈神经网络FNN怎样用最朴素的方式逼近标签分布。三者对比不是为了比谁精度更高而是让你看清“图的结构”到底给分类贡献了多少。下面从数据读取开始把一个能复现的Python项目拆给你看。2. 吃透CORA数据集节点特征、邻接矩阵和标签分布CORA数据集虽然小但结构很典型一张无向图、一份特征矩阵、一份标签向量。开源版本常见的是两个文件cora.content每行代表一篇论文第一列是论文ID接着1433个0/1值最后一个数字是类别编号cora.cites每行是两个论文ID前者引用了后者。项目里第一步就是解析这两个文件把所有ID映射成矩阵下标。2.1 读取CORA的两种方式手动解析与PyG内置加载手动解析能让你清楚地看到数据格式对后续调试很有帮助。常见做法是先用pandas读content再用字典维护ID到行号的映射import numpy as np import pandas as pd content pd.read_csv(cora/cora.content, sep\t, headerNone) node_ids content.iloc[:, 0].astype(int).values features content.iloc[:, 1:-1].values.astype(np.float32) labels pd.factorize(content.iloc[:, -1].values)[0] id2idx {id_: i for i, id_ in enumerate(node_ids)} with open(cora/cora.cites, r) as f: edges [] for line in f: src, dst line.strip().split() if src in id2idx and dst in id2idx: edges.append([id2idx[src], id2idx[dst]]) edges np.array(edges, dtypenp.int64) num_nodes features.shape[0] adjacency np.zeros((num_nodes, num_nodes), dtypenp.float32) adjacency[edges[:, 0], edges[:, 1]] 1.0 adjacency[edges[:, 1], edges[:, 0]] 1.0pd.factorize会把类别字符串转成从0开始的整数索引避免手工维护映射。edges数组里存的是行号而不是原始论文ID因为后面所有模型都按矩阵下标操作。邻接矩阵要加转置原文件只记录了一个方向的引用而图卷积需要的是双向信息传递。如果不想手动解析也可以用PyTorch Geometric的Planetoid接口一次性拿到标准化后的数据。需要注意PyG版本不同数据下载路径和缓存规则也会变所以我建议在项目里至少保留一份手动解析代码外部下载失败时仍然可以从本地目录启动。2.2 训练集/验证集/测试集划分按类别分层比全局随机更可靠CORA论文里常见的一种划分是每类固定取20个节点做训练500个做验证1000个做测试。自己实现时可以按类别分层采样保证每个类别在训练集中都有代表。下面的代码不依赖PyG直接在numpy上工作rng np.random.default_rng(42) train_mask np.zeros(num_nodes, dtypebool) val_mask np.zeros(num_nodes, dtypebool) test_mask np.zeros(num_nodes, dtypebool) for cls in range(7): cls_indices np.where(labels cls)[0] rng.shuffle(cls_indices) train_mask[cls_indices[:20]] True val_mask[cls_indices[20:120]] True test_mask[cls_indices[120:]] True参数含义每个类别取20个训练样本、100个验证样本其余进入测试集。这样训练集约956个节点验证集500个测试集1252个。掩码是布尔数组后续算损失或准确率时直接用索引过滤。如果忽略分层而做全局随机某些稀有类别可能一个训练样本都分不到导致模型完全学不到该类别的模式。2.3 预处理对称归一化邻接矩阵是GCN的标配GCN的核心是聚合邻居特征但直接使用原始邻接矩阵有两个问题一是节点自身信息会丢失聚合后表示容易受邻居分布支配二是度数大的节点聚合结果远大于度数小的节点导致训练不稳定。标准做法是加自环再做对称归一化。adj_with_self adjacency np.eye(num_nodes, dtypenp.float32) degree adj_with_self.sum(axis1) degree_inv_sqrt np.power(degree, -0.5) degree_inv_sqrt[np.isinf(degree_inv_sqrt)] 0.0 degree_inv_sqrt np.diag(degree_inv_sqrt) adj_norm degree_inv_sqrt adj_with_self degree_inv_sqrt这段代码的意义degree_inv_sqrt是D^{-1/2}的对角矩阵乘出来的adj_norm每个元素都除以两端节点度数的平方根。由于cora.cites本身没有孤立点的风险但仍建议对degree_inv_sqrt中的inf做兜底防止某天换数据时出现除零。对于SVM和FNN来说预处理只需要特征矩阵不需要邻接矩阵但如果你打算让三个模型共享同一份特征建议对features做L2行归一化第四章会给出具体写法。完成上述步骤后把features、adj_norm、labels和三个掩码打包保存成npz文件后续脚本直接加载避免每次重新解析。保存代码和运行说明放在第五章。3. GCN模型搭建与训练两层图卷积的参数与形状图卷积网络听起来高深但CORA上的标准实现其实只有两层输入层到隐藏层隐藏层到输出层。隐藏层维度通常取16或32输出维度是类别数7。移除激活函数和Dropout后这个模型的参数量不到两万却能在测试集上达到80%以上的准确率效果远比同样层数的FNN好。3.1 GCN前向传播邻接矩阵与特征矩阵的两次乘法GCN每一层的计算可以写成H^{l1} σ(Â H^{l} W^{l})其中Â是归一化后的带自环邻接矩阵W是权重矩阵。两层的模型就是先做一次聚合和激活再做第二次聚合得到logits。用PyTorch实现如下import torch import torch.nn as nn import torch.nn.functional as F class GCNLayer(nn.Module): def __init__(self, in_features, out_features): super().__init__() self.weight nn.Parameter(torch.FloatTensor(in_features, out_features)) nn.init.xavier_uniform_(self.weight) def forward(self, x, adj_norm): return torch.mm(adj_norm, torch.mm(x, self.weight)) class TwoLayerGCN(nn.Module): def __init__(self, in_features1433, hidden_features16, num_classes7, dropout0.5): super().__init__() self.layer1 GCNLayer(in_features, hidden_features) self.layer2 GCNLayer(hidden_features, num_classes) self.dropout nn.Dropout(dropout) def forward(self, x, adj_norm): h F.relu(self.layer1(x, adj_norm)) h self.dropout(h) return self.layer2(h, adj_norm)注意这里的adj_norm是稠密矩阵。2708×2708约110MB内存可以在普通笔记本上运行只有节点数到几十万时才需要考虑转换为torch.sparse_coo_tensor。hidden_features16是CORA实验中最常见的配置太小欠拟合太大在小样本下容易过拟合。dropout0.5让模型不完全依赖某几个节点。xavier_uniform_初始化能减少梯度消失的可能性比默认的uniform初始化更稳定。3.2 训练循环只对训练掩码里的节点算损失GCN和普通网络的训练方式几乎一样唯一需要留心的是损失计算必须只针对训练节点。如果让模型看到验证集或测试集的标签结果会虚高。训练循环可以这样写def train_gcn(model, features, adj_norm, labels, train_mask, epochs200, lr0.01, weight_decay5e-4): optimizer torch.optim.Adam(model.parameters(), lrlr, weight_decayweight_decay) adj_t torch.from_numpy(adj_norm).float() features_t torch.from_numpy(features).float() labels_t torch.from_numpy(labels).long() train_idx torch.where(torch.from_numpy(train_mask))[0] for epoch in range(epochs): model.train() logits model(features_t, adj_t) loss F.cross_entropy(logits[train_idx], labels_t[train_idx]) optimizer.zero_grad() loss.backward() optimizer.step() if epoch % 20 0: acc evaluate(labels_t[train_idx].numpy(), logits[train_idx].argmax(1).numpy()) print(fepoch {epoch:03d} loss {loss.item():.4f} acc {acc:.4f})这里的weight_decay是L2正则系数目的是压缩权重范数对抗2708个节点里训练样本不足的问题。lr0.01配合Adam是常见组合如果换成SGD可能需要把学习率调到0.1并加上momentum。训练200轮足够让两层GCN在CORA上收敛继续增大epoch数对精度提升有限反而可能让训练集过拟合。一个容易被忽略的点logits是全部节点的预测但cross_entropy只取train_idx对应的行反向传播不会受到未标注节点的影响。这也是半监督节点分类和普通监督学习的核心差异所在。验证集和测试集只在评估时使用不参与梯度更新。3.3 GCN训练中的四个高频坑过平滑、丢边、随机数种子和邻接矩阵方向过平滑是第一个坑。GCN层数越多节点表示越趋近于相同CORA上两层是甜蜜点三层以上准确率通常不升反降。第二个坑是丢边。cora.cites可能存在部分引用了数据集中不存在的论文的边手动解析时必须过滤否则构建邻接矩阵时索引会错位。第三个坑是随机数种子。PyTorch的卷积层虽然初始化了但不固定种子每次跑出的测试集准确率会在正常范围内波动1%到2%论文复现时必须记录种子。第四个坑是邻接矩阵方向。cites文件里第一列是引用来源第二列是被引论文构建无向图时一定记得把两个方向都加上只加一个方向会让GCN丢失一半信息。4. SVM与FNN传统方法和前馈网络在同一任务上的对照引入GCN后再看SVM和FNN会轻松很多。它们不消费图结构只把1433维特征塞进分类器。对很多人来说SVM能在这个任务上达到70%左右的准确率FNN能做到75%上下而GCN能到80%以上。差距就是图结构带来的增量也是实验报告里最值得写的数字。4.1 SVMRBF核比线性核更适合高维0/1特征CORA的特征是1433维度的0/1向量稀疏且高维。线性SVM在这种数据上表现已经不差但RBF核通常能再提升两到三个百分点。训练前先做行归一化让每个样本的向量长度变成1避免长文档因为长度过大主导距离计算。from sklearn.svm import SVC from sklearn.preprocessing import normalize features_norm normalize(features, norml2) svm SVC(kernelrbf, C1.0, gammascale, class_weightbalanced, random_state42) svm.fit(features_norm[train_mask], labels[train_mask]) svm_test_acc svm.score(features_norm[test_mask], labels[test_mask])参数说明C是误分类惩罚系数C越大模型越关注训练点是否被分对C1.0是默认起点实际可以尝试0.1到100的对数网格。gammascale表示按1/(特征数*特征方差)自动计算比手动指定gamma在稀疏高维特征上更稳妥。class_weightbalanced让类别权重与样本数量成反比CORA各类别数量不均匀加上这个参数能提升小类别上的F1。SVM训练很快但附带一个成本RBF核的推理要把每个测试样本和所有支持向量点积CORA上支持向量通常有几百个推理时间远大于GCN。如果你打算在更大的数据集上跑建议换成LinearSVC。4.2 FNN没有图信息的基线模型长什么样FNN是GCN的自然对照。两层全连接网络输入1433隐藏层16输出7激活函数用ReLU。和GCN相比唯一的区别是前向传播不再乘以邻接矩阵。代码上可以直接利用PyTorch的nn.Linearclass FNN(nn.Module): def __init__(self, in_features1433, hidden_features16, num_classes7, dropout0.5): super().__init__() self.fc1 nn.Linear(in_features, hidden_features) self.fc2 nn.Linear(hidden_features, num_classes) self.dropout nn.Dropout(dropout) def forward(self, x): h F.relu(self.fc1(x)) h self.dropout(h) return self.fc2(h)FNN不需要邻接矩阵因此训练时只传features不传adj_norm。参数量是1433×16加16×7约23000个和两层GCN相当。训练超参数可以沿用GCN那套Adam、lr0.01、weight_decay5e-4。因为FNN看不到邻居关系它只能靠每个节点自身的特征做判断在同类别特征不明显的节点上会频繁出错。训练FNN时有三个细节值得做。一是特征行归一化与SVM一样进入网络前对features做L2归一化二是Dropout可以提高到0.6因为全连接网络在训练样本只有956个时更容易记忆噪声特征三是记录每个epoch的验证集准确率判断早停时机通常60到80轮就会到平台。4.3 三个模型的复杂度与适用条件对比模型输入依赖参数量级训练时间测试集准确率范围SVM1433维特征几百个支持向量秒级65%~75%FNN1433维特征2万左右分钟级70%~78%GCN特征邻接矩阵2万左右秒级79%~84%这张表说明GCN的提升主要来自邻居聚合而不是参数量。三个模型训练时间在同一台CPU机器上都能接受GCN因为要乘邻接矩阵内存占用会高一截但仍在普通笔记本可承受范围内。不同论文的划分方式会影响具体数值所以表里的准确率范围只是一个经验区间不应当做固定结论。5. 三模型评估对比与项目运行说明复现结果时最容易踩的坑5.1 统一评估函数准确率、Macro-F1和每个类别的召回三个模型实现方式不同但评估策略必须一致。准确率只看整体而CORA的类别分布并不均衡例如Neural_Networks类的样本远少于Machine_Learning类这时Macro-F1更有说服力。下面这个函数可以复用在三个模型上from sklearn.metrics import accuracy_score, f1_score, classification_report def evaluate(y_true, y_pred): acc accuracy_score(y_true, y_pred) macro_f1 f1_score(y_true, y_pred, averagemacro) print(fAccuracy: {acc:.4f}, Macro-F1: {macro_f1:.4f}) print(classification_report(y_true, y_pred, digits4)) return acc, macro_f1使用方式SVM直接拿svm.predict输出GCN和FNN先过torch.no_grad()再argmax得到类别索引最后用evaluate比较。三个模型接收的y_true是同一个labels数组输出的数值可以直接写进实验表格。5.2 项目运行说明里必须写清的四件事给别人的项目压缩包里如果只放源码运行不起来会非常劝退。按我的习惯运行说明至少包含四块内容。第一Python环境。项目在Python 3.8到3.11上测试过建议用conda创建独立环境不要把依赖装进全局解释器。创建命令是conda create -n cora python3.9 -y然后conda activate cora。第二依赖列表。核心依赖是numpy、pandas、scikit-learn、torch。torch的安装要区分CPU和GPU版本CPU版本安装命令是pip install torch --index-url https://download.pytorch.org/whl/cpuGPU版本需要根据你的CUDA版本到PyTorch官网选对应命令。如果没有GPU这三个模型在CPU上同样能跑完。第三数据文件位置。README里要写明cora.content和cora.cites需要放在哪个相对路径下并提供一个独立下载脚本。很多镜像站或论文主页可以直接下载项目里提供一个自动下载脚本比让用户手动找文件更稳妥。第四运行顺序。常见做法是先python preprocess.py生成npz缓存再python train_gcn.py训练并保存checkpoint然后python train_fnn.py和python train_svm.py分别训练最后python evaluate.py汇总结果。保存模型时不要只存state_dict还要把类别映射和归一化参数一并存成json否则推理阶段对不上索引。npz缓存代码很简单np.savez_compressed(cora_processed.npz, featuresfeatures, adj_normadj_norm, labelslabels, train_masktrain_mask, val_maskval_mask, test_masktest_mask)测试时直接加载npz省去每次手动解析。这个文件也是你之后尝试其他图神经网络时统一使用的数据入口。5.3 用“擦除邻接矩阵”的对照验证GCN是否真的学到了结构评估完三个模型后一个容易被跳过的验证步骤是把邻接矩阵改成单位矩阵再跑一遍GCN。这样GCN退化成FNN的表亲因为每个节点只能看到自己的特征。如果准确率没有显著下降说明你的GCN根本没在利用结构信息如果下降明显说明邻居聚合确实起了作用。这个“擦除邻接矩阵”的对照组比单纯调参更能说明问题。具体做法是在训练GCN前加一行adj_diag torch.eye(num_nodes)用它替换adj_t重新训练。你大概率会看到测试集准确率掉到和FNN接近的水平这正好印证第四章的对比结论。把这个数字记录在运行说明里也是一段很有说服力的文档。除了擦除实验建议再跑五次不同随机种子记录准确率和Macro-F1的均值与标准差。CORA数据量小单次结果波动明显五次平均才是可复现的基线。在提交结果前把三个模型的数值和标准差一起写进笔记之后做任何改动都能知道到底有没有实质提升。本文还有配套的精品资源点击获取
返回列表