小波变换与图注意力网络:交通流量预测的MATLAB实现
简介针对城市交通流量预测中时空特征提取难的问题一份基于小波变换与图注意力网络WT-GAT的MATLAB项目实例文档面向智能交通研究人员、工程师及高校师生分享适合具备一定MATLAB和机器学习基础、希望掌握深度学习与信号处理融合技术的读者。文档以完整项目为主线涵盖数据预处理、邻接矩阵构建、特征归一化、模型训练、性能评估与可视化等核心环节并设计GUI界面便于交互调试可深入理解小波变换多尺度时序特征提取与GAT空间依赖建模的协同机制。资源包共1个docx文档大小65KB内部按项目背景、挑战与解决方案、模型架构等目录有序组织提供代码详解与部署思路便于快速复现实验并开展科研或工程实践。内容讲解细致尤其适合需要理解小波分解参数设置、邻接矩阵构建逻辑与GAT注意力权重计算等细节的读者。目前已有72人学习值得时空数据建模与智能交通方向的开发者参考。1. 交通流量预测为什么选小波变换加图注意力网络这套组合交通流量预测这件事最难的不是把LSTM调深而是同时吃透两条轴时间轴上的非平稳趋势空间轴上的路网依赖。MATLAB 实现里比较完整的一套组合是 WT-GAT先用小波变换WT把传感器流量序列拆成多分辨率子带再交给图注意力网络GAT在路网拓扑上学习动态邻居权重最后装进带 GUI 的预测工具里从加载数据到出图一条线走通。这套方案适合手头有路口流量传感器矩阵、想从单一时间序列模型切换到时空模型的工程人员。下面按原理、代码骨架、GUI 设计和验证顺序展开尽量让照着手敲就能跑通。2. WT-GAT 的原理与数据流从序列分解到路网注意力2.1 小波变换把流量序列拆成趋势、细节和噪声交通流量的原始序列是典型的非平稳信号24 小时周期性叠加早晚高峰突变再加上事故、天气造成的局部尖峰。直接用原始序列训练时模型要同时拟合低频趋势和高频毛刺容量被分散预测误差集中在高峰切换的十几个样本点。小波变换通过母小波的伸缩和平移把信号分解到不同尺度能把趋势项、周期项和随机项分开处理。离散小波变换的做法是每次把信号通过一对高低通滤波器得到低频近似系数和高频细节系数再对低频系数继续分解。level 级分解后得到 1 个近似子带和 level 个细节子带。MATLAB 里wavedec负责分解wrcoef可以把某级系数重建回原采样长度便于和原始时间轴对齐。对 15 分钟粒度的日数据3 级分解是常用选择近似分量对应当日趋势中间两层细节对应半小时到两小时尺度的拥塞波动第一层细节对应随机噪声。% 单节点流量序列的 3 级小波分解 x flow(1, 1:96); % 某传感器一天的数据(15min粒度) [c, l] wavedec(x, 3, db4); % c 为拼接系数, l 为各层长度 a3 wrcoef(a, c, l, db4, 3); % 近似分量, 长度回到 96 d1 wrcoef(d, c, l, db4, 1); % 第1级细节, 对应高频噪声 d2 wrcoef(d, c, l, db4, 2); d3 wrcoef(d, c, l, db4, 3);参数说明flow是 N×T 的传感器流量矩阵这里取第 1 个节点的 96 个点wavedec的第三个参数选db4它是 Daubechies 4 阶小波时域支撑短、平滑性够适合交通流这种没有剧烈尖峰的信号。level 设为 3 兼顾趋势保留和特征维度再往上会把日趋势也拆碎。校验分解正确性最直接的办法是把d1到d3和a3逐点相加结果应等于原序列。2.2 图注意力网络节点权重随路况动态变化路网上的流量传感器天然构成一张图节点是传感器边是上下游连接或者基于距离的空间邻接。普通 GCN 的做法是用归一化邻接矩阵做固定权重的邻居聚合问题在于权重一经归一化就固定了早高峰和事故场景下真实的空间依赖差别很大。图注意力网络把邻居聚合权重换成注意力系数权重随输入特征动态变化。注意力系数的做法是对节点 i 的每个邻居 j 计算相关性分数再做行 softmax。分数公式为LeakyReLU(a^T [W h_i || W h_j])其中 W 是共享的特征变换矩阵a 是注意力向量|| 表示拼接。节点 i 的更新输出是邻域特征的加权和多头注意力把多组结果拼接或平均抑制单头注意力在局部数据上的偏置。相比 GCN 的固定权重GAT 在拥堵消散、信号灯失衡这类动态场景里能把注意力及时转移到真正影响下游流量的节点上。实操中邻接矩阵不参与反向传播它只决定哪些节点对参与注意力计算。这个设计有个直接好处即使邻接矩阵是手工按路网拓扑画的模型也不会被不准确的边权重误导因为边关系变成候选集具体用多少由注意力决定。2.3 WT-GAT 整体数据流与张量形状WT-GAT 的经典接法是逐节点做小波分解把各子带沿通道维堆叠再展平成单个特征向量作为该节点的输入特征进入 GAT 层。这一步等价于先用小波做多尺度特征提取再做空间注意力聚合最后通过全连接直接输出预测窗口。展开后不再依赖 GRU/LSTM 也能拿到时间依赖因为小波分解已经按频段建模了时间结构GAT 只负责在节点间交换信息各模块职责明确梯度路径短。表 2-1 给出一个 24 节点传感器网络的数据流输入是过去 96 个时间步输出未来 6 步。阶段输入形状输出形状说明原始输入24×9624×9624 个节点、96 步历史流量小波分解24×9624×96×4A3、D3、D2、D1 各 96 点沿通道拼接特征展平24×96×424×384把 (96×4) 拉成单向量GAT 层24×38424×128多头注意力聚合节点信息输出层24×12824×6全连接输出未来 6 步窗口选 96 是因为 15 分钟粒度下一天正好 96 点模型至少能看到一个完整日周期。如果数据粒度是 5 分钟窗口要拉到 288特征维度会明显变大这时可以只保留近似分量和后两级细节把通道压到 3再用 1×1 卷积降维避免全连接参数爆炸。3. MATLAB 实现 WT-GAT小波分解、邻接矩阵与训练循环3.1 数据加载、归一化与小波分解读入数据后先把流量矩阵按行归一化。归一化统计量只能从训练段计算验证段和测试段复用同一组 min 和 max不能重新计算否则测试集信息会混进模型。function [flow, minV, maxV] load_and_scale(filepath) d load(filepath); raw d.flow; % 期望格式: N×T, N 为传感器数, T 为时间长度 minV min(raw, [], 2); maxV max(raw, [], 2); flow (raw - minV) ./ (maxV - minV eps); end参数说明按行做 min-max 归一化每行一个传感器节点避免全局 min/max 被数值大的节点主导eps防止零流量路段出现除零。读入的raw如果有缺失值建议先用fillmissing(raw, linear, 2)沿时间维线性插值再把超过 3σ 的异常点替换成前后均值。小波分解的批量封装如下输出保持和输入相同的时间长度。function features wt_features(flow, level, wavelet) % flow: N×T, 返回 N×T×(level1) 的特征张量 [N, T] size(flow); features zeros(N, T, level 1); for i 1:N [c, l] wavedec(flow(i, :), level, wavelet); features(i, :, 1) wrcoef(a, c, l, wavelet, level); for k 1:level features(i, :, k 1) wrcoef(d, c, l, wavelet, k); end end end一个容易混淆的点wrcoef(a, ...)只重建近似分量waverec重建整个信号两者用途不同。上面代码把各子带全部重建回原长度时间轴对齐为 T特征张量喂给 GAT 前再 reshape 成 N×(T×C)。如果发现 D1 通道全是噪声、对指标没有贡献可以只保留 A3、D3、D2 三个通道把通道数从 4 降到 3。3.2 邻接矩阵构造与 GAT 层实现邻接矩阵优先使用路网拓扑关系两条路物理相连且有上下游关系就建边。没有拓扑信息时用坐标按 k 近邻构造典型写法如下。function A build_adjacency(coords, k) % coords: N×2 平面坐标(投影后使用), k 为选取的邻居数 N size(coords, 1); dist zeros(N, N); for i 1:N for j 1:N dist(i, j) norm(coords(i, :) - coords(j, :)); end end A false(N, N); for i 1:N [~, idx] sort(dist(i, :)); % idx(1) 是自己 A(i, idx(2:k1)) true; % 取最近 k 个邻居 end A A | A; % 对称化 A(1:N1:end) false; % 去掉自环 end距离阈值写起来简单但对平行且靠近的道路会产生错误边按 k 近邻构造能控制每个节点的度数对称化保证无向图GAT 的聚合范围不会出现单边连接。实际项目中一般把 k 设在 35对应十字路口上下游加相邻侧路。GAT 层用最直接的标量注意力实现方便阅读和调试。function out gat_layer(X, A, W, a, alpha) % X: N×d 节点特征, A: N×N 逻辑邻接矩阵 % W: d×d_out, a: 2*d_out×1, alpha: LeakyReLU 负斜率 H X * W; % N×d_out N size(H, 1); scores -inf(N, N); for i 1:N for j find(A(i, :)) scores(i, j) leaky_relu(a * [H(i, :); H(j, :)], alpha); end end e exp(scores - max(scores, [], 2)); % 减最大值防溢出 att e ./ sum(e, 2); out att * H; % 加权聚合, 输出 N×d_out end function y leaky_relu(x, alpha) y max(x, alpha * x); end这个实现里scores中非邻居位置保留-infsoftmax 后概率为 0注意力只作用在 A(i,j)1 的邻居上。节点数超过 200 后find(A(i, :))每次都扫描整行建议改成 adjacency list 缓存邻居索引训练时间可以从小时级降到分钟级。提示GAT 层的前向里不要放extractdata否则自动微分被截断loss 不下降且梯度全为 0。3.3 训练循环、损失计算与 dlarray 前向训练要用dlarray包住参数才能走自动微分。3.2 的gat_layer是纯数值版用于调试形状训练前向需要一个接受dlarray的版本gatLayerDl两者结构一致只差输入类型。function H gatLayerDl(X, A, W, a, alpha) % X: N×D dlarray, A: N×N logical, W: D×H1, a: 2H1×1 H1 X * W; % N×H1 N size(H1, 1); scores -inf(N, N); for i 1:N for j find(A(i, :)) scores(i, j) leaky_relu(a * [H1(i, :), H1(j, :)], alpha); end end scores dlarray(scores); att softmax(scores, 2); % R2022b 之后 dlarray 支持 softmax H att * H1; end配套的自定义训练循环如下。dlfeval计算损失和梯度adamupdate完成参数更新。function [net, lossHist] train_wt_gat(features, y, A, params) % features: N×T×C 小波子带, y: N×H 目标, A: N×N 邻接矩阵 [N, T, C] size(features); X single(reshape(features, N, T * C)); % N×D, D T*C D size(X, 2); net.W dlarray(randn(D, params.hidden) * 0.01); net.a dlarray(randn(2 * params.hidden, 1) * 0.01); net.V dlarray(randn(params.hidden, params.horizon) * 0.01); lossHist zeros(params.epochs, 1); avgG []; avgSq []; Xdl dlarray(X); for ep 1:params.epochs [loss, grads] dlfeval(modelLoss, net, Xdl, A, single(y)); [net, avgG, avgSq] adamupdate(net, grads, avgG, avgSq, ep, params.lr); lossHist(ep) extractdata(loss); end end function [loss, grads] modelLoss(net, X, A, target) H gatLayerDl(X, A, net.W, net.a, 0.2); yPred H * net.V; % N×H loss mse(yPred, dlarray(target)); grads dlgradient(loss, net); % 对结构体字段分别求梯度 end参数说明hidden是 GAT 隐层维度horizon是预测步长初始化用 0.01 量级的随机值防止注意力分数一开始就饱和。这个自定义循环比trainNetwork灵活GAT 自定义层也能注册进layerGraph但注册过程对经常改网络结构的项目偏繁琐自定义循环改起来更快。3.4 参数速查表与调参顺序表 3-1 列出这套实现里最常调的参数按影响程度从上到下排列。参数推荐值调整方向小波层数 level3数据毛刺多时加到 4注意 D1 通道可能全噪声小波基db4 / sym4sym4 对尖峰更友好但边界效应更长历史窗口 T96按数据粒度换算成至少一个日周期GAT 隐层维度3264传感器少用 32节点多于 50 用 64注意力头数24单头在拥堵场景容易过拟合dropout0.2训练损失低、验证损失高时优先调大学习率1e-3预热后 5e-4配合 Adam150 轮后看 loss 曲线调参顺序建议固定为先定小波层数和基函数再定窗口 T最后调 GAT 隐层和学习率。先调学习率成功率不高因为 loss 波动多半来自前两个设置的信号错配。参数定完还要检查训练/验证切分方式这个放到最后一章专门说。4. 用 MATLAB App Designer 搭建交通流量预测 GUI 工具工程上只写脚本不好交付现场人员打开 MATLAB 就要能改参数出图。App Designer 比传统 GUIDE 更适合这套流程回调函数是标准方法控件属性在属性面板直接配生成的 .mlapp 还能用 MATLAB Compiler 打包。下面按布局、回调和排错三节走。4.1 界面布局参数面板、按钮和坐标区界面分三块左侧放参数编辑框中间放两个坐标区原始流量、预测对比底部放三个按钮加载数据、训练模型、预测出图。控件和用途如下表。控件类型名称用途ButtonLoadButton选择 .mat 数据文件EditFieldLevelField小波层数默认 3EditFieldLrField学习率默认 0.001UIAxesRawAxes展示原始数据UIAxesPredAxes展示预测对比ButtonTrainButton训练模型ButtonPredictButton预测出图代码生成的骨架如下省略自动生成的位置代码只保留核心属性和私有数据区。classdef TrafficGui matlab.apps.AppBase properties (Access public) UIFigure matlab.ui.Figure LoadButton matlab.ui.control.Button TrainButton matlab.ui.control.Button PredictButton matlab.ui.control.Button end properties (Access private) FlowData double % 加载的流量矩阵 Net % 训练好的网络参数 Graph adjacency % 邻接矩阵 Params struct % 参数汇总 end methods (Access private) % 训练流程封装 function ok runTraining(app) % 训练代码在这里调用 end end end这里把 FlowData、Net 等作为 App 的私有属性保存回调之间通过app.xxx传递数据比 guidata 的存取方式直观。界面控件默认单位是像素部署到不同分辨率屏幕前打开 UIFigure 的 Resize 策略否则按钮位置会漂移。4.2 回调函数串联数据加载、训练和预测三个按钮的回调对应三件事。加载数据用uigetfile选择文件训练按钮从编辑框读参数并调用train_wt_gat预测按钮反归一化后画对比曲线。function LoadButtonPushed(app, ~) [f, p] uigetfile(*.mat, 选择流量数据); if isequal(f, 0), return; end data load(fullfile(p, f)); app.FlowData data.flow; plot(app.RawAxes, 1:size(data.flow, 2), data.flow(1, :), LineWidth, 1); xlabel(app.RawAxes, 时间步); ylabel(app.RawAxes, 流量); end function TrainButtonPushed(app, ~) if isempty(app.FlowData) uialert(app.UIFigure, 请先加载数据, 提示); return; end app.Params.level str2double(app.LevelField.Value); app.Params.lr str2double(app.LrField.Value); app.Params.hidden 32; app.Graph build_adjacency(app.Coords, 4); features wt_features(app.FlowData, app.Params.level, db4); [app.Net, lossHist] train_wt_gat(features, app.Target, app.Graph, app.Params); plot(app.RawAxes, 1:numel(lossHist), lossHist); end function PredictButtonPushed(app, ~) if isempty(app.Net) uialert(app.UIFigure, 请先训练模型, 提示); return; end [yPred, yTrue] predict_wt_gat(app.Net, app.FlowData, app.Graph, app.Params); plot(app.PredAxes, 1:numel(yTrue), yTrue, b, ... 1:numel(yPred), yPred, r--, LineWidth, 1.5); legend(app.PredAxes, {真实值, 预测值}); end回调里第一个参数是 app 本身第二个是事件数据写~表示忽略。训练时间较长时界面会卡住解决办法是用parfeval把训练任务挂到后台线程回调里先把按钮文字改成训练中...训练结束再恢复。predict_wt_gat里要用训练阶段的 minV/maxV 做反归一化否则画出来的预测曲线量纲不对。4.3 运行报错排查表GUI 跑起来之后常见报错集中在下表。报错特征原因处理办法load 之后坐标区不显示曲线flow 变量名不对查看 .mat 实际字段名改用data.(fn)取值训练报错 dimension mismatch小波重建后的特征维度与 GAT 输入不一致检查 features 是否为 N×T×Creshape 时按 T*C 展平报错找不到可执行文件打包部署缺少 GPU 库MATLAB Compiler 打包时勾选对应的工具箱库界面卡死无法点击训练循环占用 UI 线程用 parfeval 或 timer 异步执行训练预测图全是一条直线忘记反归一化或维度不一致predict 函数末尾加 denormalize打印 size 核对排查 GUI 问题的小技巧在回调里用disp打印关键变量的 size比一步步设断点快。App Designer 的断点调试和普通脚本一致但uialert会阻塞后续代码调试时先注释掉。5. WT-GAT 落地验证与高频踩坑点5.1 消融实验与快速验证脚本验证不是看 loss 曲线多漂亮而是看三个对比实验完整 WT-GAT、去掉小波变换只保留 GAT、去掉 GAT 只用小波加全连接。三者输入输出和训练轮数保持一致比较 RMSE 和 SMAPE。这个消融能一次性回答WT 有没有用、GAT 有没有用。function res quick_validate(net, X, A, y) yP predictGat(net, X, A); diff yP - y; res.rmse sqrt(mean(diff.^2, all)); denom abs(yP) abs(y); smape abs(diff) ./ denom; smape(denom 0) 0; res.smape 200 * mean(smape, all); endSMAPE 公式里流量为零时分母为 0计算时置为 0。多步预测建议用直接多输出的方式一次输出未来 6 步而不是递归式地把预测值放回输入后者误差会随时间步累积第 4 步以后基本失去参考价值。5.2 三个高频踩坑点第一个坑是小波分解跨训练/测试集对整段序列先wavedec再切数据低频分量会把未来信息平滑进训练集验证指标虚高。正确做法是先按时间切段再在训练段内部分窗分解。第二个坑是邻接矩阵边的范围。按距离建边容易把平行道路误连预测图里会出现一条路的流量变化影响旁边路的情况。拓扑信息可靠时优先用真实路网关系不可靠时把 k 从 4 减到 3并观察注意力矩阵里是否出现持久的高权重跨道路连接。第三个坑是零流量与节假日样本。夜间零流量路段会让 min-max 归一化后的分布严重偏斜MAPE 在这些点上爆炸。处理方式是用 SMAPE 替代 MAPE并在损失函数里对低流量样本做 mask让模型把容量集中在早晚高峰的预测上。边界效应明显的短序列训练前把dwtmode(per)再跑一遍RMSE 往往能掉两三个点。本文还有配套的精品资源点击获取