news 2026/10/7 3:14:08

交通流预测实战:SAE特征提取与LSTM、GRU时序建模完整代码解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
交通流预测实战:SAE特征提取与LSTM、GRU时序建模完整代码解析

简介:这套基于深度学习的交通流预测实现,覆盖SAE、LSTM、GRU三种典型模型,面向交通运输、数据挖掘等方向的研究生和工程师,适用于交通流量短时预测研究、课程设计、毕业设计以及算法横向对比实验。压缩包共23个文件,整体大小3.32MB,包含Python训练与预测脚本(py)、训练后的模型权重(h5)、实验数据(csv)、评估与结果图表(png)以及README说明文档,文件类型分布直观,目录结构便于按模型模块定位。目前已有29人学习下载。资源在数据读取、模型搭建、训练评估和可视化环节均有代码支撑,三种模型分别保留权重文件与loss曲线,可直接运行复现,方便对比SAE、LSTM、GRU在相同交通流数据上的预测效果;附带说明文档和测试数据,有助于理解数据组织方式与模型调用流程,降低复现门槛,适合作为深度学习交通流预测的起步参考。

1. 交通流预测为什么值得做:从SAE到LSTM、GRU的代码方案

城市交通路口的流量数据每天都在堆量,但大多数人只拿它画曲线看“今天堵不堵”,真正做短时预测的少。深度学习这一块,SAE负责从高维交通特征里压出有用的表示,LSTM和GRU负责把时间依赖吃进去,三个模型放在一起,基本覆盖了从特征提取到时序建模的完整链路。我这套拆过的代码包里,最值钱的不是某个模型本身,而是把原始断面流量数据变成监督学习样本、再训练、再评估的完整流程。适合正在做智能交通课设、论文实验或者刚入门时序预测的开发者,你不需要自己从零去拼数据处理管道,照着跑就能看到预测曲线和误差指标。

2. 数据侧:把原始流量变成监督学习样本的完整做法

2.1 先从字段和时间粒度说起

交通流数据最常见的来源是线圈检测器和卡口过车记录,落库之后一般长这样:一个路口编号、一个时间戳、一个方向编号,再加两个核心数值——流量(单位时间通过车辆数)和占有率(检测器被车辆占用的时间比例)。有些数据集里还有平均车速,但那不是标配。拿到手先别急着上模型,第一件事是确认时间粒度,常见的是5分钟聚合、15分钟聚合和1小时聚合。

5分钟粒度最贴近短时预测场景,但噪声也大,经常出现连续几个窗口流量为0,等红灯排队的车一起放行后又突然冲到顶。15分钟粒度平滑很多,训练出来的曲线也好看。我做实验默认用5分钟粒度,因为LSTM这类模型在细粒度数据上才能体现出学习时序模式的价值,如果你换成1小时粒度,GRU和LSTM的差距会变得非常小,甚至不如简单的ARIMA。

代码包里第一步是统一时间索引,把原始过车记录按时间窗口聚合成标准表格。常见的做法是拿pandas的resample去重采样,但这里有个坑:原始数据的时间戳可能是字符串,也可能是带时区的,必须先转成datetime再设置为索引。

import pandas as pd import numpy as np def aggregate_traffic(raw_df, freq="5min"): # raw_df 必须包含 time, lane_id, volume 三列 # lane_id:车道或方向编号;volume:该时间戳内过车数 df = raw_df.copy() df["time"] = pd.to_datetime(df["time"]) df.set_index("time", inplace=True) # 按5分钟窗口求和,缺失窗口先用NaN占位再插值 agg = df.groupby(["lane_id"]).resample(freq)["volume"].sum().unstack("lane_id") agg = agg.resample(freq).asfreq() return agg

这段代码先按车道分组,再用resample把时间切到5分钟网格,groupby之后unstack是把每个车道变成一列,方便后续做多变量输入。asfreq的作用是补出那些没有任何过车记录的空白窗口,这些窗口在原始数据里根本不存在,但模型训练时缺一个时刻,序列就对不齐。你要是直接跳过这一步,后面构造滑动窗口时会出现标签时间错位,预测结果看着收敛,实际上学的全是错位信息。

2.2 缺失值和异常值的处理原则

交通流数据的缺失不是随机缺失,很多是设备故障导致的持续掉线,可能连续几个小时都是NaN。对这种连续缺失,线性插值会插出一段平滑到不真实的曲线,模型很容易把这段假数据当规律学进去。我一般按缺失长度分两种策略:连续缺失不超过3个窗口,用线性插值;超过3个窗口,直接用前后同时间段的历史均值填充,比如周一早上8点到9点的数据丢了,就取过去四周周一同一时段的均值。

