简介:基于LSTM网络实现降雨径流预测的Matlab代码包,适合水文水资源、环境工程等领域的研究生、工程师及入门深度学习的算法学习者。资源围绕长短期记忆网络对降雨与径流时序关系的建模展开,提供完整可运行的主程序与归一化等预处理函数,并附带示例数据与文字说明,便于快速复现和二次开发。压缩包共4个文件,以2个m脚本为主,负责模型搭建、训练与预测流程,2个txt文件则提供数据输入和操作备注,整体仅290KB,轻量易用。目前已有220人学习下载,适合需要掌握LSTM在水文预测中具体实现方法,或希望在此基础上调整模型结构、更换数据集的读者,可直接对照代码理解数据划分、归一化及预测输出等关键环节。
1. 降雨径流预测遇上LSTM:这份Matlab代码包到底能解决什么
做水文预报的人估计都遇到过这种尴尬:手里有一段日径流数据,想用LSTM做降雨径流预测,翻遍论文发现不是TensorFlow就是PyTorch,能在Matlab里直接跑通的很少。这份基于LSTM网络进行降雨径流预测附matlab代码的压缩包,就是为了填这个空。解压后一共四个文件:Normalization.m、说明.txt、main.m、01127000.txt,结构非常干净。01127000是一个公开水文站点编号,txt里放的是实测径流序列;Normalization.m负责数据归一化;main.m把读数据、建网络、训练、预测全部串起来。不需要额外折腾Python环境,有一台装着Matlab深度学习工具箱的电脑就能复现。适合正在做毕业设计或课程设计的水文、土木、资源环境方向学生,也适合想在Matlab里把LSTM时序预测链路完整走一遍的工程师。它不追求网络结构多深,而是把“数据怎么喂、参数怎么定、预测结果怎么反算”这条链路讲明白,属于能直接落地跑通的参考实现。
2. 拆包看结构:从四个文件看懂这份LSTM降雨径流工程
拿到压缩包别急着双击main.m,我习惯先把每个文件的职责摸一遍,不然出了问题都不知道去哪一行改。这个工程只有四个文件,分工却非常明确:01127000.txt是数据源,Normalization.m是数据预处理,main.m是主流程,说明.txt是运行说明。下面挨个拆开说。
2.1 01127000.txt:一段公开站点的径流序列能承载多少信息
01127000这个编号一看就是USGS水文站点编号,国内做水文的人对这类公开数据不陌生,它代表一个具体测站,txt里通常是按日采样的实测径流序列。这类数据的价值在于可复现、有据可查,用来验证算法非常合适,不用自己去河道上架设备测数据。
从任务边界看,这份资源的输入是一维的:只有一个径流序列,没有降雨、蒸散发、气温这些外强迫变量。所以它建模的是“历史径流到未来径流”的自回归关系,而不是严格意义上的多变量降雨-径流模型。把LSTM当黑匣子,用过去一段窗口的流量去预测下一时刻流量,这个思路在这个数据集上完全成立;但如果要预报一场具体暴雨引发的洪水,单靠历史流量是不够的,需要把降雨序列作为输入特征拼进去。认清这个边界,你才知道这份代码能回答什么问题、回答不了什么问题。
拿到txt我先做三件事:打开文件看前10行确认格式,统计缺失值和负值,再看数值量级判断单位。USGS的流量单位可能是ft³/s也可能是m³/s,量级差得不是一星半点,单位弄错后面所有预测结果都是错的,而且很难排查。这里列一下常见的txt文件内部形态,遇到哪种形态就知道该怎么处理:
| 文件内容形态 | 典型示例 | 读取策略 |
|---|---|---|
| 纯数值单列 | 123.4 | readmatrix直接读,最省事 |
| 日期+数值 | 2020-01-01,123.4 | 需处理日期列 |
| 带表头说明 | STATION: 01127000 | 跳过非数值行 |
| 多列混合数据 | 多年多列 | 显式取需要的流量列 |
为什么要花篇幅说这个?因为水文数据的原始文件几乎不会长得一模一样,80%的复现失败不是模型塌了,而是数据没读对。
2.2 Normalization.m:为什么第一步是归一化而不是差分
降雨径流数据最大的特点是量级跨度大:枯水期可能只有几个立方米每秒,汛期能涨到几千,差了两三个数量级。LSTM内部用的是tanh激活函数,输出范围在[-1,1]之间,输入差异过大时神经元很容易饱和,梯度变得极小,网络基本学不动。所以归一化不是可有可无的预处理,而是LSTM能不能收敛的前提。
常见做法是min-max归一化,把序列线性映射到[0,1],公式是x'=(x-x_min)/(x_max-x_min)。但这个公式有个容易被忽视的前提:x_min和x_max必须在训练集上计算,再拿这组统计量去归一化测试集。如果偷懒用全序列的min和max,测试集的信息在训练阶段就通过归一化参数泄露出去了,测试指标会虚高,换一段新数据立刻露馅,这个坑在第5章会专门展开。
也有人说要不要先差分再做归一化,因为径流序列往往有很强的季节趋势。我的做法是先试min-max,如果预测曲线呈现出明显的滞后或趋势跟随,再考虑对序列做一阶差分,把非平稳性处理掉。差分解决的是均值非平稳,归一化解决的是量纲和尺度,两者不是同一类问题。这份工程的Normalization.m从命名和分工来看就是干min-max归一化这件事,先跑它再进main.m,执行顺序是合理的。选哪种归一化方式,可以参考下面这个对比:
| 归一化方式 | Matlab实现 | 输出范围 | 适用场景 |
|---|---|---|---|
| 手写min-max | (x-min)/(max-min) | [0,1] | 单序列、统计量可控 |
| mapminmax | mapminmax(x') | [-1,1]默认 | 快速验证 |
| z-score | (x-mean)/std | 无固定范围 | 数据近似正态分布 |
2.3 main.m:读数据、建网络、训练、预测一条主线
main.m是唯一的启动入口,正常执行顺序是:读入01127000.txt、调用或内联归一化逻辑、按滑动窗口切样本、定义LSTM网络结构、配置训练参数、执行trainNetwork、用训练好的网络做预测、反归一化、画图对比实测与预测。六个环节缺一不可,每一步的写法都影响最终预测形态。
这类脚本还有个特点:它不是函数式封装,参数全写在脚本开头。好处是新手看起来直观,改一个数字就能重新跑一遍;坏处是跑完工作区变量全留着,二次开发要小心命名冲突。我一般跑通之后会把它改造成函数,把数据路径、归一化开关、隐藏单元数、滞后步长全部提成输入参数,以后换站点、换数据就不用满脚本找常量去改。
说明.txt在这个工程里承担的是运行说明角色,一般会写清数据来源、Matlab版本要求、先跑哪个文件。我每次拿到这类资源都先看说明再动手,能省掉大量试错时间,因为lstmLayer这些接口只有在装了Deep Learning Toolbox的Matlab里才有,版本太旧直接报错。从资源价值看,这套代码胜在完整闭环,尤其是“预测结果反归一化回物理量”这一步写进了主线,对初学者最友好,很多人卡在模型能跑、但不知道预测值怎么和实测流量对比,这份资源把最后一公里补齐了。
3. 数据预处理复现:从txt原始序列到能喂进LSTM的干净样本
上一章讲的是文件职责,这一章开始动手。不管模型结构怎么变,数据侧有三步绕不开:读原始文件、归一化、按时间切分。每一步都有固定套路,也有固定坑。
3.1 读取01127000.txt的正确姿势与表头陷阱
老教程喜欢用textread,它读纯数值矩阵方便,但一遇到表头、单位行、空行就报错,非常脆弱。我现在统一用readmatrix,R2019a之后的Matlab都有,能自动跳过非数值行,适合处理带说明的导出文件。
% 读取径流数据,readmatrix会自动跳过非数值行 data = readmatrix('01127000.txt'); % 如果txt只有一列数值,data就是列向量;多列则取需要的列 q = data(:, 1);这段的逻辑是先让readmatrix把文件读成矩阵,再显式取出流量列。注意readmatrix的默认行为是跳过非数值行,这个特性很多人不知道,知道后能少踩一个坑。如果文件前几行是站点信息,可以再加NumHeaderLines参数:
% 如果文件前两行是站点信息和单位,显式跳过 data = readmatrix('01127000.txt', 'NumHeaderLines', 2);读进来先做数据质量检查,我习惯用几行命令确认序列能不能用:
fprintf('缺失值数量:%d\n', sum(isnan(q))); fprintf('负值数量:%d\n', sum(q < 0)); fprintf('序列长度:%d\n', length(q));逻辑说明:isnan统计NaN,负值检查是判断是否存在异常记录,length确认样本量。如果缺失值数量不为0,需要先插值或删除,否则LSTM训练时NaN会直接传染梯度,训练过程会非常难看。
单位换算再强调一次。USGS数据很多是cfs,也就是立方英尺每秒,转m³/s要乘0.0283168。判断方法很简单:看数值量级,如果日常流量在几十到几百之间,大概率是cfs;如果流量在几到几百、峰值能过千,可能是m³/s。拿不准就先按两种单位分别跑一次,看预测曲线是否合理,这个习惯能救你一命。
3.2 min-max归一化的Matlab实现与统计量边界
归一化这一步我不太建议用mapminmax直接怼全序列,虽然写起来最短,但它会把整个输入序列的统计信息都算进去,埋下数据泄漏隐患。更可控的做法是手写min-max,并且明确只在训练集上计算统计量。
% 用训练集计算归一化统计量 q_min = min(q_train); q_max = max(q_train); % 归一化训练集和测试集,测试集复用训练集统计量 q_train_norm = (q_train - q_min) / (q_max - q_min); q_test_norm = (q_test - q_min) / (q_max - q_min);逻辑说明:前两行统计的是训练集的最小值和最大值,后面两行分别对训练集和测试集做映射。关键是q_test_norm复用了训练集的q_min、q_max,而不是重新统计测试集,这样测试集信息才不会提前进入模型。测试集归一化后落在[0,1]之外是正常的,比如-0.05或1.1,不用慌,只要训练集覆盖了全局极值,越界幅度不会太大。
如果坚持用mapminmax,必须保留返回的ps结构体,并用apply模式处理测试集:
[q_train_norm, ps] = mapminmax(q_train', -1, 1); q_test_norm = mapminmax('apply', q_test', ps);'apply'的作用就是用训练集统计出的ps去处理测试集,避免重新统计。很多人漏掉第二行,导致训练集和测试集用了不同的归一化基准,预测结果和实测尺度对不上,这个问题在论文里经常看到,属于典型的数据泄漏。预测结果反归一化同样用同一组统计量:
q_pred_real = q_pred .* (q_max - q_min) + q_min;参数说明:q_pred是模型输出的归一化预测值,反算回物理单位后才能和实测流量对比。必须用点乘,因为q_pred通常是列向量,用矩阵乘会直接报维度错误。
注意:归一化的统计量只允许来自训练集,这是时序预测的底线。谁用全序列算min/max,谁就该被请去重新读一遍数据泄漏的定义。
3.3 按时序切分训练/测试集:时间序列不能随机洗牌
分类任务里数据可以随机打乱,时序任务绝对不行。LSTM学的是时间依赖,一旦把顺序打乱,过去和未来的信息混在一起,模型学到的是“靠未来预测过去”的假规律。所以切分只能按时间顺序,前80%训练、后20%测试是最常见配置。
ratio = 0.8; idx = round(ratio * length(q_norm)); q_train = q_norm(1:idx); q_test = q_norm(idx+1:end);逻辑说明:idx是切分点,前80%做训练,后20%做测试。参数说明:ratio=0.8是常用起点,数据量小可以放到0.85,但测试集至少要留出一个完整水文年,否则看不到季节性表现。切分清楚之后,下一步才是滑窗构造样本。
还有一个实操细节:切分之后把归一化参数保存成mat文件,这个习惯在工程上很重要,因为系统上线预测时不可能重新拿旧训练数据算一遍min和max。
save('norm_params.mat', 'q_min', 'q_max');这样写的好处是预测脚本只依赖norm_params.mat,不依赖原始训练数据,代码更干净。另外注意,切分前不要做任何用到全序列统计信息的操作,包括均值填充、整体去趋势,这些都要在切分之后、只基于训练集来做,否则一样构成泄漏。
4. main.m复现:LSTM网络搭建、训练选项与滚动预测
数据准备好之后进入模型侧。这一章按main.m最常见的组织方式拆开讲,网络结构、训练参数、滑动窗口、预测写法四个部分,每一部分都有能直接抄走的代码和对应参数。
4.1 网络结构:sequenceInputLayer到regressionLayer怎么拼
Matlab深度学习工具箱里,LSTM回归网络的标准拼法是四层:序列输入层、LSTM层、全连接层、回归层。这份资源是单变量预测,输入特征数就是1。
numFeatures = 1; % 输入特征数,这里是单变量流量 hiddenUnits = 50; % LSTM隐藏单元数 layers = [ sequenceInputLayer(numFeatures) lstmLayer(hiddenUnits, 'OutputMode', 'last') fullyConnectedLayer(1) regressionLayer];逻辑说明:sequenceInputLayer接收序列数据,每个样本是1×lag的向量;lstmLayer是模型核心,hiddenUnits=50是对单变量日径流序列的常用起点;'OutputMode','last'表示只输出最后一个时间步的隐藏状态,正好匹配“用过去lag天预测下一天”的任务;fullyConnectedLayer(1)把隐状态压缩成1维输出;regressionLayer计算回归损失。
参数说明:hiddenUnits不是越大越好,我在这类尺度数据上的经验范围是50到100,再大容易过拟合。径流序列本身噪声很大,过拟合的表现就是训练Loss很低、测试集预测曲线却平得像条直线。如果你的数据和这份资源量级差不多,先按50跑通,再试100对比。
4.2 trainingOptions:Adam、学习率、梯度裁剪怎么定
训练配置决定模型能不能收敛,以及收敛到哪。我一般这样设:
options = trainingOptions('adam', ... 'MaxEpochs', 100, ... 'MiniBatchSize', 64, ... 'InitialLearnRate', 0.005, ... 'GradientThreshold', 1, ... 'Verbose', 1, ... 'Plots', 'training-progress');参数说明逐条说:求解器选adam而不是sgdm,是因为径流序列变化剧烈,adam的自适应学习率更稳,sgdm在这个任务上容易Loss震荡;MaxEpochs=100是起点,Loss还在明显下降就加到150,超过200没变化就要考虑模型结构或数据问题,而不是继续加算力;MiniBatchSize=64在几千到几万样本规模下比较合适;InitialLearnRate=0.005是LSTM训练的经验区间,0.01偏激进、0.001偏保守,建议先0.005跑一次看Loss曲线;GradientThreshold=1是防梯度爆炸的保险丝,LSTM在长序列上很容易梯度爆炸,这个参数能显著提升训练稳定性。
注意:别盯着控制台的数字看,要看训练进度图里Loss曲线的形态。锯齿状上下乱跳就降学习率,水平线就检查数据归一化和OutputMode。
4.3 构造训练样本:滑动窗口切分与cell数组格式
LSTM在Matlab里的输入格式和普通机器学习不一样:X必须是cell数组,每个cell里面是[特征数, 时间步数]的矩阵。初学者第一次写,十有八九栽在这里。常见做法是用循环构造:
lag = 12; % 用过去12天预测下一天 X = {}; Y = []; for i = lag + 1 : length(q_train_norm) X{end + 1, 1} = q_train_norm(i - lag : i - 1)'; Y(end + 1, 1) = q_train_norm(i); end逻辑说明:循环从第lag+1个样本开始,每次取前lag个值作为输入窗口,当前第i个值作为目标输出。X{end+1,1}是在cell数组末尾追加,每个元素是1×lag的行向量。Y是列向量,长度和X的cell数严格一致。
参数说明:lag=12对日尺度数据是合理起点,12天窗口能覆盖一次中小洪水过程的涨落;如果做小时尺度预测,lag可能要拉到24以上。lag越大样本数量越少,太小模型学不到过程形态,这个参数值得单独做一组对照实验。训练调用一行就够了:
net = trainNetwork(X, Y, layers, options);这里有个容易犯的错:X和Y长度不一致,十有八九是循环边界写错,比如把lag+1写成了lag,或者把i-lag写成了i,调试的时候先打印length(X)和length(Y)。
4.4 单步滚动预测与反归一化对齐
预测阶段最容易出的问题不是模型,而是维度对不上。测试集要按训练集完全一样的方式构造:
XTest = {}; YTest = []; for i = lag + 1 : length(q_test_norm) XTest{end + 1, 1} = q_test_norm(i - lag : i - 1)'; YTest(end + 1, 1) = q_test_norm(i); end YPred = predict(net, XTest); YPredReal = YPred .* (q_max - q_min) + q_min; YTestReal = YTest .* (q_max - q_min) + q_min;逻辑说明:测试集构造方式和训练集完全一致,保证predict输入的维度格式正确。predict返回的YPred是列向量,和YTest对齐后反归一化,得到和原始流量同量纲的预测值与实测值,画图对比才有意义。
这里要提醒一个概念:上面这段是“单步滚动预测”,每个测试样本的输入窗口都由真实历史值构成,评估的是模型在已知历史条件下的短期预测能力。但实际水文预报更关心多步预报,比如提前3天、7天预报,那需要把预测值当成下一步的输入窗口,形成真正的滚动外推,把模型输出回填到窗口尾部。纯预测模式误差会累积,但更接近工程实际。两种模式算出来的指标差别很大,评价模型效果时一定要说清楚用的是哪种模式,不然报告写出来都站不住脚。
5. 避坑记录:LSTM径流预测最常翻车的五个现场
模型能跑通只是开始,真正让人头秃的是结果不对。下面五个问题在各类水文LSTM代码里反复出现,有的修起来是一行的差别,有的要重写预测逻辑。全部按“现象→原因→解决”写,方便直接对着检查。
5.1 预测形态类:滞后、水平线、虚高指标
坑1:预测曲线整体滞后实测一天,洪峰对不上。现象:画图对比时预测峰总比实测峰晚1个时间步,峰值也明显偏小,曲线像是被向右平移了。 原因:模型学到的是“把上一天的值搬过来”,这是单变量径流预测最常见的病相,说明网络没有真正学到涨落过程的形态,或者输入窗口和目标有重叠。 解决:一是把lag从12提到24,给模型更长上下文;二是检查滑窗边界,输入窗口应该是i-lag到i-1,千万不能让第i个值自己出现在输入窗口里。输入和输出重叠时,模型会走捷径,预测自然滞后。
坑2:训练Loss很低,但预测输出接近一条水平线。现象:训练过程loss曲线平滑下降且数值很小,但测试集预测结果是一条接近均值的直线,算下来NSE是负的。 原因:典型的过拟合或欠拟合同时发生的假象。隐藏单元数太少时,网络退化成学习输出均值;学习率太低时,Loss卡在平缓区,模型根本没学进去。 解决:先把hiddenUnits提到100,InitialLearnRate调到0.01,MaxEpochs提到150,看曲线是否松动。如果仍然是一条直线,回去检查q_train_norm,归一化时如果除以0会得到NaN,NaN传进LSTM等于白训。
坑3:测试集NSE高达0.99,换个站点立刻崩。现象:当前数据上指标好得夸张,NSE接近1,但相同代码换到另一段数据上完全不行。 原因:九成是数据泄漏,最典型的是mapminmax用了全序列统计量,或者切分前对整段序列做了归一化,相当于模型提前偷看了测试答案。 解决:严格只用训练集算q_min、q_max,测试集和预测阶段复用同一组统计量。再教一个笨但有效的验证方法:在main.m开头临时把训练集截短一半,如果指标明显变化,说明模型真学到了规律;如果指标几乎不变,大概率是泄漏。
5.2 运行环境与读取类:连代码都跑不起来的问题
坑4:运行main.m报错“未定义函数或变量'lstmLayer'”,或Matlab启动时提示由于找不到msvcp140.dll无法继续执行代码。现象:代码本身没问题,一执行就报函数未定义;更早的时候是Matlab一启动就弹dll缺失的窗口。 原因:前者是Deep Learning Toolbox没装,lstmLayer是工具箱函数不是Matlab内置函数;后者是Windows系统缺少VC++运行库,Matlab的深度学习功能启动时需要加载这些动态链接库。 解决:前者在Matlab的“附加功能”里安装Deep Learning Toolbox,装完执行which lstmLayer确认路径可用;后者下载安装Microsoft Visual C++ Redistributable 2015-2022 x64版本,装完重启Matlab。这两个环境问题解决后基本不会再出现。
坑5:readmatrix读取01127000.txt后列数对不上,数据错位。现象:data维度不是预期的N×1,而是1×N,或者某一列全是0和NaN。 原因:txt里有表头、单位说明,或者制表符与逗号混用,readmatrix有时会把第一行当成数据,有时会把多列读成一行。 解决:先打开文件看前10行确认结构;有表头就加'NumHeaderLines',多列就显式取需要的列。还有一个笨但有效的方法:用readtable('01127000.txt')读成表格,再用table2array取数值列,表格能保留更多文件结构信息,排查起来更直观。
环境问题解决完再谈调参,顺序不要反。最后补一个环境核对清单,按这个查一遍能省出至少半小时:
| 检查项 | 确认内容 | 常见问题 |
|---|---|---|
| Matlab版本 | R2019a及以上 | 老版本没有readmatrix |
| Deep Learning Toolbox | 已安装 | lstmLayer未定义 |
| VC++运行库 | 2015-2022 x64 | msvcp140.dll缺失 |
| 当前目录 | 包含四个文件 | 路径不对读不到txt |
6. 进阶验证:用NSE和RMSE判断模型是真会还是假会
6.1 先小数据快速跑通再上全量
LSTM训练一次少则几十秒、多则几分钟,参数试错成本很高。我的习惯是先截取前2000个样本把全流程跑通,确认代码没有低级错误,再放开全量数据训练。
% 调试模式:只用前2000个点 debugLen = 2000; q_debug = q(1:min(debugLen, length(q))); % 后续归一化、切分、滑窗全部基于q_debug逻辑说明:压缩迭代周期,把“代码报错”和“模型效果差”两类问题分开处理。参数说明:debugLen取2000,按80/20切分后有1600个训练点,足够验证滑窗和trainNetwork是否正常,跑一轮只要几十秒。
6.2 NSE和RMSE的计算与判读
水文预报最常用的两个指标是NSE和RMSE。NSE越接近1越好,大于0.5算可用,大于0.65算良好,小于0说明模型比直接用均值预测还差。RMSE是带单位的绝对误差,方便直观感受误差量级。
% 计算RMSE和NSE,YPredReal和YTestReal需长度一致且已反归一化 residual = YPredReal - YTestReal; RMSE = sqrt(mean(residual.^2)); NSE = 1 - sum(residual.^2) / sum((YTestReal - mean(YTestReal)).^2); fprintf('RMSE: %.3f m3/s, NSE: %.3f\n', RMSE, NSE);逻辑说明:RMSE是预测误差的均方根,单位是流量单位,直观;NSE把模型误差和“用均值预测”的误差对比,是无量纲相对指标。参数说明:分母用的是实测序列的方差,所以NSE会惩罚对洪峰的低估,洪峰差一点,NSE掉得很快。
从复现这份资源的角度,建议下载后在Matlab里依次运行Normalization.m再跑main.m,然后把滞后步长和隐藏单元数都改一遍,分别算NSE对比,你会发现这个“调参—验证”循环比单纯跑通一次学到的东西多得多。从那以后我每次复现水文LSTM代码都强制走一遍先小数据验证、再上全量的流程,模型输出先画图看峰谷对没对上,再谈指标数值。判断“会了”的标准是曲线形状和NSE同时过关,而不是训练误差为零。希望帮到你。
本文还有配套的精品资源,点击获取