资讯详情 Python+LSTM水位预测系统:源码、模型文件与部署实践
📅 2026/11/2 11:28:44
简介一套基于Python的水位预测系统项目源码与预测模型文件面向水文预报、时序预测课程设计、毕业设计及入门实践者帮助补齐从数据处理、模型训练到预测复现的完整链路适合水文水利与时序预测场景。资源共9个文件以6个h5模型权重为主辅以1个py主程序脚本、1个ipynb实验笔记和1个csv数据样本压缩包约5.15MB体量轻便便于解压查阅。已有448人学习下载具备一定参考热度。读者可获得BiRNN、GRU、LSTM、SimpleRNN等多类循环神经网络权重结合实验笔记理解数据读取、特征处理、训练评估与结果对比流程主程序脚本便于快速运行和二次修改。整体适合作为水位序列预测的基线方案用于课程实验、模型对比、算法复现与业务数据替换验证减少从零搭建和重复训练成本。1. 从一次汛期夜班说起这套水位预测系统到底能预测什么去年汛期我在值班室遇到一个尴尬情况上游雨量站刚报来一小时降雨 18 毫米调度群里问「明早八点水位会涨到多少」我手里只有一个按经验公式计算的 Excel 表算出来的数值误差超过半米。后来拿到这套基于 Python 的水位预测系统源代码和配套预测模型文件才真正把「凭感觉判断」变成了「用模型计算」。它本质上是一套完整的多变量时序预测工程输入过去若干小时的水位、降雨、流量、季节因子输出未来 1 到 6 小时的水位序列。代码里数据清洗、滑动窗口、LSTM 网络、训练评估、推理接口、模型版本卡片一应俱全模型文件也一并给出拿到手能直接跑出预测曲线。适合三类人想复现一套可直接运行的一条龙时序项目的新手、要往水利或工业监测场景做二次开发的工程师、以及拿它当教学案例讲深度学习流程的人。2. 架构与目录模型文件、数据管道、服务层如何分层这份源码最值得先看的不是模型结构而是分层。很多同类型项目把清洗、训练、预测全塞进一个main.py跑通一次还行改任何参数都可能牵动全局。这套工程明显是按「配置 → 数据 → 模型 → 推理 → 服务」拆开的每层只依赖下一层的输出模型文件是层与层之间的连接点。理解这条依赖链后面改特征、换模型、加站点才不会改到别处。2.1 技术栈选型为什么是 Python 加 LSTM 而不是 ARIMA核心栈是 Python 3.9 以上、pandas、numpy、scikit-learn、PyTorch加一个轻量 Web 框架对外提供推理接口可视化用 matplotlib 或 plotly 出图。数据量通常是单个水文站几年到十几年的小时级记录几万到十几万行这个规模用 GPU 是浪费CPU 训练几分钟到半小时就能收敛。模型选 LSTM 的原因很实际水位变化不是单变量自回归它同时受上游来水、区间降雨、闸门调度影响而且降雨对水位的作用有明显的滞后窗口。ARIMA 这类线性模型处理多变量外生输入要一个个做差分和定阶加一个变量就要重新做一遍血泪经验是调参调到怀疑人生。树模型LightGBM、XGBoost在这个场景其实是很强的基线特征做好了效果不输深度模型但源码给的是 LSTM原因在于它自带时序结构不用手工构造十几阶滞后特征模型文件也小方便分发。提示如果你手上的站点数据只有几百条别硬上 LSTM先跑树模型基线误差对比之后再决定要不要上深度模型。依赖清单大体如下版本号按你本地实际能装上的来别死磕源码里写的具体小版本numpy1.23 pandas1.5 scikit-learn1.2 torch2.0 pyyaml6.0 joblib1.2 fastapi0.100 uvicorn0.23 matplotlib3.72.2 目录结构与各模块职责拿到压缩包先别急着pip install花三分钟把目录扫一遍能省掉后面一半的排查时间。典型结构是这样water-level-forecast/ ├── configs/ │ └── default.yaml # 全局配置路径、窗口长度、超参 ├── data/ │ ├── raw/ # 原始导出 CSV只读不改 │ ├── interim/ # 清洗后的中间态可随时重建 │ └── processed/ # 训练/验证/测试切分后的 npz ├── models/ │ ├── lstm_wl_v3.pt # 预测模型权重 超参 特征顺序 │ ├── scaler_x.pkl # 输入特征归一化器 │ ├── scaler_y.pkl # 目标水位归一化器 │ └── model_card.json # 模型版本卡片记录训练区间与指标 ├── src/ │ ├── data/ │ │ ├── loader.py # 读 CSV、字段校验 │ │ ├── clean.py # 重采样、缺失值、异常值 │ │ └── features.py # 特征工程、滑动窗口构造 │ ├── models/ │ │ ├── lstm.py # 网络定义 │ │ └── trainer.py # 训练循环、早停、指标记录 │ ├── inference/ │ │ └── predictor.py # 加载模型文件做单次/批量预测 │ ├── service/ │ │ └── api.py # HTTP 推理接口 │ └── utils/ │ ├── config.py # YAML 读取与环境变量覆盖 │ ├── logger.py # 统一日志格式 │ └── metrics.py # MAE / RMSE / NSE 计算 ├── scripts/ │ ├── train.py # 训练入口 │ ├── evaluate.py # 离线评估入口 │ └── export.py # 模型导出与卡片生成 ├── notebooks/ # 探索性分析不参与生产 ├── tests/ # 针对清洗与窗口构造的单元测试 ├── requirements.txt └── README.mdsrc/data只负责把原始表变成干净的规整时间序列不做任何模型相关的处理src/models只认数组不认 CSVsrc/inference是唯一允许torch.load的地方。这条约束很重要很多项目后来变得没法维护就是因为训练脚本里顺手写了推理逻辑特征变换在两边各写一份改了 A 忘了 B预测结果就会全部偏移。2.3 模型文件的内部约定为什么必须带 model_card.jsonlstm_wl_v3.pt不是一个纯净的state_dict它更像一个自描述包。里面通常包含state_dict、hparams隐藏层维度、层数、dropout、预测步长、feat_cols特征列顺序、lookback回看窗口、train_range训练起止时间。这些元信息必须和权重一起走否则你换个环境重新读数据列顺序一变模型照样能跑但输出的是一条完全无意义的曲线而且不报任何错。model_card.json是给人看的也是给线上监控用的形如{ model_name: lstm_wl_v3, created_at: 2024-06-11T09:20:00, train_range: [2019-01-01, 2023-12-31], valid_range: [2024-01-01, 2024-05-31], lookback_hours: 72, horizon_hours: 6, feat_cols: [level, rain, flow, up_level, hour_sin, hour_cos], metrics: {mae: 0.083, rmse: 0.121, nse: 0.912}, note: 验证集为枯水期汛期误差需复核 }我一般会在部署脚本里强制读这张卡片线上请求的特征顺序如果和feat_cols不一致直接返回 400 而不是硬跑。这条规则救过我一次那天有人在清洗脚本里加了一列rain_3h放在最前面忘了同步推理侧配置。3. 跑起来从零到看见第一张预测曲线真正上手只要四步装环境、备数据、训练或直接加载模型、跑推理看曲线。如果你只是想验证这套代码能不能用完全可以跳过训练直接用自带的模型文件做前向推理五分钟就能出图。3.1 环境与依赖安装建议用虚拟环境别在系统 Python 里折腾。PyTorch 的 CPU 版足够除非你的数据量特别大。# 建虚拟环境并激活Windows 用 venv\Scripts\activate python -m venv .venv source .venv/bin/activate # 先装 CPU 版 torch避免默认拉 CUDA 包拖慢速度 pip install torch --index-url https://download.pytorch.org/whl/cpu # 再装其余依赖 pip install -r requirements.txt # 自检能 import 且打印版本说明环境通了 python -c import torch, pandas, sklearn; print(torch.__version__, pandas.__version__)这里有个常见翻车点requirements.txt里如果写了torch2.x.x而你的 Python 是 3.12可能找不到对应轮子装到一半失败。遇到这种情况把 torch 那行删掉用上面命令单独装其余依赖照常。3.2 数据准备与配置项解读原始数据通常是一张宽表或多张表 join 后的 CSV最少要有这几列字段含义类型备注ts时间戳datetime建议带时区统一到站点本地时间level本站水位float目标变量单位米rain区间降雨float小时累计毫米flow下泄或入境流量float立方米每秒没有可留空up_level上游站水位float有则加缺则删列并同步改配置配置文件一般是configs/default.yaml内容大致如下改的时候注意缩进YAML 对空格极其敏感data: raw_path: data/raw/station_a.csv freq: 1h # 统一重采样频率 target_col: level feat_cols: [level, rain, flow, up_level, hour_sin, hour_cos] lookback: 72 # 回看 72 小时 horizon: 6 # 预测未来 6 小时 train_end: 2023-12-31 valid_end: 2024-05-31 model: hidden_size: 64 num_layers: 2 dropout: 0.2 lr: 0.001 batch_size: 64 epochs: 80 patience: 10 # 早停耐心值 output: model_dir: models model_name: lstm_wl_v3lookback和horizon是最该先调的两个参数。回看窗口太短模型看不到完整的涨水过程太长参数量不变但有效样本变少容易过拟合。经验上山区小流域用 24 到 48 小时平原河段用 72 到 120 小时先按这个范围试。预测步长 6 小时是业务上比较常用的值因为短期预报下发到一线还有处置时间。3.3 训练入口与产物说明配置改完训练就一行命令python scripts/train.py --config configs/default.yaml跑完在models/下会得到三样东西lstm_wl_v3.pt、两个 scaler 的 pkl、以及model_card.json。终端里会打印每个 epoch 的训练损失和验证集 MAE正常情况下验证 MAE 会在十几个 epoch 后趋于平缓早停触发时自动保存最优权重。如果你的数据是新站的训练前务必确认valid_end之后还留了一段没参与训练的数据做测试集。我见过有人把全部数据都划进训练区间验证指标漂亮得离谱上线后第一周就露馅。时间序列不能随机切分这是硬约束train.py里默认是按时间顺序切的别去动那段逻辑。3.4 推理验证与接口调用不训练也能验证直接拿自带模型跑# 用最近 72 小时数据做一次预测输出未来 6 小时水位 python -c from src.inference.predictor import Predictor p Predictor(models/lstm_wl_v3.pt, models/scaler_x.pkl, models/scaler_y.pkl) print(p.predict_from_csv(data/raw/station_a.csv)) 成功的话会打印一个长度 6 的数组单位米。之后再启服务uvicorn src.service.api:app --host 0.0.0.0 --port 8000 # 另开一个终端压一条请求 curl -X POST http://127.0.0.1:8000/predict \ -H Content-Type: application/json \ -d {station_id:A,recent:[[2.31,0.0,120.5,3.10,0.0,1.0]] }接口的recent字段需要按feat_cols顺序给条数等于lookback。这一步最容易出错的就是顺序和长度报 422 或长度不匹配时先回头看配置别急着改模型。4. 核心代码拆解从脏数据到一条可信的预测曲线这一章是真正要抄作业的部分。四段代码分别对应四个环节串起来就是完整的推理链路缺任何一环结果都不可信。4.1 数据清洗重采样、缺失值和物理边界原始数据通常是 5 分钟或 10 分钟一条直接喂模型噪声太大而且不同来源的采样频率对不齐。统一到小时是常规做法。# src/data/clean.py import pandas as pd def load_raw(path: str) - pd.DataFrame: df pd.read_csv(path, parse_dates[ts]) # 去重并排序时间序列一旦乱序滑动窗口就全错了 df df.drop_duplicates(ts).sort_values(ts) return df.set_index(ts) def resample_hourly(df: pd.DataFrame) - pd.DataFrame: # 水位取均值降雨取累计流量取均值——聚合方式必须符合物理含义 rule {level: mean, rain: sum, flow: mean, up_level: mean} out df.resample(1h).agg(rule) # 短缺口线性插值最多补 3 小时长缺口留 NaN后面构样本时整段丢弃 out[level] out[level].interpolate(limit3, limit_areainside) # 物理边界裁剪剔除传感器跳变比如水位瞬间抬升 5 米 out[level] out[level].clip(lower0.0, upper20.0) out[rain] out[rain].clip(lower0.0, upper200.0) return outlimit3的意思是连续缺 3 小时以内做插值超过就保留 NaN。为什么不全插因为长缺口插出来的是一条直线模型会把它当成真实平水过程学进去汛期涨水段的预测会系统性偏低。limit_areainside保证首尾的 NaN 不被填充避免序列开头凭空造出一个值。4.2 特征工程与滑动窗口构造单靠历史水位模型学不到降雨的滞后效应。加两列周期性编码是有必要的小时的正余弦能让模型区分「凌晨」和「下午」虽然水位本身跟钟点关系不大但降雨和用水调度有明显日内规律。# src/data/features.py import numpy as np import pandas as pd def add_time_features(df: pd.DataFrame) - pd.DataFrame: h df.index.hour df[hour_sin] np.sin(2 * np.pi * h / 24) df[hour_cos] np.cos(2 * np.pi * h / 24) return df def build_windows(df: pd.DataFrame, feat_cols, target_col, lookback72, horizon6): arr df[feat_cols].values.astype(float32) tgt df[target_col].values.astype(float32) X, y, idx [], [], [] total len(df) - lookback - horizon 1 for i in range(total): window arr[i:i lookback] # 窗口内含 NaN 直接跳过宁可少样本也不要污染训练集 if np.isnan(window).any(): continue X.append(window) y.append(tgt[i lookback:i lookback horizon]) idx.append(df.index[i lookback]) return np.stack(X), np.stack(y), pd.DatetimeIndex(idx)返回三个值而不是两个idx是这一批样本的预测起点时间后面画图和算指标都要靠它对齐少了它你根本不知道哪个预测对应哪个时刻。lookback72对应 3 天回看horizon6对应未来 6 小时输出这两个值与配置文件必须一致。注意feat_cols里千万不要放未来信息比如「未来 6 小时累计降雨」这种在真实推演时拿不到的字段。我在别的项目里见过把实测降雨当特征离线指标高得离谱上线直接失效。4.3 网络定义与训练循环模型本身不复杂两层 LSTM 后接一个全连接头直接输出多步。关键点是只取最后一个时间步的隐状态而不是把 72 个时间步全部展平。# src/models/lstm.py import torch import torch.nn as nn class WaterLevelLSTM(nn.Module): def __init__(self, n_feat, hidden64, layers2, horizon6, dropout0.2): super().__init__() self.lstm nn.LSTM(n_feat, hidden, layers, batch_firstTrue, dropoutdropout) self.head nn.Sequential( nn.Linear(hidden, 32), nn.ReLU(), nn.Linear(32, horizon), # 一次性输出未来 horizon 步 ) def forward(self, x): out, _ self.lstm(x) return self.head(out[:, -1, :]) # 只用最后一个时间步的语义batch_firstTrue表示输入形状是[batch, seq_len, n_feat]如果数据按[seq_len, batch, n_feat]组织记得改回去或者调整张量顺序否则训练损失会一直不降看着像模型学不动其实是维度错位。dropout0.2加在 LSTM 层间层数为 1 时不生效这是 PyTorch 的行为别以为是参数没传进去。训练循环里我一般会做三件事按时间顺序切分、用验证集 MAE 做早停、记录最佳 epoch 的权重而不是最后一个 epoch。# 训练循环核心片段简化 best_mae, patience_cnt float(inf), 0 for epoch in range(cfg.epochs): model.train() for xb, yb in train_loader: pred model(xb) loss criterion(pred, yb) # MSELoss 或 HuberLoss opt.zero_grad(); loss.backward() torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0) # 防梯度爆炸 opt.step() val_mae evaluate(model, valid_loader, scaler_y) if val_mae best_mae - 1e-4: best_mae, patience_cnt val_mae, 0 torch.save({...}, f{model_dir}/{model_name}.pt) else: patience_cnt 1 if patience_cnt cfg.patience: breakclip_grad_norm_(1.0)是防 LSTM 梯度爆炸的常规手段水位序列在汛期可能出现陡升梯度容易冲高不加这一行偶尔会跑出 NaN 损失。4.4 推理封装与反归一化推理侧最容易出的错是归一化器和特征顺序不匹配所以我把所有校验都塞进了构造函数。# src/inference/predictor.py import joblib, torch class Predictor: def __init__(self, model_path, sx_path, sy_path, devicecpu): ckpt torch.load(model_path, map_locationdevice) self.feat_cols ckpt[feat_cols] self.lookback ckpt[lookback] self.sx joblib.load(sx_path) self.sy joblib.load(sy_path) self.model WaterLevelLSTM(len(self.feat_cols), **ckpt[hparams]) self.model.load_state_dict(ckpt[state_dict]) self.model.eval() torch.no_grad() def predict(self, recent_df): # 严格按训练时的列顺序取缺列或多列都会在这里暴露 x recent_df[self.feat_cols].values[-self.lookback:] assert len(x) self.lookback, 回看窗口长度不足 x self.sx.transform(x).astype(float32) out self.model(torch.from_numpy(x).unsqueeze(0)) # 反归一化预测是在标准化空间里做的必须还原成米 return self.sy.inverse_transform(out.numpy())[0]unsqueeze(0)是补 batch 维模型需要三维输入。self.sx和self.sy千万别混用输入是 6 维、输出是 1 维用错了不会报形状错误sklearn 有时会静默处理但出来的数完全是乱的。这段的报错信息我刻意写得很直白就是为了让线上出问题时能一眼定位。5. 避坑与排查那些跑通之后才会遇到的问题训练能跑、接口能通不代表结果可信。下面五条是我实际踩过的按「现象 → 原因 → 解决」记下来遇到时按顺序对号入座。5.1 模型加载与环境相关的坑现象一torch.load报ModuleNotFoundError: No module named src.models.lstm或者UnpicklingError。原因保存模型时直接torch.save(model)序列化了整个对象pickle 里记录的是类所在的模块路径你把项目换个目录名或者从src.models改成models路径就对不上了。解决只存state_dict加一个超参字典加载时先重建网络结构再load_state_dict。新版 PyTorch 还可以加weights_onlyTrue但要求 checkpoint 里只放张量和基础类型。现象二模型在服务器上用 CPU 加载报设备错误。原因权重是在 GPU 上训练的张量带着cuda标记。解决torch.load(..., map_locationcpu)别省这个参数。5.2 数据与时间对齐相关的坑现象三重采样之后样本量凭空少了一半或者预测曲线整体平移了几个小时。原因时间戳没解析成 datetime 就做了resample或者原始数据混了两种时区导出系统一套、手工录入一套也可能parse_dates静默失败返回字符串。解决读入后立刻df.index pd.to_datetime(df.index, utcTrue).tz_convert(Asia/Shanghai)打印首尾三行确认再往下走。现象四训练集指标极好验证集也还行一上真实数据就输出一条几乎不变的直线。原因特征列顺序在清洗脚本和推理脚本里不一致或者 scaler 是全量数据 fit 出来的。LSTM 对输入顺序敏感列换了位置等于把降雨喂给了流量通道。解决把feat_cols写进 checkpoint推理时按它取列加断言scaler 只能在训练集上 fit验证和测试用 transform。5.3 效果与线上表现相关的坑现象五晴天预测正常一到降雨过程就系统性低估。原因训练数据里大暴雨样本少且降雨量用「实测值」当特征而实际推演时未来降雨只能用预报预报偏差在训练阶段完全没被建模。解决一是给降雨加一个「是否有预报」的标记位缺失时填 0 但标记位置 1让模型知道这是未知而非无雨二是在损失函数里对高水位样本加权用 HuberLoss 降低极端值影响。提示上线前一定要做一次滚动回放用过去 30 天的真实数据逐小时推演把预测值和实测值画在同一张图上。图上如果出现系统性滞后八成是特征里混进了未来信息。6. 二次开发从单站预测扩到多站和分级预警这套代码最值钱的地方是结构干净扩展成本低。我按改造难度排了个序从易到难改造点主要改动文件工作量风险预测步长改为 12 小时configs/default.yaml、lstm.py 的 horizon半天低样本数会减少加降雨预报特征features.py、更新 checkpoint一到两天中注意缺失标记分级预警阈值新增 service/alert.py半天低纯规则多站共用一个模型加 station_id embedding三到五天中高需重训多站分组建模trainer.py 循环 配置拆分一周中运维成本上升分级预警是最快能出效果的一环纯规则不用重训模型。我一般的做法是把预测水位和站点警戒水位比较分蓝黄橙红四级再用「未来 6 小时涨幅」做二次判定避免高水位平稳时反复报警。# src/service/alert.py def classify(forecast, warn_level, base_level): peak max(forecast) rise peak - base_level if peak warn_level 0.5 or rise 1.5: return red if peak warn_level or rise 1.0: return orange if peak warn_level - 0.3 or rise 0.5: return yellow return bluewarn_level是站点警戒水位base_level是当前实测水位。阈值别照抄按你所在流域的涨水速率调山区小流域 6 小时涨 1 米很常见平原河段涨 0.3 米就得留神。多站扩展我建议先用「一个模型 站点 id 做 embedding」这条路模型只多一个嵌入层训练数据量翻几倍泛化反而更好。等站点数量上了二三十个、各站特性差异很大时再考虑分组建模否则维护成本会拖垮你。# 多站改造的关键一步把站点 id 也编码进去 class MultiStationLSTM(nn.Module): def __init__(self, n_station, n_feat, hidden64, horizon6, emb8): super().__init__() self.station_emb nn.Embedding(n_station, emb) # 站点画像 self.lstm nn.LSTM(n_feat, hidden, batch_firstTrue) self.head nn.Linear(hidden emb, horizon) def forward(self, x, sid): out, _ self.lstm(x) h torch.cat([out[:, -1, :], self.station_emb(sid)], dim-1) return self.head(h)改造完之后验证方式比代码更重要。从那以后我每次上新模型都强制先走一遍影子回放新模型在后台对最近 30 天的数据逐小时推演只记录不对外和现网模型的预测结果一起进对比表连续一周误差不劣于旧模型才允许切流量。这套流程看着慢但比半夜被电话叫起来解释「为什么预测的值和实际差了一米」要省心得多。希望帮到你。本文还有配套的精品资源点击获取