异常值更要小心。流量不可能为负,也不可能在5分钟内突然从10跳到500。这里不能单纯按标准差过滤,因为交通流的波动本身就是重尾的,峰值时段的标准差天然大。代码包里实现的是分位数截断法。

def clean_traffic(series, lower_quantile=0.001, upper_quantile=0.999): # 超过99.9%分位数的一律视为设备误报 low = series.quantile(lower_quantile) high = series.quantile(upper_quantile) cleaned = series.clip(lower=low, upper=high) # 连续0值超过12个窗口(1小时),认为是设备离线,做标记 zero_streak = (cleaned == 0).astype(int) streak = zero_streak.groupby((zero_streak != zero_streak.shift()).cumsum()).cumsum() cleaned[streak > 12] = np.nan return cleaned.interpolate(method="linear")

clip不是万能的,上限设太大会把真实峰值削掉,设太小又滤不掉误报。0值连续超过12个窗口这一条是经验值,5分钟粒度下等于持续1小时一个车都没有,这在城市主干路基本不可能,宁可当成缺失去插值,也不能拿一堆0去骗模型。真实项目中我还会加一道基于上下游断面一致性的校验,比如A断面和B断面相邻,同一时段流量差超过某阈值就两边都标记为可疑,但这属于进阶玩法,基础代码包里没放。

2.3 窗口构造:时间步长和标签错位

监督学习样本的构造是整个项目最容易被忽视、但最决定上限的一步。交通流预测常用的是滑动窗口方式:用过去window_size个时刻的流量预测未来horizon个时刻的流量。窗口大小不是拍脑袋,它和数据的自相关强相关。5分钟粒度的数据,一般取6到12个窗口,也就是看过去30分钟到60分钟,预测未来15到30分钟。

窗口太小,模型看不到完整的拥堵形成过程;窗口太大,输入维度膨胀,LSTM的训练时间成倍增加,而且序列过长时梯度回传路径变长,GRU的优势也会被稀释。代码里我留了create_sequences函数,一次性把多车道的流量矩阵切成样本。

def create_sequences(data, window_size=12, horizon=3, step=1): X, y = [], [] for i in range(0, len(data) - window_size - horizon + 1, step): X.append(data[i : i + window_size]) y.append(data[i + window_size : i + window_size + horizon]) return np.array(X), np.array(y) # 假设 data 形状为 (样本数, 车道数) # X 输出形状: (样本数, window_size, 车道数) # y 输出形状: (样本数, horizon, 车道数)

这里有一个新手极其容易犯的错:预测目标是未来horizon个时刻的流量,但很多人画样本时,把i时刻到i+window_size时刻作为输入,标签却用了i+window_size时刻的当前值。这等于让模型用已知信息预测已知信息,训练误差低得离谱,一旦部署就崩。我每次构造完样本都要打印一次X和y的最后几个时刻做人工核对,确认标签确实在输入时间窗口的后面,这个习惯帮我挡掉过好几次数据泄漏事故。

step参数控制滑动步长,默认1表示样本高度重叠,训练数据量很大但很多样本几乎一样,容易被模型背下来。如果数据量充足,我建议设成horizon,让样本之间不重叠,泛化能力明显更好。数据量少的时候再退回step=1,用重叠样本做数据增强。

2.4 归一化要在切分之前还是之后

这个问题我被人问过很多次,答案是必须在切分之前。先把全部数据做MinMax归一化,再切训练集和测试集,这样测试集的分布和训练集保持一致。如果先切再归一化,测试集的归一化参数只反映测试集自己的范围,模型在训练时见过的输入分布和测试时完全对不上,预测曲线会整体偏移。

from sklearn.preprocessing import MinMaxScaler scaler = MinMaxScaler(feature_range=(0, 1)) scaled_data = scaler.fit_transform(data) # 按时间顺序切分,不能随机打乱 train_ratio = 0.8 train_size = int(len(scaled_data) * train_ratio) train_data = scaled_data[:train_size] test_data = scaled_data[train_size:]

时间序列数据切分不能用train_test_split的随机模式,原因很简单:模型必须见过“昨天之前”的数据,才能预测“今天”的数据,随机打乱等于时间穿越。代码包里已经固定用前80%做训练、后20%做测试,如果你数据里有明显的早晚高峰区别,建议按整周切分,比如取前六周训练、最后一周测试,避免训练集里全是工作日而测试集全是周末。

