简介:基于PyTorch的雷达降水预测模型完整项目包,面向气象人工智能研究与深度学习开发者,解决多源气象数据融合与降水短临预测的关键问题。项目整合雷达反射率、差分反射率、差分相移率及地面雨量计观测数据,通过自定义数据加载器完成时序数据预处理与归一化,并利用卷积神经网络从多源数据中提取与降水相关的空间特征,从而更全面地捕捉降水过程复杂性,有效提升预测精度与泛化能力。包内共102个文件,以7个Python脚本(模型构建、数据加载与训练流程)、3个pth权重文件及5个log训练日志为核心,辅以84张降水重建效果图以及txt、docx、md格式的说明文档,整包约45.73MB,目录结构清晰,便于对照复现实验、分析训练过程并理解多源数据融合思路。已有127人学习下载,适合作为气象算法科研、课程设计或工程落地的参考实现,也可直接进行二次开发与改进。
1. 雷达降水预测:比单用反射率多一倍的输入信息
做短临预报的人基本都吃过 Z-R 关系的亏:单靠反射率因子 Z 去反演雨强,在冰雹、滴谱变化大、或者强衰减的对流单体面前,系统性低估是常态,而且离雷达越远偏得越厉害。新一代双偏振雷达之所以能在定量降水估计上翻身,靠的就是把差分反射率 ZDR 和差分相移率 KDP 这两路偏振信息一起纳入输入:ZDR 反映降水粒子的形状和取向,KDP 对大雨滴极敏感、抗衰减能力强。再叠加地面雨量计观测做标签校准,等于从三个不同侧面描述同一场降水,信息量比单通道多了不止一倍。这篇文章要拆的,是一套基于 PyTorch 的雷达降水预测完整流程,包含雷达多源数据组织、时序数据预处理与归一化、自定义数据加载器的实现、卷积神经网络建模与训练。适合手里有双偏振雷达数据、想跑通一个完整深度学习降水基线的人,也适合刚接触 PyTorch、想知道多源时序数据到底怎么灌进卷积网络的人。
2. 多源数据怎么组织:Z、ZDR、KDP 和雨量计的对齐
雷达降水预测项目上手第一个关卡不在模型,在数据组织。原始雷达基数据是极坐标的,由方位角、距离库构成;雨量计是地面离散站点,观测的是不同物理量。这几路数据要能灌进卷积网络,得先统一成规则的笛卡尔网格,并且时间步对齐。本章先把四种观测各自的物理含义讲清楚,再落到网格化和滑窗切片上。
2.1 四个物理量:单位、范围和降水物理含义
| 变量 | 全称 | 典型单位 | 典型范围 | 降水物理含义 |
|---|---|---|---|---|
| Z | 雷达反射率因子 | dBZ | -10 ~ 70 | 回波强度,与粒子直径六次方相关,雨强的主要载体 |
| ZDR | 差分反射率 | dB | -2 ~ 8 | 水平与垂直偏振回波比值,反映粒子扁度与滴谱宽度 |
| KDP | 差分相移率 | deg/km | -1 ~ 10 | 双偏振相位差沿路径的变化率,大雨滴敏感且抗衰减 |
| 雨量计 | 地面降水量 | mm/h | 0 ~ 大几十 | 地面真实降水强度,训练时的标签 |
这四种观测之所以要一起用,是因为它们各自有盲区。Z 的强度是雨强估计的主力,但它对大粒子饱和,冰雹和融化层里的 Z 甚至会异常偏高;ZDR 能帮助区分大水滴和冰雹,可 ZDR 本身对衰减很敏感;KDP 来自电磁波在降水中的相位变化,不随衰减衰减,反而在强降水区表现最好,但小雨区信噪比很差。三者实际上是互补关系。雨量计在这里承担的是地面真值角色,负责把雷达观测到的体扫信息校准到地面降水强度上。注意一个关键点:雨量计是点观测,雷达是体观测,两者物理尺度不同,直接做像素级回归必然有偏差,后面第 5 章会单独说这个坑。
2.2 极坐标到笛卡尔网格:统一坐标系是第一步
很多入门项目直接把极坐标雷达基数据拼成一个张量喂给网络,这既不高效也不符合卷积操作的平移等变性假设。卷积神经网络在提取空间特征时,希望同一特征在图上任何位置都能被识别,而极坐标下同一块降水区域在不同方位角上的采样密度完全不同,卷积核很难学到稳定的空间模式。所以第一步是插值到等距的笛卡尔网格,常见做法是用 scipy 的 griddata 做线性插值,或者用 pyart 自带的网格化工具。
import numpy as np from scipy.interpolate import griddata def radar_polar_to_grid(azimuth, r, field, grid_x, grid_y): """把极坐标(方位角x距离库)的雷达场插值到笛卡尔网格 azimuth: (n_az,) 方位角数组, 单位deg r: (n_range,) 距离库数组, 单位km field: (n_az, n_range) 单一仰角上的物理量场 grid_x, grid_y: 目标网格坐标, 单位km, 由np.meshgrid生成 """ az_mesh, r_mesh = np.meshgrid(azimuth, r, indexing='ij') # 极坐标转平面直角坐标 x = r_mesh * np.cos(np.deg2rad(az_mesh)) y = r_mesh * np.sin(np.deg2rad(az_mesh)) points = np.stack([x.ravel(), y.ravel()], axis=1) values = field.ravel() # 对目标网格做线性插值, 外插点用最近邻填补 grid = griddata(points, values, (grid_x, grid_y), method='linear') return grid这段代码的核心逻辑是把每个极坐标采样点拆解成平面直角坐标 x、y,再以这些散点为基础,对目标笛卡尔网格做插值。实际操作时,Z、ZDR、KDP 三个变量都要各自调用一遍,所以最好把网格坐标提前生成好,循环复用。网格分辨率一般取 1km,范围取雷达有效探测半径,比如 150km 半径对应 300×300 的网格。插值方法选线性就好,cubic 在数据稀疏区域容易产生过冲,反而不利于后续归一化。如果你的数据源已经是拼图后的格点产品,这一步直接跳过,但要确认三套格点数据的投影方式和网格原点完全一致,这是后面坐标对齐坑的源头。
2.3 时间对齐与滑窗样本:输入 5 帧,预测未来 30 分钟
雷达体扫一般 6 分钟一个体扫,雨量计则通常按分钟上报。做短临降水预测时,常见的设计是输入过去 5 帧共 30 分钟的观测,预测未来 30 分钟或 1 小时的降水场。雨量计需要在每个雷达时间戳上做时间匹配,取当前时刻前后 3 分钟内的观测做空间插值,得到与雷达网格对齐的面降水场。这个匹配逻辑如果写得太随性,会出现标签和输入在时间上错位的问题,后面训练出来的模型怎么看都不对。
滑窗样本的构造是整个数据流水线的核心索引逻辑:
def build_sample_indices(data_len, window_size=5, forecast_steps=5): """生成滑窗样本的索引对 data_len: 数据集总时间帧数 window_size: 输入历史帧数, 默认5帧 forecast_steps: 输入最后一帧到预测目标帧的偏移, 默认5帧(约30分钟) """ x_idx, y_idx = [], [] for t in range(data_len - window_size - forecast_steps): x_idx.append(list(range(t, t + window_size))) y_idx.append(t + window_size + forecast_steps - 1) return x_idx, y_idx每次取连续 5 个时间步作为输入窗口,目标标签取窗口末尾再往后推 5 帧的那个时刻。这样输入和标签之间隔着一段预报时效,模型学到的不是单纯滞后外推,而是真正意义上的短临预测。数据切分方面,训练集、验证集、测试集必须按时间顺序切,比如按月份划断,绝不能随机洗牌,否则相邻时间步的强相关性会让验证指标虚高得离谱。这个索引函数建议单独调试,打印出前几个窗口的时间戳人工核对一遍,再进 Dataset。
3. 自定义数据加载器:把时序预处理和归一化写进 Dataset
PyTorch 的 DataLoader 是深度学习中数据流水线的标准入口,但直接把原始文件路径丢给它没用。雷达降水数据的特殊性在于每个训练样本是由多帧、多变量拼接而成的时序块,还要在读取时完成归一化,这些逻辑都要写进自定义 Dataset。本章给出一个可以直接改用的数据加载器骨架,重点说清滑窗切片、时序堆叠和归一化的实现位置。
3.1 为什么必须自定义 Dataset,而不是预生成全部数组
一个常见的偷懒做法是先把所有样本预处理好,存成一个大 npy 再训练。这个思路在小数据集上没问题,但雷达数据的样本量很容易过百万,单是生成这些数组就可能吃掉几十 GB 内存,而且一旦要调整窗口大小或归一化范围,整套数据又得重新生成。自定义 Dataset 的好处是把「按需读取 + 实时预处理」做成标准流水线:每个 epoch 都在内存里处理当前 batch,文件只读一次缓存到系统页缓存里,调整超参数只需改 dataset 参数。代价是需要保证单次读取不能太慢,这一点第 5 章会展开说。
3.2 Dataset 骨架:把多帧多变量拼成一个训练样本
import os import numpy as np import torch from torch.utils.data import Dataset class RadarPrecipDataset(Dataset): """多源雷达时序滑窗数据集 目录结构约定: file_dir/z/ : 201901010000.npy (H, W) 反射率, 单位dBZ file_dir/zdr/ : 同名 (H, W) 差分反射率, 单位dB file_dir/kdp/ : 同名 (H, W) 差分相移率, 单位deg/km file_dir/gauge/: 同名 (H, W) 雨量计插值面场, 单位mm/h """ def __init__(self, file_dir, time_list, window_size=5, z_min=-10.0, z_max=75.0, zdr_min=-2.0, zdr_max=8.0, kdp_min=-1.0, kdp_max=10.0): self.file_dir = file_dir self.time_list = time_list self.window_size = window_size # 归一化参数在初始化时固定, 不从数据里现算 self.norm_params = { 'z': (z_min, z_max), 'zdr': (zdr_min, zdr_max), 'kdp': (kdp_min, kdp_max), } def __len__(self): # 保证最后一帧取满窗口且标签不越界 return len(self.time_list) - self.window_size - 1 def _load(self, var, time_str): path = os.path.join(self.file_dir, var, f"{time_str}.npy") return np.load(path).astype(np.float32) def __getitem__(self, idx): # 输入: 从idx开始连续window_size帧 t_windows = self.time_list[idx: idx + self.window_size] frames = [] for t in t_windows: z = self._load('z', t) zdr = self._load('zdr', t) kdp = self._load('kdp', t) # 每个时间步堆成 (C, H, W), C=3 frames.append(np.stack([z, zdr, kdp], axis=0)) # 输入张量形状: (window_size, C, H, W) x = np.stack(frames, axis=0) # 标签: 窗口结束时刻的雨量计面场, 加通道维 -> (1, H, W) t_target = self.time_list[idx + self.window_size] y = self._load('gauge', t_target)[np.newaxis, :, :] return torch.from_numpy(x).float(), torch.from_numpy(y).float()逻辑上有几个关键设计。输入张量保留了时间步独立维度,形状为(window_size, C, H, W),而不是直接拼成(C×window_size, H, W)。这么做的原因是让后续网络可以自由选择 2D 卷积加通道拼接,还是 3D 卷积直接卷时间维,灵活性更大。标签只取窗口结束时刻的单帧雨量场,对应的是「用 5 帧历史预测当前之后某个时刻」的任务设定。__len__里减掉的window_size + 1是为了防止最后一帧窗口越界,这个边界条件在调试时最容易翻车,建议先打印len(dataset)和最后一个样本的时间戳确认。
3.3 归一化:三种雷达量各归各的,别混在一起
雷达降水数据的一个显著特点是三个输入变量的量纲完全不同:Z 在 dBZ 尺度,ZDR 在 dB 尺度,KDP 在度每千米尺度,范围差异很大。如果直接统一做一次 min-max,KDP 会被压到零点几个数值区间,Z 的细节也会被拉伸得毫无区分度。正确做法是每个变量用各自的物理合理范围做线性归一化,这里用上一章说的各变量典型范围即可。
def normalize_input(self, x): """在Dataset中做离线归一化 x: (window_size, C, H, W) 的torch.Tensor 返回: 各通道归一化到[0,1]后的张量 """ x_norm = x.clone() # 通道顺序固定: 0=z, 1=zdr, 2=kdp for c, name in enumerate(['z', 'zdr', 'kdp']): lo, hi = self.norm_params[name] x_norm[:, c:c+1, :, :] = (x[:, c:c+1, :, :] - lo) / (hi - lo) # clamp截掉超出物理范围的值 return x_norm.clamp(0.0, 1.0)clamp到 [0,1] 这一步很关键。雷达数据里总有地物杂波或硬件饱和造成的极端值,比如反射率超过 75dBZ 的冰雹回波,不截断会让整个样本的对比度被拉低。归一化的位置我坚持放在 Dataset 内部而不是网络里,因为这样方便在验证时用完全相同的参数反算回物理量,不会出现训练和推理两套逻辑。注意归一化参数是写死的常量,不是从训练集统计出来的 min/max,这个区别后面第 5 章的避坑部分会重点讲。
3.4 DataLoader 参数:num_workers、pin_memory、shuffle 的逻辑
Dataset 写好后,DataLoader 的参数配置同样影响流水线效率,尤其是离散文件读多的情况。
from torch.utils.data import DataLoader train_loader = DataLoader( train_dataset, batch_size=16, shuffle=True, # 训练集shuffle, 打乱时序相关性 num_workers=8, # 多进程预取 pin_memory=True, # 锁页内存, 减少CPU到GPU拷贝 prefetch_factor=2, # 每个worker预取2个batch drop_last=True, # 丢弃最后不完整batch ) valid_loader = DataLoader( valid_dataset, batch_size=16, shuffle=False, # 验证集保持时间顺序 num_workers=4, pin_memory=True, )shuffle只在训练集开,验证集和测试集必须关掉。注意prefetch_factor是 PyTorch 1.5 之后的参数,老版本要检查一下。num_workers不是越大越好,在机械硬盘上开太多 worker 反而会让磁头频繁寻道,速度掉得厉害;我一般先看训练时 GPU 利用率,低于 80% 就先加num_workers,加到 12 还没效果就排查文件读取本身。
4. 卷积神经网络:输入张量排布、网络结构与损失选择
数据流水线准备好之后,模型结构的设计决定了预测能力的上限。雷达降水场本质上是空间图像序列,卷积神经网络当然是主力。本章先解决张量排布问题,再给出一个完整可跑的编解码结构,最后讨论零膨胀降水数据下损失函数的选择。
4.1 时间维度和变量维度怎么进入卷积网络
第 3 章的 Dataset 输出形状是(window_size, C, H, W),也就是(5, 3, H, W)。这个形状不能直接喂 2D 卷积,需要重新排布。常见做法有两种。第一种是把时间步当成通道拼接,变成(C×window_size, H, W),即(15, H, W),用 2D 卷积处理;第二种是保持(C, window_size, H, W),用 3D 卷积同时卷时间和空间。两种方案各有取舍:
| 方案 | 张量形状 | 卷积类型 | 优点 | 缺点 |
|---|---|---|---|---|
| 通道拼接 | (15, H, W) | Conv2d | 实现简单、参数少、训练快 | 卷积核看不到时间递进关系 |
| 3D卷积 | (3, 5, H, W) | Conv3d | 显式建模时间演化 | 参数量大、显存占用高、易过拟合 |
我一般先用通道拼接方案搭一个快速基线。原因是雷达数据样本量大、空间分辨率不高,通道拼接在绝大多数场景下能把降水落区预测到可用的程度,而 3D 卷积的收益主要在对流演变剧烈的短临场景里才明显。如果数据量和 GPU 显存都够,再改 3D 卷积做对比实验。
4.2 编解码结构:从多通道输入到降水场输出
降水预测的核心任务是从输入的雷达观测场预测未来的降水强度场,输入输出都是二维空间网格,天然适合编解码结构。编码器逐层压缩空间分辨率、提取高层特征,解码器把特征图恢复到原始分辨率。聚合在汇聚层(池化层)完成,这里用 MaxPool2d 做空间下采样。
import torch.nn as nn class ConvPrecipNet(nn.Module): """卷积神经网络降水预测模型: 编码-解码结构 in_channels: 输入通道数, 通道拼接场景下=window_size * 3 hidden: 隐层通道数, 控制模型容量 out_channels: 输出通道数, 降水预测为1 """ def __init__(self, in_channels=15, hidden=64, out_channels=1): super().__init__() # 编码器: 两个卷积块提取空间特征 self.encoder = nn.Sequential( nn.Conv2d(in_channels, hidden, 3, padding=1), nn.BatchNorm2d(hidden), nn.ReLU(inplace=True), nn.Conv2d(hidden, hidden, 3, padding=1), nn.BatchNorm2d(hidden), nn.ReLU(inplace=True), ) # 汇聚层: 空间分辨率减半 self.pool = nn.MaxPool2d(2) # 解码器: 转置卷积恢复空间分辨率 self.decoder = nn.Sequential( nn.ConvTranspose2d(hidden, hidden, 3, stride=2, padding=1, output_padding=1), nn.BatchNorm2d(hidden), nn.ReLU(inplace=True), nn.Conv2d(hidden, out_channels, 1), ) # 输出约束到[0,1], 与归一化后的标签一致 self.output_act = nn.Sigmoid() def forward(self, x): # x: (B, C*window_size, H, W) h = self.encoder(x) h = self.pool(h) h = self.decoder(h) return self.output_act(h)ConvTranspose2d的参数是容易踩坑的地方。stride=2会让特征图尺寸翻倍,但转置卷积的输出尺寸不是天然对齐的,必须配合padding=1, output_padding=1才能保证输入输出空间尺寸一致。如果发现输出的 H 和 W 跟输入差了 1 个像素,优先检查这两项。输出层用Sigmoid是因为标签已在第 3 章归一化到 [0,1],网络输出同样约束在这个区间,收敛会稳定很多。最后一个1×1卷积负责把 hidden 通道压缩成单通道降水场。
4.3 零膨胀数据的损失函数:MSE 为什么不管用
降水场数据有个天然特性:大部分像素是零降水。一次中等强度降水过程内,无降水像素占比通常在 70% 以上。如果用普通 MSE,模型只需要把所有像素预测成 0 就能拿到很低的损失,训练初期网络几乎都在往这个方向走,最终学出一个「全图抹平」的模型。加权损失是缓解零膨胀最直接的手段。
def weighted_mse_loss(pred, target, zero_weight=0.3, rain_threshold=0.1): """对非零降水像素加权, 减轻零膨胀的影响 pred, target: 归一化后的预测与标签, 形状 (B, 1, H, W) rain_threshold: 标签反归一化后对应0.1mm/h的归一化值 """ # 归一化后0.1mm/h对应约(0.1-0)/(max_gauge-0), 这里假设max_gauge=50 threshold = rain_threshold / 50.0 weight = torch.where(target > threshold, torch.ones_like(target), torch.full_like(target, zero_weight)) return torch.mean(weight * (pred - target) ** 2)这个加权策略的核心是给降水像素完整权重、给非降水像素 0.3 的权重,让网络不敢学「全零摆烂」。rain_threshold需要根据标签归一化的实际最大值换算,不要像我第一次那样直接用 0.1 去比,结果权重全落到非降水侧。更进阶的做法是对雨强做log1p变换后再归一化,把 0 到 50mm/h 的强偏态分布拉成近似正态,但这个要改标签预处理,保持和反归一化一致。
5. 避坑排查:数据加载、归一化和训练里最容易翻车的四件事
这章内容全部来自实际跑项目时的血泪经验。每一条都是现象、原因、解决三个层次,可以对照自己的日志排查。
5.1 训练的瓶颈根本不是 GPU,而是数据加载器
现象:GPU 利用率在 30% 到 50% 之间反复跳动,每个 epoch 的 wall time 长得离谱。我最初怀疑是模型太小,后来发现大量时间花在数据读取上。
原因:Dataset 的__getitem__里每次都要从磁盘读取 5 个时间步、每个时间步 3 个变量共 15 个 npy 文件,文件小而多,IO 开销远超计算开销。加上num_workers设成 1,所有读取都在主进程串行执行,GPU 只能干等。
解决:第一,用 memmap 或者把同一天的多个变量合并成一个 npz 文件,减少文件句柄切换;第二,把num_workers提到 8 到 12,prefetch_factor=2;第三,如果数据总量能放进内存,直接一次性np.load到 list 里缓存,用空间换时间。我在小数据集上常用第三种,速度提升最直观。
5.2 归一化参数泄漏:验证集指标虚高的元凶
现象:模型在验证集上的 MSE 低得漂亮,但换一段全新的降水过程去推理,效果崩成一团,落区完全不靠谱。
原因:归一化参数用了全量数据的 min/max,相当于验证集的数值范围在训练时已经通过归一化参数泄露给了模型。雷达数据的 min/max 受极端天气影响很大,训练期包含一场大暴雨和验证期是否包含,会让同一套归一化参数产生完全不同的映射效果。
解决:归一化参数只用训练集统计,而且最好用物理阈值而不是数据统计值。我现在的做法是把 Z、ZDR、KDP 和雨强标签的归一化范围写死成一个norm_params.json,训练、验证、推理全程加载同一个文件,从源头上杜绝泄漏。
5.3 预测落区整体偏移,空间结构看着对但位置不对
现象:预测的强降水中心和真实落区错开 10 到 20 公里,回波强度数值接近,但空间对齐一塌糊涂。
原因:这个问题的根因通常在标签侧。把雨量计站点插值到雷达网格时,用了过大的搜索半径或者未做距离权重,导致地面降水信息被抹开。另一个常见原因是雷达和雨量计使用的投影基准不一致,比如一个用经纬度另一个用了某个局部坐标系,网格原点漂移。
解决:检查标签插值参数,把雨量计插值半径从 10km 收紧到 3km,使用 IDW 权重而不是简单的最近邻;再画一张反射率底图叠加雨量计散点,用肉眼确认空间对齐,这一步比任何自动化检验都直观。
5.4 强降水一直被低估,模型只会给出平庸的预测
现象:训练收敛后,小雨区预测得还不错,但 20mm/h 以上的强降水区域峰值总是比观测低 30% 到 50%。
原因:强降水像素在训练集里占比本来就少,加权损失虽然缓解了全零问题,但对强降水像素的重视程度还不够。另外,雨强的偏态分布让 MSE 对大值误差的惩罚被小雨区淹没。
解决:标签做log1p变换后再归一化,让不同强度档的误差在损失函数里的量级更接近;同时把训练样本按雨强分桶重采样,对强降水样本适当提高采样权重。这两步配合使用,强降水的低估问题基本能压到 20% 以内,代价是整体 MSE 可能略微上升,但对短临业务来说,把大雨报准比平均误差低更重要。
6. 从预测张量到降水图:反归一化、分雨强评估与滑动推理
模型训练告一段落后,最忌讳的是只盯着训练曲线看。预测结果要还原成物理量、画出来和观测对比,再从业务角度分雨强评分,这套流程才完整。
6.1 反归一化与可视化:别把归一化后的图直接展示
网络输出是 [0,1] 的归一化值,直接画出来会得到一个看起来还行、但完全无法和观测对比的图。反归一化需要严格使用训练时的那套参数:
def inverse_norm(pred, var_range=(0.0, 50.0)): """把归一化预测还原到原始雨强量纲(mm/h) pred: 模型输出, 范围[0,1] var_range: 雨强标签归一化时用的min,max """ lo, hi = var_range return pred * (hi - lo) + lo import matplotlib.pyplot as plt pred_rain = inverse_norm(pred_cpu) # pred_cpu: (1, H, W) numpy数组 fig, axes = plt.subplots(1, 3, figsize=(13, 4)) im0 = axes[0].imshow(obs_rain, vmin=0, vmax=30, cmap='jet') axes[0].set_title('Observed (mm/h)') im1 = axes[1].imshow(pred_rain, vmin=0, vmax=30, cmap='jet') axes[1].set_title('Predicted (mm/h)') im2 = axes[2].imshow(obs_rain - pred_rain, cmap='RdBu', vmin=-10, vmax=10) axes[2].set_title('Bias') fig.colorbar(im1, ax=axes[1])画图时vmin, vmax固定住,不要用imshow的自动拉伸,否则每一帧的色标都在变,很难横向对比。Bias 图用 RdBu 色带,蓝色偏预测强、红色偏预测弱,一眼就能看出系统性偏差。
6.2 分雨强评分:只报一个总指标一定会被喷
降水预测的业务价值在不同强度档位是完全不同的。一次评估里只报整体 MSE,就像把所有年龄的病人混在一起报平均治愈率,业务方根本没法用。按雨强分档统计是必须的,下面是一组某次对流过程测试集上的典型评分:
| 雨强等级 | 阈值(mm/h) | POD | FAR | BIAS |
|---|---|---|---|---|
| 小雨 | 0.1 ~ 2 | 0.82 | 0.31 | 0.87 |
| 中雨 | 2 ~ 8 | 0.71 | 0.36 | 0.90 |
| 大雨 | 8 ~ 20 | 0.63 | 0.42 | 0.78 |
| 暴雨 | > 20 | 0.45 | 0.51 | 0.69 |
POD 是命中率,FAR 是空报率,BIAS 是预测面积与观测面积之比。看到这个表心里应该有数:模型对小雨有实用价值,对暴雨只能算倾向性参考。如果项目目标是强对流预警,下一步就该重点提升大雨以上档位,而不是继续在整体 MSE 上死磕。
6.3 滑动窗口推理与模型保存
预测要用起来,就得支持连续滚动。每次拿到新一帧雷达观测,丢掉最老的一帧、把预测拼到窗口末尾,实现滚动外推:
def rolling_infer(model, last_window, device): """last_window: (window_size, C, H, W) numpy数组, 已经归一化""" model.eval() x = torch.from_numpy(last_window).float().unsqueeze(0).to(device) with torch.no_grad(): pred = model(x).squeeze(0).squeeze(0).cpu().numpy() # (H, W) # 滚动: 丢弃第一帧, 把预测作为新一帧拼入 next_window = np.concatenate([last_window[1:], np.tile(pred, (3, 1, 1))[np.newaxis, ...]], axis=0) return next_window注意这里拼接时要把单通道预测复制成三通道,保持和输入变量通道数一致。模型保存用torch.save(model.state_dict(), "precip_net.pth"),推理时重新加载结构再load_state_dict,不要贪方便直接保存整个 model 对象,换环境后容易出序列化问题。跑这种滚动推理时,我从那以后每次都会把预测输出落盘存一份带时间戳的 npy,方便后面复盘出问题时的责任定位。这个习惯在调试 5.3 那种落区偏移问题时帮我节省了大量时间,也把每天的预测和观测保留齐全,随时能回头对比回归。希望这个流程能帮你少走几步弯路。
本文还有配套的精品资源,点击获取