简介:这份资源面向机器学习入门者与海洋工程、气象水文方向的科研人员,提供用Python实现海浪波高预报的完整源码。针对传统数值预报成本高、周期长的问题,作者分别搭建LSTM与RNN两种循环神经网络模型,对站点风速与波高数据进行训练与对比实验,最终预报误差约0.2m,准确性较高,也可迁移到股票预测等时序任务中参考。压缩包共2个文件,包含1个nc数据文件与1个py源码文件,整体约207KB,nc文件存放站点风速与波高观测信息,py脚本则涵盖数据读取、模型构建、训练及预测全流程,并附有代码注释。源码会输出散点拟合图与预报折线图,便于直观评估模型效果。目前已有1081人学习下载,读者可借此掌握时序数据预处理、LSTM与RNN建模调参及误差分析思路,遇到疑问还可与作者交流讨论,适合作为课程设计或科研实验的参考案例。
1. 海浪波高预报为什么值得用 LSTM 和 RNN 重做一遍
近岸海浪波高直接决定施工窗口、航运安全和养殖设施的抗风险等级,传统做法靠数值模式跑谱方程,算一次动辄几十分钟,近岸浅水区还容易因为地形数据粗糙而失真。这几年做海浪波高预报的工程团队开始转向数据驱动:用浮标、雷达、卫星高度计积累的历史序列,训练 LSTM 或 RNN 直接拟合「过去 N 小时波高 → 未来 M 小时波高」的映射。它不替代物理模式,但能在秒级给出滚动预报,且对单点历史规律吃得比粗网格模式更细。这篇笔记面向已经会写 Python、想把这套东西真正跑起来的人:从数据组织、模型搭建、训练调参到上线前验证,把 LSTM 时间序列预测在波高场景里的完整链路讲清楚,源码结构也一并给出,方便你照着改。
2. 数据准备:波高序列怎么整理成 LSTM 能吃的张量
2.1 波高数据的来源与清洗口径
海浪波高预报的第一步不是建模,是把数据弄干净。常见来源有三类:浮标观测(逐小时或逐半小时有效波高 Hs)、雷达反演(时间分辨率高但噪声大)、再分析数据集(如 ERA5 的显著波高,空间覆盖好但时间粒度粗)。我一般优先用浮标数据做主训练集,再分析数据做补充和交叉验证,因为浮标是实测,最能反映近岸真实波动。
拿到原始序列后要处理四件事。第一是缺测,浮标因供电或通信中断常有整段缺失,直接插值会伪造趋势,正确做法是标记缺失段,训练时用掩码跳过,或者只保留连续长度超过阈值的片段。第二是异常值,波高出现 0 或超过物理上限(比如 25 米)基本是传感器故障,按 3σ 或物理阈值剔除。第三是时间对齐,不同来源时间戳要统一到整点,避免错位。第四是重采样,如果原始是 10 分钟粒度,做小时级预报就按小时聚合,取均值或最大值取决于你要预报的是平均波高还是极值波高。
清洗完的数据建议存成两列:时间戳和波高值,中间不要夹带其他字段,后续做滑窗时逻辑最清晰。
2.2 滑动窗口构造与归一化
LSTM 吃的是序列样本,需要把一条长序列切成「输入窗口 + 预测目标」的样本对。假设用过去 24 小时预测未来 6 小时,那每个样本的输入是 24 个点,标签是紧接着的 6 个点。窗口之间可以有重叠,重叠越多样本量越大,但要注意别让训练集和测试集因为重叠而泄漏。
import numpy as np import pandas as pd def make_windows(series, input_len=24, pred_len=6, stride=1): """ series: 一维归一化后的波高数组 input_len: 输入序列长度(小时) pred_len: 预测长度(小时) stride: 窗口滑动步长 返回 X: (样本数, input_len, 1), y: (样本数, pred_len) """ X, y = [], [] total = len(series) - input_len - pred_len + 1 for i in range(0, total, stride): X.append(series[i : i + input_len]) y.append(series[i + input_len : i + input_len + pred_len]) X = np.array(X).reshape(-1, input_len, 1) y = np.array(y) return X, y # 归一化:用训练集统计量,避免测试集信息泄漏 train_raw = df.loc[:'2022-12-31', 'Hs'].values mu, sigma = train_raw.mean(), train_raw.std() series_norm = (df['Hs'].values - mu) / sigma X, y = make_windows(series_norm, input_len=24, pred_len=6, stride=1)这段代码的关键点有三个。input_len和pred_len决定问题难度,输入越长模型看到的上下文越多,但太长会引入无关历史、拖慢训练,波高场景一般 12 到 48 小时比较合理。stride控制样本密度,训练集可以设小一点增加样本,测试集建议设成pred_len避免重叠泄漏。归一化必须用训练集的均值和方差,然后套用到验证和测试集,这是时间序列里最容易翻车的地方之一,用全量数据算统计量等于把未来信息喂给了模型。
提示:波高序列有明显的日周期和季节周期,如果数据跨年,建议在特征里额外加入小时、月份的正弦编码,纯波高值本身不含这些信息。
2.3 训练集、验证集、测试集的时间切分
时间序列不能随机切分,必须按时间先后切。常见比例是 7:1.5:1.5 或 8:1:1,训练集在前,验证集居中,测试集在最后。验证集用于早停和调参,测试集只在最后评估一次,不要反复拿测试集调模型,否则评估结果会虚高。如果数据量足够,还可以做滚动验证:用前几年训练、下一年验证,逐年滚动,这样更能反映模型在不同年份的泛化能力。
3. LSTM 与 RNN 模型搭建:从单层到堆叠的取舍
3.1 RNN 的梯度问题与 LSTM 的改进逻辑
RNN 的核心是循环单元,每个时间步把当前输入和上一时刻隐状态结合,理论上能记住任意长历史。但实际训练时,普通 RNN 的梯度在反向传播中会指数衰减或爆炸,导致它只能记住最近几步,长序列上表现很差。LSTM 引入输入门、遗忘门、输出门和细胞状态,让信息可以选择性保留或丢弃,梯度能沿细胞状态更稳定地流动,这就是它在波高这类中长序列任务上普遍优于普通 RNN 的原因。
选型上,如果输入窗口只有 6 到 12 小时,普通 RNN 也能凑合;一旦窗口拉到 24 小时以上,直接上 LSTM。GRU 是 LSTM 的简化版,参数更少、训练更快,在数据量不大时值得一试,但波高预报里 LSTM 的稳定性通常更好。
3.2 用 PyTorch 搭一个可训练的 LSTM 预报网络
import torch import torch.nn as nn class WaveLSTM(nn.Module): def __init__(self, input_size=1, hidden_size=64, num_layers=2, pred_len=6, dropout=0.2): super().__init__() self.lstm = nn.LSTM( input_size=input_size, hidden_size=hidden_size, num_layers=num_layers, batch_first=True, dropout=dropout if num_layers > 1 else 0.0 ) self.fc = nn.Linear(hidden_size, pred_len) def forward(self, x): # x: (batch, input_len, input_size) out, (h_n, c_n) = self.lstm(x) last = out[:, -1, :] # 取最后一个时间步的隐状态 return self.fc(last) # 输出 pred_len 个预测值 model = WaveLSTM(input_size=1, hidden_size=64, num_layers=2, pred_len=6)结构说明:hidden_size是隐状态维度,波高这种单变量任务 32 到 128 够用,太大容易过拟合。num_layers是堆叠层数,两层通常比一层好,三层以上收益递减且训练变慢。dropout只在多层时生效,用来抑制过拟合。最后接一个全连接层把隐状态映射到pred_len维输出,这是多步预测最直接的实现方式。
训练循环里几个参数要盯紧。学习率用 1e-3 起步,配合ReduceLROnPlateau在验证损失不降时减半。损失函数用 MSE 或 Huber,波高有极端值时 Huber 更稳。批次大小 32 到 128,序列任务里太小会让梯度噪声大。早停耐心值设 10 到 20 轮,避免无效训练。
criterion = nn.HuberLoss() optimizer = torch.optim.Adam(model.parameters(), lr=1e-3) scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(optimizer, patience=5, factor=0.5) for epoch in range(200): model.train() for xb, yb in train_loader: optimizer.zero_grad() pred = model(xb) loss = criterion(pred, yb) loss.backward() torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0) optimizer.step() # 验证阶段略,记录 val_loss 后 scheduler.step(val_loss)clip_grad_norm_是 LSTM 训练的后悔药,梯度爆炸时它能救回来,阈值 1.0 是常用起点。
3.3 多步预测的两种输出策略
上面用的是直接多输出:一次前向就吐出未来 6 个点。另一种是自回归:每次预测一步,把预测值拼回输入再预测下一步。直接多输出训练稳定、推理快,但各步之间独立性较强;自回归能保持时序连贯,但误差会累积,6 步以上容易漂移。波高预报里我一般用直接多输出,如果发现预测曲线过于抖动,再考虑加一个平滑后处理或改用自回归。
4. 训练调参与评估:让预报准确性真正站得住
4.1 波高预报该看哪些指标
MSE 和 MAE 是基础,但工程上更关心有效波高的绝对误差和相对误差。常用指标包括 RMSE(米)、MAE(米)、MAPE(百分比)以及相关系数。波高较小时 MAPE 会失真,所以低波高段建议看绝对误差,高波高段看相对误差。另一个实用指标是预报时效曲线:分别统计未来 1、3、6、12 小时的 RMSE,看误差随时效增长的速度,这直接决定你的预报能用到第几小时。
| 指标 | 含义 | 波高场景参考 |
|---|---|---|
| RMSE | 均方根误差 | 1 小时预报常见 0.1~0.2 米 |
| MAE | 平均绝对误差 | 比 RMSE 更抗极端值 |
| 相关系数 | 预测与实测相关性 | 0.9 以上算可用 |
| 时效 RMSE | 各预测步误差 | 判断有效预报时长 |
4.2 超参数怎么调才不玄学
调参顺序建议:先定窗口长度,再定网络容量,最后调学习率和正则。窗口长度用验证集 RMSE 选,24 和 48 各跑一遍对比。网络容量从hidden_size=32, num_layers=1开始,欠拟合就加,过拟合就减或加 dropout。学习率用网格 1e-2、1e-3、1e-4 试,配合学习率衰减。批次大小对最终精度影响不大,但影响训练速度,显存够就大一点。
如果验证损失震荡不降,先检查数据归一化和窗口切分有没有泄漏,再检查梯度是否爆炸。如果训练损失很低但验证损失高,是过拟合,加 dropout、减层数、增数据。如果两者都高,是欠拟合,加容量或加特征。
4.3 用测试集做一次诚实的评估
model.eval() with torch.no_grad(): preds = model(X_test_tensor).numpy() # 反归一化 preds_real = preds * sigma + mu y_real = y_test * sigma + mu rmse = np.sqrt(((preds_real - y_real) ** 2).mean()) mae = np.abs(preds_real - y_real).mean() print(f"RMSE={rmse:.3f} m, MAE={mae:.3f} m")反归一化别忘,否则指标全是归一化尺度,没有物理意义。评估时按预测步分别算 RMSE,画一张误差随时效增长的曲线,比单一数字更有说服力。
5. 避坑与排查:波高预报里最容易翻车的五件事
现象:验证损失远低于测试损失。原因通常是窗口重叠导致训练集和验证集信息泄漏,或者归一化用了全量统计量。解决:测试集窗口 stride 设为 pred_len,归一化只用训练集统计量,切分严格按时间。
现象:预测曲线整体滞后于实测。原因多是模型学到了「上一时刻值约等于当前值」的捷径,输入窗口太长而模型容量不足。解决:缩短输入窗口,增加 hidden_size,或在损失里对变化剧烈段加权。
现象:极端波高完全预测不出来。原因是极端样本少,MSE 被大量平静海况主导。解决:对高波高样本加权,或改用分位数损失,也可以单独训练一个极值分类器做触发。
现象:训练 loss 突然变 NaN。原因是梯度爆炸或学习率过大。解决:加梯度裁剪,学习率降到 1e-4,检查输入里有没有异常大值没清洗干净。
现象:换一个浮标站点精度骤降。原因是模型过拟合了单站点的局部规律。解决:多站点数据混合训练,加入站点编码或经纬度特征,做跨站验证。
6. 进阶技巧:把预报从「能跑」推到「敢用」
模型跑通只是起点,真正上线前我习惯做两件事。第一件是残差修正:把 LSTM 预测值和实测值的差再建一个轻量模型(哪怕就是线性回归),对系统性偏差做二次校正,这一步在波高预报里经常能把 RMSE 再压 5% 到 10%。第二件是集成:训练 3 到 5 个不同随机种子或不同窗口长度的 LSTM,预测取平均,方差明显下降,极端情况也更稳。
# 简单集成:多模型预测取均值 preds_ensemble = np.mean([m(X_test_tensor).numpy() for m in models], axis=0)验证方法上,除了常规指标,我强烈建议做一次「历史回放」:挑一段有代表性的天气过程(比如一次台风过境),把模型预报曲线和实测曲线叠在一起看。指标好看但过程曲线对不上,说明模型没抓住物理过程,这种模型上线后遇到没见过的海况就会翻车。回放时重点看峰值时刻和峰值高度,这两项对了,预报才敢交给业务用。
我自己的习惯是每次改完模型都留一份预测结果和对应实测,按月份归档,半年后回头看哪些月份系统性偏差大,往往能发现数据源或模型结构的隐藏问题。这套流程不复杂,但坚持下来比反复调参有用得多。希望帮到你。
本文还有配套的精品资源,点击获取