3. 模型侧:SAE特征提取与LSTM、GRU的PyTorch实现

3.1 为什么中间夹一个SAE

如果你直接用LSTM接原始多车道流量矩阵,也能收敛,但训练会比较吃力。原因在于交通流数据是高相关的:相邻车道的流量走势几乎一致,占有率也和流量强相关,这些冗余特征会让LSTM的输入门和遗忘门花很多额外参数去学习“哪些特征该忽略”。SAE(栈式自编码器)的价值就是先把原始输入压缩成低维稠密表示,把冗余去掉之后再喂给时序模型。

这个思想放在深度学习里叫特征预训练,放在传统交通工程里其实就是主成分分析的升级版。SAE的隐层可以设成两层,第一层把特征从原始维度压到一半,第二层再压到四分之一。代码包里的实现是逐层预训练,而不是直接堆一个多层自编码器一次性训练,因为一次性训练深层自编码器容易陷入局部最优。

import torch import torch.nn as nn import torch.optim as optim class SAE(nn.Module): def __init__(self, in_dim, hidden_dims): super(SAE, self).__init__() layers = [] prev = in_dim for h in hidden_dims: layers.append(nn.Linear(prev, h)) layers.append(nn.ReLU()) prev = h self.encoder = nn.Sequential(*layers) # 解码器是编码器的对称结构,输出还原到 in_dim decode_layers = [] dims = [in_dim] + hidden_dims for i in range(len(dims) - 1, 0, -1): decode_layers.append(nn.Linear(dims[i], dims[i - 1])) if i > 1: decode_layers.append(nn.ReLU()) self.decoder = nn.Sequential(*decode_layers) def forward(self, x): encoded = self.encoder(x) decoded = self.decoder(encoded) return decoded, encoded

self.encoder输出作为后续LSTM的输入,self.decoder只在预训练阶段参与损失计算,预训练结束后解码器整个扔掉。之前有读者把解码器也留在训练流程里,导致模型同时优化两个目标,效果反而不如不用SAE。预训练时每个样本是单个时刻的多车道流量向量,不需要窗口信息,所以SAE不感知时间顺序。训练轮数不用多,20到30个epoch就够,早期轮数就能看到重构误差快速下降,超过50轮容易把噪声也编码进来,特征反而变差。

3.2 LSTM的参数配置和PyTorch实现

LSTM在这个项目里是主力预测模型。标准LSTM单元内部有输入门、遗忘门和输出门,对交通流这种有明显周期性的序列非常合适。但它的参数量比GRU多三分之一,训练更慢,对小数据集容易过拟合。

代码包里的LSTMPredictor接收SAE输出的低维特征,然后过一个单层LSTM,最后接全连接层输出未来horizon个时刻的流量。我不推荐用两层LSTM,因为交通流预测单序列的复杂度没到需要堆叠才能表达的程度,一层LSTM配合足够大的隐层维度,在大多数路口数据上效果已经够好。

class LSTMPredictor(nn.Module): def __init__(self, input_size, hidden_size=64, num_layers=1, horizon=3): super(LSTMPredictor, self).__init__() self.lstm = nn.LSTM( input_size=input_size, hidden_size=hidden_size, num_layers=num_layers, batch_first=True ) self.fc = nn.Linear(hidden_size, horizon) def forward(self, x): # x 形状: (batch, window_size, input_size) out, (hidden, cell) = self.lstm(x) # 取最后一个时间步的隐藏状态,再映射到 horizon 个输出 last_hidden = hidden[-1] return self.fc(last_hidden)

hidden_size取64,是我在多数路口数据上试出来的甜点区间。取32,模型拟合不足,早高峰的峰值会被系统性低估;取128,训练时间翻倍,但误差下降不足5%。num_layers不加深的原因是LSTM每一层都自带遗忘机制,层数加深后训练稳定性快速下降,需要同步调高学习率调度器的耐心,对新手很不友好。

batch_first=True这个参数一定记得加。PyTorch默认输入维度是(seq_len, batch, feature),但绝大多数人习惯把batch放第一位,不加这个参数后期处理输出维度时特别容易弄混。

3.3 GRU:参数更少,速度更快,收敛更稳

