简介面向人工智能创新挑战赛的海洋气象预测任务这份zip压缩包围绕厄尔尼诺-南方涛动(ENSO)现象提供了一套可供参赛者与机器学习、深度学习、数学建模爱好者参考的完整实践方案。包内共16个文件大小仅211KB包含两个Jupyter Notebook、Python预测脚本、训练好的H5模型文件、Shell运行脚本以及多张JPEG/JPG格式的结果截图和说明文档兼顾代码、模型、图表与文字资料。已有162人学习/下载适合用来了解海洋气象数据挖掘的典型流程。通过Notebook可回顾从数据探索、特征处理、模型训练到Docker提交的完整思路py与sh脚本便于本地复现和批量运行H5模型可直接加载使用图片和文档则能帮助理解评分指标、结果可视化及关键实现细节对数学建模和深度学习实践均有参考价值也有助于将相关方案迁移到其他海洋气象数据任务中。1. 人工智能创新挑战赛海洋气象预测解压之后真正的门槛才刚开始拿到“人工智能创新挑战赛海洋气象预测.zip”先别急着解压跑基线。这类包通常装着数据说明、若干 NetCDF 文件和一套能跑通的参考脚本很多人以为难点在模型结构实际上绝大多数队伍是倒在数据预处理和验证集构造上的。海洋气象预测本质是一个时空序列回归任务输入过去若干时刻的海表温度、风场和气压场输出未来窗口的 SST 分布它对误差的评价比图像任务更苛刻不仅要看 RMSE还要看异常相关系数和锋面结构是否保真。这篇内容写给三类人准备参赛刷排名的学生、做风电功率或海洋牧场调度的从业者、想验证 AI 在物理场预测边界的算法工程师。我会沿着数据、模型、训练、避坑四条线把这套流程讲到可以直接复现的程度。2. 海洋气象预测的数据底子从 NetCDF 到可训练的 NumPy 张量2.1 变量怎么选SST、U10/V10、MSL 三件套就够了我习惯的第一原则先做小再做复杂。海洋气象预测最常用的公开再分析数据以 CMEMS 的日平均产品为代表空间分辨率 0.25°时间尺度覆盖过去几十年。最常用的变量组合是海表温度 SST、10 米经向风 U10、10 米纬向风 V10 和海平面气压 MSL。SST 既是预测目标也是最重要的自回归输入风场决定上层海流的平流输送气压场则提供大尺度天气系统的背景信息。再往下可以加海表盐度 SSS 和混合层深度但对 1 到 7 天的短期预报来说边际收益有限训练成本却接近翻倍。0.25° 分辨率意味着全球场大约 1440×720直接训练对显存和迭代速度都是负担。我一般会裁剪到目标海域比如西北太平洋区域纬度 25°N 到 45°N、经度 120°E 到 150°E裁剪后网格大约 120×120训练和推理都快得多。裁剪这个动作不是偷懒而是消除无关区域的干扰同时避免模型把大量参数花在不需要预测的洋面上。真正需要想清楚的另一点是时间分辨率日平均数据适合做 1 天以上的预报如果你要做 6 小时或 12 小时的短期预报就得换小时级产品样本量和训练开销都要重新评估。2.2 从 NetCDF 到样本序列xarray 切片与滑窗切片的单文件脚本这里给一段可以直接照抄的预处理脚本它把 NetCDF 读进来、裁剪海域、再按滑动窗口切出输入和目标张量。import xarray as xr import numpy as np from pathlib import Path def load_and_crop(nc_path, lon_range(120, 150), lat_range(25, 45)): ds xr.open_dataset(nc_path) ds ds.sel(lonslice(*lon_range), latslice(*lat_range)) return ds def make_samples(ds, input_steps10, target_steps5, var_list[sst, u10, v10, msl]): arrays [np.expand_dims(ds[var].values, axis1) for var in var_list] data np.concatenate(arrays, axis1) # (T, C, H, W) T data.shape[0] samples_x, samples_y [], [] for t in range(T - input_steps - target_steps 1): x data[t:t input_steps] # (input_steps, C, H, W) y data[t input_steps:t input_steps target_steps, 0:1] # 只预测SST samples_x.append(x) samples_y.append(y) return np.stack(samples_x), np.stack(samples_y)逻辑说明第一步用xarray.open_dataset读取 NetCDF再通过sel按经纬度切片只保留目标海域。第二步把多个变量沿着通道维拼接得到形状为(T, C, H, W)的四维数组然后用一个循环滑动窗口切样本。input_steps10表示用过去 10 天的数据target_steps5表示预测未来 5 天的 SST窗口每次移动 1 个时间步T 个时刻能切出T - input_steps - target_steps 1个样本。这里有一个容易忽略的细节y只取通道 0因为预测目标只有 SST而x保留了全部 4 个通道这样才能让模型从风场和气压场里学驱动信号。参数说明lon_range和lat_range要根据你的任务海域调整切换海域时只需要改这两个元组。如果数据集时间分辨率是小时级input_steps和target_steps的单位就变成小时10 小时输入预测 5 小时这种配置要重新按业务需求定。数据量不足时可以把stride加进去让窗口重叠滑动增加样本数但要注意重叠窗口会放大样本之间的相关性后续做验证集切分时要留出足够的时间间隔。2.3 归一化这步最容易埋雷只统计训练窗口MinMax 要序列化很多人图省事对全局数据做 MinMax结果验证集和测试集的信息提前渗进训练过程模型看起来收敛得又快又好实际上是在“开卷考试”。正确做法是只在训练集的时间段上统计每个变量的 min 和 max存成 JSON 文件然后用同一组参数去归一化验证集和测试集。import json import numpy as np from sklearn.preprocessing import MinMaxScaler def fit_scaler(train_x, save_pathscaler.json): # train_x: (N, T, C, H, W) scalers {} C train_x.shape[2] for c in range(C): scaler MinMaxScaler() flat train_x[:, :, c].reshape(-1, 1) scaler.fit(flat) scalers[str(c)] {min: float(scaler.data_min_[0]), max: float(scaler.data_max_[0])} with open(save_path, w) as f: json.dump(scalers, f) def apply_scaler(x, scaler_pathscaler.json): with open(scaler_path) as f: scalers json.load(f) x x.copy() for c in range(x.shape[2]): s scalers[str(c)] x[:, :, c] (x[:, :, c] - s[min]) / (s[max] - s[min]) return x逻辑说明fit_scaler把训练集所有样本、所有时间步的同一通道数据拉平后拟合 MinMax每个通道得到一对独立的 min/max。apply_scaler用训练集的参数去归一化任意数据集保证验证集、测试集和推理阶段使用的是同一套变换。参数说明风场 U10/V10 的取值范围一般在 [-20, 20]SST 在 [0, 30]如果不分开归一化模型会天然偏向数值大的变量。更精细的做法是气压场只做异常值处理先减去季节气候态再归一化因为 MSL 的绝对值在 1000 hPa 附近微小波动才是天气信号直接 MinMax 会把主导信号压没。提示scaler.json 是你整个项目的后悔药。推理阶段部署模型时没有它预测结果还原不到物理单位后面所有评估都失去意义。3. 模型选型与最小实现ConvLSTM 比 Unet 更配时空连续性3.1 三个候选模型的取舍逻辑常见选择是纯卷积 Unet、ConvLSTM 和视觉 Transformer。Unet 擅长空间特征的编解码但对时间维度的处理方式是把时间步堆在输入通道上相当于把时序关系“拍扁”成空间特征无法显式建模时间依赖。Transformer 的全局注意力适合捕捉大范围非线性关系可一旦训练数据不够过拟合来得非常快而且在气象场上还要小心位置编码的物理含义——经纬度网格不是自然语言里的 token 顺序相对位置编码和绝对位置编码的表达能力差异很大。ConvLSTM 把 LSTM 的门控机制搬进卷积层既能通过卷积保持空间局域性又能通过门控跨时间步传递信息是海洋气象这类强时空相关任务里最省算力的折衷方案。我在实际项目中对比过中等数据规模下ConvLSTM 比同参数量的 Unet 在验证 ACC 上高 0.03 到 0.05训练时间只有 ViT 的四分之一左右。这并不是说 Transformer 不行而是它的优势需要海量数据和精细调参才能兑现挑战赛的时间预算通常不允许你慢慢磨。如果你的预测时效超过 7 天可以考虑把 ConvLSTM 的输出再接一个 Transformer 层做全局修正但作为 baselineConvLSTM 的性价比是最稳的。3.2 ConvLSTM 的 PyTorch 最小实现下面这个实现可以作为你跑通流程的起点关键部分都写了注释。import torch import torch.nn as nn class ConvLSTMCell(nn.Module): def __init__(self, input_dim, hidden_dim, kernel_size3): super().__init__() self.hidden_dim hidden_dim pad kernel_size // 2 self.conv nn.Conv2d(input_dim hidden_dim, 4 * hidden_dim, kernel_size, paddingpad) def forward(self, x, state): h, c state combined torch.cat([x, h], dim1) gates self.conv(combined) i, f, o, g torch.split(gates, self.hidden_dim, dim1) i, f, o torch.sigmoid(i), torch.sigmoid(f), torch.sigmoid(o) g torch.tanh(g) c_new f * c i * g h_new o * torch.tanh(c_new) return h_new, c_new class ConvLSTM(nn.Module): def __init__(self, input_dim, hidden_dims, output_dim, kernel_size3): super().__init__() self.hidden_dims hidden_dims self.cells nn.ModuleList() prev_dim input_dim for h_dim in hidden_dims: self.cells.append(ConvLSTMCell(prev_dim, h_dim, kernel_size)) prev_dim h_dim self.head nn.Sequential( nn.Conv2d(hidden_dims[-1], 64, 3, padding1), nn.ReLU(), nn.Conv2d(64, output_dim, 1) ) def forward(self, x): # x: (B, T, C, H, W) B, T, _, H, W x.shape states [] for cell in self.cells: states.append((torch.zeros(B, cell.hidden_dim, H, W).to(x.device), torch.zeros(B, cell.hidden_dim, H, W).to(x.device))) outputs [] for t in range(T): xt x[:, t] for i, cell in enumerate(self.cells): h, c cell(xt, states[i]) states[i] (h, c) xt h outputs.append(h) last outputs[-1] return self.head(last)[:, None] # (B, 1, output_dim, H, W)逻辑说明ConvLSTMCell内部把输入和上一时刻的隐状态拼接后过一个卷积输出分成四份分别作为输入门 i、遗忘门 f、输出门 o 和候选记忆 g再按 LSTM 公式更新记忆细胞和隐状态。ConvLSTM的 forward 在每个时间步依次跑所有层然后用最后一时刻的隐状态经过 head 卷积输出预测场。head里的 output_dim 可以设为 1也可以直接设为 target_steps让网络一次输出未来 5 天的场比逐个时刻递归输出更稳定。参数说明hidden_dims 我一般从 [32, 64] 起步显存不够就降成 [32, 32]kernel_size 用 3 最稳5 的感受野更大但显存和计算时间涨得很快120×120 的网格上用 3 已经能覆盖足够大的局地区域。output_dim 如果设成 target_steps需要把输出 reshape 成(B, target_steps, 1, H, W)再和标签算损失而上面的代码返回的是(B, 1, output_dim, H, W)这个形状差异很容易在算 loss 时翻车。3.3 损失函数MSE 入门梯度损失才能把锋面保住MSE 是回归任务的默认选择但它只约束数值误差不约束空间结构。海洋气象场最典型的特征是锋面——SST 梯度急剧变化的狭长区域MSE 会让模型学到模糊的“平均场”锋面被平滑掉视觉上好看物理上失真。解决思路是加一个梯度损失项约束预测场和真实场在空间梯度上接近。import torch.nn.functional as F def total_loss(pred, target, alpha0.1): # pred/target: (B, T_out, 1, H, W) mse F.mse_loss(pred, target) grad_pred_x pred[:, :, :, :, 1:] - pred[:, :, :, :, :-1] grad_target_x target[:, :, :, :, 1:] - target[:, :, :, :, :-1] grad_pred_y pred[:, :, :, 1:, :] - pred[:, :, :, :-1, :] grad_target_y target[:, :, :, 1:, :] - target[:, :, :, :-1, :] grad_loss F.mse_loss(grad_pred_x, grad_target_x) F.mse_loss(grad_pred_y, grad_target_y) return mse alpha * grad_loss逻辑说明梯度损失用相邻网格的差值近似水平梯度分别计算 x 方向和 y 方向的梯度误差再加到 MSE 上。alpha 控制梯度约束的强度我一般从 0.1 开始调调大了整体数值误差会升高但结构更清晰调小了锋面会被磨平。参数说明这个方法只对规则网格有效如果你的数据是经纬度规则网格直接用差分没问题如果是非结构网格就得改用球面谐波或有限元梯度算子复杂度会高很多。注意梯度损失不是玄学它是把物理场的“形状”监督引入训练。做过锋面跟踪的人应该有体会MSE 低不代表场长得像。4. 训练与推理的完整闭环参数怎么设、结果怎么看4.1 按时间连续切分数据随机切分会让验证集“开卷考试”数据切分是挑战赛里最常见的失分点。如果把所有样本随机 shuffle 再按比例切出训练集和验证集训练集里 1 月 5 日的样本和验证集里 1 月 6 日的样本几乎共享同一个海洋状态模型在验证集上看到的输入场和训练时高度重叠指标虚高。这个问题在气象领域叫时空泄漏比普通机器学习里的数据泄漏更隐蔽因为画损失曲线时根本看不出来。正确做法是按时间连续切块比如数据覆盖 2015 到 2021 年训练集取 2015 到 2018 年验证集取 2019 年测试集取 2020 到 2021 年。如果训练数据不够可以用滚动窗口交叉验证但要保证每个 fold 之间至少留出 15 天以上的间隔把自相关的尾巴切断。这里的核心逻辑是海洋状态的时间自相关尺度在几天到几周之间间隔太短相当于把同一个物理过程复制到两边。4.2 四个必调参数时间步长、学习率、Batch Size、早停我通常固定一套初始参数跑通流程然后逐个调整。下面是经验值。参数推荐范围影响input_steps5 到 15 天过短学不到过程过长引入噪声learning_rate1e-4 到 1e-3大于 1e-3 时 loss 容易震荡batch_size8 到 32显存允许时取大值更稳patience8 到 15 轮防止过拟合同时留出调参空间学习率建议用 Adam cosine 衰减初始 1e-3 跑前 5 个 epoch如果 loss 不降就降到 3e-4。batch size 对时空模型的影响比图像分类更大因为每个样本本身包含多个时间步显存占用是普通图像的 10 倍左右120×120 的输入下 batch size 16 通常需要 16GB 以上显存。如果你的卡不够优先减小 hidden_dims 而不是 batch sizebatch size 太小会导致梯度噪声大训练不稳定。时间步长这个参数值得多说两句。10 天输入预测 5 天是常用的起点但你要根据数据的时间分辨率调整如果是 6 小时分辨率的数据10 个时间步只代表 60 小时远不够捕捉天气过程的演变。反过来时间步长拉到 30 天以上模型很容易学会“照着气候态外推”SSIM 看着不错实际异常相关性大幅下降。4.3 推理要做自回归单步预测与滚动预测的差距在 72 小时后显现训练时模型看到的是真实的历史输入推理时却要用自己的预测结果作为下一步输入这个分布偏移叫 exposure bias。如果业务上要预测未来 5 天你就不能只训练一个单步模型然后反复喂自己的输出误差会逐日累积到第 3 天基本变成气候态值。常见做法有两种第一种是训练时做 scheduled sampling以一定概率把上一时刻的预测值作为下一步输入第二种是直接训练多步输出让网络一次生成 5 天的结果损失函数对这 5 天同时计算误差。我一般先用多步输出做一个 baseline因为实现简单且稳定。scheduled sampling 的坑在衰减策略概率降太快模型学不到自回归修正能力降太慢训练不收敛。验证阶段要额外做一次完整的滚动预测评估只给模型前 10 天真实数据让它自己滚 5 步输出第 1 到第 5 天的预测然后逐日对比 ACC。这里你会看到第 1 天 ACC 可能很高第 3 天以后掉得很快这个衰减速度就是模型的上限也是后面调优要盯的指标。def autoregressive_eval(model, x_start, steps5): # x_start: (B, T_in, C, H, W) model.eval() preds [] cur x_start with torch.no_grad(): for _ in range(steps): out model(cur)[:, 0] # (B, output_dim, H, W) pred_step out[:, :1] # 取第一步预测 preds.append(pred_step) cur torch.cat([cur[:, 1:], pred_step], dim1) return torch.stack(preds, dim1) # (B, steps, 1, H, W)逻辑说明每步只取模型输出的第一个时间步拼到当前输入末尾同时丢掉最旧的一帧保持输入窗口长度不变这就是自回归滚动推理。参数说明x_start的最后一维和模型输出的通道数必须对齐这里假设只有 SST 参与自回归真实风场和气压场不更新如果你想输入真实风场做强迫就只替换 SST 通道其他通道保持历史观测不变这种混合方式是实际业务里更常见的做法因为风场和气压场是外部强迫模型预测不准它们时用观测值更可靠。5. 海洋气象预测避坑指南时空泄漏、翻车与玄学现场5.1 验证集与训练集“时空重叠”导致指标虚高现象本地验证 ACC 高达 0.95一提交线上测试分数直接腰斩。原因随机切分让验证集和训练集在时间上相邻模型的输入场和验证标签几乎来自同一个海洋状态相当于把正确答案的一部分写进了输入。解决按时间连续切分验证集和测试集确保验证时间段完全在训练时间段之后。我还会额外做一次“时间留一法”检查比如剔除某一年训练数据重新训练看模型在那一年的表现这能暴露模型是否真的学到了跨年泛化能力。5.2 时间步长拉得越长效果反而越差现象把 input_steps 从 10 加到 30验证 RMSE 从 0.35 涨到 0.42。原因海洋气象场具有很强的自相关但超过 20 天的历史信息对 5 天预报的帮助趋近于零反而给模型塞入大量低频气候信号干扰它对近期演变过程的捕捉。解决做一次步长敏感性测试分别用 5、10、15、20 步训练画一条验证 RMSE 随步长的曲线选择曲线的最低点。血泪经验是步长调到 30 以上时模型会开始输出“历史平均场”RMSE 看着没涨太多但锋面细节完全消失。5.3 归一化还原出错预测值全卡在阈值上现象预测结果还原成物理单位后SST 场出现大量接近 0°C 或 35°C 的平台区与实际场严重不符。原因推理时加载的 scaler.json 是验证集统计的或者干脆用了全部数据的 min/max导致归一化后的数值分布不同模型输出落在异常区间。解决只用训练期间保存的 scaler.json并在还原后做一次范围检查超出物理阈值比如 SST 在 -2°C 到 40°C 之外的点要标记出来而不是直接当有效值用。这类问题最坑的地方在于归一化只影响数值范围不影响空间分布形态所以很难从热力图形状上发现。5.4 模型输出“物理不守恒”锋面消失、均温漂移现象预测场非常平滑看起来比真实场“干净”但 SST 的全局均值比历史气候态偏高 1°C 以上。原因MSE 作为损失函数时模型倾向于输出条件期望——也就是所有可能结果的平均而平均场天然是模糊的锋面被平滑掉极端值被压缩。解决在损失函数里加入梯度损失或对抗性损失。如果发现均温漂移可以考虑给输出加一个全局均值约束让预测场的面积加权平均值与输入窗口的历史均值保持一致这个约束可以用一个简单的正则项实现。提示物理不守恒的问题在可视化时容易被忽视因为颜色映射的范围拉伸后看起来“差不多”。一定要算预测场的面积加权均值和梯度统计量和真实场做分布对比。5.5 挑战赛基线评分与自定义指标不一致现象本地 RMSE 优化得很好但排名不涨。原因很多比赛的官方评分是 ACC 或结构相似性指标它们和 RMSE 的相关性不是单调的——RMSE 低可能只是数值接近但空间相位偏移 1 个网格RMSE 不会飙升ACC 会大幅下降。解决本地评估至少同时算 RMSE、MAE、ACC 三个指标。ACC 的计算公式是预测异常和观测异常的空间协方差对相位错位非常敏感是我在调参时最依赖的指标。具体来说每个时间步单独计算空间 ACC 再取平均如果 ACC 低于 0.6说明模型的预测已经失去业务价值。6. 用显著性检验判断模型学到了物理规律而不是在拟合气候态模型跑通只是第一步真正值得花时间的是判断它到底学到了什么。这里推荐一个简单但有效的显著性分析对输入样本做梯度扰动观察哪个变量、哪个时间步对预测结果的影响最大。具体做法是构造一个样本把某个通道的数据置为零再计算输出相对于输入的梯度幅值梯度集中的区域就是模型决策的关键输入。如果你看到梯度几乎均匀分布说明模型没有建立起变量之间的因果关系只是在做一个模糊的统计映射。更直观的方法是做变量消融实验分别拿掉 SST、风场、气压场重新训练模型比较验证 ACC 的下降幅度。某一年我们模型的验证 ACC 很高后来把风场通道置零后精度几乎没掉才意识到模型只是把 SST 做了一次平滑外推真正决定海洋状态演变的 wind-driven 过程一点没学到。自那以后我把变量显著性检查写进了每一次实验的固定流程——先跑通再做消融再谈调参。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?