简介:基于图卷积神经网络(GCN)的数据分类预测Matlab完整源码与配套数据,面向需要处理图结构化数据分类问题的研究人员、学生及工程实践者。方案将不同特征视作节点,用邻接矩阵刻画特征间相关系数后输入GCN,结构新颖,适用于社交网络、生物信息学、推荐系统等场景,也适合希望在MATLAB中快速上手深度学习模型的入门用户。压缩包共6个文件,以5个m源码文件和1个xlsx数据集为主,整体仅76KB,便于快速下载与部署,源码包含主程序、模型定义、损失计算、参数初始化及评估指标模块,逻辑清晰。已有200人学习下载,代码全程中文注释,无需改动即可替换Excel数据集运行,可直接生成分类效果图、迭代优化图和混淆矩阵图,并附测试数据集,便于对照验证结果,对新手十分友好。
1. GCN 分类到底是什么:图卷积神经网络在 Matlab 里能跑通的数据分类预测方案
我第一次认真看“GCN分类基于图卷积神经网络的数据分类预测Matlab完整源码和数据”这个标题,是想拿它处理水文数据。水文测站之间天然存在上下游和空间邻近关系,普通分类模型把每条样本当独立点,等于丢掉了一张明摆着的图。GCN 把样本间的边写进邻接矩阵,每个节点的预测由自己和邻居共同决定,这类问题就顺过来了。标题里“Matlab完整源码和数据”的本质,是提供了一个可信赖的起点:不需要自己从零推导矩阵公式,拿到数据就能跑通 train/test,再根据自己的场景改图结构。适合两类人:一类是想在 Matlab 里做数据分类预测但被图论和深度网络吓住的新手;另一类是已经会跑普通神经网络、想知道 GCN 和 MLP 到底差在哪的熟手。
2. 拆解 GCN 前向传播:邻接矩阵归一化与两层图卷积的 Matlab 实现
GCN 和普通神经网络最大的差别,是每一层多了一个“邻居聚合”步骤。用数学语言说,普通全连接层算的是 H W,GCN 算的是 A H W,其中 A 不是原始邻接矩阵,而是做过分母归一化的邻接矩阵。先把这个 A 构造对,后面的训练才有意义。很多人第一次看到 GCN 源码,最难理解的也是这一小段,所以这一章专门把它拆开讲。
2.1 邻接矩阵为什么要加自环:归一化公式与稀疏实现
GCN 里最常用的归一化是对称归一化:
A_hat = D^(-1/2) * (A + I) * D^(-1/2)
A 是原始邻接矩阵,I 是单位矩阵。加自环的目的是让节点在聚合时把自己的特征也保留下来,否则每个节点只“听”邻居的,自己的信息会被稀释。D 是 A+I 的度矩阵,也就是每个节点有多少条边(包括自环)。对称归一化会让高热度节点(度很大)的特征被压得更低,防止 hub 节点主导整个图。
在 Matlab 里实现时,最忌讳的是把 N×N 的稀疏矩阵转成稠密矩阵。如果只有几千个节点,full(A) 可能还能忍;到几万节点,一个 N×N double 矩阵就已经是 GB 级,直接内存溢出。所以从第一步开始,A 始终保持 sparse。
function A_norm = normalize_adjacency(A) % A : N x N 稀疏邻接矩阵,A(i,j)=1 表示 i 和 j 之间有边 % 返回:对称归一化后的稀疏矩阵 A_norm N = size(A, 1); A_tilde = A + speye(N); % 加自环,仍然保持稀疏 deg = sum(A_tilde, 2); % 每个节点的度 d_inv_sqrt = deg .^ (-0.5); % D^{-1/2} d_inv_sqrt(isinf(d_inv_sqrt)) = 0; % 用 find 逐条处理非零元素,避免生成稠密的 d*d' [row, col, ~] = find(A_tilde); vals = d_inv_sqrt(row) .* d_inv_sqrt(col); A_norm = sparse(row, col, vals, N, N); end这段代码里最关键的是find技巧。如果写成A_tilde .* (d_inv_sqrt * d_inv_sqrt'),d_inv_sqrt * d_inv_sqrt'会先生成一个 N×N 稠密矩阵,归一化没做先爆内存。用find取出所有边的端点,再对每条边计算两端 d 值的乘积,重新sparse,全程只保留非零边。speye(N)生成稀疏单位阵,和 A 相加也是稀疏结果,不需要额外处理。
参数上要注意deg是 N×1 列向量,d_inv_sqrt(row)得到的是和边数等长的列向量,sparse的第四个参数 N,N 会做去重合并,相同 (row,col) 位置的多个值相加。如果数据里本身有重复边,先对 A 做A = spones(A)把它变成 0/1 无向图再归一化,是更稳的做法。对于无向图,A 必须对称;如果源码里的图是有向的,入度和出度要分开算,上一层的聚合和下一层的聚合方向不同,不能照搬这个函数。
2.2 一个可复用的 gcn_layer 函数:聚合、映射、激活的参数说明
有了 A_norm,GCN 的一层就可以抽象成三件事:聚合邻居、线性映射、非线性激活。这个函数后面训练和预测都会反复用到。
function H_out = gcn_layer(A_norm, H_in, W, bias, act) % A_norm : N x N sparse,归一化邻接矩阵 % H_in : N x C 输入特征或上一层激活 % W : C x F 可训练权重 % bias : 1 x F 偏置,可省略 % act : 'relu' 或 'softmax' 或 'none' Z = A_norm * H_in * W; % 先聚合,再映射 if nargin >= 4 && ~isempty(bias) Z = Z + bias; % Matlab 自动按行广播 end switch act case 'relu' H_out = max(Z, 0); case 'softmax' eZ = exp(Z - max(Z, [], 2)); H_out = eZ ./ sum(eZ, 2); otherwise H_out = Z; end end理解这个函数的顺序很重要。我是先算A_norm * H_in再乘 W,而不是先H_in * W再乘 A。两者数学上等价,但A_norm * H_in得到的是“聚合后的邻居特征”,语义直观;如果输入维度 C 很大,先乘 W 反而能先把通道数降到 F,计算矩阵更小。实际用哪个看数据规模:特征上万、embedding 很小时,先乘 W 更快;特征几百、hidden 几百时差别不大。这里为了和反向传播代码保持一致,统一用先聚合再映射。
bias 的广播规则是:Z 是 N×F,bias 是 1×F,Matlab 会自动加到每一行,不需要 repmat。softmax 分支里Z - max(Z,[],2)是为了数值稳定,防止某些类别分数很大时 exp 溢出。对于最后一层分类,输出维度 F 必须是类别数 K;中间层一般用 relu,F 取 16 或 32。
2.3 为什么两层 GCN 是分类预测的默认起点
一层 GCN 只能看到直接邻居,两层 GCN 每个节点能看到最多两跳范围内的信息。在大多数标签传播和数据分类预测任务里,两跳信息已经足够区分节点;再往上堆,节点感受野重叠越来越严重,特征向量会慢慢趋同,这就是后面要讲的过平滑。常见做法是:输入特征 N×D -> 第一层 hidden 16,relu -> 第二层 hidden K,softmax。如果你的节点带边权重,直接把权重放进 A,聚合就变成加权平均。
这样的默认结构只有两个权重矩阵,手工推导反向传播不容易出错,用 Matlab 的dlgradient自动微分反而会因为稀疏矩阵支持问题处处受限。这也是为什么完整源码包里用手写矩阵实现很常见,下一章会看到手写训练循环,而不是调用 trainNetwork。
3. 跑通最小训练:数据划分、完整源码和 300 轮迭代里的 3 个关键参数
标题里的“完整源码和数据”对应到落地场景,就是拿到包后先跑通默认训练。这一章我把一套常见的最小脚本拆开,从 .mat 数据到训练循环,每一步都给你能直接抄的代码。
3.1 源码包里的数据长什么样:变量名、标签和 split 函数
大多数 GCN 源码包里的 .mat 文件,变量名基本是这三件套:
data = load('gcn_data.mat'); X = data.X; % N x D 特征矩阵,行是样本,列是特征 A = data.A; % N x N 稀疏邻接矩阵 y = data.y; % N x 1 标签,取值 1..K有的包里把标签做成 one-hot,有的做成 0-based 索引。分类预测建议让 y 从 1 开始,因为 Matlab 的索引从 1 开始,后面算 accuracy 时pred == y最方便。如果发现 y 里有 0,先加 1。邻接矩阵要检查对称性:isequal(A, A')为假说明是有向图,多数源码默认无向,有向时归一化要换成入度出度分别处理。
划分数据不能偷懒。直接randperm随机抽样是最常见的 baseline,但类别不平衡时,可能某个小类在训练集里一个样本都没有。我一般会用分层抽样,保证每个类别在训练、验证、测试里都出现。
function [train_idx, val_idx, test_idx] = stratified_split(y, train_ratio, val_ratio) % y : N x 1 标签 % train_ratio : 训练集比例,例如 0.1 % val_ratio : 验证集比例,例如 0.1 % 返回三个下标列向量 classes = unique(y); train_idx = []; val_idx = []; for c = classes' idx_c = find(y == c); n_c = numel(idx_c); n_train = max(1, round(n_c * train_ratio)); n_val = max(1, round(n_c * val_ratio)); idx_c = idx_c(randperm(n_c)); train_idx = [train_idx; idx_c(1:n_train)]; val_idx = [val_idx; idx_c(n_train+1:n_train+n_val)]; end test_idx = setdiff((1:numel(y))', [train_idx; val_idx]); end这个函数每次运行会重新随机,复现实验时在调用前执行一次rng(42)即可。max(1, ...)保证每个类至少有一个训练样本,但如果某个类只有 1 个样本,训练和验证同时占用它,泄漏就发生了。更合理是当类样本数少于 5 时,不放验证集,只保留在测试里并单独记录。小类是分类预测最容易翻车的地方,宁可牺牲一点平均指标也要保证评估完整。
3.2 完整训练循环:手工反向传播与 full-batch 更新的写法
这一节是核心。手写反向传播会稍微长,但好处是每一步都能在 Matlab 里打断点看维度,不用依赖深度学习工具箱,也不会有稀疏矩阵被 dlarray 拒绝的问题。
% 完整训练脚本:两层 GCN clear; clc; load('gcn_data.mat'); X = zscore(X); % 特征标准化,避免量纲差异 A = normalize_adjacency(A); % 调用 2.1 的函数 rng(42); [train_idx, val_idx, test_idx] = stratified_split(y, 0.1, 0.1); % 初始化 d = size(X, 2); K = max(y); n = size(X, 1); hidden = 16; W1 = randn(d, hidden) * sqrt(2 / d); b1 = zeros(1, hidden); W2 = randn(hidden, K) * sqrt(2 / hidden); b2 = zeros(1, K); % 超参数 lr = 0.01; wd = 5e-4; max_epoch = 300; % 训练集 one-hot Y_train = zeros(numel(train_idx), K); for i = 1:numel(train_idx) Y_train(i, y(train_idx(i))) = 1; end for epoch = 1:max_epoch % ---------- 前向 ---------- S1 = A * (X * W1) + b1; % N x hidden H1 = max(S1, 0); % ReLU S2 = A * (H1 * W2) + b2; % N x K maxS2 = max(S2, [], 2); P = exp(S2 - maxS2); P = P ./ sum(P, 2); % softmax % ---------- 损失 ---------- loss = -mean(sum(Y_train .* log(P(train_idx, :) + 1e-12), 2)); loss = loss + 0.5 * wd * (sum(W1(:).^2) + sum(W2(:).^2)); % ---------- 反向 ---------- dS2 = P; % softmax 导数 dS2(train_idx, :) = dS2(train_idx, :) - Y_train; dS2(setdiff((1:n)', train_idx), :) = 0; % 只监督训练节点 gW2 = H1' * (A * dS2) / numel(train_idx) + wd * W2; gb2 = sum(A * dS2, 1) / numel(train_idx); dH1 = (A * dS2) * W2'; dS1 = dH1 .* (S1 > 0); % ReLU 反向 gW1 = X' * (A * dS1) / numel(train_idx) + wd * W1; gb1 = sum(A * dS1, 1) / numel(train_idx); % ---------- 更新 ---------- W1 = W1 - lr * gW1; b1 = b1 - lr * gb1; W2 = W2 - lr * gW2; b2 = b2 - lr * gb2; if mod(epoch, 30) == 0 [~, pred] = max(P(test_idx, :), [], 2); acc = mean(pred == y(test_idx)); fprintf('epoch %3d | loss %.4f | test acc %.4f\n', ... epoch, loss, acc); end end反向传播里最容易写错的是A的位置。看S2 = A * (H1 * W2),它其实是先算聚合Z = A * H1,再算Z * W2。所以 W2 的梯度是Z' * dS2,也就是(A * H1)' * dS2 = H1' * (A' * dS2)。因为 A_norm 对称,A'就是A,代码里的H1' * (A * dS2)才是对的。如果漏乘 A,等于把邻居聚合这个操作在反向里丢掉了,模型会退化成一个只靠自身特征的 MLP。
dS2初始化为P,然后在训练行上减 one-hot,这是 softmax + cross-entropy 的合并梯度,比先算 log 再求导简单。非训练行在反向时清零,因为 loss 只对训练行计算。不过要注意,前向时所有节点都参与了卷积,非训练节点的特征会通过 A 反向传回训练节点,这就是 GCN 的转导学习。测试节点的标签从不参与 loss,但它们的特征仍被图卷积用到了。
参数上,hidden=16是小图基线,节点上千、特征上千时可以试 32 或 64;lr=0.01对应手写 full-batch SGD,如果换成 Adam,lr 通常降到 0.001 附近;wd=5e-4是常用 L2 惩罚,防止 W 太大导致过拟合。300 轮 full-batch 迭代在几千节点的图上只需要几秒到十几秒,如果数据上万,每轮复杂度是 O(N * d * hidden),CPU 还能扛,但超过五万节点就要考虑分块。
3.3 训练时看什么:loss、验证准确率和提前停止
只看测试准确率是新手最容易犯的错。我一般会在训练时把 val_acc 记下来,当验证集连续 30 轮不涨就停,然后用那一步的 W1、W2 去测测试集。因为 GCN 很容易在 100 轮内就把训练集拟合到 95% 以上,但验证集还在 70%,说明已经过拟合或图结构有问题。
提前停止的简单实现是记录最佳 val_acc,保存当时的权重。源码包里如果没有这个逻辑,我会自己加:
best_val_acc = 0; best_step = 0; for epoch = 1:max_epoch % ... 训练同上 ... [~, pred_val] = max(P(val_idx, :), [], 2); val_acc = mean(pred_val == y(val_idx)); if val_acc > best_val_acc best_val_acc = val_acc; best_step = epoch; W1_best = W1; W2_best = W2; end if epoch - best_step > 30 fprintf('early stop at epoch %d\n', epoch); break; end end保存的最好权重导出的测试结果,才是这个模型真实能力的估计。如果 val_acc 一直上不去,优先怀疑邻接矩阵有没有加自环、特征有没有 zscore,而不是急着加层或调 lr。
4. 调参和评估:GCN 分类预测该盯的 4 个指标与过平滑红线
模型能跑起来只是第一步,接下来要回答“效果好不好、该往哪调”。GCN 分类预测的评估和普通分类有一个关键区别:图结构会把训练节点的信息传播到测试节点,所以指标必须同时看准确率和 F1,不能只盯着 loss。
4.1 分类预测指标选哪个:CrossEntropy、Accuracy 和 Macro F1
训练阶段用交叉熵,因为它能提供连续梯度。评估阶段,准确率最直观,但类别不平衡时准确率会骗人。比如 90% 的节点属于 A 类,模型全猜 A 就有 90% 准确率,B 类一个没分对。这种场景下,Macro F1 才是更该看的指标。它把每个类别的 F1 单独算出来再平均,小类被漏掉会立刻拉低分数。
function [acc, f1_macro] = evaluate(pred, true) % pred : N x 1 预测标签 % true : N x 1 真实标签 acc = mean(pred == true); classes = unique(true); f1s = zeros(size(classes)); for i = 1:numel(classes) tp = sum((pred == classes(i)) & (true == classes(i))); fp = sum((pred == classes(i)) & (true ~= classes(i))); fn = sum((pred ~= classes(i)) & (true == classes(i))); precision = tp / (tp + fp + eps); recall = tp / (tp + fn + eps); f1s(i) = 2 * precision * recall / (precision + recall + eps); end f1_macro = mean(f1s); endeps放在分母里是为了避免某个类在预测中完全没有出现时除以零。调用时,用验证集做 early stop,用测试集做最终评估。如果 val_acc 和 test_acc 差距超过 5 个点,先怀疑划分泄漏,再怀疑过拟合。
4.2 必调的超参数区间:hidden、lr、weight decay 的搭配
不同源码包给的默认参数不一样,但 GCN 在中小规模数据上的合理范围比较固定。下面这组参数区间是我在多个图上试过之后保留下来的基线。
| 参数 | 常见区间 | 作用 | 改大/改小的影响 |
|---|---|---|---|
| hidden | 16 ~ 64 | 节点表示维度 | 太小欠拟合,太大过拟合且吃内存 |
| lr | 0.01 ~ 0.05(SGD) / 0.001 ~ 0.01(Adam) | 更新步长 | 太大会震荡,太小收敛慢 |
| weight_decay | 1e-4 ~ 5e-3 | L2 正则 | 过大会把所有权重推向 0,图结构作用被抹掉 |
| epochs | 200 ~ 500 + early stop | 拟合程度 | 更多不一定更好 |
| dropout | 0.3 ~ 0.6 | 随机丢弃节点特征 | 大规模图有效,小图上可能没用 |
调参顺序建议是先固定 hidden=16、lr=0.01、wd=5e-4 跑通一遍。loss 不降,优先降 lr;loss 降但 val_acc 不涨,优先加 dropout 或增大 wd;val_acc 涨到平台期但训练 acc 很高,再考虑加大 hidden。不要上来就网格搜索,GCN 的一个诡异特点是对 lr 很敏感,lr 一旦超过 0.1,训练 loss 会在前 50 轮剧烈震荡,而且很难恢复。
4.3 过平滑:层数越多不代表越准,怎么判断
很多人在 GCN 上第一个直觉是“多堆两层更准”,实际恰恰相反。GCN 每多一层,节点看到的信息范围更大,但到第三层以上,大部分节点的邻居集合高度重叠,表示会趋同,最终输出几乎一样,这就是过平滑。
判断过平滑的简单办法是看中间层表示的方差。如果从第一层到第三层,H 每一行的标准差越来越小,说明节点之间的区分度在消失。
H1 = max(A * (X * W1) + b1, 0); rep_std = std(H1, 0, 1); % 每个特征维度上的标准差 mean_rep_std = mean(rep_std); % 越小说明节点越趋同如果发现mean_rep_std比第一层小一个数量级,就不要继续加层了。缓解手段有三个:把自环权重调大,让节点更依赖自身;给每层加残差连接,即H = relu(A*H*W + H*V),其中 V 是另一组权重;或者干脆用两层模型,把多余层去掉。对分类预测来说,两层三层通常是性价比最高的方案。
5. GCN 分类常见问题与避坑:邻接矩阵、稀疏内存和划分泄漏的 5 个血泪记录
下面这 5 个坑,基本覆盖了复现 GCN 源码时最常见的翻车现场。每一条我都按“现象 -> 原因 -> 解决”的方式写,方便你对照排查。
5.1 邻接矩阵没加自环:loss 卡住不降
现象:训练循环能跑,但 loss 在前 50 轮只从 2.4 降到 2.2,之后几乎不动,test acc 一直在 30%~40% 徘徊。
原因:没有加自环时,每个节点的表示只由邻居决定。如果一个节点没有邻居,它在这一层的输出直接是零向量,梯度也传不回去。在稀疏图里,大量低度节点会长时间得不到有效更新。
解决:在归一化前先执行A_tilde = A + speye(N)。如果原始 A 的对角上已经有值,先A(1:N+1:end) = 0清掉再加重,避免对角线被重复计数。加自环后,每个节点至少能保留自己的特征。
5.2 特征没标准化:SGD 在 GCN 里同样会震荡
现象:loss 曲线像锯齿,20 轮一轮震荡,lr 调小后收敛极慢。
原因:GCN 前向里有A * (X * W),如果 X 的某一列是 1e3 量级,另一列是 1e-3 量级,W 的梯度会被量纲大的特征主导,同一个 lr 对两个特征不适用。
解决:训练前对 X 做 zscore。X = zscore(X)按列把均值归零、标准差归 1。对于图节点特征,如果某些特征本身是稀疏 one-hot 或计数,zscore 会不会破坏语义?一般不会,GCN 的线性层能靠 W 重新缩放。如果 zscore 后效果反而下降,再退回 min-max 归一化试试。
5.3 稀疏矩阵被转成稠密:内存翻车的排查路径
现象:代码跑到某个矩阵乘法时卡住,Matlab 报Out of memory,或者直接闪退。
原因:大概率在某个地方出现了 N×N 稠密中间变量。典型来源有三个:full(A)、diag(deg)、d_inv_sqrt * d_inv_sqrt'。diag(deg)会生成 N×N 对角阵,虽然对角阵本身稀疏,但后续一旦参与.*广播就会变稠密。
解决:检查脚本里有没有这三个模式。diag(deg)改成spdiags(deg, 0, N, N);d_inv_sqrt * d_inv_sqrt'用 2.1 节find的方式替代;初始化邻接矩阵时用sparse,不要用zeros(N)再手动赋值。几千个节点时问题不明显,到几万节点才暴露,所以一开始就养成用 sparse 的习惯最省事。
5.4 数据划分泄漏:随机打乱和分层抽样到底差在哪
现象:训练时 val_acc 很高,测试时 acc 暴跌 20 个点;或者某个类别在测试集里完全没出现。
原因:直接按文件前 80% 做训练、后 20% 做测试,数据如果按类别排过序,测试集里可能只含少数类别。另外,随机划分前没设随机种子,两次运行结果差异巨大。
解决:用 3.1 节的分层抽样函数,并在划分前rng(2025)固定种子。划分后再做一轮检查:计算训练集和测试集的类别分布比例,用histcounts(y(train_idx))和histcounts(y(test_idx))对比,如果有些类别缺失,重新调整比例或换一个种子。
5.5 索引错位:train_idx 是逻辑值还是下标值
现象:直接跑源码没问题,自己改成train_idx = rand(n,1) < 0.1后,训练报维度不匹配,或者P(train_idx, :)结果变成 N×N 矩阵。
原因:P(train_idx, :)在 train_idx 是逻辑向量时按 mask 索引,结果行数等于sum(train_idx);在 train_idx 是整数向量时按下标索引。两种方式混用,很容易把 mask 和 row index 搞混。
解决:统一用下标值。如果必须用 mask,在行首转换一次:
train_mask = rand(n, 1) < 0.1; train_idx = find(train_mask);之后所有地方都用train_idx,不要一会儿 mask 一会儿下标。另一个常见错误是Y_train的 one-hot 顺序。Y_train 的行必须和train_idx的顺序一一对应,不能用y(train_idx)直接按类别值赋值。
6. 把 GCN 迁移到自己的数据:KNN 建图与结构有效性验证
真实项目很少有现成邻接矩阵,更多情况是手里只有一张特征表。要把 GCN 用起来,第一步是建图,第二步是确认这张图真的有用。
6.1 无图数据怎么建图:KNN 与阈值化的 Matlab 实现
最常见做法是特征空间 KNN:每个样本和最近的 k 个样本连边。距离用欧氏距离,所有特征先 zscore。
function A = knn_graph(X, k) % X : N x D 标准化特征 % k : 每个样本的邻居数,经验区间 5~15 n = size(X, 1); D = pdist2(X, X); [~, idx] = sort(D, 2); idx = idx(:, 2:(k+1)); % 去掉自身 row = repmat((1:n)', 1, k); A = sparse(row(:), idx(:), 1, n, n); A = max(A, A'); % 对称化 endk 太小时图会散成多个不连通小团,梯度在孤立子图里传不动;k 太大时图接近全连接,GCN 退化成 MLP 加一层平滑。我的经验是先跑 k=10,再看度分布,如果超过一半节点度小于 3,就把 k 加到 15。
6.2 验证图结构真的有用:随机化邻接矩阵对拍
建完图,先别急着调参。我会做个反事实测试:把 A 的行列随机打乱,保持特征和标签不变,再训练一次。
rng(7); perm = randperm(n); A_random = A(perm, perm);如果随机图后的 acc 和原图 acc 差不多,甚至更高,说明模型主要靠特征,图结构没有提供有效信息。这时候要么换建图方式,要么对这个任务根本不需要 GCN,用 MLP 就够了。反过来,如果随机图后 acc 明显下降,说明边确实在传递有用信息,GCN 才值得继续调。
我现在的习惯是先跑一个不带图结构的 MLP 基线,再跑 GCN;两者差不到 2 个点,不值得为建图和维护邻接矩阵付出额外成本。数据样本之间只要有一点空间、时间或逻辑关系,GCN 就值得试;如果只是独立样本的表格分类,直接上 MLP 反而更稳。希望帮到你。
本文还有配套的精品资源,点击获取