GRU是LSTM的简化版,把遗忘门和输入门合并成更新门,还引入了一个重置门。从交通流预测的实际效果看,GRU在多数场景下预测精度和LSTM相当,但训练速度快20%到30%,对小数据集更稳。如果样本量不到几千条,我建议直接用GRU,LSTM容易过拟合却不会带来精度优势。

实现上一个很巧的点是:只要把LSTMPredictor里的nn.LSTM换成nn.GRU,输出结构多了一个维度差异。LSTM的forward返回(output, (hidden, cell)),GRU返回(output, hidden),取隐藏状态时要注意。代码包里单独写了GRUPredictor,避免在训练循环里做条件分支。

class GRUPredictor(nn.Module): def __init__(self, input_size, hidden_size=64, num_layers=1, horizon=3): super(GRUPredictor, self).__init__() self.gru = nn.GRU( input_size=input_size, hidden_size=hidden_size, num_layers=num_layers, batch_first=True ) self.fc = nn.Linear(hidden_size, horizon) def forward(self, x): out, hidden = self.gru(x) last_hidden = hidden[-1] return self.fc(last_hidden)

GRU的隐藏状态初始化默认是全零张量,用在全零填充的输入上会有问题:交通流夜间低峰期可能出现连续几个窗口全是0,GRU会把全零输入和全零隐藏状态搞混,输出接近零向量,然后平缓期预测就一直偏低。这种情况LSTM因为有独立的遗忘门和候选记忆,反而能自动调整偏置。解决这个问题的常用做法是给输入序列加一个时间位置编码,或者归一化时把0值映射成小正数,我习惯选后者,改动最小。

3.4 SAE和时序模型怎么拼接

训练流程分两阶段:第一阶段单独训练SAE,第二阶段冻结encoder参数,把归一化后的原始数据过一遍encoder得到压缩特征,再训练LSTM或GRU。第二种做法是把SAE的encoder接到LSTM前面,端到端微调。代码包里给了两种模式,默认是第一种,因为它快、稳定、且结果可复现。

端到端微调理论上能让特征表示更适配预测任务,但实际操作中会遇到一个问题:LSTM训练时的梯度要回传到SAE的每一层,深层梯度消失导致encoder参数几乎不动,却白白增加了计算量。我在代码里加了freeze_sae开关,你想实验端到端模式就把开关设成False,但建议只在数据量超过5万条样本时尝试。

def build_model(model_type, input_size, hidden_size, horizon): if model_type == "sae_lstm": sae = SAE(in_dim=input_size, hidden_dims=[32, 16]) predictor = LSTMPredictor(input_size=16, hidden_size=hidden_size, horizon=horizon) return sae, predictor elif model_type == "sae_gru": sae = SAE(in_dim=input_size, hidden_dims=[32, 16]) predictor = GRUPredictor(input_size=16, hidden_size=hidden_size, horizon=horizon) return sae, predictor

hidden_dims=[32, 16]意味着输入假如是8条车道,第一层压到32又升维?不对,这里in_dim是原始特征维度,hidden_dims=[32,16]表示中间层维度分别为32和16。如果in_dim小于32,升维再降维的意义不大。代码包里默认in_dim是车道数,通常4到8条,所以hidden_dims我改成[16, 8]更合理。拆项目时你要根据实际车道数量调整这个参数,不是越大越好。

4. 训练与评估:指标选型、超参数搜索和三种模型的横向对比

4.1 评估指标:MSE、MAE和MAPE各管什么

训练损失用的是MSE,因为它对大误差的惩罚强,模型会优先去拟合晚高峰的尖峰。但MSE的数值受流量绝对大小影响,不同路口之间没法横向比较。评估阶段我建议同时输出MAE和MAPE,MAPE是百分比误差,最能反映模型在业务上的表现,但要注意流量接近0时MAPE会暴躁地放大。

交通流预测里常见的一个指标陷阱:MAPE在夜间低峰期没有意义,因为真实值很小,预测值稍偏一点,百分比就飙到几百。所以代码包里对MAPE做了保护,只计算真实流量大于某阈值的样本,比如只算流量大于50辆/小时的时刻。

def evaluate_model(y_true, y_pred, min_volume=50): y_true = np.asarray(y_true) y_pred = np.asarray(y_pred) mse = np.mean((y_true - y_pred) ** 2) mae = np.mean(np.abs(y_true - y_pred)) mask = y_true > min_volume if mask.sum() > 0: mape = np.mean(np.abs((y_true[mask] - y_pred[mask]) / y_true[mask])) * 100 else: mape = float("inf") return mse, mae, mape

