简介:一套基于深度学习的交通流量预测可视化网站项目源码,面向计算机、数学、电子信息等专业学生,适用于课程设计、期末大作业、毕设项目,也可作为深度学习与前端可视化开发的小白实战练习。压缩包共包含2007个文件,大小约102.37MB;其中1852个Markdown文档承担项目说明与学习笔记,另有JavaScript脚本、JSON配置、CSS样式、Python脚本和Word文档,覆盖页面交互、数据配置、样式布局及模型调用等环节,便于按需查阅和调试。下载后可直接运行体验,前端可视化页面能直观展示交通流量预测结果;同时,源码中保留了数据流说明、模型处理逻辑及环境配置要点,方便二次开发和改造,适合希望从零搭建同类系统的学习者。已有170人浏览学习,可作为同类课题的重要参照。
1. 基于深度学习的交通流量预测可视化网站:先看清这个工程包能解决什么
做过交通流量预测相关毕设或课程设计的同学,大概率经历过这种尴尬:模型在 Jupyter 里跑得挺好看,loss 曲线一路往下掉,可一到交付环节,导师或评委要看的是“能打开、能交互、能演示”的网站,而不是一沓训练日志。这个项目包的定位就是把“深度学习算法 + 后端服务 + 可视化大屏”串成一条完整链路,让你从数据清洗一路做到网页展示,是一个典型的深度学习实战项目案例。它适合两类人:一类是拿它当毕设案例的在校生,需要快速理解算法与工程怎么结合;另一类是入门深度学习的开发者,想找一份能直接复现、能改参数、能扩展成自己作品的参考工程。下面我按实际拆解的顺序,把这个包从数据到模型、从接口到页面一层层讲透。
2. 数据准备与预处理:把路口流量记录变成模型能吃的滑窗样本
2.1 数据来源与字段设计:时间序列数据怎么组织才喂得进模型
交通流量预测本质上是一个多变量时间序列回归问题。网站底层的模型输入不是一张图片,也不是一行行零散的流水账,而是按时间排序的连续观测值序列。这个项目包里常见的做法是使用结构化表格数据,每一行代表某个路口在某个时间粒度的统计结果。字段一般包括:时间戳、路口编号、车道方向、车流量计数、平均速度、车道占用率。
我一般会先把原始 CSV 读进来,看看字段类型和缺失情况,再按时间粒度做聚合。为什么强调聚合?因为原始检测器上报的数据往往是随机间隔的,比如高峰期每秒一条、平峰期 5 秒一条,直接丢给模型会导致序列长度和采样间隔不一致,训练出来的模型对时间的理解是乱的。常见做法是统一重采样到 5 分钟或 15 分钟一个点,把车流量计数求和、平均速度做均值。
import pandas as pd # 读取原始检测数据,假设包含 time, road_id, direction, volume, speed, occupancy df = pd.read_csv("traffic_raw.csv", parse_dates=["time"]) df = df.set_index("time") # 按路口+方向分组,重采样到 15 分钟粒度 # volume 用 sum:15 分钟内通过的车总数 # speed 和 occupancy 用 mean:平均速度和平均占用率 df_agg = df.groupby(["road_id", "direction"]).resample("15min").agg({ "volume": "sum", "speed": "mean", "occupancy": "mean" }).reset_index() # 按时间排序,构建单一连续序列 df_agg = df_agg.sort_values(["road_id", "direction", "time"]) print(df_agg.head())这段代码的关键逻辑是resample("15min")把不规则时间戳对齐成固定间隔,groupby保证不同路口和方向的数据不会被混在一起。参数说明:volume用sum是因为车流量这种计数型字段在时间上具备可加性,而speed和occupancy用mean更合理,取平均才能代表这段时间的整体状态。如果你手里的数据是 5 分钟粒度,也可以不改聚合窗口,直接进入下一步,但要注意后续滑窗长度要对应调整。
2.2 归一化、滑窗与训练集划分:参数怎么定才有复现性
数据组织好之后,下一步是把序列切成“输入窗口 + 预测目标”的样本对。这个环节有两个高频翻车点:一是忘了做归一化,导致 loss 震荡;二是滑窗顺序切分没做干净,测试集里混进训练集的信息。先看代码:
import numpy as np from sklearn.preprocessing import MinMaxScaler # 选择用于建模的特征列 feature_cols = ["volume", "speed", "occupancy"] data = df_agg[feature_cols].values.astype("float32") # 关键点:只用训练集 fit,验证集和测试集只 transform train_size = int(len(data) * 0.7) val_size = int(len(data) * 0.15) train_data = data[:train_size] val_data = data[train_size:train_size + val_size] test_data = data[train_size + val_size:] scaler = MinMaxScaler(feature_range=(0, 1)) scaler.fit(train_data) train_scaled = scaler.transform(train_data) val_scaled = scaler.transform(val_data) test_scaled = scaler.transform(test_data) def create_sequences(data, seq_len=24, pred_len=3): X, y = [], [] for i in range(len(data) - seq_len - pred_len + 1): X.append(data[i:i + seq_len]) # 预测未来 pred_len 个时间点的 volume y.append(data[i + seq_len:i + seq_len + pred_len, 0]) return np.array(X), np.array(y) seq_len, pred_len = 24, 3 X_train, y_train = create_sequences(train_scaled, seq_len, pred_len) X_val, y_val = create_sequences(val_scaled, seq_len, pred_len) X_test, y_test = create_sequences(test_scaled, seq_len, pred_len) print(X_train.shape, y_train.shape)这里最值得注意的参数是scaler.fit(train_data)而不是scaler.fit(data)。如果拿全量数据做归一化,测试集的 min/max 信息会提前泄露给模型,测试指标会虚高,真上了网站、接上新数据就露馅。seq_len=24的含义是“用过去 24 个 15 分钟窗口(即 6 小时)预测未来 3 个窗口(45 分钟)”,这个取值决定了模型能看到多长的历史;pred_len=3是预测步长,毕设场景下 3 步足够演示,业务上想预测更远可以调到 6 或 12,但误差会快速累积。
2.3 特征扩展:加时间特征比换模型更有效
很多新手一上来就折腾模型结构,但在这个项目里,加时间特征是性价比最高的提点方式。交通流量有极强的周期性:早高峰 7 点到 9 点、晚高峰 17 点到 19 点、周末和工作日形态完全不同。把这些信息作为额外特征喂给模型,LSTM 能更快学到规律。
# 从时间索引中提取小时、星期、是否节假日 time_index = df_agg["time"] hour = time_index.dt.hour.values / 24.0 # 归一化到 [0, 1) dayofweek = time_index.dt.dayofweek.values / 7.0 is_weekend = (time_index.dt.dayofweek >= 5).astype("float32") # 把特征拼到原始数据后面 feature_cols_ext = feature_cols + ["hour", "dayofweek", "is_weekend"] data_ext = np.hstack([ data, hour.reshape(-1, 1), dayofweek.reshape(-1, 1), is_weekend.reshape(-1, 1) ]).astype("float32")这里的参数含义要理顺:hour / 24.0和dayofweek / 7.0是归一化处理,把周期性编码到 0~1 区间,避免数值尺度差异过大影响梯度;is_weekend是二值特征,属于硬编码。后续的滑窗生成逻辑不变,只是feature_cols从 3 列变成 6 列,LSTM 的input_size也要对应改成 6。做完这步,模型对周末和平日的流量差异会敏感很多,这是我在实测里验证过的,比把 LSTM 隐藏层从 32 加到 64 的提升更明显。
3. 模型选型与训练:LSTM 作为基线的落地参数与评估方式
3.1 为什么基线模型选 LSTM:时序预测的选型逻辑
这个项目包的模型部分是基于深度学习的,选 LSTM 作为基线是合理的默认方案。交通流量序列有长依赖特征:今天早高峰的拥堵程度往往和相关路口前几天的同时段数据有关,普通 RNN 在梯度回传时容易衰减,学不到这种跨天规律。LSTM 通过输入门、遗忘门、输出门结构,把长期信息保存在细胞状态里,天然适合这种周期性强的时间序列。GRU 是 LSTM 的简化版,参数更少、训练更快,如果数据集不大(比如只有几千条样本),GRU 往往效果不输 LSTM。Transformer 这类模型在流量预测上能做,但需要的数据量和调参成本都更高,对毕设和入门项目来说性价比偏低,容易把时间耗在“跑不动”和“过拟合”上。所以这个包里以 LSTM 为主力,是动手深度学习框架下的务实选择:模型不黑匣子,超参数好理解,出问题也好排查。如果想让项目有亮点,可以后期加一个注意力层或把 LSTM 换成 GRU 做对比实验,这部分我放到最后一章讲。
3.2 训练脚本与超参数:Learning Rate、Batch Size、Early Stopping 怎么配
训练环节是整个项目里最容易“玄学”的部分。同样的模型,学习率差一个数量级,loss 可能从正常收敛变成直接爆炸。下面是一份可复现的 PyTorch 训练代码,我把关键超参都写在注释里:
import torch import torch.nn as nn from torch.utils.data import TensorDataset, DataLoader # 定义 LSTM 模型结构 class TrafficLSTM(nn.Module): def __init__(self, input_size=6, hidden_size=64, num_layers=2, output_size=3): super(TrafficLSTM, self).__init__() self.lstm = nn.LSTM( input_size=input_size, hidden_size=hidden_size, num_layers=num_layers, batch_first=True, dropout=0.2 if num_layers > 1 else 0 ) self.fc = nn.Linear(hidden_size, output_size) def forward(self, x): # x shape: (batch, seq_len, input_size) out, _ = self.lstm(x) # 取最后一个时间步的隐藏状态作为输出 out = out[:, -1, :] return self.fc(out) # 超参数:新手先用这一组,不要一上来就调大 input_size = 6 # volume + speed + occupancy + hour + dayofweek + is_weekend hidden_size = 64 # 隐藏层维度,数据量大可以翻倍到 128 num_layers = 2 # 层数,2 层足够,再深容易过拟合 output_size = 3 # 预测未来 3 个 15 分钟窗口的 volume learning_rate = 1e-3 batch_size = 64 num_epochs = 60 train_dataset = TensorDataset(torch.tensor(X_train), torch.tensor(y_train)) train_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True) model = TrafficLSTM(input_size, hidden_size, num_layers, output_size) optimizer = torch.optim.Adam(model.parameters(), lr=learning_rate, weight_decay=1e-5) scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(optimizer, mode="min", factor=0.5, patience=5) criterion = nn.MSELoss() best_val_loss = float("inf") patience_counter = 0 for epoch in range(num_epochs): model.train() train_loss = 0.0 for X_batch, y_batch in train_loader: optimizer.zero_grad() pred = model(X_batch) loss = criterion(pred, y_batch) loss.backward() # 梯度裁剪:防止 loss 震荡和梯度爆炸 nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0) optimizer.step() train_loss += loss.item() # 验证集评估 model.eval() with torch.no_grad(): val_pred = model(torch.tensor(X_val)) val_loss = criterion(val_pred, torch.tensor(y_val)).item() scheduler.step(val_loss) # Early Stopping:连续 8 个 epoch 验证集不下降就停 if val_loss < best_val_loss: best_val_loss = val_loss torch.save(model.state_dict(), "best_model.pt") patience_counter = 0 else: patience_counter += 1 if patience_counter >= 8: print(f"Early stopping at epoch {epoch}") break print(f"Epoch {epoch}, train_loss={train_loss:.4f}, val_loss={val_loss:.4f}")参数说明分成三部分。第一是结构参数:input_size=6对应上一章扩展后的 6 个特征;hidden_size=64控制模型容量,数据量在 1 万条以下用 64 足够,硬调到 128 只会让训练变慢且更容易过拟合;num_layers=2是经验值,单层表达能力有限,三层以上在中小数据集上收益很小。第二是优化参数:learning_rate=1e-3配合 Adam 是时序预测的标准起点,如果 loss 前几个 epoch 就变成 NaN,直接降到 3e-4;weight_decay=1e-5是 L2 正则化,防止参数过大,这也是我在实际调参中习惯性加上的保险丝;clip_grad_norm_梯度裁剪是我强烈建议保留的一行,它能在极端情况下把梯度范数压到 1.0,避免一次异常 batch 把整个模型权重冲垮。第三是训练策略:ReduceLROnPlateau在验证集 loss 连续 5 轮不降时把学习率减半,配合patience=8的 Early Stopping,可以在 60 个 epoch 之内收敛到稳定值,同时避免后期过拟合。
3.3 模型评估:别只盯 MAE,要看预测曲线和真实曲线的贴合度
训练完的模型不能只看 loss 数字,交通流量预测的评估要落实到“预测的早晚高峰位置准不准”上。常用的指标有三个:MAE(平均绝对误差)、RMSE(均方根误差)、MAPE(平均绝对百分比误差)。但 MAPE 在流量接近 0 的夜间时段会变得极大,因为分母趋近于 0。这个坑我在实际项目中踩过:夜间车流量为 0 的样本把 MAPE 拉高到 300% 以上,看起来模型完全不可用,但其实白天时段的预测非常准。
import numpy as np # 反归一化:把预测结果还原成真实车流量 y_test_inv = scaler.inverse_transform( np.concatenate([y_test, np.zeros((len(y_test), 5))], axis=1) )[:, :pred_len] y_pred_inv = scaler.inverse_transform( np.concatenate([y_pred, np.zeros((len(y_pred), 5))], axis=1) )[:, :pred_len] # 过滤掉夜间接近 0 的样本再算 MAPE mask = y_test_inv[:, 0] > 10 mae = np.mean(np.abs(y_pred_inv[mask] - y_test_inv[mask])) rmse = np.sqrt(np.mean((y_pred_inv[mask] - y_test_inv[mask]) ** 2)) mape = np.mean(np.abs((y_pred_inv[mask] - y_test_inv[mask]) / y_test_inv[mask])) * 100 print(f"MAE={mae:.2f}, RMSE={rmse:.2f}, MAPE={mape:.2f}%")这段代码里的inverse_transform有个小技巧:由于反归一化要求输入维度必须与训练时的特征维度一致,而y只有 volume 一列,所以要先用 0 填充到 6 列,再取回第一列。mask = y_test_inv[:, 0] > 10这个阈值过滤就是上面说的踩坑应对:只统计流量大于 10 的时段,避免 0 流量样本干扰 MAPE。除了指标计算,我建议你务必把测试集的预测曲线和真实曲线画在一起,用 ECharts 或 matplotlib 都行。如果两条曲线的早晚高峰峰值位置完全错开,说明模型根本没学到周期性,这时再看 MAE 没有意义。可视化对比这一步,是判断模型是否真正“学会”的最直观手段,也是在答辩时最有说服力的素材。
4. 后端服务与可视化大屏:把模型输出变成可交互的预测网站
4.1 Flask 后端:模型加载、推理接口与缓存设计
模型训练完成并保存为best_model.pt后,网站的职责就是加载这个模型、接收前端传来的最近流量窗口、返回预测结果。这个项目包里后端用的是 Flask,选它的理由是足够轻量、生态成熟,对毕设和中小型可视化网站完全够用。如果你对性能有更高追求,可以换成 FastAPI,但没必要,因为流量预测网站的并发压力很小,瓶颈在模型推理而不是框架本身。
from flask import Flask, request, jsonify import torch import numpy as np from collections import OrderedDict app = Flask(__name__) # 加载训练好的模型 model = TrafficLSTM(input_size=6, hidden_size=64, num_layers=2, output_size=3) state_dict = torch.load("best_model.pt", map_location="cpu") # 处理训练时可能出现的 module. 前缀 new_state_dict = OrderedDict() for k, v in state_dict.items(): new_state_dict[k.replace("module.", "")] = v model.load_state_dict(new_state_dict) model.eval() # 全局缓存:同一个输入窗口 5 分钟内不重复推理 cache = {} @app.route("/api/predict", methods=["POST"]) def predict(): data = request.get_json() window = data.get("window") # 前端传来的最近 24 个时间步特征 if window is None or len(window) != 24: return jsonify({"error": "window must contain 24 steps"}), 400 # 时间戳作为缓存 key,避免重复计算 cache_key = data.get("timestamp", "") if cache_key in cache: return jsonify(cache[cache_key]) input_tensor = torch.tensor(np.array(window, dtype=np.float32)).unsqueeze(0) with torch.no_grad(): pred = model(input_tensor).squeeze(0).numpy().tolist() result = {"predicted_volume": pred} cache[cache_key] = result return jsonify(result) if __name__ == "__main__": app.run(host="0.0.0.0", port=5000, debug=False)接口设计有三个细节值得注意。第一是window的长度校验:前端必须传 24 个时间步,每个时间步 6 个特征,这和后端模型训练时的seq_len、input_size严格对应,如果前端传 23 步,模型推理会因为维度对不上直接报错。第二是cache的设计:前端大屏通常每 5 分钟轮询一次,同一个输入窗口在短时间内会被重复请求,加一层内存缓存可以避免模型重复推理,这也是我在实际部署时吃过亏后加上的——不缓存的话,4 个路口同时刷新页面,Flask 单线程直接卡死。第三是map_location="cpu":训练可能在 GPU 上进行,但部署环境通常是纯 CPU,加载模型时不指定map_location会报 CUDA 不可用的错误。
4.2 可视化大屏:ECharts 对接预测接口的页面结构
可视化部分是整个网站的“脸面”,也是评委第一眼看到的东西。这个项目包里的前端展示用 ECharts 实现,折线图同时绘制历史流量和未来预测,这是交通流量预测最经典的可视化形态。我建议页面布局按“总览 + 明细”来组织:顶部是一个大屏折线图,展示当前选中路口的 24 小时流量曲线,其中实线部分是历史实测值,虚线部分是模型预测值;下方可以用卡片或柱状图展示各路口未来 1 小时流量排行。
// 前端 ECharts 配置:对接 /api/predict 接口 const chart = echarts.init(document.getElementById("trafficChart")); async function fetchPrediction(roadId) { // 从后端获取最近 24 个时间步的窗口数据 const historyResp = await fetch(`/api/history?road_id=${roadId}`); const historyData = await historyResp.json(); const predictResp = await fetch("/api/predict", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ window: historyData.window, timestamp: new Date().toISOString() }) }); const predictData = await predictResp.json(); // 合并历史 + 预测数据 const timeLabels = historyData.time.concat(predictData.time); const volumeValues = historyData.volume.concat(predictData.predicted_volume); chart.setOption({ xAxis: { type: "category", data: timeLabels }, yAxis: { type: "value", name: "车流量" }, series: [ { name: "历史流量", type: "line", data: historyData.volume, lineStyle: { width: 2 } }, { name: "预测流量", type: "line", data: volumeValues.slice(historyData.volume.length), lineStyle: { type: "dashed", width: 2 } } ] }); } // 每 5 分钟轮询一次,刷新预测数据 setInterval(() => fetchPrediction("road_001"), 5 * 60 * 1000);这里的historyData.window是后端api/history接口返回的最近 24 个时间步的多维特征数组,前端不需要理解特征含义,原样传给预测接口即可。ECharts 配置中的setOption是整个图表渲染的核心,series数组里放两条线:type: "line"表示折线图,历史流量用实线、预测流量用虚线,视觉上一眼就能分清边界。轮询间隔5 * 60 * 1000毫秒是一个折中值:交通流量 15 分钟一个数据点,5 分钟刷新足够及时,也不会给 Flask 后端造成压力。我把这个页面结构称为“大屏”的原因在于,ECharts 的图表是流式渲染的,配合深色主题和粗线条,整个页面放到 1080P 大屏上效果很出彩,这也是答辩演示时的加分项。
5. 避坑与常见问题:训练、部署、数据三个环节的踩坑记录
5.1 训练曲线震荡不收敛,loss 始终在 0.5 上下抖
现象:训练了十几个 epoch,train_loss 忽高忽低,一直降不到 0.1 以下,val_loss 也不稳定。
原因:最常见的是学习率偏大、数据未归一化或 batch 里混入了异常样本。交通流量数据有时会出现传感器故障导致的极大值,比如某个 15 分钟窗口的 volume 突然变成 9999,这会拉爆梯度。
解决:按顺序检查三件事。第一,确认scaler.fit(train_data)已经执行,而不是直接把原始数据丢进模型;第二,把learning_rate从 1e-3 降到 3e-4;第三,在loss.backward()后加clip_grad_norm_(model.parameters(), max_norm=1.0)。这三步做完,大多数震荡问题都能解决。如果还不行,检查数据里有没有异常值,可以用df["volume"].quantile(0.999)看一下上限。
5.2 测试集预测曲线整体滞后一个时间步,峰值对不上
现象:把真实曲线和预测曲线画在一起,预测的早高峰总比真实晚一个时间点,看起来像是“昨天”的数据。
原因:这是时序预测的经典问题。流量序列的自相关性很强,第 t 时刻的值和第 t+1 时刻高度相似,模型发现“复制上一个值”就能把 loss 压得很低,于是学会了偷懒——输出接近输入窗口最后一个时间步的值,导致预测曲线滞后。
解决:两个手段配合使用。一是在损失函数上做文章,不只计算单步损失,而是把未来 3 步的损失都计入,逼模型去学趋势而不是复制;二是做差分特征,把“当前时刻流量”换成“当前时刻流量减上一时刻流量”,让模型去预测增量,预测结果再还原成绝对值。从我的经验看,差分特征对滞后问题的改善最明显。
5.3 加载模型时报错:size mismatch for fc.weight
现象:model.load_state_dict(state_dict)抛出size mismatch,fc.weight维度对不上。
原因:训练时的模型结构和部署时的模型结构不一致。常见情况有两种:第一种是训练时改了hidden_size但部署代码里还是旧的 64;第二种是训练用nn.DataParallel,保存的权重 key 全部带了module.前缀,加载时 key 对不上。
解决:先检查训练脚本里的模型参数和部署脚本是否完全一致,hidden_size、input_size、num_layers一个都不能差。如果是DataParallel的问题,用我第 4 章写过的OrderedDict循环把module.前缀去掉再加载。我自己就栽过这个跟头:训练时顺手套了个DataParallel,部署时忘了处理前缀,排查了半天才发现是 key 的问题。
5.4 可视化大屏数据刷新卡顿,页面转圈
现象:网页打开正常,但每次刷新图表都卡顿,后端接口偶尔超时,多个路口同时切换时尤其严重。
原因:两层问题叠在一起。前端每次setOption都是全量替换数据,没有做增量更新;后端每次收到请求都重新执行模型推理,4 个路口轮询时单线程 Flask 处理不过来。
解决:前端把setOption改成merge: true增量合并,只更新变化的 series;后端加缓存,同一个输入窗口在 5 分钟内直接返回缓存结果。如果并发压力再大,就把轮询间隔从 5 分钟调整到 15 分钟,毕竟流量数据本身就是 15 分钟一个点,5 分钟刷新更多是心理安慰。
5.5 测试集指标特别好,但上线后预测效果崩了
现象:训练时 MAE 很低,但把模型接到网站上、用实时数据测试时,预测值和真实值差得离谱。
原因:数据泄漏。最典型的是归一化时用了全量数据的scaler.fit(data),测试集的 min/max 信息参与到了尺度变换中;另一种是滑窗切分时没有按时间顺序,而是随机打乱后再切分,导致训练集和测试集相互混杂。
解决:严格按 2.2 的方法做——训练集fit,验证集和测试集只transform;时间序列必须按顺序切分,不能随机shuffle。这个问题的隐蔽之处在于,泄漏时测试指标很好但模型泛化能力很差,属于“看起来对,实际没用”的典型坑,我在做第一个版本时就被它坑了一周。
6. 进阶用法:从单步预测到滚动预测的模型更新细节
网站如果要展示“未来 6 小时预测曲线”,单靠pred_len=3是不够的,因为模型一次性只能输出未来 3 个 15 分钟窗口。扩展思路是滚动预测:先预测出第 1 步,把这个预测值作为新的输入拼接到窗口末端,再预测第 2 步,以此类推。这是把短时预测模型扩展成长序列预测的常见做法,也是“预测值反馈”的核心机制。
def rolling_predict(model, initial_window, steps=24): """ initial_window: shape (seq_len, input_size) 的初始窗口 steps: 想要预测的未来时间步数量 """ model.eval() window = initial_window.copy() predictions = [] with torch.no_grad(): for _ in range(steps): # 取当前窗口的最后 seq_len 步作为模型输入 input_tensor = torch.tensor(window[-seq_len:]).unsqueeze(0) pred = model(input_tensor).squeeze(0).numpy() # 把预测的 volume 填入新时间步 new_step = window[-1].copy() new_step[0] = pred[0] # volume 列 # 时间特征递推:每步前进 15 分钟 # 真实场景中,hour 和 dayofweek 需要按时间推算 # 这里简单取当前窗口最后一行的时间特征 new_step[3] = (new_step[3] * 24 + 0.25) / 24 # hour + 15min if new_step[3] >= 1.0: new_step[3] -= 1.0 new_step[4] = new_step[4] # dayofweek 简化处理 # 窗口推进:丢掉最早的 1 步,拼接新预测 window = np.vstack([window[1:], new_step]) predictions.append(pred[0]) return np.array(predictions)这段代码的关键点有三个。第一,window[-seq_len:]是滚动预测的核心:每预测一步,窗口就整体向前滑一步,最早的数据被丢弃,最新预测值补充进来,这模拟了真实场景中“用最新状态预测下一步”的流程。第二,new_step[0] = pred[0]表示只用预测的 volume 回填,因为模型输出只有 volume 一列,speed 和 ocupancy 这些特征在滚动过程中没有对应预测值,只能用上一个窗口的末值近似,这是滚动预测误差累积的主要来源。第三,new_step[3]对 hour 特征做了手动的 15 分钟递推,这是为了保持时间特征与预测步长一致,不然模型会认为所有预测都发生在同一时刻。你可以把steps=24理解为预测未来 6 小时,但要注意:滚动步数越多,误差累积越大,越靠后的预测越不可信。
最后说一个实战习惯:我第一次做滚动预测时,直接拿预测值连续 24 步回填,结果误差像滚雪球一样越来越大,第 12 步之后的预测曲线已经是一条直线,彻底失去参考价值。从那以后,我每次部署预测网站都会强制走一遍“先单步、后滚动、误差排序”的验证流程——先确认单步预测准确,再测 6 步滚动误差,最后根据误差增长速度决定页面上展示多少步的预测范围,而不是盲目地把 24 步预测全部摆上大屏。这个习惯帮我避掉了不少“演示翻车”的场景,希望帮到你。
本文还有配套的精品资源,点击获取