做中短期天气预测,很多朋友一上来就把原始温度序列直接丢给LSTM,结果不是训练半天不收敛,就是预测曲线贴着真实值平移好几步,看起来好像“预测成功”,实际根本没法用。后来我换了一个思路:先用变分模态分解(VMD)把温度这类气象序列拆成不同尺度的本征模态函数(IMF),再用长短期记忆网络(LSTM)对每个模态分别建模,最后叠加重构得到最终预测。这一套VMD-LSTM组合下来,实测效果确实稳不少。
这篇文章就把这个项目实例从头到尾拆开讲:数据怎么预处理、VMD参数怎么调、LSTM网络怎么搭、MATLAB代码怎么写、GUI怎么设计,以及我踩过的那些坑。不管你是准备做毕业设计、课程项目,还是单纯想试试时序预测组合模型,这篇都值得你花几分钟看完。我会尽量写清楚每个选择的“为什么”,而不是直接甩一堆代码让你碰运气。
1. 为什么用VMD-LSTM做天气预测
1.1 中短期天气预测难在哪
中短期预测一般指未来1到7天的预测,比“几小时后”的临预报难,比“未来几个月”的气候预测又更实。难点在于气象序列本身是高度非线性、非平稳的:有年周期、日周期,有从高压脊到冷锋过境带来的突变,还有各种噪声。如果你拿原始序列直接训练LSTM,模型会把噪声和高频毛刺也当作规律学进去,导致过拟合。最典型的表现是:训练集损失很低,测试集一遇到转折点就明显滞后,或者预测曲线特别“抖”。
处理非平稳时序的经典思路是“先分解,后预测”。把原始序列拆成几个相对稳定的分量,再对每个分量做预测,最后叠加。VMD就是做这件事的工具之一。
1.2 VMD是什么,LSTM是什么,为什么组合有效
先简单说VMD。变分模态分解是一种自适应的信号分解方法,它把原始信号分解成若干个带限的本征模态函数,每个模态都有一个中心频率和带宽。VMD通过迭代求解一个变分问题,目标是让所有模态的带宽之和最小,同时保证所有模态相加能还原原始信号。你可以把它理解成:厨师把一锅混合汤按“频率”过滤成几碗不同口感的汤,最后想还原时再倒回同一个锅里。
LSTM则是循环神经网络(RNN)的一种改进结构,引入了“门控机制”:遗忘门、输入门、输出门和细胞状态。它的核心优势是可以记住长时间以前的信息,又不会像普通RNN那样容易梯度消失。对于天气数据里“今天的温度可能和前一周的天气过程有关”这种性质,LSTM天然适合。
为什么这两个要搭配?因为VMD擅长把复杂序列分离,LSTM擅长对每个相对平稳的分量做时序建模。气象序列经常是“低频趋势 + 高频波动 + 噪声”的混合体,直接让LSTM去学混合体,它分不清主次。VMD拆完以后,低频分量的规律性强,LSTM好学;高频分量噪声多,我们可以单独处理,甚至可以在一定程度上抑制噪声后再预测。
1.3 这个项目的整体技术路线
整个项目的流程可以概括为:数据读取 → 数据预处理 → VMD分解 → 对每个IMF分量分别归一化 → 滑动窗口构造训练样本 → LSTM逐分量预测 → 逆归一化 → 分量预测结果累加重构 → 评估指标 → GUI展示。
我做的单变量版本以历史温度序列为预测目标,训练集占前80%,测试集占后20%,预测未来14天的逐日均温。项目整体结构分成下面几个模块:
| 模块 | 作用 | 关键点 |
|---|---|---|
| 数据读取与清洗 | 读取Excel/CSV,处理缺失值和异常值 | 缺失值不能直接补零,温度数列补零会彻底破坏周期 |
| VMD分解 | 将原始温度序列分解成多个IMF | K和alpha两个参数最影响效果 |
| 样本构造 | 用过去一段时间预测未来一段时间 | 窗口长度和预测步长相匹配 |
| LSTM训练 | 对每个IMF训练一个回归网络 | 归一化参数要保存到测试阶段 |
| 重构评估 | 累加各分量预测结果 | 必须逆归一化后再累加,否则求和没有物理意义 |
| GUI展示 | 一键读取、训练、预测、画图 | 回调函数里注意参数类型转换 |
我个人建议新手先不要把多变量气象因子一次全加进来,先把单变量VMD-LSTM整个流程跑通,再考虑是否引入湿度、气压等外生变量。一口吃不成胖子,项目也一样。
2. 数据准备与预处理
2.1 数据来源与字段设计
做天气预测项目,最常用的数据是气象站点历史观测数据,可以从公开气象数据平台下载,也可以使用某些模拟数据来验证算法流程。这里我不会绑定具体平台,但要注意字段至少包括:时间、温度,最好再带上湿度和气压备用。
我这次实验用的是某地气象站点两年的逐日均温数据,一共730条记录,目标是预测未来14天的均温。如果你使用逐小时数据,可以先重采样成逐日均值,这样序列的波动会小一些,VMD分解也更稳。
字段建议如下:
| 字段名 | 含义 | 示例 |
|---|---|---|
| Time | 日期 | 2023-01-01 |
| Tavg | 日平均气温 | 12.5 |
| Tmax | 日最高气温 | 18.2 |
| Tmin | 日最低气温 | 7.3 |
| RH | 相对湿度 | 66.0 |
本项目先只使用Tavg作为预测目标。后面想拓展多变量预测时,可以把RH、Tmax等作为LSTM输入的额外特征,但要注意这些特征也需要做同样的预处理。
2.2 缺失值处理与归一化
气象数据经常有缺失。我的处理办法是:先检查连续缺失段,如果连续缺失不超过3天,用前后两点线性插值补上;如果连续缺失太多,我会直接去掉整段,避免插值引入虚假信息。温度是连续变化量,线性插值通常比均值填充靠谱得多。
还有一个很容易忽略的问题:异常值。某些情况下日数据会飞来一个明显不合理的值,比如夏天突然出现零下40度。这种野值对LSTM的损失函数打击巨大,因为LSTM回归用的是均方误差,一个极端值就可能把整个模型的梯度带偏。我一般先用一个简单的阈值规则:超过均值的3倍标准差,或者超过历史月极值一定范围,判断为异常值,然后用前后邻接值平滑掉。
归一化方面,我习惯用最大最小归一化到[0,1]区间。公式不复杂:
x_scaled = (x - x_min) / (x_max - x_min)但有一个关键点:测试集必须使用训练集计算出的x_min和x_max,而不能把自己混进去。原因很简单——在真实预测中,未来的数据是未知的,你不能拿未来的最大值来缩放过去的训练数据。如果整条序列一起归一化,等于提前看到了测试集的统计信息,测出来的误差会偏乐观。
2.3 训练集/测试集划分与滑动窗口构造
时序预测最忌讳随机划分。我见过很多新手用类似cvpartition的方式把样本随机打乱,然后分成训练和测试,这样测试集的时间点可能穿插在训练集之间,LSTM在很大程度上等于“看到了邻居时间步”,结果当然虚高。
正确做法是严格按时间顺序划分:前80%样本训练,最后10%作为验证,最后10%作为测试。如果数据量不够,可以用滚动窗口交叉验证,但核心原则永远是预测未来,而不是预测随机抽取的过去。
构造样本的方式是滑动窗口。假设我们用过去30天温度预测未来14天温度,那么对长度为N的序列,样本形如:
输入:x_1, x_2, ..., x_30 输出:x_31, x_32, ..., x_44再下一个样本就平移一步:
输入:x_2, x_3, ..., x_31 输出:x_32, x_33, ..., x_45MATLAB里可以这样构造:
function [XData, YData] = createSequenceData(data, past, horizon) N = length(data) - past - horizon + 1; XData = zeros(N, past); YData = zeros(N, horizon); for i = 1:N XData(i, :) = data(i : i + past - 1); YData(i, :) = data(i + past : i + past + horizon - 1); end % LSTM输入维度格式为 [numFeatures, numTimesteps, numObservations] XData = permute(XData, [2, 1]); XData = reshape(XData, 1, past, N); YData = YData'; end这个函数返回的三维数组XData,第一个维度是特征数量(单变量就是1),第二个维度是时间步长(即past),第三个维度是样本个数。YData是每个样本对应的未来horizon步真实值。
窗口长度选择上,我试过7、15、30、45天。30天相对稳妥,包含了一个月的气候记忆;15天则更敏感,但预测转折点时容易过拟合。如果你做的是逐小时数据,窗口可以适当放大到72到168小时。
3. VMD-LSTM模型详解与参数设置
3.1 VMD的变分模型与参数填坑心得
VMD的数学思想可以看成构造一个带约束的最优化问题:在保证所有模态之和等于原始信号的前提下,让每个模态的估计带宽之和最小。实现时有几个关键参数:
| 参数 | 含义 | 常见范围 |
|---|---|---|
| K | 模态个数 | 4~10 |
| alpha | 惩罚项,控制带宽约束强度 | 500~3000 |
| tau | 噪声容忍参数 | 0 或较小值 |
| DC | 是否保留直流分量 | 0或1 |
| init | 中心频率初始化方式 | 1(均匀初始化) |
在MATLAB里调用VMD,如果是新版本自带的vmd函数,写法一般长这样:
[imfs, ~, info] = vmd(temperatureData, 'NumIMF', 6, 'Alpha', 2000);如果你用的是网上流传很广的开源VMD函数,调用方式可能是:
[imfs, u_hat, omega] = VMD(temperatureData, alpha, tau, K, DC, init, tol);两种用法的参数顺序不同,建议先查函数帮助再改参数。
填坑心得第一条:K不能贪多。K设太大,会把原本连续的物理过程切得过碎,产生很多频率相近的假模态;K设太小,两个频率成分混在一个模态里,分解效果就没意义。我一般先K=5或6,看分解后的中心频率是否分离均匀。如果omega里两个中心频率靠得很近,说明过分解,减K;如果第一个模态出现明显趋势叠加高频毛刺,说明欠分解,加K。
第二条心得:alpha对平滑性的影响最明显。alpha越大,得到的模态越窄带、越平滑,但可能丢失局部特征;alpha太小,模态带宽过宽,容易混入噪声。我用2000做默认值,再根据曲线调整到1000或3000。tau平时保持0,不过度调。
3.2 LSTM网络结构与超参数选择
VMD分解完成后,每个IMF就是一个相对平稳的一维序列。接下来我对每个IMF单独训练一个LSTM回归网络,网络结构非常简单:
numFeatures = 1; numHiddenUnits = 128; numOutputs = horizon; layers = [ sequenceInputLayer(numFeatures) lstmLayer(numHiddenUnits, 'OutputMode', 'last') dropoutLayer(0.2) fullyConnectedLayer(numOutputs) regressionLayer];为什么中间加一个dropout?因为逐分量预测时,样本量并不大,很容易过拟合。dropout层在训练时随机丢弃一部分神经元输出,可以缓解过拟合。测试时dropout不起作用,所以不需要担心预测结果随机。
训练选项我用adam优化器:
options = trainingOptions('adam', ... 'MaxEpochs', 120, ... 'MiniBatchSize', 64, ... 'InitialLearnRate', 0.001, ... 'GradientThreshold', 1, ... 'Plots', 'training-progress', ... 'Verbose', false);这里有几个参数值得说。OutputMode必须设为'last',因为我们只要序列最后一个隐藏状态,然后接全连接层输出未来的horizon个值;如果设成'sequence',输出会变成每个时间步都有预测,维度会乱。GradientThreshold设为1的作用是梯度裁剪,防止RNN在长序列上出现梯度爆炸。学习率从0.001开始通常比较安全,如果损失曲线剧烈振荡,我会降到0.0005。
3.3 预测效果评估指标
模型训练完,不能只画个曲线说“看着差不多”。我通常用下面几个指标评估:
| 指标 | 公式 | 说明 |
|---|---|---|
| RMSE | sqrt(mean((y_true - y_pred)^2)) | 对大误差敏感,最常用 |
| MAE | mean( | y_true - y_pred |
| MAPE | mean( | y_true - y_pred |
| R2 | 1 - sum((y_true - y_pred)^2) / sum((y_true - mean(y_true))^2) | 越接近1越好 |
MATLAB里可以这样写:
rmse = sqrt(mean((yTrue - yPred).^2)); mae = mean(abs(yTrue - yPred)); r2 = 1 - sum((yTrue - yPred).^2) / sum((yTrue - mean(yTrue)).^2);需要特别注意:RMSE和MAE要回到原始温度单位再算。如果直接在归一化后的[0,1]区间上算指标,0.05的RMSE换算成温度可能对应5度误差,会严重低估误差。
4. MATLAB代码实现与GUI设计
4.1 主程序框架与关键函数说明
一个完整的MATLAB项目最好不要把所有代码堆在一个脚本里。我的建议文件结构如下:
weather_vmd_lstm/ ├── main_run.m % 主入口 ├── preprocessData.m % 数据清洗和归一化 ├── vmdDecompose.m % VMD分解封装 ├── createSequenceData.m % 构造时序样本 ├── trainLSTMModel.m % 训练单个LSTM ├── predictLSTM.m % 预测并逆归一化 ├── evaluateMetrics.m % 计算指标 ├── app_weather_vmd_lstm.mlapp % GUI文件 └── data/ └── weather_data.xlsx主程序大致逻辑可以写成:
% main_run.m %% 1. 读取数据 raw = readtable('data/weather_data.xlsx', 'PreserveVariableNames', true); temperature = raw.Tavg; %% 2. 预处理 dataClean = preprocessData(temperature); %% 3. VMD分解 imfs = vmdDecompose(dataClean, 6, 2000); %% 4. 逐分量训练与预测 numIMF = size(imfs, 2); forecasts = zeros(horizon, numIMF); for k = 1:numIMF [XTrain, YTrain] = createSequenceData(imfs(:, k), past, horizon); [XTest, YTest] = createSequenceData(imfs(:, k), past, horizon, trainLen); net = trainLSTMModel(XTrain, YTrain); forecasts(:, k) = predictLSTM(net, XTest); end %% 5. 重构并评估 totalForecast = sum(forecasts, 2); metrics = evaluateMetrics(yActual, totalForecast);这里有一点要特别说明:createSequenceData里如果要用测试单元,需要额外传入一个起始位置参数,让测试样本严格从训练集之后开始。我在实际代码里会给它加一个startIdx参数,避免测试样本和训练样本重叠。
4.2 基于App Designer的GUI设计
MATLAB里做GUI,建议直接用App Designer,因为它比老旧的GUIDE好用得多,控件现成,回调接口清晰。我设计的界面不算复杂,但能覆盖完整流程:
界面从上到下分四块:
- 数据选择区:一个“读取数据”按钮,一个路径显示文本框。
- 参数设置区:VMD的K和alpha输入框,LSTM的隐藏单元数输入框,训练/测试比例滑条,预测步长输入框。
- 操作按钮区:“开始训练”“开始预测”“导出结果”。
- 结果展示区:两个坐标轴,一个显示原始序列和预测曲线,另一个显示VMD分解后的IMF曲线;旁边加一个表格展示RMSE、MAE、R2。
核心回调函数里,最值得注意的有两个。
第一个是“开始训练”按钮的回调:
function TrainButtonPushed(app, event) K = app.VMDKEditField.Value; alpha = app.AlphaEditField.Value; past = app.WindowEditField.Value; horizon = app.HorizonEditField.Value; % 对数据做VMD分解 [imfs, info] = vmd(app.DataCleaned, 'NumIMF', K, 'Alpha', alpha); % 将分解结果显示在右边的坐标轴 cla(app.IMFPlot); plot(app.IMFPlot, imfs, 'LineWidth', 1.2); % 循环训练 app.StatusTextArea.Value = '正在训练各分量模型...'; drawnow; ... end注意App Designer的输入框返回的是double,但在某些旧版本里也可能是char类型,建议先str2double包一层,避免类型错误。状态文本框要及时更新,并调用drawnow让界面刷新,否则用户以为程序卡死了。
第二个是“预测”按钮的回调:
function PredictButtonPushed(app, event) % 读取已训练好的模型列表 predAll = zeros(app.HorizonEditField.Value, length(app.nets)); for i = 1:length(app.nets) predAll(:, i) = predict(app.nets{i}, app.XTest_cell{i}); end forecast = sum(predAll, 2); forecast = forecast * app.dataStd + app.dataMean; % 逆标准化 % 画预测对比图 plot(app.ForecastPlot, app.testTime, app.actualTest, 'LineWidth', 1.5); hold(app.ForecastPlot, 'on'); plot(app.ForecastPlot, app.testTime(1:length(forecast)), forecast, '--', 'LineWidth', 1.5); legend(app.ForecastPlot, '实际值', 'VMD-LSTM预测值'); end4.3 代码疑难点详解
很多人在写LSTM预测代码时会踩一个很隐蔽的坑:训练时用了随机打乱,但时序数据样本之间本来就是有重叠的,打乱之后模型看到的相邻样本时间上高度重叠,虽然没有未来数据泄漏,但会造成内存访问模式混乱,并让验证损失曲线变得特别“毛”。我通常设置'Shuffle', 'never',保持样本顺序,然后靠dropout和梯度裁剪来防止过拟合。
还有一个坑是VMD的边界效应。MATLAB自带的vmd在序列两端可能产生轻微失真,导致分解后的分量首尾几个点异常。如果直接拿首尾数据训练,LSTM容易被带偏。解决方法是先对原始序列延长一点,比如向前多放10个数据点,分解截断时丢掉边界部分,或者对首尾做镜像延拓后再分解。
再提一个必须强调的重构顺序:
原始温度序列 = IMF1 + IMF2 + ... + IMFk + 残余趋势如果你对每个IMF分别做最大最小归一化并分别预测,那预测完必须先把每个IMF的预测值逆归一化回原始幅度,再相加。如果先归一化IMF、再求和、最后用总序列的统计量逆归一化,结果是错的,因为每个分量归一化后总和并不是原始序列的归一化结果。我在第一次做这个项目时就是这个细节没想清楚,最后预测曲线整体偏得离谱,排查了大半天。
5. 实测效果与常见问题排查
5.1 实测案例与预测曲线分析
我用两年日平均温度数据做过一次对比实验:单一LSTM预测未来14天,RMSE大约2.8度,预测曲线在波峰处明显滞后;改成VMD-LSTM后,RMSE降到1.9度左右,MAE从2.1度降到1.4度,R方从0.86提升到0.94。
拆解看原因,VMD把温度序列分成了几个不同频率的模态。低频模态(比如年周期趋势)预测起来非常平缓,LSTM学得很稳;高频模态噪声大,预测精度确实差一些,但它的绝对值小,对最终误差贡献有限。所以整条预测曲线看起来既能抓住大趋势,又不会跟着随机噪声乱抖。
如果你观察到某个数据环境下VMD-LSTM反而不如单一LSTM,优先怀疑K设置过大。模态分得太碎,每个分量的样本性质都不稳定,LSTM反而更难学。另一个可能是alpha太大,把有用的高频变化全部平滑掉了,导致重构后的信号细节缺失。调参不是越多越好,而是刚刚好。
5.2 常见问题速查表
| 问题现象 | 可能原因 | 解决方法 |
|---|---|---|
| VMD分解结果中两个IMF中心频率太近 | K设置偏大 | 减小K,观察中心频率分布 |
| 训练损失出现NaN | 学习率过高或数据含NaN | 加入缺失值处理,学习率降到0.0005 |
| 预测曲线比实际晚一个波峰 | 窗口长度太短,或LSTM输出趋势滞后 | 增大past,或改用seq2seq结构 |
| 训练损失下降很快,测试误差很大 | 过拟合 | 增大dropout,减小hiddenUnits,或者减少maxEpochs |
| 分解后的IMF和原始信号对不上 | 边界效应或参数未收敛 | 延拓数据,增加tol迭代次数 |
| GUI按钮点击后卡死 | 训练过程阻塞了主线程 | 将训练封装到普通函数,回调里用drawnow刷新状态 |
| App Designer中编辑框返回类型不对 | 不同版本控件返回值有差异 | 用str2double或str2num转换 |
5.3 几个提升精度的经验小技巧
第一,VMD参数调好以后,不要每个IMF都用相同结构的LSTM。高频IMF噪声大,隐藏单元可以小一点,比如64,训练轮次少一点;低频IMF变化缓慢,隐藏单元可以设置到128甚至160。分开设置虽然麻烦,但能缩短训练时间,还能明显改善高频分量的过拟合。
第二,每次训练前用rng固定随机种子。LSTM初始化有随机性,如果不固定种子,同一个程序跑两次结果可能差很多,调参时根本分不清是参数效果还是随机波动。我用rng(42)做固定种子,至少保证实验可复现。
第三,预测未来14天时,更稳的做法是“递归预测”加“多步输出”结合。首次预测时输入过去30天,得到未来14天;下一次预测时,把预测结果的一部分当作已知输入,再继续向后预测。这种递归方式会让误差累积,所以不能无限递推。我在项目里只做一次性多步输出,对14天尺度来说已经足够。
第四,如果数据量非常少,不建议强行上VMD-LSTM。VMD分解会把样本量进一步摊薄,每个分量的训练样本都不多,LSTM容易欠拟合。这种情况下不如先用ARIMA或简单线性回归做基线,等数据量上来了再上组合模型。
最后再分享一个现场调参的小技巧:第一次跑通流程时,先用过去7天预测未来3天,快速验证代码逻辑。等训练和预测曲线都正常了,再改成30天预测14天,逐步加大规模。这样调试定位问题会快得多。我自己每次写新时序预测项目都从最小例子起步,能省下一大半排查时间。希望这个VMD-LSTM的实例能帮到你,如果你在自己的数据上调出了更好的参数,欢迎回来交流。