这个min_volume参数在代码包里明确留出来了。做课设时很多人复制了一份评估函数却发现评估结果不理想,原因就是把夜间零点到凌晨五点的样本全算进去了。时间维度的另一个细节:预测未来三个窗口(15分钟)时,通常只需要评估第三个窗口的误差,前两个窗口误差天然更小,它们只能作为参考,不能拿第一个窗口的误差去验证15分钟预测能力。

4.2 超参数怎么定:时间步、学习率和早停

我拆这个代码包时最深的感受是:模型结构对结果的影响远小于数据预处理和训练参数。时间步window_size对结果的影响最直接,我在一个两车道路口样本上做过简单网格搜索,window_size从6提到12,MSE下降约12%;从12提到24,MSE反而回升。原因不是模型学不到更长依赖,而是5分钟粒度的交通流自相关在60分钟之后衰减到很低,喂太多无关信息反而干扰了关键近邻信息。

学习率用Adam优化器时初始值1e-3最稳妥,loss震荡不下降时降到1e-4。代码里还加了ReduceLROnPlateau,它在验证集指标连续5个epoch不降时把学习率除以5。

optimizer = optim.Adam(model.parameters(), lr=1e-3) scheduler = optim.lr_scheduler.ReduceLROnPlateau( optimizer, mode="min", factor=0.5, patience=5, verbose=False ) for epoch in range(max_epochs): model.train() for x_batch, y_batch in train_loader: optimizer.zero_grad() pred = model(x_batch) loss = nn.MSELoss()(pred, y_batch) loss.backward() nn.utils.clip_grad_norm_(model.parameters(), max_norm=5.0) optimizer.step() val_loss = validate(model, val_loader) scheduler.step(val_loss) if early_stopping.should_stop(val_loss): break

clip_grad_norm_是这里最重要的防翻车手段,很多读者直接套用图像分类的训练代码,没有做梯度裁剪。交通流序列数据里偶尔会出现一个明显偏差的样本,它的梯度可能比正常样本大几个数量级,一次更新就把模型参数推出正常区域,之后loss变成NaN。max_norm=5.0是我常用的值,你可以理解为“每个参数的梯度向量长度上限”,和batch size没有直接关系。

训练轮数不要硬定死。代码包里的EarlyStopping耐心是15个epoch,也就是说验证集连续15轮不下降才停,给模型足够时间去跳出局部极小。我先跑30个epoch看基础收敛情况,再决定是否加大模型容量。模型容量加大后必须同步加大耐心,否则模型还没收敛就早停了。

4.3 batch size和denormalization的配合

batch size在时序预测里的作用和图像任务不同,它影响的是每个batch里时间窗口的覆盖范围。batch size取32时,一个batch包含32个不同起始时刻的窗口,分布在一天的不同时段;batch size取256时,极端情况下一个batch可能全部来自同一个时段,比如全是夜间数据,参数更新方向就偏向夜间模式。这就是为什么交通流预测里batch size不要盲目往大调。

代码包里默认32,跑大数据集时可以调到64。如果显存紧张,不要先砍batch size,优先砍window_size,因为砍batch size导致训练波动太大,要用更多epoch才能弥补。

预测结果反归一化是最容易出现细微错误的一环。很多人在训练前用MinMaxScaler做了归一化,预测后忘了用同一个scaler.inverse_transform把结果变回真实流量数值。这里有一个隐藏问题:scaler是在完整训练集上fit的,测试集预测结果反归一化时,用的仍是训练集的缩放参数,这是正确的。如果你在测试集上重新fit了一个scaler再去反归一化,得到的数值整体偏差,而且偏差量随测试集分布漂移变化。

pred_scaled = model(x_test_tensor).detach().cpu().numpy() pred_real = scaler.inverse_transform(pred_scaled.reshape(-1, num_lanes)).reshape(-1, horizon, num_lanes) y_test_real = scaler.inverse_transform(y_test.reshape(-1, num_lanes)).reshape(-1, horizon, num_lanes)

reshape的维度顺序在代码包里特别容易错:模型的输出形状是(batch, horizon, num_lanes),inverse_transform要求二维(样本数, 特征数),必须先合并batch和horizon两个维度,反归一化后再拆回来。顺序搞反了会得到一组形状正确但数值错位的矩阵,plot出来的曲线看着像模像样,其实每个时间点的数据都对不上。

4.4 三种模型的横向对比:别只盯测试集均值

