ARTICLE DETAIL

资讯详情

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

PyTorch从零实现STGCN:交通速度预测与时空图卷积实战

PyTorch从零实现STGCN:交通速度预测与时空图卷积实战 简介「Pytorch复现STGCN基于图卷积时空神经网络在交通速度中的预测」是一份面向交通预测与深度学习方向的算法学习者、科研入门者的复现笔记围绕 IJCAI 2018 论文《Spatio-Temporal Graph Convolutional Networks》展开。内容从论文思路、摘要与引言切入梳理图卷积 GCN、时空卷积块 ST-Conv Block、门控一维卷积 GLU 实现的时间卷积、切比雪夫近似的空间图卷积以及 PeMSD7(M) 数据集上的数据处理并给出网络架构与 L2 损失下的模型输出说明适合对照源码理解 STGCN 在交通速度预测中的建模逻辑。资源为单个 pdf 文档压缩包约 408KB已有 4486 人学习下载。读者可借此理清图结构时空依赖建模的关键环节为交通流预测的复现与调参提供一条清晰的参考路径。1. 交通速度预测为什么绕不开 STGCN从时序模型到图卷积的转折在 PeMS 这类交通速度数据上很多人第一反应是上 LSTM 或者 Transformer单点预测的 MAE 看起来还能接受但把结果铺回整张路网就会发现明显掉链子。原因不复杂这些模型把每个传感器当成独立序列来处理而现实中上游路段一旦拥堵下游在几分钟内必然被拖慢这种依赖关系是写在路网拓扑里的。STGCNSpatio-Temporal Graph Convolutional Network时空图卷积网络做的事情就是把这个拓扑显式地编码进模型用图卷积分支沿着邻接矩阵聚合相邻节点的信息用门控一维卷积分支沿着时间轴提取趋势两者堆叠成时空卷积块。因为图卷积用切比雪夫多项式做谱近似、时间卷积只有一个很小的卷积核整个网络的参数量比循环结构少一个数量级训练也更快。接下来要落地的具体问题是手里有一份「时间 × 节点」的速度矩阵和一份邻接矩阵怎么在 PyTorch 里从零把这个结构复现出来邻接矩阵怎么归一化、切比雪夫阶数怎么选、训练怎么不发散、多步预测怎么组织输出。2. STGCN 的时空图卷积原理与 PyTorch 环境落地2.1 交通路网怎么变成图邻接矩阵与节点特征STGCN 的第一个前置条件是把路网抽象成一张带权无向图 G (V, E, A)。节点 V 就是检测器或传感器断面边权 A[i][j] 表示两个断面之间的空间关联强度。PeMSD7 系列数据集的常见做法是基于真实路网距离构造高斯核邻接矩阵先算出任意两个节点的路网距离 d(i, j)再按阈值 κ 截断距离超过 κ 的直接置 0避免远距离节点互相干扰阈值以内的按 exp(-d²/σ²) 赋权。这样构造出来的 A 是稀疏且对称的对角线上通常补 1表示节点自身的信息也要保留。节点特征本身很简单就是每个时间片的速度值。但组织成张量之后维度顺序很容易搞错建议一开始就把形状约定写死符号形状含义X(S, C, T_in, N)输入张量S 是样本数C 是通道数速度单变量取 1T_in 是历史步数N 是节点数Y(S, N, T_out)预测标签多步预测直接给出 T_out 个未来时间片A(N, N)原始邻接矩阵L_tilde(N, N)缩放后的拉普拉斯矩阵喂给图卷积层把形状定死之后后面所有报错基本都能通过打印.shape定位。5 分钟采样间隔下 T_in 12 对应过去 1 小时T_out 12 对应未来 1 小时这是最常被引用的配置。提示邻接矩阵一定要跟速度矩阵的节点顺序严格对齐。如果速度和距离来自两份不同来源的文件先按节点 ID 排序再构造否则模型学到的空间关系完全是错的而且指标还看不出异常。2.2 切比雪夫图卷积与门控时间卷积的原理对比STGCN 的核心是把「空间卷积」和「时间卷积」拆开交替做而不是像循环网络那样一次性建模。空间侧用的是切比雪夫多项式近似谱图卷积对归一化拉普拉斯 L 做特征值缩放把谱半径压到 [-1, 1]然后用递推式 T_k(x) 2x·T_{k-1}(x) - T_{k-2}(x) 计算 K 阶项最终输出是各阶项加权求和。K 取 3 是常见选择对应每个节点聚合三跳以内的邻居再大收益很小但计算量线性增长。时间侧用的是门控线性单元加一维卷积而不是 LSTM。一维卷积沿时间轴滑动卷积核共享到所有节点感受野由 kernel_size 决定门控部分把卷积输出按通道一分为二一半过 sigmoid 当门另一半与之逐元素相乘这样模型能自主决定哪些时间片的信息该保留。两者对比可以列成一张表建模方式空间依赖时间依赖参数量级主要问题LSTM无各节点独立循环结构大忽略路网拓扑长序列训练慢CNN 全连接隐式靠全连接学一维卷积中大参数量随节点数平方增长图卷积 门控时间卷积显式由 A 决定一维卷积小依赖 A 的质量A 错了效果直接崩一个完整的时空卷积块按「时间卷积 → 图卷积 → 时间卷积」的顺序排列中间的图卷积把时间步并入 batch 维单独处理节点维度两侧的时间卷积负责压缩和恢复时间维度块外用层归一化和残差连接兜住梯度。2.3 conda 安装 PyTorch 与环境自检环境这一步踩坑的人最多尤其是 GPU 版本对不上。先在命令行确认驱动和 CUDA 运行时版本再决定装哪个 wheel# 1. 查看驱动支持的最高 CUDA 版本右上角 CUDA Version 是上限 nvidia-smi # 2. 建一个独立环境避免和已有的 tensorflow / comfyui 环境互相污染 conda create -n stgcn python3.10 -y conda activate stgcn # 3. 按需选择GPU 版走官方源CPU 版直接装默认 wheel pip install torch --index-url https://download.pytorch.org/whl/cu121 pip install numpy pandas scipy scikit-learn matplotlib tqdm # 4. 自检三项都要打印 True / 版本号缺一项说明装错了 python -c import torch; print(torch.__version__, torch.cuda.is_available(), torch.cuda.device_count())参数说明python3.10是为了跟主流 wheel 对齐太新的解释器有时没有预编译包cu121这类后缀必须小于等于nvidia-smi显示的 CUDA Version装高了会在第一次.to(cuda)时报错。Windows 上如果遇到OSError: [WinError 1114] 动态链接库(DLL)初始化例程失败 ... c10.dll绝大多数情况是缺 Visual C 运行库或者 CPU 版和 GPU 版混装过处理方式是卸干净重装并补上 VC Redistributable而不是去改 PATH。2.4 数据集切分、滑动窗口与 z-score 归一化原始速度矩阵是 (T_total, N)需要切成监督学习样本。滑动窗口写法固定注意归一化统计量只能用训练集算import numpy as np def zscore_fit(data): 只用训练段拟合统计量避免未来信息泄漏 mu data.mean(axis0, keepdimsTrue) sigma data.std(axis0, keepdimsTrue) 1e-6 return mu, sigma def make_windows(data, T_in12, T_out12): data: (T_total, N) - X:(S,1,T_in,N) Y:(S,N,T_out) X, Y [], [] for t in range(len(data) - T_in - T_out 1): X.append(data[t:t T_in]) # (T_in, N) Y.append(data[t T_in:t T_in T_out].T) # (N, T_out) X np.asarray(X)[:, None, :, :] # 增加通道维 return X.astype(np.float32), np.asarray(Y).astype(np.float32) # 按时间顺序 6:2:2 切分绝对不能 shuffle 后再切 n len(speed) train_raw speed[:int(n * 0.6)] mu, sigma zscore_fit(train_raw) speed_norm (speed - mu) / sigma X, Y make_windows(speed_norm) Xtr, Ytr X[:int(len(X) * 0.6)], Y[:int(len(Y) * 0.6)]逻辑说明zscore_fit在每个节点维度上独立求均值和标准差因为不同断面的速度量纲和分布差异很大全局归一化会让慢速路段被淹没。切分必须按时间顺序任何随机打乱都会让相邻时间片跨集合验证指标虚高。反归一化时对预测结果执行pred * sigma mu注意 sigma 的形状是 (1, N)要对齐到标签的 (S, N, T_out) 上。3. 用 PyTorch 手写 STGCN 核心模块图卷积块与时空块3.1 邻接矩阵的对称归一化与拉普拉斯构造图卷积能不能训起来八成取决于拉普拉斯构造得对不对。标准流程是先对称归一化 A再算 L I - D^(-1/2) A D^(-1/2)最后把特征值缩放到 [-1, 1] 供切比雪夫递推使用。如果不做缩放高阶项会随着阶数指数放大训练几个 batch 就出现 NaN。import numpy as np def scaled_laplacian(A): 对称归一化 特征值缩放返回 L_tilde 供切比雪夫递推 N A.shape[0] d A.sum(axis1) d_inv_sqrt np.zeros_like(d, dtypenp.float64) nz d 0 d_inv_sqrt[nz] np.power(d[nz], -0.5) # 孤立节点度数为 0单独置 0 L np.eye(N) - d_inv_sqrt[:, None] * A * d_inv_sqrt[None, :] lambda_max np.linalg.eigvalsh(L).max() # 对称矩阵用 eigvalsh比 eig 快且无虚部 return (2.0 / lambda_max) * L - np.eye(N)参数说明d_inv_sqrt里必须做d 0判断PeMS 里偶尔有孤立节点np.power(0, -0.5)会直接产出 inf然后整个矩阵被污染。lambda_max用eigvalsh求因为 L 是对称实矩阵虚部恒为 0用eig反而引入浮点误差。N 228 时求一次特征值在秒级N 1026 时建议缓存成.npy文件复用别每次启动都重算。3.2 切比雪夫图卷积层 ChebConv 的 PyTorch 实现图卷积层只需要保存 K 组权重前向时先递推生成各阶项再逐阶加权求和。写成 einsum 比循环 matmul 更清晰import torch import torch.nn as nn class ChebConv(nn.Module): K 阶切比雪夫图卷积out sum_k T_k(L_tilde) X W_k def __init__(self, in_channels, out_channels, K): super().__init__() self.K K self.weight nn.Parameter(torch.empty(K, in_channels, out_channels)) nn.init.xavier_uniform_(self.weight) # 初始量级过大会让 LayerNorm 前激活爆炸 self.bias nn.Parameter(torch.zeros(out_channels)) def forward(self, x, L_tilde): # x: (B, N, C_in) L_tilde: (N, N) T [x, torch.einsum(nm,bmc-bnc, L_tilde, x)] for k in range(2, self.K): T.append(2 * torch.einsum(nm,bmc-bnc, L_tilde, T[-1]) - T[-2]) out sum(torch.einsum(bnc,kco-bno, T[k], self.weight[k]) for k in range(self.K)) return out self.bias逻辑说明T列表按递推式逐阶生成第 0 阶是自身特征第 1 阶是邻居聚合第 k 阶表示 k 跳邻域。einsum(nm,bmc-bnc)把 (N, N) 的传播矩阵作用在 batch 里每个样本的节点维度上这一步就是图卷积区别于普通全连接的实质。K 一般取 3调到 4 以上时参数量线性增长而指标改善往往在 1% 以内性价比很低。3.3 门控时间卷积 GLU 与时空卷积块 STConvBlock时间卷积沿 T 轴滑动节点维当作共享权重的空间维这样参数量与节点数无关。门控部分用 chunk 把输出切成两半class TemporalConv(nn.Module): def __init__(self, in_channels, out_channels, kernel_size3): super().__init__() self.kernel_size kernel_size self.conv nn.Conv2d(in_channels, 2 * out_channels, (kernel_size, 1)) def forward(self, x): # x: (B, C_in, T, N) x nn.functional.pad(x, (0, 0, self.kernel_size - 1, 0)) # 只在时间轴左侧补零 x self.conv(x) p, q torch.chunk(x, 2, dim1) # 一半当信息一半当门 return p * torch.sigmoid(q)参数说明padding的顺序是 (左, 右, 上, 下)对应最后两个维度 (N, T)所以(0, 0, K-1, 0)表示只沿时间轴左侧补 K-1 个零保证当前时刻看不到未来这一点在预测任务里是硬要求。2 * out_channels的通道翻倍是为了给门控留位置实际输出通道数仍是out_channels。把三种层拼起来就是时空卷积块注意图卷积前要把时间步并入 batch 维class STConvBlock(nn.Module): def __init__(self, in_channels, hidden, out_channels, K, kernel_size3, dropout0.5): super().__init__() self.temporal1 TemporalConv(in_channels, hidden, kernel_size) self.graph ChebConv(hidden, hidden, K) self.temporal2 TemporalConv(hidden, out_channels, kernel_size) self.norm nn.LayerNorm(out_channels) self.dropout nn.Dropout(dropout) self.residual (nn.Conv2d(in_channels, out_channels, 1) if in_channels ! out_channels else nn.Identity()) def forward(self, x, L_tilde): h self.temporal1(x) # (B, hidden, T, N) B, C, T, N h.shape h h.permute(0, 2, 3, 1).reshape(B * T, N, C) # 时间步并入 batch h self.graph(h, L_tilde) # (B*T, N, hidden) h h.reshape(B, T, N, -1).permute(0, 3, 1, 2) # 还原成 (B, hidden, T, N) h self.temporal2(h) h h.permute(0, 2, 3, 1) res self.residual(x).permute(0, 2, 3, 1) return self.dropout(self.norm(h res)).permute(0, 3, 1, 2)permute reshape的两步必须成对出现前面的 permute 把通道挪到最后后面的 permute 负责还原中间如果少了.contiguous()某些 PyTorch 版本上 reshape 会直接抛错。层级输入形状输出形状作用TemporalConv 1(B, C_in, T, N)(B, hidden, T, N)时间维升维、压缩感受野ChebConv(B*T, N, hidden)(B*T, N, hidden)按 A 聚合空间邻居TemporalConv 2(B, hidden, T, N)(B, C_out, T, N)时间维降回目标通道LayerNorm 残差(B, T, N, C_out)同上稳住深层的激活量级3.4 堆叠输出层与整体模型组装输出层做两件事先用时间卷积把通道降下来再用一个线性层沿时间轴把 T_in 压成 T_out。这样多步预测变成一次前向不需要自回归迭代。class OutputBlock(nn.Module): def __init__(self, in_channels, T_in, T_out): super().__init__() self.temporal TemporalConv(in_channels, 1, kernel_size3) self.fc nn.Linear(T_in, T_out) def forward(self, x): # x: (B, C_in, T_in, N) - (B, N, T_out) h self.temporal(x).permute(0, 3, 1, 2) # (B, N, 1, T_in) return self.fc(h).squeeze(2) # (B, N, T_out) class STGCN(nn.Module): def __init__(self, num_nodes, T_in12, T_out12, K3, channels(64, 64, 64)): super().__init__() c1, c2, c3 channels self.block1 STConvBlock(1, c1, c1, K) self.block2 STConvBlock(c1, c2, c2, K) self.block3 STConvBlock(c2, c3, c3, K) self.out OutputBlock(c3, T_in, T_out) def forward(self, x, L_tilde): for blk in (self.block1, self.block2, self.block3): x blk(x, L_tilde) return self.out(x)参数说明channels(64, 64, 64)是 STGCN 里最常被沿用的三层配置N 228 时显存占用大约在 23 GB节点数涨到 1026 时第一层通道建议降到 32否则中间张量 (B, 64, 12, 1026) 会把显存吃满。K3对应三跳邻域改动前先确认邻接矩阵的稀疏度如果图本身很稠密减小 K 比调通道数更有效。4. PeMS 数据上的训练循环、关键参数与排错4.1 损失函数、Adam 优化器与学习率衰减回归任务直接用均方误差配 Adam 加阶梯衰减就够了不需要复杂技巧。关键是梯度裁剪要有因为切比雪夫高阶项在训练初期偶尔会给出比较大的梯度。import torch import torch.nn as nn device torch.device(cuda if torch.cuda.is_available() else cpu) model STGCN(num_nodesA.shape[0]).to(device) L_tilde_t torch.tensor(scaled_laplacian(A), dtypetorch.float32, devicedevice) optimizer torch.optim.Adam(model.parameters(), lr1e-3) scheduler torch.optim.lr_scheduler.StepLR(optimizer, step_size5, gamma0.7) criterion nn.MSELoss() for epoch in range(50): model.train() total 0.0 for xb, yb in train_loader: xb, yb xb.to(device), yb.to(device) optimizer.zero_grad() loss criterion(model(xb, L_tilde_t), yb) loss.backward() torch.nn.utils.clip_grad_norm_(model.parameters(), 5.0) # 防高阶项梯度尖峰 optimizer.step() total loss.item() * xb.size(0) scheduler.step() print(fepoch {epoch:02d} lr{optimizer.param_groups[0][lr]:.2e} loss{total/len(train_set):.5f})参数说明lr1e-3配合step_size5, gamma0.7是常见配置50 轮内学习率衰减到 1e-3 × 0.7^10 ≈ 2.8e-5。如果验证损失在第 10 轮就开始上升先减小初始学习率到 5e-4而不是急着加正则。clip_grad_norm_的阈值 5.0 对这批数据够用设成 1.0 会明显拖慢收敛。超参数建议值调整方向影响T_in126 / 24历史窗口太短抓不到周期太长涨显存T_out12与评估粒度对齐越长误差累积越明显batch_size3264显存允许就加大过小会让 LayerNorm 统计量抖动K324阶数过高收益递减dropout0.50.30.5过拟合明显时上调lr1e-35e-41e-3出现 NaN 优先下调4.2 输入张量形状与 batch 组织方式DataLoader 出来的张量形状必须和模型约定一致形状错位是新手最常见的卡点。把 numpy 数组转成 TensorDataset 时顺手确认一次from torch.utils.data import TensorDataset, DataLoader train_set TensorDataset(torch.from_numpy(Xtr), torch.from_numpy(Ytr)) train_loader DataLoader(train_set, batch_size64, shuffleTrue, num_workers2, pin_memoryTrue) xb, yb next(iter(train_loader)) print(xb.shape, yb.shape) # 期望 torch.Size([64, 1, 12, N]) torch.Size([64, N, 12]) pred model(xb.to(device), L_tilde_t) print(pred.shape) # 期望 torch.Size([64, N, 12])逻辑说明训练集可以 shuffle但时间上相邻的样本被分到不同 batch 是允许的因为每个样本自身已经包含了完整的历史窗口不存在标签泄漏。pin_memoryTrue只在 GPU 训练时开CPU 训练反而拖慢。如果打印出来的 xb 形状是[64, 12, N]说明make_windows里漏了增加通道维的那一步直接补[:, None, :, :]。4.3 CUDA、显存与 DLL 初始化失败的排查报错按类型分成三类处理最省时间。第一类是环境类torch.cuda.is_available()返回 False或者加载c10.dll失败属于安装问题回退到 2.3 节重装并确认 VC 运行库齐全不要试图用降级 numpy 之类的偏方绕过。第二类是显存类典型信息是CUDA out of memory处理顺序是先降 batch_size再降第一层通道数最后考虑把 L_tilde 从 float32 转成稀疏张量。第三类是数值类loss 在几个 batch 内变成 NaN检查三处拉普拉斯有没有 inf、学习率是否过大、输入有没有未处理的缺失值。缺失值这块值得单独说。原始 PeMS 数据里存在速度为 0 或空白的时间片如果不处理归一化后会出现极端负值几个样本就能把梯度带偏。常见做法是用前后时间片的线性插值补齐再统一做 z-score并在训练时对原始值为 0 的位置计算掩码损失避免模型去拟合无效点。注意clip_grad_norm_只能缓解梯度尖峰治不了根本的数值问题。如果加了裁剪还是 NaN一定是某个张量里存在 inf 或 nan用torch.isnan(x).any()逐层打点定位比盲调学习率快得多。4.4 MAE/RMSE/MAPE 指标与预测曲线怎么读三个指标各有盲区必须一起看。MAE 反映平均绝对偏差单位就是速度单位RMSE 对大误差更敏感通常比 MAE 明显大两者差距过大说明存在个别离谱的预测点MAPE 是百分比但速度接近 0 时会爆炸必须加掩码。def masked_mape(pred, true, eps1.0): eps 单位是 km/h低于该阈值的真实值不参与 MAPE 计算 mask true eps if mask.sum() 0: return torch.tensor(0.0) return (torch.abs((pred[mask] - true[mask]) / true[mask])).mean() * 100 def evaluate(model, loader, mu, sigma): model.eval() preds, trues [], [] with torch.no_grad(): for xb, yb in loader: out model(xb.to(device), L_tilde_t).cpu() # 反归一化后再算指标否则数值没有物理意义 preds.append(out * torch.tensor(sigma) torch.tensor(mu)) trues.append(yb * torch.tensor(sigma) torch.tensor(mu)) p, t torch.cat(preds), torch.cat(trues) mae torch.abs(p - t).mean() rmse torch.sqrt(((p - t) ** 2).mean()) print(fMAE{mae:.2f} RMSE{rmse:.2f} MAPE{masked_mape(p, t):.2f}%)参数说明eps1.0是为了过滤掉接近静止的路段不设阈值时 MAPE 动辄上百。反归一化必须用测试集之外拟合的 mu / sigma但评估时的反归一化用的还是训练集统计量这一点容易写错。除了看数字建议按预测步长分别画 15 / 30 / 45 / 60 分钟的误差曲线通常会看到前 30 分钟误差缓慢上升超过 45 分钟后陡增这个拐点决定了 T_out 该设多大。5. 自适应邻接矩阵与多步预测把 STGCN 调到能用的技巧5.1 让图结构自己学自适应邻接矩阵基于距离的邻接矩阵有个天然缺陷它只表示地理邻近而两个相距很远的路段完全可能因为通勤流向而强相关。自适应图卷积的思路是给每个节点学一个低维嵌入用嵌入内积生成关联强度让 A 跟着数据一起更新。class AdaptiveAdj(nn.Module): def __init__(self, num_nodes, emb_dim10): super().__init__() self.E1 nn.Parameter(torch.randn(num_nodes, emb_dim) * 0.1) self.E2 nn.Parameter(torch.randn(num_nodes, emb_dim) * 0.1) def forward(self): # ReLU 截掉负关联softmax 逐行归一化后可直接当传播矩阵用 return torch.softmax(torch.relu(self.E1 self.E2.T), dim1)参数说明emb_dim10是常用取值节点数越大可以适当上调到 16但超过 32 后容易过拟合。得到的矩阵已经是行随机矩阵可以直接和物理邻接矩阵做加权融合A_final alpha * A_phys (1 - alpha) * A_adpalpha 取 0.5 是个不错的起点。这里有个性能坑必须提醒如果每次前向都对A_final做特征分解算lambda_max训练会慢到无法接受。工程上的做法是放弃严格的特征值缩放直接对A_final I做对称归一化得到传播矩阵用它替代L_tilde参与前向计算精度损失通常在可接受范围内但训练速度能提升一个数量级。融合权重 alpha 和嵌入维度都建议单独做一轮网格这两个参数对结果的影响比 K 的取值大得多。5.2 多步预测的输出组织与误差累积对照多步预测有两种实现路线一种是像 3.4 节那样用线性层一次性输出 T_out 步另一种是自回归地每次预测一步再喂回去。前者训练时误差不会累积但长程依赖靠的是最后一层线性映射后者理论上能建模更长的依赖实际却会因为单步误差被反复放大。输出方式训练方式60 分钟误差显存适用场景单次线性输出端到端较低低固定预测步长T_out ≤ 12自回归迭代逐步或端到端明显偏高高需要任意长度预测多任务输出头端到端最低中同时要 15/30/45/60 分钟结果第三种做法值得优先尝试在输出层挂 T_out 个独立的线性头每个头负责一个预测步长损失函数里给远期步长更高的权重比如 15 分钟权重 1.0、60 分钟权重 1.5。这样模型不会为了照顾近期精度而牺牲远期表现训练时只在最后几层做加法代价很小。5.3 消融对照与冻结微调评估一个改动是否真的有效必须做消融对照否则很容易把随机波动当成改进。固定随机种子跑三组配置只有物理邻接矩阵、只有自适应矩阵、两者融合其他超参数完全不动对比验证集 MAE。常见结果是融合版本比单用物理邻接矩阵低 3%8%而单用自适应矩阵在训练集上表现最好验证集上却不如物理版本这就是过拟合的典型信号此时应该提高 alpha 的取值。另一个能省大量时间的技巧是冻结微调。当节点数从 228 扩到 1026重新训练太贵可以复用已训练模型的时间卷积分支和输出层只重新初始化图卷积层和自适应嵌入用较小的学习率 3e-4 训练 10 轮左右让空间部分适应新图的规模。冻结哪一层用param.requires_grad False控制但一定要把ChebConv里的 weight 和AdaptiveAdj的 E1、E2 保持可训练否则整个模型等于只学了个时间卷积空间信息全丢。判断微调是否起效的方法很简单对比微调前后的验证集 MAE如果 10 轮内没有下降 5% 以上直接换回全量训练别在冻结策略上继续耗。本文还有配套的精品资源点击获取
返回列表