简介:这套基于PyTorch的STGCN时空图卷积网络实现代码,面向人体行为分析、智能监控等场景的深度学习研究者与学生,用于解决骨骼序列数据的时空特征联合建模问题。资源包共12个文件,以Python源码、Markdown文档、ZIP压缩包及备份文件为主,整体大小约28.71MB,其中源码涵盖模型构建、训练主程序与数据处理工具函数,文档提供说明与数据格式介绍。已有122人学习,适合从入门到进阶的开发者参考。代码源自IJCAI 2018论文官方实现,完整包含图卷积空间特征提取、时序卷积动态建模及空间-时间混合模块的流水线,并配有归一化、坐标转换等预处理与交叉熵损失训练流程。通过研读该实现,既可深入理解时空图卷积的理论基础,也能掌握模块化PyTorch项目的组织方式与调参思路,为后续改进如引入注意力机制或融合LSTM提供良好起点。
1. STGCN到底是什么:一次把时空图卷积网络讲清楚
交通流量预测这类任务,传感器散落在路网上,每个测点既受自己历史数值影响,也受相邻路段影响。很多人一开始用 LSTM 或 TCN 做,跑完发现时间维建模没问题,空间关联基本没用上——相邻路口的信息不会自己钻进时间序列里。这正是基于 PyTorch 的 STGCN 时空图卷积网络实现代码要处理的问题:把路网看成图,节点是传感器,边是道路连接,用图卷积做空间依赖建模,用门控时间卷积做时间依赖建模。这份代码从邻接矩阵构造、切比雪夫图卷积到训练避坑全流程覆盖,适合做交通流、能耗、工业时序预测的人当作 pytorch 实战模板来复现。新手照着调通模型,熟手可以直接替换成自己的节点数据和邻接矩阵。
2. 拆解STGCN核心组件:图卷积、门控时间卷积与ST-Conv块
2.1 图卷积:为什么用切比雪夫近似而不是普通GCN
普通卷积只能在欧氏网格上滑动,传感器网络是典型的非欧结构,节点邻居数不固定,没法套固定的卷积核。图卷积的原始定义在谱域:对拉普拉斯矩阵 L 做特征分解 L = UΛUᵀ,用 U 把图信号变换到谱域,乘以可学习的频率响应 g_θ(Λ),再逆变换回来。但这一步需要特征分解,代价高且对大规模图不稳定,所以实际实现都用切比雪夫多项式去近似 g_θ(Λ)。定义归一化拉普拉斯 L~ = 2L/λ_max − I,把特征值压到 [−1, 1],然后递推生成多项式序列:
T₀(L~) = I,T₁(L~) = L~,T_k(L~) = 2L~T_{k−1}(L~) − T_{k−2}(L~)
图卷积输出就是 K 个多项式项的加权和:
Z = Σ_{k=0}^{K−1} T_k(L~) X W_k
这里的 W_k 是每个多项式项独立的线性变换矩阵,也是图卷积层里唯一需要学习的参数。K 是图卷积的感受野半径,K=2 只聚合一阶邻居,K=3 会把二阶邻居也纳入计算。STGCN 里一般取 K=3,因为超过三阶对交通流数据边际收益很低,参数量却线性增长,还容易过平滑——这是图网络特有的现象,层数越深节点的表示越趋同。
顺带说一个和直觉相反的点:切比雪夫多项式虽然是预处理阶段的固定值,但它在训练中不参与梯度计算,图卷积层里只有 W_k 是 nn.Parameter,多项式矩阵只是乘在信号上的常数。这意味着一开始算错一个符号,整个训练看起来可能还是会收敛,只是精度上不去,排查时最容易被忽略。
2.2 门控时间卷积:GLU 如何避免 LSTM 的串行瓶颈
时间维度的处理,STGCN 没有用 GRU 或 LSTM,原因很实际:循环网络按时间步串行,12 步输入就要 12 次递归展开,训练慢,梯度也容易衰减。STGCN 用的是因果膨胀卷积加门控线性单元(GLU)。因果卷积保证输出 t 时刻的状态只依赖 t 及之前的输入,预测时不会偷看到未来信息。
GLU 的做法是对同一份输入跑两个并行的卷积,一个主卷积 P,一个门卷积 G,然后做逐元素相乘:
x_out = P ⊙ σ(G)
σ 是 Sigmoid,相当于给主路径的每个通道输出打一个 0 到 1 的保留权重,网络可以学到什么时刻的信息该放过去、什么时刻需要抑制。这个结构对交通流的突发变化响应很快,不像 LSTM 需要一步步通过门控循环传递。
实现时时间卷积是针对每个节点独立做的,所以用nn.Conv2d,卷积核形状是 (1, kernel_size),kernel 宽度是时间感受野。STGCN 默认取 3,配合 padding 保证输出序列长度不变。我一般会写成两个并行的Conv2d来实现 GLU,而不是用一个卷积加拆分,这样参数上更容易对齐输入输出通道,代码也更直观。
2.3 ST-Conv块:时间、空间、时间的交替顺序
STGCN 的基本单元叫 ST-Conv 块,顺序是“时间卷积 → 空间图卷积 → 时间卷积”。为什么不是空间先做?直观解释是:图卷积聚合的是相邻节点信息,如果先做空间卷积,当前时间步每个节点都会带上邻居的数值,再接时间卷积时,时间维上混入的空间信息会被再一次放大,噪声更容易扩散。先做时间卷积相当于先把每个节点自己的时间特征提纯,再做空间聚合,最后再做一次时间卷积融合和通道映射,稳定性明显好。
一个完整 STGCN 会堆两个 ST-Conv 块,再接输出块。输出块由一层时间卷积加一个跨时间维全连接组成,把时间压缩到预测步数。下面这张表是我常用的结构:
| 模块 | 输入形状 | 输出形状 | 主要作用 |
|---|---|---|---|
| 输入 | (B, 1, N, 12) | - | 单变量流量 |
| ST块1 | (B, 1, N, 12) | (B, 64, N, 12) | 时间提纯 + 空间聚合 |
| ST块2 | (B, 64, N, 12) | (B, 64, N, 12) | 高层时空特征 |
| 输出块 | (B, 64, N, 12) | (B, H, N, 1) | 压缩时间维,映射多步 |
B 是 batch size,N 是传感器数量,H 是预测步数。时间维在块内保持不变,直到输出块才被压缩,好处是堆叠多个 ST 块时不用反复调整时间长度。每个 ST 块里的残差连接和 BatchNorm 让两层堆叠后的梯度传播依然顺畅,这也是为什么 64 通道配置下模型训起来比同等深度的纯 RNN 结构稳定得多。
3. 准备图数据:邻接矩阵与输入张量的关键细节
3.1 邻接矩阵构造:距离阈值与高斯核
STGCN 的输入除了时间序列,最重要的是图结构。最常见的构造方式是从传感器经纬度或路网距离计算距离矩阵 dist,再用阈值截断和高斯核加权生成邻接矩阵 A:
A_ij = exp(−(dist_ij² / σ²)),当 dist_ij ≤ threshold;否则 0
threshold 通常取距离分布的某个百分位,比如所有配对距离的中位数或 80% 分位数。阈值太小会让图碎片化,很多节点没有邻居,图卷积退化成纯时间模型;阈值太大则图过密,矩阵乘变慢,每个节点都收到冗余信息。σ 是距离尺度参数,一般取所有非零距离的中位数,σ 越小,远处的边衰减越严重,模型会非常依赖近邻。
构造完 A 后要检查两个点:对角线是否为 1(需要加自环),以及是否有整行全零的孤立节点。孤立节点会让图卷积输出变成零向量,该节点预测永远是基准值,在代码里表现就是该节点 loss 奇高。我通常会写一个检查函数assert (A.sum(1) > 0).all(),跑数据前先过一遍。
3.2 拉普拉斯归一化与切比雪夫预处理
拿到 A 之后不能直接塞给图卷积,要先做对称归一化。记度矩阵 D = diag(sum(A, axis=1)),归一化拉普拉斯为:
L_sym = I − D^(−1/2) A D^(−1/2)
然后取最大特征值 λ_max,把拉普拉斯缩放到 [−1, 1],再递推生成 K 阶切比雪夫多项式。下面这段是数据预处理的完整代码,源码包里命名为preprocess_adj.py:
import numpy as np def scaled_laplacian(A): # A: 带自环的邻接矩阵, 对称 d = np.sum(A, axis=1) d_inv_sqrt = np.power(d, -0.5) d_inv_sqrt[np.isinf(d_inv_sqrt)] = 0 mat_d_inv_sqrt = np.diag(d_inv_sqrt) L = np.eye(A.shape[0]) - mat_d_inv_sqrt @ A @ mat_d_inv_sqrt return L def cheb_polynomials(L, K): N = L.shape[0] L_scaled = L / np.max(np.linalg.eigvals(L)) * 2 - np.eye(N) T_list = [np.eye(N), L_scaled.copy()] for k in range(2, K): T_list.append(2 * L_scaled @ T_list[-1] - T_list[-2]) return T_list两段代码的逻辑说明:scaled_laplacian先按对称归一化公式计算 L,d_inv_sqrt对度取 −0.5 次幂,孤立节点的度为 0,把 inf 替换成 0 避免出现 NaN。cheb_polynomials里第一行算出缩放后的拉普拉斯,最大特征值用np.linalg.eigvals求,节点数不超过几千时这个特征分解开销可以接受;随后按递推公式生成 K 个多项式矩阵,返回的 list 里每个元素形状都是 [N, N]。
参数说明:K 对应图卷积的阶数,和模型里的 spatial K 必须一致,我常用的值是 3;L_scaled 的缩放系数2/λ_max是最关键的数,做错的话特征值范围不对,切比雪夫递推会发散成很大的数值。预处理时建议保存一份未缩放的归一化拉普拉斯,方便后续排查。
3.3 滑动窗口与训练/验证/测试切分
模型输入需要组织成四维张量 (B, C_in, N, T),C_in 在单变量交通流里是 1,N 是节点数,T 是历史窗口长度。生成样本时用滑动窗口:给定序列长度为 M,窗口为 T,预测步数为 H,从第 m 个时刻取样 X[m : m+T] 作为输入,Y[m+T : m+T+H] 作为标签,m 从 0 滑到 M−T−H。
这里有个容易被忽略的细节:切分数据集必须按时间顺序,不能随机打乱。随机打乱会把未来样本混进训练集,验证集效果虚高,上线就翻车。我采用的比例是 7:2:1,训练集在前 70%,验证集和测试集依次跟着时间轴往后排。
标准化也要小心。正确的做法是先用训练集的均值 μ 和标准差 σ 去归一化所有数据,验证集和测试集都用同一组统计量,而不是每个集合单独算。单独算的后果是模型在测试时看到的数值分布和训练时不一致,尤其交通流数据有明显日夜周期,单独归一化会把周期信号扭曲掉。保存 μ 和 σ 到文件里,推理时还要用来反归一化预测结果。
注意:不要在样本生成后再对整个数组做 StandardScaler.fit,那样验证集统计量已经被训练段和测试段的混合数据污染。
4. 用PyTorch实现STGCN:核心代码与参数说明
4.1 源码包结构与输入输出约定
源码包我习惯按职责拆成四个文件,拿到后可以直接跑通。下面是文件清单:
| 文件 | 职责 | 关键接口 |
|---|---|---|
| model.py | STGCN 模型定义 | STGCN(n_nodes, c_in, c_hid, K, seq_len, horizon) |
| preprocess_adj.py | 邻接矩阵与切比雪夫多项式 | cheb_polynomials(A, K) |
| data_utils.py | 滑动窗口与标准化 | make_dataset(seq, T, H) |
| train.py | 训练、验证、模型保存 | 命令行入口 |
所有坐标统一成 (B, C, N, T):B 是 batch,C 是通道数,N 是传感器节点数,T 是时间步。这个约定直接影响后续每一层输出形状的判断,代码里所有注释都按这个顺序写,避免维度混乱。
4.2 图卷积层:切比雪夫项如何参与计算
图卷积层接收两个输入:上一层输出的四维张量 x,以及预计算好的切比雪夫多项式列表。核心逻辑是把时间和通道视作可合并的维度,对每个时间步分别做图信号变换:
import torch import torch.nn as nn import torch.nn.functional as F class SpatialGraphConv(nn.Module): def __init__(self, c_in, c_out, K): super().__init__() self.K = K # 每个切比雪夫阶数对应一个独立的线性映射 self.weight = nn.Parameter(torch.randn(K, c_in, c_out)) self.bias = nn.Parameter(torch.zeros(c_out)) nn.init.xavier_uniform_(self.weight) nn.init.zeros_(self.bias) def forward(self, x, cheb_polynomials): # x: (B, C_in, N, T) B, C_in, N, T = x.shape # 转成 (B*T, N, C_in),一次处理所有时间步 x = x.permute(0, 3, 2, 1).reshape(B * T, N, C_in) out = torch.zeros(B * T, N, self.weight.shape[-1], device=x.device, dtype=x.dtype) for k in range(self.K): # 多项式作用: (N,N) x (B*T,N,C_in) -> (B*T,N,C_in) cheb_k = torch.as_tensor(cheb_polynomials[k], dtype=x.dtype, device=x.device) h = torch.einsum('ab,cbd->cad', cheb_k, x) out = out + h @ self.weight[k] out = out.reshape(B, T, N, -1).permute(0, 3, 2, 1).contiguous() return out + self.bias.view(1, -1, 1, 1)逻辑说明:循环内torch.einsum('ab,cbd->cad', ...)完成切比雪夫矩阵和节点特征的批量矩阵乘,第一个维度 c 是 BT,它把不同时间步当作独立样本处理,效率比按时间步 for 循环高很多。h @ self.weight[k]完成该阶数下的通道线性变换,cheb_k在层内统一转成和输入同 dtype 的张量,避免 numpy 数组混用报错。最后一行的 reshape 把 (BT, N, C_out) 恢复成 (B, C_out, N, T),保证下游时间卷积拿到的还是四维张量。
参数说明:K和预处理时生成的多项式数量必须一致,默认 3;c_in是输入通道,单变量预测是 1,多变量如果有多个传感器指标可以设成多通道。weight初始化用 xavier_uniform_,对线性层比较稳妥;不建议用全零初始化,否则梯度更新对称破缺很慢。
4.3 门控时间卷积层与ST-Conv块
时间卷积用两个并行Conv2d表示 GLU,卷积核只覆盖时间维,节点维保持不变。ST-Conv 块按时-空-时顺序组合,最后接 BatchNorm 和残差:
class TemporalConv(nn.Module): def __init__(self, c_in, c_out, kernel_size=3): super().__init__() # kernel_size 需为奇数, 这里保证输出时间长度不变 self.conv = nn.Conv2d(c_in, c_out, (1, kernel_size), padding=(0, kernel_size // 2)) self.gate = nn.Conv2d(c_in, c_out, (1, kernel_size), padding=(0, kernel_size // 2)) def forward(self, x): # x: (B, C_in, N, T) p = self.conv(x) g = torch.sigmoid(self.gate(x)) return p * g class STConvBlock(nn.Module): def __init__(self, c_in, c_out, K, kernel_size=3): super().__init__() self.tcn1 = TemporalConv(c_in, c_out, kernel_size) self.gcn = SpatialGraphConv(c_out, c_out, K) self.tcn2 = TemporalConv(c_out, c_out, kernel_size) self.bn = nn.BatchNorm2d(c_out) self.residual = (nn.Conv2d(c_in, c_out, 1) if c_in != c_out else nn.Identity()) def forward(self, x, cheb_polynomials): h = self.tcn1(x) h = self.gcn(h, cheb_polynomials) h = self.tcn2(h) h = self.bn(h) return F.relu(h + self.residual(x))逻辑说明:TemporalConv 里padding=(0, kernel_size//2)当 kernel_size=3 时 padding 为 1,输出时间长度不变,这样多个块堆叠不会累积长度收缩。p * g就是 GLU 的逐元素门控,主卷积和门卷积都从同样的输入学不同的投影。残差连接让 STGCN 能堆叠更深而不退化,时间卷积改变通道数时用 1x1 卷积对齐,通道数一致时直接用恒等映射。BatchNorm2d 作用于通道、节点、时间三个维度,把激活分布拉回 0 均值附近,训练初期收敛快很多。
参数说明:c_in和c_out在第一个 ST 块通常设为 1 和 64,第二个块保持 64 到 64。K控制图卷积阶数,两个块可以不同,但更常见是统一取 3。kernel_size是时间核宽,3 是原论文默认值,改成 5 会扩大时间感受野但增加参数量,对 12 步输入意义不大。
4.4 完整模型组装与前向传播
两个 ST 块加输出块就是完整网络。输出块需要把时间步压缩成预测的 H 步,用卷积核宽度等于当前序列长度的Conv2d一次完成:
class STGCN(nn.Module): def __init__(self, n_nodes, c_in=1, c_hid=64, K=3, seq_len=12, horizon=3): super().__init__() self.block1 = STConvBlock(c_in, c_hid, K) self.block2 = STConvBlock(c_hid, c_hid, K) self.output = nn.Sequential( TemporalConv(c_hid, c_hid, 3), nn.Conv2d(c_hid, horizon, (1, seq_len)) ) def forward(self, x, cheb_polynomials): h = self.block1(x, cheb_polynomials) h = self.block2(h, cheb_polynomials) out = self.output(h) # (B, horizon, N, 1) -> (B, N, horizon) return out.squeeze(-1).permute(0, 2, 1)逻辑说明:前向过程的时间维在 ST 块内保持 12 不变;输出块第一层仍是 TemporalConv,长度不变,随后Conv2d(c_hid, horizon, (1, seq_len))把整个时间维卷积塌缩,输出通道数直接等于 horizon,每个通道代表一个预测步。最后的 permute 把输出整理成 (B, N, horizon),和标签 shape 一致,方便直接算 MSE 或 MAE。
参数说明:seq_len必须与实际输入时间长度相同,否则输出层卷积核宽度对不上,会直接报维度错误。horizon是预测步数,交通流短时预测常用 3,也可以设成 6 或 12。n_nodes在模型结构里主要用于可读性,没有直接参与线性层维度,但保留这个参数可以让调用方一开始就确认图尺寸对齐。这个配置下模型参数量通常在几十万量级,可以用sum(p.numel() for p in model.parameters())一行数出来。
5. 训练与避坑:常见错误排查与参数调优
5.1 训练主流程与超参配置
先说环境。如果你还没把 PyTorch 环境搭好,我一般会建议用 conda 建独立虚拟环境,按机器的 CUDA 版本装对应 PyTorch;别在 CPU 上硬跑这个模型,切比雪夫矩阵乘一次前向就要算 K 个 N×N 对 N×C 的乘法,节点上百后每个 epoch 能慢到让人怀疑人生。
训练脚本的核心是 Adam 优化器、ReduceLROnPlateau 学习率调度,以及按验证集损失保存最优模型:
model = STGCN(n_nodes=N, seq_len=12, horizon=3) opt = torch.optim.Adam(model.parameters(), lr=0.01, weight_decay=1e-4) sched = torch.optim.lr_scheduler.ReduceLROnPlateau(opt, mode='min', factor=0.5, patience=5) for epoch in range(50): model.train() train_loss = 0.0 for xb, yb in train_loader: xb, yb = xb.to(device), yb.to(device) opt.zero_grad() pred = model(xb, cheb_list) loss = F.l1_loss(pred, yb) loss.backward() torch.nn.utils.clip_grad_norm_(model.parameters(), 5.0) opt.step() train_loss += loss.item() * xb.size(0) val_loss = validate(model, val_loader, cheb_list, device) sched.step(val_loss)逻辑说明:损失用 L1 loss,对应 MAE,比 MSE 对离群点温和,交通流预测的业界评估也以 MAE/RMSE 为主。梯度裁剪到 5.0 防止图卷积深堆叠后梯度突然放大,尤其前几个 epoch。train_loader 在训练集内可以 shuffle=True,验证集 loader 保持 shuffle=False。
超参经验值整理如下:
| 参数 | 推荐值 | 说明 |
|---|---|---|
| lr | 0.01 | 配合 ReduceLROnPlateau,别一上来就 0.001 |
| batch_size | 64 | 节点多时调小,防止显存爆掉 |
| c_hid | 64 | 小数据集可降到 32 |
| K | 3 | 图卷积阶数,一般不用超过 4 |
| seq_len | 12 | 历史窗口 12 个时间步 |
| horizon | 3 | 预测 3 个未来时间步 |
| weight_decay | 1e-4 | 对应 L2 正则,防止过拟合 |
5.2 维度不匹配:图卷积最常见的报错
现象:程序跑到第一次前向就抛RuntimeError: mat1 and mat2 shapes cannot be multiplied,或者输出层 kernel 宽度和序列长度对不上。
原因:九成是输入张量通道维和图卷积的 c_in 不对齐。很多人从 CSV 直接读数据得到形状 (N, T),忘了补通道维;另外切比雪夫多项式的 K 和模型 K 不一致,循环里取到不存在的项也会出问题。
解决:我习惯在最开始写一行 shape 打印print(x.shape),每一层 forward 里也临时打印输入输出,跑一次确认后再删掉。批量数据一定是 (B, C, N, T),CSV 读取后先x = x.unsqueeze(1)补上通道维;模型 K 和预处理 K 统一放在一个 config 常量里,避免两处各写一个数字。
5.3 NaN 与梯度爆炸:切比雪夫矩阵的隐形陷阱
现象:前几个 epoch loss 正常,突然变成 NaN;或者 loss 一开始很大,训练几步后直接发散。
原因:常见有三种。第一,邻接矩阵度矩阵出现 0,d_inv_sqrt除以 0 产生 inf,归一化拉普拉斯里带 NaN;第二,切比雪夫缩放系数算错,特征值超出 [−1,1] 区间,递推项指数级放大;第三,学习率太大或数据本身有 inf。
解决:预处理保存文件后立刻执行两条检查:assert np.isfinite(cheb_list[0]).all()和assert np.max(np.abs(l)) <= 1 + 1e-3。训练里加梯度裁剪是保底手段,同时把 lr 回退到 0.005 试试。如果还出 NaN,切换到 CPU 跑一个 batch 对比,排除 CUDA 计算的问题。
5.4 只降训练不降验证:过拟合和过平滑要分开看
现象:训练 loss 降到 0.5 以下,验证 loss 还停在 2.0 附近;或者两个都一直不降。
原因:样本量少时 64 通道的 STGCN 参数量偏大,容易记住训练样本细节。另一个更隐蔽的原因是图卷积堆叠过深导致过平滑:每个节点表示趋同,空间信息失效,验证自然上不去。
解决:过拟合方向,先降c_hid到 32,加 weight_decay,再加早停:连续 10 个 epoch 验证 MAE 不降就停止。过平滑方向,两个 ST 块够用,不要加第三个;如果一定要加深,在 ST 块之间加 Dropout,概率 0.3 左右。判断是哪一种,可以打印每个节点的输出均值和方差,方差趋近 0 说明过平滑。
5.5 数据泄露:看起来很好,实际上不能用
现象:验证 loss 低得离谱,测试集却正常,或者验证集效果显著优于训练集。
原因:最常见是用一个全局 StandardScaler 对整个序列 fit,训练、验证、测试的均值和方差都被污染;其次是滑动窗口切分时窗口跨过训练/验证边界,让验证样本里包含了训练段的时间步。
解决:先按时间顺序切好三段,再对训练段 fit 统计量,验证和测试只 transform 不 fit。写代码时严格保证窗口生成函数不跨边界:训练序列取 [0 : train_len],验证取 [train_len : train_len+val_len],中间的窗口各自独立。标准化统计量的差异虽然小,但足以让模型在实际部署时变差,别小看这个细节。
6. 进阶验证:从训练曲线到多步预测的评估技巧
6.1 多步预测的指标拆解
训练完不要只看 loss,把预测结果落回到真实数值再评估,指标固定用 MAE、RMSE 和 MAPE。反归一化后这样计算:
pred_inv = pred * std + mean true_inv = true * std + mean mae = abs(pred_inv - true_inv).mean() rmse = ((pred_inv - true_inv)**2).mean()**0.5 mape = (abs((pred_inv - true_inv) / true_inv)).mean() * 100一个具体技巧:horizon=3 时分别打印每个预测步的指标,而不是合并成一个。交通流预测中第一步误差通常远小于第三步,合并计算会把前序步的高精度掩盖掉。我一般会把第 1、2、3 步的 MAE 单独存成列表画成折线,这一步能直接判断模型学的是惯性延续还是真正的短时突变。
6.2 三个固定习惯与部署扩展
有几个验证习惯值得固定下来。第一,固定随机种子:torch.manual_seed(42)、np.random.seed(42),图卷积有随机初始化,不固定的话两次实验无法对比,看到的现象可能是初始化差异造成的假象。第二,把切比雪夫多项式和标准化统计量都落盘成 .npy 文件,推理阶段必须从文件加载,不要临时算,保证训练和部署的预处理完全一致。第三,如果后续要上线上服务,把训练好的模型用torch.onnx.export导出成 ONNX,输入输出形状固定为 (1, 1, N, 12) 和 (1, N, 3),推理速度会好很多。
从那以后,我每次换数据跑 STGCN,都会强制走一遍“shape 检查 → 切比雪夫有限性检查 → 训练集统计量标准化”三件套,顺序固定,缺一个都可能让结果难以复现。希望帮到你。
本文还有配套的精品资源,点击获取