用同一份预处理数据分别训练SAE+LSTM和SAE+GRU后,代码包会输出一个对比表。典型的结论是:LSTM在流量突变时段表现略好,GRU在平稳时段误差更低,总体差异不超过8%。这个差异程度在交通流预测领域是常态,不要指望GRU一定不如LSTM。真正显著的差异发生在训练时间上,GRU在CPU上训练一个epoch大约比LSTM快接近30%,在路口数量多、模型反复调参的场景里,这个时间优势很实用。

做横向对比时有个评估陷阱:只看整体平均误差。交通工程业务更关心的是早高峰和晚高峰两个时段的表现。我通常把一天分成四个时段——早高峰(7-9点)、晚高峰(17-19点)、平峰(10-16点)、夜间(0-6点),分别计算MAE。这样能看出模型是不是只在夜间数据上表现好,白天高峰时段预测差得离谱。代码包里没有直接做这个分段评估,但我建议你自己加上,一个pd.cut或者np.where就能实现。

对比结果还受到随机种子影响。PyTorch默认初始化权重的随机性很大,同一个模型两次训练之间MSE可能漂移5%以上。如果报告里只写了某一次实验的数值,说服力不足,我通常在对比前固定三个种子各跑三遍,取中位数。代码包里在训练脚本开头就设了随机种子,但只固定了PyTorch,Python内置random和NumPy的种子也需要固定,三者不一致依然无法完全复现。

import random import numpy as np import torch random.seed(42) np.random.seed(42) torch.manual_seed(42) torch.cuda.manual_seed_all(42)

如果你在GPU上训练,还必须在训练前设置torch.backends.cudnn.deterministic = True和torch.backends.cudnn.benchmark = False,否则cuDNN的算法选择具有随机性,同一份代码重复跑两次loss曲线也可能不同。代价是训练速度变慢一些,但换来可复现性,写论文时这笔交易非常划算。

5. 避坑指南:交通流时序预测中常见的四个翻车现场

5.1 训练集和测试集之间时间泄漏

现象:验证集误差低到不可思议,MAPE不到2%,但把模型部署到新数据上预测,误差立刻翻几倍。

原因:构造滑动窗口时没有按时间顺序切分,或者切分前做了随机打乱。模型在训练时已经见过测试集的“未来”数据,预测变成了记忆。另一个常见原因是测试集本身包含训练集时间范围内窗口的一部分,尤其当窗口大小大于切分边界时,最后一个训练样本的窗口延伸到了测试集区间。

解决:先构造全部样本,再按索引切分。永远不要让任何一个样本的输入时间窗口和标签时间窗口跨越训练/测试边界。做法是以时间点为基准做切分,而不是以样本索引为基准,比如train_size = int(len(scaled_data) * 0.8)之后,确保窗口构造的循环只在range(0, train_size - window_size - horizon + 1)里运行。

5.2 归一化参数和逆变换的错位

现象:训练曲线很漂亮,但反归一化出来的流量预测值整体偏低,或者高峰期被明显压缩。

原因:训练集fit了scaler,测试时又用测试集的min/max反归一化,两套缩放基准不一致。或者是反归一化前reshape维度错位,把时间维度和特征维度搞混。

解决:训练前只对训练集scaler.fit(),然后用同一个scaler对训练集和测试集做transform。预测完成后也必须用同一个(不是重新fit的)scaler做inverse_transform。在代码里把scaler保存为joblib.dump(scaler, "scaler.bin"),部署阶段重新加载,不要重新计算。

5.3 LSTM训练到一半loss变成NaN

现象:训练前几个epoch正常,某轮loss突然变成NaN,然后永远回不来。

原因:最常见是梯度爆炸。交通流数据里突发拥堵时的流量值会猛然升高,这个样本产生的梯度极大,把一个batch的参数更新推爆。其次可能是输入数据里混入无穷值,归一化前原始数据中存在极大异常值没有被过滤干净。

解决:梯度裁剪是第一道防线,clip_grad_norm_(model.parameters(), max_norm=5.0)写在每次backward之后,这是我排查时第一个加的保护。第二道防线是把学习率从1e-3降到1e-4。如果还是NaN,检查输入数据里有没有inf或NaN残留,np.isnan(data).sum()在构造数据集后必须打印一次确认。如果数据没问题,验证模型最后一层输出是否有指数运算,有的话考虑换成线性输出。

5.4 同样的代码,换一批数据效果差很多

