简介:这是一套面向交通流预测与时空图神经网络研究者的完整项目代码包,围绕“城市交通流预测”场景,集成了T-GCN、A3T-GCN、AST-GCN、KST-GCN四种GCN变体,并附有HA、ARIMA、SVR、GCN、GRU五种基线模型,可帮助读者系统理解时空相关性建模、注意力机制与知识驱动方法,便于横向对比与论文复现。压缩包共164个文件,以Python源码(42个py)、交通流数据集(42个csv)、训练检查点与指标记录(meta/data/checkpoint、batch_loss、batch_rmse等)及说明文档(9个md)为主,涵盖模型定义、训练脚本、数据预处理与可视化等,整体约43.12MB,目录按模型分别组织,便于按需检索。目前已有71人学习下载,代码均经过运行测试,适合计算机相关专业的在校学生、科研人员用于毕业设计、课程设计或算法进阶。下载后可通过README.md快速上手;若遇到环境或运行问题,也支持私聊远程教学,获得针对性指导。
1. 图卷积网络预测城市交通流:这个 python 项目到底解决什么问题
拿到一批城市路网传感器数据,想预测 15 分钟后每条路的车辆速度,大多数新手会直接拿 LSTM 硬训,结果预测曲线像一条被拉直的橡皮筋——全天都贴着平峰均值走。这个 python 项目之所以值得做,是因为它把路网当成一张图而不是一堆孤立检测点,用图卷积网络(GCN)让相邻路段“互相通气”,再配合 LSTM 之类的时序模块捕捉早晚高峰的演化节奏。源码和文档说明覆盖了从数据处理、邻接矩阵构建到训练评估的完整链路,适合做毕设选题、时空数据挖掘入门,以及智慧交通方向的技术预研。这篇笔记按“原理、复现、调参、避坑”讲清楚怎么把它跑通,以及哪些参数最值得花时间调。
2. 用图卷积还是普通卷积:路网结构建模与图卷积层的核心代码
2.1 把路网流量数据切分成图:邻接矩阵和特征矩阵的构造
交通流预测的输入数据,最常见的形态是一张二维表:每一行是一个时间片(通常 5 分钟),每一列是一个路测传感器,单元格里是该传感器在这个时刻上报的速度、流量或占有率。拿到仓库里的原始数据后,第一步不是建模,而是先把长表转成这种标准形态。
import pandas as pd import numpy as np # 原始探测数据通常是长表: timestamp, sensor_id, speed df = pd.read_csv('raw_sensor_data.csv') pivot = df.pivot_table(index='timestamp', columns='sensor_id', values='speed') pivot = pivot.sort_index() # 传感器偶尔会漏报, 先做前向填充, 再对开头缺失做后向填充 pivot = pivot.ffill().bfill() data = pivot.values.astype(np.float32) print('数据形状:', data.shape) # [时间片数, 传感器数]这段代码里pivot_table负责把长表转成宽表,ffill().bfill()处理漏报。工业落地时往往还会加一步:删除连续缺失超过 10 分钟的传感器列,因为纯靠插值补出来的序列会对模型产生误导。形状对齐是复现源码的第一步,很多报错都出在这,因为仓库文档里写的num_nodes和你实际转出来的列数对不上。
有了节点特征,还需要定义“哪些节点相邻”。路网上的传感器不是均匀网格,不能用普通卷积;常见的做法是用传感器之间的实际路网距离构造邻接矩阵A,让距离近的节点互相影响。
def build_adjacency(dist_matrix, sigma2=0.1, threshold=0.5): # dist_matrix: [num_nodes, num_nodes], 单位是 km, 由路网拓扑算出来 num_nodes = dist_matrix.shape[0] adj = np.zeros((num_nodes, num_nodes)) for i in range(num_nodes): for j in range(num_nodes): if i == j: adj[i][j] = 0.0 elif dist_matrix[i][j] < threshold: adj[i][j] = np.exp(-dist_matrix[i][j] ** 2 / sigma2) return adjsigma2控制高斯核的衰减速度,threshold决定多远算邻居。这两个参数在 4.2 节单独展开,因为它们在源码里属于“调了会明显影响结果”的项目。这里先记住一个结论:邻接矩阵的物理含义是“节点之间的空间相关性”,不是随便生成一个对称矩阵就行。
2.2 图卷积层的两种实现思路和一段可直接运行的 GCN 代码
图卷积在论文里分频谱域和空间域两派。频谱域从图拉普拉斯矩阵的特征分解出发,用 Chebyshev 多项式做近似;工程实现里更常见的是它的简化版——一阶 GCN,公式长这样:
H' = σ(D̂⁻¹ᐟ² Â D̂⁻¹ᐟ² H W)
其中 Â 是加自环后的邻接矩阵,D̂ 是它的度矩阵。翻译成人话就是:先把自己和邻居的特征加权求和,再做一次线性变换,最后过激活函数。所谓“卷积”在图上其实就是一个聚合操作。
import torch import torch.nn as nn class GCNLayer(nn.Module): def __init__(self, in_dim, out_dim): super().__init__() self.weight = nn.Parameter(torch.randn(in_dim, out_dim)) self.bias = nn.Parameter(torch.zeros(out_dim)) def forward(self, x, adj): # x: [batch, num_nodes, in_dim] support = torch.matmul(adj, x) # 聚合邻居特征 out = torch.matmul(support, self.weight) + self.bias return torch.relu(out)这段代码的关键在torch.matmul(adj, x):它把每个节点的特征替换成邻居特征的加权平均,权重就是邻接矩阵里的值。weight是跨所有节点共享的,这也是图卷积能处理“节点数量变化”的原因——换一张路网,只要邻接矩阵维度对,模型权重不用改。
这里有个容易忽略的预处理,聚合之前一定要做对称归一化,否则度数高的节点特征会被放大,训练不稳定。
def normalize_adj(adj): # 加自环, 让节点自身信息也参与聚合 adj = adj + np.eye(adj.shape[0]) rowsum = adj.sum(axis=1) d_inv_sqrt = np.power(rowsum, -0.5) d_inv_sqrt[np.isinf(d_inv_sqrt)] = 0.0 d_mat_inv_sqrt = np.diag(d_inv_sqrt) return d_mat_inv_sqrt @ adj @ d_mat_inv_sqrt归一化后的邻接矩阵每一行加起来是 1,特征聚合后数值范围基本可控。源码里的compute_adjacency.py通常就是做这件事,只是可能换成了稀疏矩阵写法以省内存。结构上不用被公式吓住,落地时就是“归一化 + 聚合 + 线性变换”三步。
2.3 空间图卷积不够,时间维度才是交通流预测的难点
图卷积解决了“空间依赖”问题,但它的输出仍然是逐时间步的。交通流是一个时间序列,当前时刻的速度和过去一小时的速度强相关,早晚高峰有明显的周期性。所以项目源码里的完整模型一般是“GCN + 时间序列模块”的组合:一种是用 LSTM/GRU 沿时间维建模,另一种是用一维卷积或门控时间卷积。
我一般建议先跑通第一种,因为 LSTM 的输入输出形状更容易理解。整个模型的形状流转是:原始数据[batch, seq_len, num_nodes],先做图卷积聚合成[batch, seq_len, num_nodes, hidden],再压成时间序列格式喂给 LSTM,最后通过全连接输出预测值。
这个组合也是该领域公开论文基线的常见结构,像 DCRNN 用扩散卷积加 GRU,STGCN 用图卷积加门控时间卷积,Graph WaveNet 用自适应邻接矩阵加扩张卷积。它们之间最大的区别就在于“时间维怎么建模”和“邻接矩阵是否可学习”。新手复现源码时不需要一上来看懂所有变体,先抓住“空间靠图卷积,时间靠 LSTM/卷积”这个骨架就够了。
3. 从项目源码跑通到最小训练流程:文档说明怎么看、数据怎么喂
3.1 拿到源码和文档说明后,按这个顺序读项目
源码仓库里通常有一份文档说明,内容一般包括环境依赖、数据集下载方式、训练入口、复现指标和文件结构说明。对第一次接触图卷积交通流预测的人,我建议按这个顺序读:先看 README 里的数据格式说明,再看模型定义文件,最后看训练脚本。不要一上来就pip install -r requirements.txt然后直接跑,因为数据集路径、传感器数量和模型里的num_nodes很可能不匹配。
文档说明里最容易被忽略的是环境版本。很多源码是在旧版本 PyTorch 上写的,如果你用新版跑,可能遇到torch.load的weights_only参数变化,或者np.matrix被移除之类的兼容问题。遇到这类问题,先看报错发生在哪个文件,再结合文档里的环境要求定位;不要轻易重装环境,多数情况是某一行 API 需要适配。
代码结构上,这类项目一般会有几个固定模块:数据预处理脚本、邻接矩阵构建脚本、模型定义文件、训练主脚本、评估脚本,外加一个放实验配置的目录。搞清楚它们之间的调用关系,比逐行读源码高效得多。
3.2 数据预处理与滑窗切分,复现前先把数据形状对齐
训练样本的构造方式决定了模型看到什么。交通流预测的标准做法是滑窗:用过去 12 个时间步(1 小时)预测未来若干个时间步。
def sliding_window(data, seq_len=12, pred_len=3): # data: [num_samples, num_nodes], 时间片在前 num_samples, num_nodes = data.shape xs, ys = [], [] for i in range(num_samples - seq_len - pred_len + 1): x = data[i:i + seq_len] # 过去 1 小时 y = data[i + seq_len:i + seq_len + pred_len] # 未来 15 分钟 xs.append(x) ys.append(y) return np.array(xs), np.array(ys) train_x, train_y = sliding_window(train_data, seq_len=12, pred_len=3) print('train_x:', train_x.shape) # [样本数, 12, num_nodes] print('train_y:', train_y.shape) # [样本数, 3, num_nodes]seq_len取 12 是因为交通数据通常是 5 分钟粒度,12 步正好是 1 小时,超过 1 小时的历史信息对短期预测帮助递减。pred_len取 3 是预测未来 15 分钟,这是交通预测里最常用的短期场景。注意滑窗切分时不要打乱时间顺序,训练集、验证集、测试集按时间先后切分,而不是随机切分,否则会引入数据泄露。
3.3 最小可运行的训练循环与参数配置表
为了让训练流程先跑通,我一般会用一个简化版模型替代源码里的完整模型,确认数据处理没问才换回原版。简化版同样包含图卷积聚合和 LSTM 时间建模,只是省了注意力机制和可学习邻接矩阵。
import torch import torch.nn as nn from torch.utils.data import DataLoader, TensorDataset class SimpleSTGCN(nn.Module): def __init__(self, num_nodes, hidden_dim=64): super().__init__() # 图卷积聚合后, 每个时间步的特征还是 num_nodes # LSTM 沿时间维建模, 输入是每个节点聚合后的速度 self.lstm = nn.LSTM(input_size=num_nodes, hidden_size=hidden_dim, batch_first=True) self.fc = nn.Linear(hidden_dim, num_nodes) def forward(self, x, adj): # x: [batch, seq_len, num_nodes], adj: [num_nodes, num_nodes] x_gcn = torch.matmul(adj, x.permute(0, 2, 1)) # 聚合邻居 x_gcn = x_gcn.permute(0, 2, 1) # 还原为时序格式 h, _ = self.lstm(x_gcn) # [batch, seq_len, hidden] out = self.fc(h[:, -1, :]) # 取最后时间步 return out.unsqueeze(-1) # [batch, num_nodes, 1] model = SimpleSTGCN(num_nodes=train_x.shape[2], hidden_dim=64) optimizer = torch.optim.Adam(model.parameters(), lr=1e-3, weight_decay=1e-5) criterion = nn.MSELoss() def train(model, train_x, train_y, val_x, val_y, adj, epochs=50): dataset = TensorDataset(torch.tensor(train_x), torch.tensor(train_y)) loader = DataLoader(dataset, batch_size=32, shuffle=True) best_mae = float('inf') for epoch in range(epochs): model.train() total_loss = 0 for x, y in loader: optimizer.zero_grad() out = model(x, adj) # 输出 [batch, num_nodes, 1] loss = criterion(out, y.unsqueeze(-1)) loss.backward() optimizer.step() total_loss += loss.item() val_pred = model(torch.tensor(val_x), adj).squeeze(-1).detach().numpy() val_mae = np.mean(np.abs(val_pred - val_y)) if val_mae < best_mae: best_mae = val_mae torch.save(model.state_dict(), 'best_model.pth') if epoch % 10 == 0: print(f'epoch {epoch}: loss {total_loss / len(loader):.4f}, ' f'val_mae {val_mae:.4f}')这段代码里的model(x, adj)中x的维度是[batch, seq_len, num_nodes],adj是预计算好的归一化邻接矩阵,在训练开始前用 numpy 算好转成 torch tensor。注意y.unsqueeze(-1)这一步,源码里常常因为输出和标签形状不一致报错,所以要在数据处理时就定死约定。
参数配置上,我一般按这个表起步:
| 参数 | 建议值 | 说明 |
|---|---|---|
| 学习率 | 1e-3 | Adam 默认,loss 震荡就降到 3e-4 |
| batch_size | 32 或 64 | 取决于显存,序列模型显存占用偏高 |
| epoch | 50 | 配合早停,patience 设 10 |
| seq_len | 12 | 过去 1 小时 |
| pred_len | 3 | 未来 15 分钟 |
| hidden_dim | 64 | LSTM 隐层维度,数据量大可以加到 128 |
| weight_decay | 1e-5 | 防止过拟合 |
这个简化版模型跑通后,换回源码里的完整模型只是替换model的部分,数据加载、训练循环、评估逻辑完全可以复用。如果简化版都跑不出合理结果,大概率是数据形状或邻接矩阵构建出了问题,而不是模型的问题。
4. 必调参数:预测步长、邻接矩阵构建方式和归一化策略
4.1 预测步长选 1 步还是 12 步,多步预测的两种写法
预测步长是第一个要敲定的参数。短期预测(未来 5~15 分钟)相对稳定,误差主要来自随机波动;长期预测(1 小时以上)需要模型真正理解拥堵的演化,难度明显上升。经验上,5 分钟粒度的数据里,预测未来 1 步的 MAE 最低,3 步(15 分钟)还在可用范围,12 步(1 小时)开始出现明显的平滑效应——预测曲线逐渐失去细节。
多步预测有两种实现方式。一种是迭代预测:模型只输出下一步,然后把预测值拼回输入序列,滚动预测接下来的每一步。
def iterative_predict(model, x, adj, pred_steps=12): # x: [1, seq_len, num_nodes], 初始输入 model.eval() cur = x.clone() preds = [] with torch.no_grad(): for _ in range(pred_steps): y_next = model(cur, adj) # [1, num_nodes, 1] preds.append(y_next.squeeze(0)) y_expand = y_next.squeeze(-1).unsqueeze(0) # [1, 1, num_nodes] cur = torch.cat([cur[:, 1:], y_expand], dim=1) return torch.cat(preds, dim=0).numpy() # [pred_steps, num_nodes]迭代预测的优点是模型结构简单,训练时只需要拟合下一步;缺点是误差会累积,预测步数越多偏差越大。另一种是直接多输出:修改输出层,一次预测未来多个时间步。这种方式训练时标签就是[batch, pred_len, num_nodes],直接优化多步误差,但输出层参数量变大,训练难度也高。
源码里两种实现都可能出现,判断方法是看训练时y的维度。如果y是三维[batch, pred_len, num_nodes],就是直接多输出;如果y是四维且每次只算下一步 loss,就是迭代式。我习惯先跑通迭代式,因为调试更容易。
4.2 邻接矩阵的三种构建方式,以及 sigma 和 threshold 怎么给初值
邻接矩阵是整个图卷积模型的“空间先验”,它的质量直接影响预测上限。最常见的三种构建方式是距离阈值法、KNN 法和自适应法。
| 构建方式 | 核心思路 | 优点 | 缺点 |
|---|---|---|---|
| 距离阈值法 | 距离小于阈值就建边,权重按高斯核衰减 | 物理意义明确,实现简单 | 对阈值敏感,可能产生孤立节点 |
| KNN 法 | 每个节点只连接最近的 K 个节点 | 保证图连通性,边数可控 | K 值需要调,连边可能不对称 |
| 自适应法 | 邻接矩阵作为可学习参数 | 摆脱手工先验,能发现隐含依赖 | 训练不稳定,需要额外约束 |
距离阈值法里最关键的两个参数是sigma2和threshold。threshold的初值取所有传感器距离的中位数比较稳妥,太小会切断有效连接,太大则让远处的节点也互相干扰。sigma2控制权重衰减快慢,初值可以设在 0.05 到 0.2 之间,然后观察训练 loss 是否稳定下降来微调。
KNN 法里我一般用 K = 10 起步。K 太小,图变得稀疏,消息传不出去;K 太大,等价于全连接,图卷积退化成全局平均。自适应法在数据量充足时效果最好,但训练初期需要配合邻接矩阵正则项,否则容易学出一个全是小数的稠密矩阵,数值分布很难看。
判断邻接矩阵好坏有一个很土但有效的方法:训练 20 个 epoch 后,把预测曲线和真实曲线叠在一起看趋势。如果高峰时段的预测明显滞后,多半是邻接矩阵中关键连接权重偏低,或者阈值范围没覆盖到真正的上下游传感器。
4.3 归一化策略选 min-max 还是 z-score,归一化参数只能从训练集算
交通流数据的数值特征对训练稳定性影响极大。速度数据的范围通常 0~100 km/h,流量数据可能 0~1000 辆/小时,而且分布长尾,早高峰时段会出现极端值。源码里的预处理脚本一般会做归一化,但用哪种、参数怎么算,是复现指标差异的常见来源。
# 错误写法: 用全量数据计算归一化参数, 测试集信息掺入训练 mean, std = all_data.mean(), all_data.std() train_norm = (train_data - mean) / std test_norm = (test_data - mean) / std # 正确写法: 只从训练集计算, 验证集和测试集复用这组参数 train_mean = train_data.mean() train_std = train_data.std() train_norm = (train_data - train_mean) / train_std val_norm = (val_data - train_mean) / train_std test_norm = (test_data - train_mean) / train_std速度数据我一般用 z-score,也就是(x - mean) / std,因为速度的均值在 60 km/h 左右,标准差相对稳定,z-score 让数据天然落在 0 附近。流量数据更适合 min-max,因为流量有明确的物理下界 0,且分布右偏,min-max 能保留稀疏性。如果数据里有明显的极端峰值,z-score 会对异常值更敏感,训练时容易把注意力吸引到少数样本上。
另一个细节是反归一化。模型输出的是归一化后的数值,评估和可视化之前必须还原成真实值,否则 MAE 的单位是“归一化单位”,没法跟论文里的 km/h 对比。
pred_real = pred_norm * train_std + train_mean mae = np.mean(np.abs(pred_real - test_real))这一点在 5.3 节还会再踩一次,数据泄露和单位不统一是“复现不出来论文指标”的两大元凶。
5. 图卷积交通流预测避坑:5 条翻车记录与排查步骤
5.1 预测结果是一条平均曲线,Loss 却不低
现象:训练过程 loss 正常下降,画出来的预测曲线却像一条横向拉直的波浪线,早晚高峰完全没有凸起。
原因:交通流数据里平峰时段样本占绝对多数,模型用 MSE 作为损失函数时,只要输出全天均值,平峰部分的误差就很小,整体 loss 看起来不错,但高峰时段的误差被平均淹没了。
解决:先按小时维度看分时段误差,确认高峰和平峰的 MAE 差距;然后给损失函数加权重,让高峰时段样本的梯度占比更大。最简单的做法是在训练时给peak_mask对应的样本乘一个系数,比如 1.5;更稳妥的做法是改用分段损失,把样本按速度值分成若干个桶,每个桶单独计算误差再平均。
5.2 训练到第 3 轮 Loss 变成 NaN
现象:前几轮 loss 正常,第三轮开始突然变成nan,重跑一次还是一样。
原因:邻接矩阵没有做归一化就直接参与矩阵乘法,节点度数差异大导致聚合后的特征数值爆炸;加上 LSTM 的时间反向传播,梯度在几步内就溢出。
解决:先确认normalize_adj已经执行,且邻接矩阵的行和不为零。另一个常见原因是学习率偏高,配合torch.nn.utils.clip_grad_norm_把梯度裁剪到 5.0 以内,再观察前 5 个 epoch 的 loss 曲线是否平缓下降。
5.3 验证集指标虚高,归一化的数据泄露
现象:验证集 MAE 远低于论文报告的数值,甚至比训练集还低,总感觉哪里不对。
原因:归一化参数用了全量数据计算,验证集和测试集的统计信息混入了训练过程。模型在训练时已经“见过”测试集的分布偏移,评估自然失真。
解决:把归一化参数的计算限定在训练集,验证集和测试集直接复用训练集的均值、标准差;如果源码里是在整个数据集上先归一化再切分,果断改成先切分再归一化。这一步改完,指标通常会小幅上升,那才是真实水平。
5.4 图卷积层数堆到 3 层效果反降
现象:把图卷积层从 1 层加到 3 层,训练 loss 下降更慢,验证指标变差。
原因:图卷积本质上是在做邻居信息的聚合和扩散,层数越多,每个节点的特征越趋向于整个图的平均状态,这就是“过平滑”。交通路网的邻接矩阵本来就稀疏,1 层卷积已经能让相邻传感器互通,堆深度带来的收益很小。
解决:深度不是图卷积的主要增益来源,不如把算力花在时间维建模和注意力机制上。如果确实需要加深,每层之间加残差连接,同时配合 Dropout,能稍微缓解过平滑。
5.5 复现论文指标对不上,先查这两处
现象:严格按照文档说明跑了训练流程,最终 MAE 和文档里写的差距明显,反复调参也拉不回来。
原因:最常见的是评估口径不一致——文档可能报告的是单步预测 MAE,你评估的是多步预测的平均 MAE,数值自然差很多;也可能是随机种子不同导致的结果波动,或者训练集和测试集的时间划分边界不一样。
解决:先确认文档里指标的定义,是 15 分钟预测还是 60 分钟预测,是全部节点平均还是只统计有效节点;然后在训练脚本里固定随机种子,和文档保持一致的数据切分方式。这两处对齐后,大部分“复现不出来”的问题都能收敛。
6. 进阶:预测曲线可视化与分时段误差拆解
6.1 预测曲线对比图要怎么看
模型训练完,最先要做的是把某个传感器的预测值和真实值画在同一张图里,观察趋势是否对齐,而不是只看最终指标。Matplotlib 就能胜任这件事。
import matplotlib.pyplot as plt node_id = 25 # 选一个数据完整的传感器 plt.figure(figsize=(12, 4)) plt.plot(test_y[:, node_id], label='observed', linewidth=2) plt.plot(pred_real[:, node_id], label='predicted', linestyle='--') plt.xlabel('time step (5 min)') plt.ylabel('speed (km/h)') plt.legend() plt.grid(alpha=0.3) plt.savefig('prediction_curve.png', dpi=150)拿到图之后先看三点:高峰拐点是否按时出现,预测曲线是否比真实曲线平滑太多,以及低谷时段是否存在系统性偏差。如果高峰拐点每次都晚一两个时间步出现,说明模型对拥堵传播的建模偏慢,问题大概率出在邻接矩阵对上下游关系的刻画上。
6.2 分时段误差拆解:高峰和平峰分开算 MAE
全局 MAE 会掩盖模型在高峰时段的真实表现。我习惯把一天划分成早高峰、晚高峰、平峰三个时段,分别计算误差。
# 假设 test_timestamps 是测试集每个样本对应的真实时间 hours = test_timestamps.hour peak_mask = ((hours >= 6) & (hours <= 10)) | ((hours >= 16) & (hours <= 20)) def mae(y_true, y_pred): return np.mean(np.abs(y_true - y_pred)) print('peak MAE:', mae(test_y[peak_mask], pred_real[peak_mask])) print('off-peak MAE:', mae(test_y[~peak_mask], pred_real[~peak_mask]))如果高峰 MAE 明显高于平峰,说明模型对拥堵演化不够敏感,改进的方向是在损失函数里提高高峰样本的权重;如果平峰 MAE 也高,那问题更多出在数据质量或邻接矩阵构建;如果两类时段的误差比接近 1.5 倍以内,模型整体状态就比较好。
6.3 在这套源码基线上继续投入的三个方向
跑通并复现了源码指标之后,想继续投入可以优先考虑三个方向。第一个是换注意力机制,把图卷积里的固定邻接矩阵换成 GAT 的动态注意力权重,让模型自己决定邻居的重要程度;第二个是加外部特征,把天气、节假日、事件等因素拼入节点特征,对节假日和突发拥堵的预测会明显改善;第三个是改造训练策略,用 cosine annealing 学习率调度替代固定学习率,通常能在同样 epoch 数下把 MAE 再压低 2%~3%。
这套流程走完,你会对“空间聚合、时间建模、数据泄露”这三个词有比论文更直观的理解。我自己的习惯是把可视化和分时段评估脚本沉淀到仓库的analysis/目录里,每次实验先看这两张图再决定调哪里,试错成本比光盯 loss 低得多。希望帮到你。
本文还有配套的精品资源,点击获取