
1. 先把 METEO-DLNet 的定位摆正METEO-DLNet 这个名字里的 METEO 指的是气象DLNet 指的是深度学习网络合起来就是一套面向气象要素预报的深度学习网络结构。我第一次看到这个名字的时候第一反应是又是一个把所有东西往里塞的缝合怪但真正把它的设计逻辑拆开看之后会发现它其实回答了一个非常具体的问题在已经有成熟数值预报产品的今天一个深度学习网络到底应该站在链路的哪个位置才不至于白费算力。它做的事情说白了就是吃进一段历史时段的多源气象格点场吐出未来若干小时的要素预报场并且在结构上同时照顾了空间多尺度特征、时间演变趋势和一部分物理合理性约束。用得上这套东西的人大概是这么几类做气象数据算法岗、想把模式输出再提一档准确率的工程师做新能源功率预测、需要更细粒度气象输入的打工人还有高校里做时空序列预测、想找个真实落地场景验证模型的学生。如果你只是想跑个 demo 看看 loss 能不能降那其实没必要上这个结构一个轻量 U-Net 就够了。我写这篇东西的目的不是复述论文而是把我在复现和改造这套网络的过程中那些文档里不会写、但你不注意就会白跑三天的细节摊开来讲。包括输入张量该怎么排、归一化为什么不能想当然、频谱损失到底在治什么病、显存怎么估算、指标怎么看才不被自己骗。这些东西靠读结构图是读不出来的只能靠一次次跑崩了再调。提示下文提到的所有数据集、场景、团队一律用泛称参数和数值是基于常见实践给出的参考量级实际项目请按自己的数据分辨率和算力重新核算。2. 网络结构逐层拆开看2.1 输入张量到底该怎么排很多人上手这类模型第一个卡点不是网络而是张量维度。METEO-DLNet 的输入一般是个五维张量形状是[B, T, C, H, W]其中 B 是批大小T 是历史时间步数C 是变量通道数H 和 W 是格点高宽。这个排布看着简单但里面有两个隐含决策必须先定死。第一个是 T 怎么取。常见做法是取过去 6 到 12 个时次间隔 1 小时。T 太小模型看不到演变趋势只能靠单帧做空间外推遇到快速生消的对流系统基本抓瞎T 太大显存和时间成本线性上涨而且早期帧的信息对短期预报的边际贡献会迅速衰减。我实测下来 6 帧是个性价比拐点再往上加RMSE 的改善已经进到小数点后第三位了。第二个是 C 怎么组。变量不是越多越好通道膨胀带来的不只是算力问题还有变量之间尺度差异巨大的问题——位势高度的量级是几千降水是零点几风分量是几到几十。如果不做分通道标准化就直接堆进去网络前期基本在学习怎么补偿这个尺度差。我一般把变量分成三类动力学量风、位势高度、气压、热力学量温度、露度、湿度、水凝物量降水、云量每类内部做独立标准化再拼成通道。2.2 时空编码器为什么把 3D 卷积拆成 2D 加 1D这是 METEO-DLNet 结构里我觉得最值得讲的一个取舍。理论上处理[T, C, H, W]最直接的办法是上 3D 卷积一次同时卷时间维和空间维。但 3D 卷积的参数量和显存占用是按kT × kH × kW三次方级别增长的一个3×3×3的核比起3×3的核参数量直接翻三倍而气象场的空间相关性远强于时间相关性——同一时刻相邻格点的相关性可以到 0.95 以上而相隔 6 小时同一格点的相关性可能只有 0.6。所以 METEO-DLNet 采用的是因子化分解先用 2D 卷积做空间特征提取再用 1D 卷积沿时间轴做时序融合两者交替堆叠。这样做的好处很直接参数量降下来了而且空间分支和时间分支可以分开调感受野。空间分支用空洞卷积扩感受野到几百公里尺度时间分支用kernel3的因果卷积保证不看到未来帧。代价是丢失了一部分时空联合模式比如锋面在移动过程中同时变形这类耦合现象分解结构表达起来会弱一些。如果你的任务是强对流这类快速演变过程可以考虑在深层加一两个真正的 3D 卷积块做补偿浅层还是用分解结构省算力。2.3 多尺度融合与跳跃连接的门控编码器下采样一般做 3 到 4 次每次分辨率减半、通道翻倍。以 512×512 输入为例四层下采样后特征图变成 32×32通道从 64 涨到 512。解码器对称上采样回去中间靠跳跃连接把浅层的高分辨率细节接回来。这部分和标准 U-Net 一样没什么新鲜的。真正有区别的是跳跃连接上的门控机制。原始 U-Net 的直接 concat 有个毛病浅层特征里噪声多、信噪比低直接拼到解码器会把一堆无效响应也带上去表现出来就是预报场出现细碎的斑点和毛刺。METEO-DLNet 在每条跳跃连接上加了一个轻量的注意力门控用解码器当前层的特征作为门信号对编码器传上来的特征做一次空间加权权重经过 sigmoid 压到 0 到 1 之间。这个改动参数量增加很少大概每层几千个参数但对输出场的平滑度改善很明显。我自己复现的时候试过把这个门控去掉结果降水预报场上出现了大量孤立的单点强降水TS 评分掉了将近 4 个百分点。原因是浅层的高频噪声被无差别地传到了输出端。所以这个门控不是可有可无的装饰建议保留。2.4 输出头确定性回归还是概率分布输出头有两个流派。第一种是纯确定性回归最后一层1×1卷积直接把特征映射到目标变量通道数损失用 MAE 或 MSE。结构简单、训练稳定但预报出来的是条件均值天生偏平滑极端值会被系统性低估。第二种是概率化输出每个目标变量输出两个通道一个是均值 μ一个是方差 σ²损失用高斯负对数似然。METEO-DLNet 的完整版走的是这条路因为气象业务上越来越需要不确定性信息——同样是 20mm 降水方差大和方差小对下游调度决策的意义完全不同。不过概率头有个训练陷阱σ² 很容易塌缩。模型发现把 σ² 调大能让损失下降于是所有位置的方差都往大里跑最后预报区间宽得毫无信息量。解决办法是给 σ² 加一个下界约束并且在损失里加一项对 σ² 的弱正则惩罚它偏离历史气候态方差太远。我一般把下界设成对应变量气候方差的 0.01 倍正则系数放在 1e-3 量级。2.5 参数量与显存的实算这部分建议每个人上手前自己算一遍能省掉很多跑起来才发现爆显存的时间。假设输入T6, C16, HW512主干通道基数 64。单帧单样本的输入元素数是16 × 512 × 512 4,194,304六个时次就是约 2517 万个浮点数。用 fp32 存储是2517万 × 4B ≈ 100MB。这个还不算什么真正吃显存的是中间激活值。第一层卷积输出 64 通道、512×512单样本就是64 × 262144 × 4B ≈ 67MBbatch size 取 4 的话就是 268MB而这只是一层。整个编码解码链路下来激活值占用量大概是输入的好几倍。粗略估算公式可以这么记激活显存 ≈B × Σ(每层 H×W×C) × 4B × 系数系数取 1.5 到 2 之间考虑反向传播要存的中间量。24GB 显存的卡上512×512输入、C16、T6的配置batch size 开到 8 基本就是上限再往上要靠梯度检查点或者混合精度。开了 AMP 之后显存能压到大概 60%速度还能快 30% 左右几乎是必开选项。3. 数据管线模型效果八成埋在这里3.1 时空对齐与网格重采样多源数据最大的坑是时空不一致。雷达是极坐标扫描转成的笛卡尔网格卫星是卫星投影地面站是点位观测再分析资料是等压面经纬网格。这些东西要拼成一个张量必须统一到同一套网格和时间戳上。时间上我一般把所有数据对齐到整点非整点观测做时间线性插值插值窗口不超过 10 分钟超过的直接丢弃不用。空间上统一重采样到等经纬度网格分辨率取所有数据源里最粗的那个避免上采样引入虚假细节。重采样方法上连续量用双线性类别量比如降水等级、云类型用最近邻这条如果搞反了类别场会出现一堆中间值网络学出来的东西完全没法解释。还有个容易被忽略的点目标网格的经纬度范围要比你实际关心的区域往外扩一圈至少扩 64 个格点。原因是卷积在边界处有padding效应边缘区域的预报质量天然差一截把边界留在关心区域外评估的时候截掉就行。3.2 归一化踩坑全局均值方差是陷阱这是我见过最多人翻车的地方。很多人拿到数据直接算全样本的均值和标准差然后对所有样本做同一套标准化。这在图像任务里没问题在气象任务里会出大事。原因在于气象要素有强烈的季节循环和日变化。夏季降水均值可能是冬季的好几倍用全年均值去标准化冬季的降水场全被压到接近零模型学出来就是冬天不下雨。正确做法是按时间段做条件标准化比如按月份或者按滑动窗口计算统计量或者干脆用物理上更有意义的变换。这里给几个我验证过效果不错的变换变量类型推荐变换说明位势高度、温度减气候态 除以气候标准差去掉季节循环保留距平信息风分量 u/v直接除固定尺度因子尺度因子取 10 m/s避免极端风速拉偏统计降水log1p后再标准化降水长尾严重取对数后分布接近正态气压减 1000 hPa 后除尺度因子数值本身太大减掉基准后网络好处理相对湿度直接除以 100本身有界简单缩放即可注意标准化用的统计量必须只用训练集算验证集和测试集的统计量绝对不能混进来。这条听着是常识但我见过至少两个项目在这上面栽跟头离线指标漂亮得不像话上线直接打回原形。3.3 派生物理量的构造清单原始变量往往不够用网络需要一些物理上有意义的组合量才能捕捉关键结构。METEO-DLNet 的输入通道里我一般会额外构造这几类风切变低层和高层风矢量的差反映大气垂直不稳定度对强对流预报很关键。计算时用sqrt((u2-u1)² (v2-v1)²)。温度平流-(u·∂T/∂x v·∂T/∂y)反映冷暖空气的输送是判断锋面移动的重要依据。水汽通量q × V其中 q 是比湿V 是风矢量反映水汽输送强度。散度和涡度用有限差分近似计算虽然分辨率有限会有误差但对识别辐合辐散区很有用。对流有效位能的简化近似可以用低层温度和高层温度、湿度组合算个粗略版本不必上完整的抬升过程计算。构造这些量的时候注意一件事差分计算会引入边界误差所以要么先把网格往外扩一圈再算要么在边界处用单边差分。我吃过这个亏边缘一圈的涡度值异常大训练前期 loss 一直在抖后来发现是边界差分的问题。3.4 数据泄漏的隐蔽来源气象时序数据做训练集划分最容易出的问题不是随机划分这个大家都有意识了而是一些更隐蔽的泄漏。一是时间窗口重叠。假设你用过去 6 帧预测未来 12 帧样本之间的时间跨度是 18 帧。如果训练集切到 6 月 30 日验证集从 7 月 1 日开始那 6 月 30 日 23 时的这个样本它的目标帧已经落到 7 月 1 日了。解决办法是在训练集和验证集之间留一段空白期长度至少等于输入窗口加输出窗口。二是统计量泄漏。前面提过的归一化统计量还有损失函数里的权重系数如果是在全量数据上统计出来的那就是泄漏。三是空间泄漏。如果你的训练集和验证集是按格点随机划分的相邻格点之间相关性极高等于模型在训练时已经见过验证集邻域的信息。正确做法是按区域块划分或者按时间划分。四是预处理顺序。插值、平滑、去噪这些操作如果在划分数据集之前对全体数据做了也会带来轻微泄漏尤其是滑动窗口平滑这种操作。4. 损失函数与训练策略4.1 为什么主损失用加权 MAE 而不是 MSEMSE 对大误差的惩罚是平方级的这在气象预报里很危险因为数据里总有那么几个极端样本台风、强对流它们的误差量级比普通样本大几十倍。用 MSE 训练梯度会被这些极端样本主导模型会牺牲大量普通样本的精度去拟合那几个极端值。MAE 的梯度是常数级别对异常值更鲁棒代价是在最优点附近梯度不连续收敛会慢一些。实际用法是主损失用 MAE配合一个小的 MSE 项做平滑权重比大概是0.8 : 0.2。更关键的是加权。不同变量的可预报性差异很大位势高度这种大尺度场模型很容易学到接近完美的程度误差早早就降到很低降水这种局地性强、随机性大的量误差怎么都降不下去。如果不加权总损失会被位势高度主导模型实际上是在优化一个不重要的目标。我的做法是按变量给权重权重和该变量在气候态上的相对误差水平成反比让各变量对总损失的贡献大致持平。还有一层是空间加权。降水在空间上极度稀疏可能 95% 的格点都是零。如果均匀加权模型很快学会了全输出零这个策略损失能降到一个看起来还行的水平但实际毫无用处。解决办法是给有降水的格点更高权重权重可以取降水值的某个变换比如1 α·log1p(rain)α 控制在 5 以内太大又会让极端格点主导。4.2 频谱损失对付预报糊成一片做过降水外推的人都知道那个经典现象训练到后期RMSE 一直在降但把预报场画出来一看全是糊的雷达回波上的精细纹理全没了。这不是模型没训好这是像素级损失函数的固有缺陷——当模型对未来不确定时最小化期望误差的最优策略就是输出均值而均值场天生就是平滑的。频谱损失就是治这个病的。做法是把预报场和真值场各做一次二维傅里叶变换取幅度谱算两者的差异。这样一来高频分量对应小尺度纹理的缺失就会被直接惩罚。实现上可以用现成的 FFT注意做fftshift把低频挪到中心然后对幅度谱取对数压缩动态范围不然低频项会完全主导。权重上频谱损失不能给太大。我试过给到和 MAE 同量级结果模型开始生成虚假的高频噪声场面上全是棋盘格。合适的比例大概是频谱损失占总损失的 5% 到 10%。除了频谱损失还有两个替代方案可以考虑一是梯度损失直接算预报场和真值场的空间梯度差异实现更简单二是对抗损失加个判别器逼模型生成真实纹理但训练稳定性差调参成本高。我一般先上梯度损失不够再考虑频谱。4.3 物理约束项怎么加才不拖后腿METEO-DLNet 里有个物理约束分支本意是让预测结果满足一些基本的物理规律。常见的约束有两类一类是非负约束降水、湿度这类量不能为负。实现上有两种方式一是在输出层套个 ReLU 或者 softplus直接保证非负二是在损失里加惩罚项对负值做惩罚。我倾向于前者简单可靠不需要调惩罚系数。另一类是动力学约束比如地表气压场和风场之间的平衡关系或者连续方程。这类约束实现起来复杂而且网络输出本身是离散格点上的近似硬套连续方程会引入额外误差。我的经验是只在损失里加一个弱约束项权重放在 1e-3 到 1e-4 量级作用更多是正则化而不是硬性限制。有个坑要提醒物理约束项如果权重给大了训练会变得非常不稳定loss 会出现周期性震荡。原因是约束项的梯度方向和主损失经常冲突。缓解办法是用梯度归一化或者对约束项做梯度裁剪。4.4 学习率、批大小、混合精度的配比这几个超参是联动的不能单独调。批大小首先受显存限制。在前面算过的配置下512×512、C16、T624GB 卡上 batch 8 是相对稳妥的选择。批大小定了之后学习率按线性缩放规则给个初值lr base_lr × batch / 256。如果 base_lr 取 3e-4batch 8 对应就是 9.4e-6这个值偏小实际训练会太慢。所以实践中更多是用固定学习率加 warmup从 1e-6 线性升到 1e-4用 500 到 1000 步完成 warmup然后接余弦退火到 1e-6。优化器用 AdamW权重衰减给 1e-2。这里要注意权重衰减不应该作用在归一化层的参数和偏置项上需要分组设置。混合精度基本必开。fp16 在气象场上有个隐患位势高度的数值范围大某些中间激活值可能溢出。用 bfloat16 更安全动态范围和 fp32 一致如果硬件支持优先选 bf16。用 fp16 的话记得开梯度缩放初始缩放因子给 2^16如果连续 2000 步没有出现 inf 就翻倍出现 inf 就减半。梯度裁剪也要开max_norm设 1.0。气象数据的极端样本容易产生大梯度不裁剪的话偶尔会炸一次前面的训练全白费。5. 从零跑通一次完整训练5.1 环境与目录约定我习惯把项目结构固定下来换机器、换人接手都不用重新理解。meteo-dlnet/ ├── configs/ │ ├── base.yaml │ └── exp_512.yaml ├── data/ │ ├── raw/ │ ├── processed/ │ └── stats/ # 归一化统计量只从训练集算 ├── src/ │ ├── dataset.py │ ├── model.py │ ├── losses.py │ ├── metrics.py │ └── train.py ├── scripts/ │ ├── prepare_data.py │ └── evaluate.py └── runs/ # 每次实验一个子目录关键点是stats/目录和runs/目录的隔离。统计量是数据的一部分跟着数据走实验产物跟着实验走每次训练生成一个带时间戳的子目录里面存配置快照、日志、检查点。这样回头复现任何一次实验都能精确还原。配置文件用 YAML把数据路径、模型超参、训练超参、损失权重全部外置。不要把这些硬编码在代码里不然跑三次实验之后就分不清哪次用的什么配置了。5.2 数据集与 DataLoader 的关键代码数据加载这块核心难点是既要随机采样又要保证时间连续性。下面是我常用的骨架import numpy as np import torch from torch.utils.data import Dataset class MeteoWindowDataset(Dataset): def __init__(self, files, in_len6, out_len12, statsNone): self.files files self.in_len in_len self.out_len out_len self.stats stats self.window in_len out_len # 预计算所有合法起始索引避免运行时判断 self.index [] for fi, f in enumerate(files): n f[length] for s in range(0, n - self.window 1): self.index.append((fi, s)) def __len__(self): return len(self.index) def __getitem__(self, i): fi, s self.index[i] arr self.files[fi][array] # 内存映射不实际读盘 seq arr[s : s self.window] # [T_total, C, H, W] seq self.normalize(seq) x seq[: self.in_len] y seq[self.in_len :] return torch.from_numpy(x).float(), torch.from_numpy(y).float() def normalize(self, seq): out np.empty_like(seq, dtypenp.float32) for c, (mu, sd, tf) in enumerate(self.stats): v seq[:, c] if tf log1p: v np.log1p(np.maximum(v, 0)) out[:, c] (v - mu) / sd return out几个实现要点值得说用np.load的mmap_moder做内存映射不要把整个数据集读进内存。气象数据动辄几十 GB全读进来机器直接卡死。内存映射让操作系统帮你管理缓存实际效率反而更高。索引表在初始化时一次性算好__getitem__里只做切片。这个优化能让数据加载速度提升好几倍尤其是样本量大、窗口滑动密集的时候。归一化放在__getitem__里做而不是预处理阶段做是为了保持原始数据的可逆性方便后续反归一化算物理量误差。代价是每个 epoch 都要重复计算如果 CPU 成为瓶颈可以考虑用num_workers开多进程或者把归一化后的数据落盘。DataLoader 配置上num_workers设成 CPU 核数的一半左右pin_memoryTruepersistent_workersTrue避免每个 epoch 重建进程。drop_lastTrue在训练时开避免最后一个不完整批次影响批归一化的统计。5.3 模型与损失的组装模型的简化骨架大概是这样import torch import torch.nn as nn class ConvBlock(nn.Module): def __init__(self, cin, cout, k3): super().__init__() self.body nn.Sequential( nn.Conv2d(cin, cout, k, paddingk // 2), nn.GroupNorm(8, cout), nn.GELU(), nn.Conv2d(cout, cout, k, paddingk // 2), nn.GroupNorm(8, cout), nn.GELU(), ) def forward(self, x): return self.body(x) class TemporalBlock(nn.Module): 因果时间卷积保证不看未来帧 def __init__(self, c, k3): super().__init__() self.pad k - 1 self.conv nn.Conv1d(c, c, k) def forward(self, x): # x: [B*H*W, C, T] x nn.functional.pad(x, (self.pad, 0)) return self.conv(x) class METEO_DLNet(nn.Module): def __init__(self, cin16, base64, out_c8, t_out12): super().__init__() self.t_out t_out self.stem ConvBlock(cin, base) self.enc1 ConvBlock(base, base * 2) self.enc2 ConvBlock(base * 2, base * 4) self.pool nn.MaxPool2d(2) self.mid ConvBlock(base * 4, base * 4) # 解码段略结构与标准 U-Net 对称 self.head nn.Conv2d(base, out_c * t_out, 1) def forward(self, x): B, T, C, H, W x.shape # 把时间维压进批维复用 2D 空间分支 x x.reshape(B * T, C, H, W) h self.stem(x) h self.pool(self.enc1(h)) h self.pool(self.enc2(h)) h self.mid(h) # ... 解码回原分辨率 ... out self.head(h) # [B*T, out_c*t_out, H, W] out out.reshape(B, T, -1, H, W) return out[:, -1] # 只用最后一个时次出预测这里有个实现细节值得单独说把B和T压成一个维度去做空间卷积是省显存的一个常用技巧但也意味着空间分支完全看不到时间信息。所以时间融合必须靠单独的TemporalBlock插在中间或者在解码后对[B, T, C, H, W]沿 T 维做一次卷积。我一般是在瓶颈层做时间融合那里分辨率最小算力开销可以接受。损失函数组装def total_loss(pred, target, cfg): # 主损失逐变量加权 MAE w cfg.var_weights.view(1, -1, 1, 1) l_main (torch.abs(pred - target) * w).mean() # 平滑项小幅 MSE改善最优点附近的梯度行为 l_mse ((pred - target) ** 2).mean() # 梯度损失治模糊 dp torch.diff(pred, dim-1), torch.diff(pred, dim-2) dt torch.diff(target, dim-1), torch.diff(target, dim-2) l_grad sum(torch.abs(a - b).mean() for a, b in zip(dp, dt)) # 频谱损失治高频缺失 fp torch.fft.fft2(pred, normortho).abs() ft torch.fft.fft2(target, normortho).abs() l_spec torch.abs(torch.log1p(fp) - torch.log1p(ft)).mean() return (cfg.w_main * l_main cfg.w_mse * l_mse cfg.w_grad * l_grad cfg.w_spec * l_spec)权重配置上我的常用起点是w_main1.0, w_mse0.2, w_grad0.05, w_spec0.02。这几个数不用一次调到位先跑个三五轮看各项损失的量级如果某一项比主损失大了两个数量级说明权重给高了。5.4 训练循环与断点续训训练循环本身不复杂关键是断点续训要做扎实。除了模型参数优化器状态、学习率调度器状态、梯度缩放器的状态、当前 epoch 和 step、随机数种子状态这些都要存。少存一样续训之后的结果就和没断过不一样。def save_ckpt(path, model, opt, sched, scaler, epoch, step, best): torch.save({ model: model.state_dict(), opt: opt.state_dict(), sched: sched.state_dict(), scaler: scaler.state_dict(), epoch: epoch, step: step, best: best, rng_torch: torch.get_rng_state(), rng_numpy: np.random.get_state(), }, path)检查点保存策略上我一般存三份最近一次的last.pt每轮覆盖、最优指标的best.pt、以及每 N 轮一份的epoch_{n}.pt保留最近 3 份。第一份用于续训第二份用于最终评估第三份用于回溯问题。日志方面每个 step 记录各项损失分量和学习率每个 epoch 记录验证集指标。损失分量一定要分开记只记总损失的话出问题根本不知道该往哪查。5.5 训练曲线怎么读曲线判读是门手艺我给几个我总结的信号曲线形态可能原因处理方向训练损失降、验证损失平后上升过拟合加 dropout、减模型宽度、加数据增强两条曲线都平且很高欠拟合或学习率太小加宽模型、提学习率、检查数据管线损失周期性剧烈震荡学习率太大或物理约束权重过高降学习率、减小约束权重损失突然变成 NaN梯度爆炸或数值溢出开梯度裁剪、检查是否有 inf 输入主损失降但梯度损失上升模型在往平滑方向退化提高梯度/频谱损失权重还有一个隐蔽情况验证损失一直比训练损失低。这通常不是模型泛化好而是验证集用了和训练集不同的归一化统计量或者验证集样本恰好比较简单比如都在晴好天气。遇到这种先别高兴把验证集按天气类型分层看一遍。6. 评估、推理与落地部署6.1 指标组合RMSE 之外必须看的三个只盯 RMSE 是会被骗的。RMSE 对平滑的预报场特别友好一个全输出气候平均值的模型RMSE 可能还不错但业务上完全没用。我一般看这四个指标的组合RMSE 和 MAE是基础反映整体误差水平分开看能判断误差分布是均匀的还是被少数大误差拉起来的。ACC距平相关系数反映预报场和实况场在空间形态上的一致性把气候态扣除后算相关。这个指标对大尺度形势报得对不对很敏感是判断模型有没有真正学到环流结构的关键。TS 评分Threat Score用于分类预报比如降水是否超过某个阈值。计算方法是对每个阈值统计命中、空报、漏报TS 命中 / (命中 空报 漏报)。这个指标对稀有事件很敏感是检验极端降水预报能力的主要依据。建议至少看 1mm、10mm、25mm、50mm 四档。FSS分数技巧评分是 TS 的空间版本在计算前先对预报和实况做邻域平滑允许一定空间容差。这个指标比 TS 更适合评估高分辨率预报因为高分辨率预报的位置偏差很常见用 TS 会过度惩罚。如果模型是概率输出还要加CRPS连续 ranked 概率评分这个指标同时评估了预报的准确性和不确定性刻画的可靠性越小越好。6.2 推理加速的几条实际路径训练完的模型要上线推理效率是硬约束。几条我试过的路径按投入产出排序第一条是混合精度推理。这个几乎零成本改几行代码就能拿到 30% 到 50% 的加速。注意推理时用 fp16 可能导致输出的细微差异如果下游对数值精度敏感关键变量还是走 fp32。第二条是算子融合和静态图导出。把模型导出成 ONNX 格式然后用推理引擎跑通常能再拿 20% 到 30%。导出时要注意动态维度设置把 batch 维和空间维设成动态的不然换个输入尺寸就得重新导出。有些自定义算子比如前面提到的注意力门控可能不被支持需要在导出时替换成等效的标准算子。第三条是减少重复计算。业务上往往是滚动预报每小时的输入窗口只滑动一格但相邻两次推理的输入有 5/6 是重叠的。理论上可以缓存编码器的中间结果只算新增的那一帧。实际实现比较麻烦因为时间卷积的感受野会跨帧但如果是纯 2D 空间编码器加独立时间融合的结构这个优化是可行的。第四条是模型蒸馏或者剪枝。如果算力实在紧张可以用大模型蒸馏一个小模型或者做通道剪枝。我做过一次通道剪枝砍掉 40% 的通道推理速度快了近一倍RMSE 只涨了 3%性价比很高。剪枝要注意别一刀切按通道的重要性排序来剪重要性可以用 BN 层的缩放因子或者梯度统计来评估。6.3 业务上线前的对拍流程模型在离线指标上好看不等于能上线。上线前我一般做三轮对拍第一轮是历史回算对拍。挑一段完整的、包含各种天气类型的时段至少一个月把模型预报和现有业务产品逐时次对比看有没有系统性偏差。重点看偏差的日变化——很多模型在夜间和白天表现差异很大原因是输入里的辐射相关变量处理不一致。第二轮是极端个例对拍。专门挑几次强降水、大风、寒潮过程看模型在这些关键时段的表现。这一步经常能发现问题比如模型在强降水峰值时刻系统性低估 30% 以上这种在平均指标里完全看不出来。第三轮是实时并行对拍。模型和现有系统同时跑跑一到两周观察稳定性。重点看推理耗时是否稳定、内存是否泄漏、异常输入是否会导致崩溃。我遇到过一次因为某个时次的数据缺测导致全零输入模型输出了一堆离谱的值后来加了输入有效性检查才解决。7. 常见问题速查与踩坑记录7.1 问题速查表现象排查顺序常见根因训练 loss 不降1. 检查输入归一化 2. 用小学习率单批次过拟合 3. 检查标签对齐归一化统计量错误、标签错位、学习率过大预报场全零或接近常数1. 看输出层激活 2. 查损失权重 3. 查类别不平衡稀疏目标未加权、输出层饱和预报场有明显网格状伪影1. 查上采样方式 2. 查卷积 padding 3. 查数据拼接转置卷积棋盘效应、padding 模式不一致验证指标远好于训练指标1. 查统计量来源 2. 查数据划分 3. 按天气分层统计数据泄漏、验证集偏简单边界区域误差异常大1. 查差分边界处理 2. 查重采样边界边界效应、padding 引入的虚假信息输出有 NaN1. 查输入是否有 inf 2. 查 log 操作 3. 查除法log(0)、除零、fp16 溢出显存溢出1. 降 batch 2. 开梯度检查点 3. 开 AMP激活值占用过大、输入分辨率过高训练后期指标突然恶化1. 查学习率曲线 2. 查是否有异常批次学习率退火到底后过拟合、脏数据7.2 几个印象最深的坑第一个坑是关于log1p的。降水做对数变换时我图省事用了np.log结果遇到零降水格点直接产生-inf训练到第二个 epoch loss 就变 NaN 了。后来改成np.log1p(np.maximum(x, 0))才解决。这类问题在气象数据里特别常见因为零值和缺测值太多了所有数学变换都得先考虑零值情况。第二个坑是 GroupNorm 的组数。默认我按经验给了 8 组但通道数是 64 的时候没问题通道数降到 12 的时候 8 组除不尽直接报错。GroupNorm 的组数必须能整除通道数这个约束写代码时很容易忘。后来我改成动态计算取通道数的最大公约数里最接近 8 的那个。第三个坑是关于因果卷积的。时间分支我一开始用的是普通 1D 卷积加对称 padding训练集上表现完美验证集一塌糊涂。查了半天才发现对称 padding 让模型在预测当前时刻时看到了未来帧等于作弊。改成左 padding 的因果卷积之后验证指标才正常。这个错误的隐蔽性在于训练 loss 会降得非常漂亮让人误以为模型很强。第四个坑是关于多卡训练的。用分布式训练时如果 BatchNorm 是默认设置每张卡上的统计量是独立算的等效批大小变小了训练效果会明显下降。解决办法是换成 SyncBN 或者干脆用 GroupNorm 避开这个问题。我现在的习惯是只要涉及多卡一律用 GroupNorm 或者 LayerNorm省心。第五个坑是关于评估时的反归一化。模型输出的是标准化空间的值评估前要反归一化回物理量。这一步如果用了错误的统计量比如用了验证集的统计量指标会偏乐观。更隐蔽的是降水这种做了对数变换的变量反变换要用expm1而不是exp差一个 1小量级降水的误差会被显著低估。7.3 一些不太好归类但很实用的经验关于数据量这套结构对数据量的胃口不小。我的经验是单个格点上至少要有几千个有效时次才能训出稳定结果如果数据年限不够靠增加空间样本是补不回来的因为空间样本之间的相关性太高了。关于训练时长在单卡 24GB、batch 8 的配置下跑 50 个 epoch 大概要一两天具体取决于数据规模。如果超过三天还没收敛基本可以判断是配置有问题别硬等。关于模型选择不要一上来就堆最大的模型。我一般先用 base32、两层编码的轻量版跑通全流程确认数据管线和损失函数没问题再逐步加宽加深。这个过程能省掉大量跑了两天才发现数据错了的时间。关于版本管理除了代码用 git 管理数据和模型检查点也要有版本号。我吃过一次亏同一个文件名覆盖了三次不同的检查点最后分不清哪个是最优的了。现在我的习惯是检查点文件名里带上关键超参和验证指标比如bs8_lr1e-4_rmse0.842_ep37.pt一眼就能看出是什么。关于可解释性模型训好之后建议做一次通道重要性分析看看哪些输入变量对输出贡献最大。方法很简单逐个通道置零或者替换成气候平均值看输出变化多少。这个分析经常能发现意外——我就遇到过一次模型对某个我随手加进去的派生变量依赖度极高去掉之后指标掉了 8%而那个变量的物理意义我当时都没想清楚。关于后续扩展这套结构还有几个方向可以继续挖一是引入集合预报的思想让模型输出多个成员的预报用成员离散度刻画不确定性二是把数值模式的输出作为额外输入通道做模式订正而不是纯外推这条路在实际业务里往往更容易出效果三是往更高分辨率走配合超分辨率结构把粗网格预报细化到公里级。最后分享一个我自己踩过的小细节。模型训完之后我一直觉得输出场看着不太自然但又说不上哪里不对指标也都正常。后来把预报场和实况场并排放大看了半天发现预报场里所有的小尺度结构都是圆形的实况里是条带状的。原因是空间分支用了太多圆形感受野的堆叠卷积对方向性特征捕捉不足。在卷积里加入可变形卷积或者方向性卷积核之后这个问题明显改善。这件事让我意识到指标之外还得靠眼睛看场很多时候图比数字诚实。