现象:在示例数据上测试集的MAPE稳定在12%左右,换到自己的路口数据后MAPE变成28%,而且怎么调参都降不下来。

原因:数据分布特性不同。示例数据可能是高速路段,流量波动平稳;你的数据是城市交叉口,受信号灯周期和排队影响,5分钟流量序列的突变程度完全不同。另一个潜在问题是传感器质量,部分路口的检测器在高峰期丢包严重,数据本身就不可靠。

解决:先做数据诊断,打印出连续7天的流量曲线,肉眼确认有没有周期性、有没有大量的块状缺失、晚高峰峰值是否稳定出现。如果原始数据质量不理想,把聚合粒度从5分钟改成15分钟,通常能大幅消除噪声。高峰期预测偏低是普遍现象,不要强求模型把尖峰完美拟合,交通流预测在突变时刻的精度上限是由数据本身决定的,模型只能尽可能接近这个上限。

提示:遇到效果变差时,先把预处理代码单独跑一遍,输出各时段统计量,再谈调模型。超过一半的情况问题出在数据管道,不是模型结构。

5.5 早停一轮就把权重恢复到最佳状态

现象:训练结束后保存的模型不是早停时权重,而是最后一个epoch的权重,导致验证集指标和最终测试指标不一致。

原因:我最初踩过这个坑,early_stopping.should_stop(val_loss)返回True时,确实触发了停止,但模型参数停留在最后一个epoch的状态上,而这个epoch的验证loss可能已经反弹了许多。

解决:早停逻辑里维护一个best_val_loss,每次验证后如果当前loss更低,就深拷贝一份model.state_dict()。训练结束后,先加载最佳权重再跑测试集。代码包里特意把load_best_weights接口单独暴露出来,复现实验前先确认这一点,否则你对比的LSTM和GRU可能都不是各自的最佳状态。

6. 扩展用法:把模型部署到更大路网,并用MATLAB做交叉验证

6.1 从单路口到多路口的迁移玩法

这套代码默认处理单路口多车道数据,但路网级交通流预测并不是把每个路口单独训练一个模型那么简单。我试过的可行做法是:先做路网拓扑编码,把相邻路口的流量作为额外特征拼进输入,然后用一个全局SAE压缩所有路口的特征,再用GRU做时序预测。这样单个模型就能服务一片区域,而不是每个路口各养一套模型。

代码包里没有实现完整的图神经网络,但特征拼接的接口已经留好:create_sequences接收的data矩阵可以自由扩展列数。我在把5个连续路口的数据拼到一起做实验时,模型输入维度从原来的单向4车道变成20维,SAE的hidden_dims需要改成[40, 16],其余训练逻辑完全不用动。这种做法在数据量足够时比单路口模型更稳,因为它让模型学到了上下游之间的传播关系,拥堵从一个路口蔓延到下一个路口的规律被显式捕捉。

多路口实验时要注意标签对齐:不同路口的峰值时间可能错开。下游路口的早高峰峰值通常比上游晚10到15分钟,如果直接用同一时刻的流量当预测目标,模型会混乱。我把特征矩阵里每一列都做了独立归一化,不共享scaler参数,部分缓解了这个错位问题。

6.2 导出预测结果和MATLAB交叉验证

深度学习的训练和预测在Python里完成没问题,但很多交通工程项目的数据清洗、基础统计和最终报告图表用MATLAB做。这套代码包里在预测结束后会把结果导出为CSV,方便两边共享数据格式。

import pandas as pd result_df = pd.DataFrame({ "time": test_timestamps, "y_true": y_test_real[:, -1].flatten(), "y_pred_lstm": lstm_pred[:, -1].flatten(), "y_pred_gru": gru_pred[:, -1].flatten(), }) result_df.to_csv("prediction_results.csv", index=False, float_format="%.2f")

在MATLAB里加载这个CSV后,我一般写一个简单脚本做两件事:一是画时间序列对比图,把y_true和两个模型的预测曲线叠在一起,按工作日和周末分面;二是计算滑动平均误差曲线,看误差是不是集中在特定时段。MATLAB在画图定制上比Matplotlib方便,论文出图我用它比较多。

交叉验证的意义不是证明Python模型的正确性,而是确认预测曲线和工程上常用的基线方法差异在可接受范围内。我习惯在MATLAB里跑一个简单的历史均值模型作为下界,如果LSTM预测误差比历史均值还差,那一定是数据或预处理环节出了问题,而不是模型不够强。

6.3 多步预测的两种输出策略

代码包默认一次输出未来三个时刻的预测,这是多步预测里的“直接多步”策略,实现简单,但三个预测窗口共享同一个隐藏状态,误差会随时间步累积。第二种策略是“递归多步”,把第一步预测结果当作输入,依次预测下一步,在代码里改动很小,但误差累积更严重,我不推荐用于交通流。

更稳妥的做法是训练三个独立模型,分别预测未来5分钟、10分钟、15分钟。代价是训练时间变三倍,但每个模型只专注一个目标,精度普遍比单模型多输出更好。我在正式实验里都是这么做的,把horizon=3的任务拆成三个horizon=1的模型。代码包为了演示方便保留了多输出模式,你换数据时建议比较一下这两种做法的差异。

6.4 部署时最容易忽视的时间戳同步

把模型接到实时数据流时,最后一个隐患是时间戳对齐。离线训练时数据是完整的历史序列,实时部署时你需要维护一个长度为window_size的滑窗,每来一个新数据,踢掉最旧的数据,再调用模型预测。这个滑窗里的时间戳必须是严格递增的,任何一个时刻数据延迟到达或重复推送,预测结果就会错位。

我见过一个生产事故:数据采集程序把同一个5分钟窗口的数据推送了两次,滑窗里出现两个相同的时刻,模型预测出的流量值比真实值高了将近一倍。解决办法是在数据入口加一个去重逻辑,按时间戳精确去重,重复数据直接丢弃。这段逻辑代码量不大但价值极高,我每次部署前都会把这个脚本单独测试一遍,确保实时数据流的每个时刻只被消费一次。

从那以后,我每次拿到新的交通流数据都会强制走一遍完整流程:先检查时间戳连续性,再做缺失值填充,构造窗口时打印首尾样本核对标签,最后才训练模型。这套习惯帮我挡掉了非常多莫名其妙的预测问题。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/7 3:14:04

Win11右键菜单太难用?注册表一键恢复经典菜单完整实操

从Windows 11正式发布那天起,右键菜单的变化就让不少办公党破防。日常高频使用的刷新、复制路径、重命名、压缩包解压这些功能,全被塞进了一个叫"显示更多选项"的二级菜单。每天重复点击几百次,手感和效率都肉眼可见地下降。更麻烦…

作者头像 李华
网站建设 2026/10/7 3:13:56

Win11一键恢复经典右键菜单:注册表原理与实操指南

Win11自从发布那天起,那套新右键菜单就是最大的槽点之一。特别是对于办公党来说,每天要重命名、压缩解压、复制路径几十次,结果每次右键都得先点一下“显示更多选项”,多一步操作,多一次烦躁。网上对Win11右键菜单的吐…

作者头像 李华
网站建设 2026/10/7 3:13:52

Windows服务命令行操作全攻略:sc、net与PowerShell实战解析

前阵子帮朋友处理一台Windows Server上的数据库服务问题,他打开服务管理器(services.msc)找了半天也没找到问题服务在哪,我直接在命令行里一条sc query过去,三秒钟定位到状态和依赖关系,顺手把启动类型改了…

作者头像 李华
网站建设 2026/10/7 3:13:49

DeepSeek论文降AI完全指南:从检测原理到实操案例

最近帮几个研究生朋友看论文,发现一个特别普遍的现象:初稿是DeepSeek写的,速度快到让人上瘾,但交给导师前自己用AIGC检测工具扫一遍,AI率动不动就是70%、80%,甚至更高。更头疼的是,很多同学不知…

作者头像 李华
网站建设 2026/10/7 3:13:05

医疗陪诊系统开发全解析:从微信小程序到后台架构实战

医院陪诊这块业务这两年肉眼可见地火了起来,尤其是一二线城市,独居老人就医、异地就诊、孕妇产检、术后复查这些场景,需求非常刚性。我之前帮一家本地生活服务公司从零搭过一整套医疗陪诊系统,涵盖微信小程序用户端、陪诊师端APP和…

作者头像 李华
网站建设 2026/10/7 3:13:02

Java毕设项目Linux部署全攻略:环境搭建到云服务器上线

1. 项目概述1.1 为什么把Java和Linux放在一起做毕设每年到毕业季,都会被同一个问题刷屏:毕设到底选什么方向?我的建议一直很明确——Java后端 Linux服务器部署这套组合,是风险最低、性价比最高的选择之一。原因很直白。Java生态足…

作者头像 李华