全球天气预报长期以来是数值天气预报的领地,但数据驱动的深度学习方法在近些年已经走出了完全不同的技术路线。它不直接求解大气运动方程,而是把过去一段时间的大气状态当作高维输入,学习如何映射到未来某个时刻的状态。这套思路里有一个容易被忽略、却非常关键的设计点:模型到底要预测未来 6 小时、24 小时还是 168 小时的天气场?不同超前时间对应的预测难度、平滑程度和误差分布都不一样。Timestep-Conditioned Transformer 的核心思想,就是把“要预测多久以后”这个信息作为条件显式注入模型,让同一个模型有能力处理多个超前时间,而不是为每个超前时间单独训练一套权重。
这篇文章围绕这个思路展开。先说明全球天气预报模型预测的对象是什么,再解释 timestep conditioning 到底做了什么、为什么比把时间步直接拼进输入更合理,然后给出一个基于 PyTorch 的最小可运行实现,最后覆盖训练验证、指标计算、常见坑和生产环境建议。读完以后,你可以把这种结构迁移到自己的气象或时空预测任务里,也能把它作为阅读相关论文和复现实验的起点。
1. 先搞清楚全球天气预报模型在预测什么
1.1 天气状态不是一张图,而是一个多变量物理场
很多入门者第一次接触气象数据时,会下意识地把它当成一张普通图像:经度是宽度,纬度是高度,RGB 三个通道换成气象变量。这个理解不完整,也是后续设计模型时犯错的根源。
全球天气状态实际上是多个物理变量在多个气压层上的三维场。以常用的 ERA5 再分析数据为例,它在全球使用 0.25 度的规则网格,纬度方向约 720 个格点,经度方向约 1440 个格点。每个格点上并不只有一个数值,而是一组变量:
- Z:位势高度
- T:气温
- U、V:纬向风和经向风
- Q:比湿
这些变量会分布在多个气压层上,常见的有 13 层或 37 层。如果取 5 个变量乘以 13 个气压层,每个格点上就有 65 个通道。再叠加地表附近的 2 米温度、海平面气压、10 米风等单层变量,输入通道数很容易超过 70。
这意味着什么?Transformer 处理的是一个 token 序列,而每个 token 需要承载“某个空间位置上的多个变量值”。天气预测模型不需要像图像分类那样把整张图压缩成一个类别,它需要把输入的高维场映射成另一个同样尺寸的高维场,属于稠密预测问题。任何一步把空间信息过度压缩的设计,都会让输出失去可用的细节。
1.2 数值天气预报与数据驱动模型的本质差异
传统数值天气预报(NWP)的做法是把大气运动方程离散化,然后在超算上做时间积分。它依赖物理参数化方案处理云、辐射、对流等过程,计算量非常大。业务化运行一次全球中期预报,需要在成千上万个 CPU 核心上跑较长时间。
数据驱动模型的做法完全不同。它用历史再分析数据构造训练样本对:已知 t 时刻的天气场,预测 t+τ 时刻的天气场。模型学习的是统计映射关系,而不是物理方程。这个过程在 GPU 上只要一次前向推理就能完成,推理成本比数值预报低几个数量级,因此很适合做快速预报、集合预报和大规模敏感性实验。
但这不代表数据驱动模型更简单。它把计算成本转移到了训练阶段,需要处理海量数据、设计合理的损失函数、防止时空泄漏、评估极端天气事件的表现。模型不仅要预测得准,还要预测得“像真实天气”,不能输出一片毫无物理结构的平滑场。
1.3 为什么“预测多久以后”是一个关键条件
训练样本对的形式是 (X_t, X_{t+τ}),其中 τ 就是超前时间,也叫 lead time。业务预报通常同时需要多个超前时间:短期的 0 到 3 天,中期的 3 到 10 天,甚至更长。如果为每个 τ 分别训练一个模型,参数数量成倍增加,而且每个模型都只能看到固定 τ 的样本,无法共享不同时间尺度上的共同模式。
反过来,如果只训练一个模型,把所有 τ 的样本混在一起,但不告诉模型当前这个样本的 τ 是多少,会发生什么?
模型面对的是一个“平均任务”。同一个输入 X_t,有的样本要预测 6 小时后的状态,有的要预测 168 小时后的状态。模型不能区分这两种请求,只能输出一个折中的预测。结果往往是短期的预测偏模糊,长期的预测又不够平滑。Timestep conditioning 要解决的问题正是这个:把 τ 从“数据属性”变成“模型输入的一部分”,让模型在推理时知道当前需要预测多远。
这个概念类似于扩散模型里的时间步嵌入,也类似于图像生成里的类别条件。它不是把 τ 当作一条新通道拼进去,而是通过专门的编码和注入机制,让时间信息控制整个特征提取过程。
2. Timestep-Conditioned Transformer 的核心设计思路
2.1 “条件”和“输入”的区别在哪里
要理解 timestep conditioning,先要区分两个概念:模型观察了什么,模型要完成什么任务。
输入 X_t 描述的是“现在的大气状态”,它来自观测或再分析数据,是模型做预测的依据。τ 描述的是“任务难度和目标时刻”,它决定模型应该把注意力放在哪些物理过程上。
如果把 τ 直接当作输入张量的一个额外通道拼接进去,比如把一张全是 τ 数值的图拼在气象变量后面,问题在于:Transformer 的注意力机制会对 token 内部的所有通道做混合,τ 作为一个常数通道,每个 token 上都是一样的数值。经过几层注意力后,它对特征的调制能力非常有限,位置信息可能被淹没,无法形成全局的任务感知。
更好的做法是把 τ 编码成一个条件向量,用它去调整特征图的通道或注意力结构。这也是 FiLM、Cross-Attention 等机制在条件生成模型里被广泛使用的原因。条件向量不参与空间 token 的主干计算,而是在关键时刻调制主干特征,从而控制模型的整体行为。
2.2 Timestep Embedding:把小时数变成向量
τ 是一个标量,单位通常是小时,取值可能是 6、24、72、168。直接把这个标量输入全连接层,会遇到数值尺度问题:不同量程的 τ 会给梯度带来不稳定因素,而且标量本身缺乏周期性表达能力。
更通用的做法是参考 Transformer 的位置编码,用一组正弦和余弦函数把 τ 映射成高维向量。
import math import torch import torch.nn as nn class TimestepEmbedding(nn.Module): def __init__(self, dim): super().__init__() self.dim = dim # 先用正弦编码展开,再用 MLP 做非线性映射 self.mlp = nn.Sequential( nn.Linear(dim, dim * 4), nn.SiLU(), nn.Linear(dim * 4, dim), ) def forward(self, t): # t: (B,), 超前时间,单位小时 half = self.dim // 2 freqs = torch.exp( -math.log(10000.0) * torch.arange(half, dtype=torch.float32, device=t.device) / half ) args = t[:, None] * freqs[None, :] emb = torch.cat([torch.cos(args), torch.sin(args)], dim=-1) return self.mlp(emb)这里用10000作为频率基数是位置编码的默认选择。实际项目中可以把它当作超参数调整。更重要的是,输入t最好先做一次尺度归一化,比如统一除以最大超前时间,让模型看到的时间值落在相对稳定的区间内,避免 6 小时和 168 小时相差过大的数值影响早期网络层。
2.3 条件注入的三种常见方式
拿到 timestep embedding 之后,怎么把它用起来是设计的关键。下面三种方式在实践中最常见。
| 注入方式 | 基本做法 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| 拼接 Concat | 把 embedding 复制到每个 token 上,与 token 特征拼在一起 | 实现简单,改动最小 | 条件信息容易被注意力稀释 | 小规模快速验证 |
| FiLM 调制 | 用 embedding 经线性层生成每个通道的缩放系数 gamma 和偏移 beta,对特征逐通道调制 | 全局调制能力强,参数少,训练稳定 | 表达能力不如交叉注意力 | 大多数中等规模模型 |
| Cross-Attention | 把 embedding 作为 query 或 key/value 序列,参与交叉注意力计算 | 表达能力强,能建模复杂的条件依赖 | 计算量更大,训练难度更高 | 长序列、高精度需求场景 |
FiLM 的结构非常直观,它不改变 token 的空间排列,只对每个 token 的特征通道做仿射变换。代码如下:
class FiLM(nn.Module): def __init__(self, d_model, cond_dim): super().__init__() self.gamma = nn.Linear(cond_dim, d_model) self.beta = nn.Linear(cond_dim, d_model) def forward(self, x, cond): # x: (B, N, d_model) # cond: (B, cond_dim) gamma = self.gamma(cond).unsqueeze(1) beta = self.beta(cond).unsqueeze(1) return gamma * x + beta注意这里gamma和beta是逐 token 共享的,作用在通道维度上。它告诉模型:当 τ 不同时,某些物理变量的特征应该被放大或抑制。这比把时间值拼接进输入更符合物理直觉,因为它是一种全局调制,而不是局部噪声。
2.4 整体模型结构:从气象场到气象场
Timestep-Conditioned Transformer 的整体数据流可以概括成一条主线:
- 输入 X_t 经过 Patch Embedding,切成不重叠的 patch,每个 patch 展平成一个 token。
- 加上可学习的位置编码,保留空间位置信息。
- 经过多层 Transformer Block,做空间注意力建模。
- 在某个或某些层注入 timestep embedding,让模型知道当前预测的 τ。
- 解码阶段把 token 特征映射回 patch 维度,再还原成规则网格。
这里存在两个设计自由度:条件注入的位置,以及是否在解码器也使用条件。常见做法是只在主干网络尾部注入一次 FiLM,简单且稳定;复杂模型会在每个 Block 内部注入,让每一层都感知时间条件。两者没有绝对优劣,需要根据数据量和训练稳定性来选择。
3. 数据准备与环境配置
3.1 使用 ERA5 再分析数据构造训练样本
ERA5 是欧洲中期天气预报中心发布的全球再分析数据,覆盖长期历史时间范围,空间分辨率 0.25 度,时间分辨率 1 小时。它把历史观测和数值模式同化结果融合在一起,是训练数据驱动预报模型最常见的底座数据。
构造训练样本时,并不是把所有时间步都直接喂给模型。要从连续的时间序列里随机采样起点 t 和超前时间 τ,取 X_t 和 X_{t+τ} 组成一对。这样每个 epoch 都能看到不同的样本组合,变相扩大数据集。
采样时还要遵守一个关键纪律:训练集、验证集、测试集必须按时间划分,不能随机打乱。因为相邻时间的天气场高度相关,如果同一段时间的数据同时出现在训练集和测试集,评估结果会严重虚高,模型实际的泛化能力并没有那么强。
3.2 通道设计和变量选择
通道设计决定模型能学到哪些物理信息。下表是一个常见的中等规模配置。
| 变量 | 含义 | 气压层数 | 说明 |
|---|---|---|---|
| Z | 位势高度 | 13 | 表示气压面的起伏 |
| T | 气温 | 13 | 热力结构 |
| U | 纬向风 | 13 | 水平风分量 |
| V | 经向风 | 13 | 水平风分量 |
| Q | 比湿 | 13 | 湿度信息 |
| T2M | 2 米气温 | 1 | 地面温度 |
| MSL | 海平面气压 | 1 | 地面气压 |
| U10 / V10 | 10 米风 | 2 | 地面风 |
这样输入通道数是 13×5+4=69。如果原始数据分辨率太高导致显存不够,可以先用双线性插值降到 1 度或 2.5 度,把网格从 1440×720 降到 360×180 或更小,验证模型逻辑先跑通,再逐步提高分辨率。
3.3 归一化与纬度权重
天气变量之间的尺度差异极大。气温在 300K 附近波动,比湿通常只有 0.01 量级,位势高度则可能是几千米。如果不做归一化,模型训练会偏向数值大的变量,小数值变量很难学到有效特征。
标准做法是按通道统计训练集的均值和标准差,做 z-score 归一化:
# data: (N, C, H, W) means = data.mean(dim=(0, 2, 3), keepdim=True) stds = data.std(dim=(0, 2, 3), keepdim=True) data_norm = (data - means) / (stds + 1e-8)还有一类权重不能忽略:纬度权重。全球网格上,高纬度格点代表的实际面积比赤道附近小。如果损失函数对每个格点一视同仁,模型会把更多容量浪费在极地附近,因为那里格点数量占比高但实际面积小。通常按 cos(纬度) 给每个格点加权。
3.4 环境依赖和项目目录
实现这类模型不需要太多额外依赖,核心是 PyTorch。常用组件如下。
| 组件 | 建议 | 用途 |
|---|---|---|
| Python | 3.9 或更高 | 基础环境 |
| PyTorch | 2.0 或更高 | 模型训练 |
| xarray | 最新稳定版 | 读取 NetCDF 格式的 ERA5 数据 |
| numpy | 最新稳定版 | 数组运算 |
| einops | 可选 | 简化张量维度变换 |
| tensorboard 或 wandb | 可选 | 训练日志可视化 |
项目目录可以按这个结构组织:
weather_transformer/ ├── config.py # 模型和训练超参数 ├── data/ │ ├── dataset.py # 样本采样与数据集类 │ └── normalize.py # 通道统计与纬度权重 ├── models/ │ ├── timestep_embed.py # 时间步编码 │ ├── transformer.py # Transformer Block │ └── forecast_model.py # 整体模型 ├── train.py # 训练入口 ├── evaluate.py # 评估指标计算 └── logs/ # 日志输出这个结构并不复杂,但能把数据、模型、训练和评估分开,方便后面单独调试数据分析代码和模型代码。
4. 用 PyTorch 实现一个最小可运行的 Timestep-Conditioned Transformer
下面用一个小规模的合成数据示例说明完整实现。真实 ERA5 数据替换掉合成数据即可跑通,不需要改动模型主体。
4.1 Patch Embedding 与位置编码
Transformer 处理的是 token 序列,不能直接处理 (B, C, H, W) 的四维张量。先用卷积把输入切成 patch,每个 patch 映射成 embedding 向量。
class PatchEmbed(nn.Module): def __init__(self, in_channels, embed_dim, patch_size): super().__init__() self.patch_size = patch_size self.proj = nn.Conv2d( in_channels, embed_dim, kernel_size=patch_size, stride=patch_size ) def forward(self, x): # x: (B, C, H, W) x = self.proj(x) # (B, E, H/p, W/p) B, E, ph, pw = x.shape x = x.flatten(2).transpose(1, 2) # (B, ph*pw, E) return x, ph, pwpatch 大小对模型行为影响很大。patch 越小,token 越多,空间细节越丰富,但注意力计算量按 token 数量的平方增长。ERA5 的 0.25 度网格是 1440×720,如果 patch 取 4,token 数量是 360×180=64800,这个规模对普通单卡的注意力开销是不可接受的。学习阶段建议先降低分辨率,或者增大 patch。
4.2 Transformer Block
每个 Block 包含自注意力、前馈网络、层归一化和残差连接。
class TransformerBlock(nn.Module): def __init__(self, d_model, nhead, dim_ff, dropout=0.1): super().__init__() self.attn = nn.MultiheadAttention( d_model, nhead, dropout=dropout, batch_first=True ) self.norm1 = nn.LayerNorm(d_model) self.linear1 = nn.Linear(d_model, dim_ff) self.linear2 = nn.Linear(dim_ff, d_model) self.norm2 = nn.LayerNorm(d_model) self.dropout = nn.Dropout(dropout) def forward(self, x): h = self.norm1(x) x = x + self.dropout(self.attn(h, h, h, need_weights=False)[0]) h2 = self.norm2(x) x = x + self.dropout(self.linear2(torch.nn.functional.gelu(self.linear1(h2)))) return x这里把 LayerNorm 放在残差分支之前,也就是 Pre-LN 结构。相比 Post-LN,它在深层网络中更稳定,训练初期不容易出现梯度爆炸。
4.3 整体模型与解码回网格
把 TimestepEmbedding、PatchEmbed、TransformerBlock、FiLM 和输出头组装起来,就得到完整模型。
class TimestepConditionedForecastModel(nn.Module): def __init__( self, in_channels, patch_size, embed_dim, depth, nhead, out_channels=None, ): super().__init__() out_channels = out_channels or in_channels self.patch_size = patch_size self.patch_embed = PatchEmbed(in_channels, embed_dim, patch_size) self.pos_embed = nn.Parameter(torch.zeros(1, 1024, embed_dim)) self.t_embed = TimestepEmbedding(embed_dim) self.blocks = nn.ModuleList([ TransformerBlock(embed_dim, nhead, embed_dim * 4) for _ in range(depth) ]) self.film = FiLM(embed_dim, embed_dim) self.norm = nn.LayerNorm(embed_dim) self.head = nn.Sequential( nn.Linear(embed_dim, embed_dim * patch_size * patch_size), nn.GELU(), nn.Linear( embed_dim * patch_size * patch_size, out_channels * patch_size * patch_size, ), ) def forward(self, x, t): B, C, H, W = x.shape p = self.patch_size x, ph, pw = self.patch_embed(x) N = ph * pw if self.pos_embed.shape[1] < N: self.pos_embed = nn.Parameter( torch.zeros(1, N, x.shape[-1], device=x.device) ) x = x + self.pos_embed[:, :N, :] for blk in self.blocks: x = blk(x) t_emb = self.t_embed(t) x = self.film(x, t_emb) x = self.norm(x) x = self.head(x) # (B, N, C*p*p) x = x.transpose(1, 2).view(B, -1, ph, pw) x = torch.nn.functional.pixel_shuffle(x, p) return x # (B, C, H, W)解码部分使用了pixel_shuffle,它能把通道维度的p*p分组重新排列到空间维度,正好把 token 特征还原成原始网格。需要说明的是,这里的pos_embed动态扩展示例并不优雅,正式项目应该在初始化时就根据网格大小确定 token 数量,或者使用可插值的位置编码。
4.4 训练循环
训练循环需要处理三个输入:初始场、目标场、超前时间。下面是一个包含混合精度训练的示例。
import torch import torch.nn.functional as F from torch.cuda.amp import autocast, GradScaler def train_one_epoch(model, loader, optimizer, device, scaler=None): model.train() total_loss = 0.0 total_samples = 0 for x0, xt, lead in loader: x0 = x0.to(device) xt = xt.to(device) lead = lead.to(device).float() optimizer.zero_grad() if scaler is not None: with autocast(): pred = model(x0, lead) loss = F.mse_loss(pred, xt) scaler.scale(loss).backward() scaler.step(optimizer) scaler.update() else: pred = model(x0, lead) loss = F.mse_loss(pred, xt) loss.backward() optimizer.step() total_loss += loss.item() * x0.size(0) total_samples += x0.size(0) return total_loss / total_samples合成数据集可以用随机张量模拟,目的是先验证整个流程能跑通:
from torch.utils.data import TensorDataset def make_synthetic_dataset(num_samples=128, C=20, H=64, W=128): x0 = torch.randn(num_samples, C, H, W) # 用带噪声的平移近似目标场,真实项目应替换为 ERA5 样本对 xt = x0 + 0.05 * torch.randn_like(x0) lead = torch.randint(6, 168, (num_samples,), dtype=torch.float32) return TensorDataset(x0, xt, lead)这里示例只是链路验证,没有任何物理意义。替换成真实数据时,要把x0和xt换成同一段历史时间序列里相隔 τ 的两个天气场,lead就是对应的 τ。
5. 训练策略与验证指标
5.1 损失函数:为什么常用 MSE
预测目标是稠密天气场,MSE 是默认选择。原因很直接:它逐格点计算预测和真实值的平方差,梯度在误差大的地方更强,训练容易收敛。
但 MSE 有一个明显缺点:它天然偏好“平均预测”。如果模型对某个格点不确定,给出接近气候平均值的预测可以降低平方误差的期望风险。这会导致输出场偏平滑,极端天气强度被低估。
实践中可以组合多个损失:
- 空间域 MSE:关注逐点精度。
- 谱域损失:对预测场做傅里叶变换,在高频分量上计算误差,缓解平滑问题。
- 物理一致性损失:例如地转平衡约束,但这部分实现复杂度较高。
对于第一版模型,直接用加权 MSE 就可以。纬度权重需要在损失函数里体现:
def lat_weighted_mse(pred, target, lat_weight): # lat_weight: (1, 1, H, 1) 广播到 (B, C, H, W) diff = pred - target loss = (diff ** 2 * lat_weight).mean() return loss5.2 RMSE 与 ACC 的计算方式
验证数据驱动预报模型时,两个指标最常用:RMSE 和 ACC。
RMSE 是全格点的加权均方根误差,反映预测与真实场的平均偏差大小。
ACC 是异常距平相关系数,衡量预测的异常场与真实异常场的空间相关程度。它需要气候态参考场,通常是训练期多年平均的逐日气候值。预测减去气候态得到异常场,再和真实异常场做相关,数值越接近 1 越好。
def compute_acc(pred, target, clim, lat_weight): pred_anom = pred - clim target_anom = target - clim numerator = (pred_anom * target_anom * lat_weight).sum(dim=(-2, -1)) denominator = torch.sqrt( (pred_anom ** 2 * lat_weight).sum(dim=(-2, -1)) * (target_anom ** 2 * lat_weight).sum(dim=(-2, -1)) ) return (numerator / (denominator + 1e-8)).mean()ACC 在地学领域有明确经验阈值:中期预报中,ACC 降到 0.6 以下通常认为预报失去实用参考价值。这个阈值可以作为衡量模型可用超前时间的标准,例如“该模型 ACC 高于 0.6 的预报天数约为多少”。
5.3 训练日志里应该看什么
不要只盯着训练损失。至少要同时记录以下内容:
- 训练集和验证集的加权 MSE,观察是否过拟合。
- 分超前时间段的 RMSE 和 ACC,例如把 6、24、72、168 小时分开统计,看模型是否对不同时段都有稳定表现。
- 输出场的标准差,如果标准差远小于真实场标准差,说明模型在输出平均态,需要警惕平滑问题。
- 显存占用和单步训练时间,为扩大规模提供依据。
5.4 学习环境与生产环境的差异
| 维度 | 学习环境 | 生产环境 |
|---|---|---|
| 数据 | 小范围、低分辨率、部分变量 | 全变量、全分辨率、多年历史 |
| 模型规模 | 小 embed_dim、少层数 | 大规模、多卡并行 |
| 训练稳定性 | 能跑通即可 | 需要调参、重启恢复、检查点 |
| 推理 | 单次前向 | 需要多超前时间批量推理、错误处理 |
| 监控 | 本机日志 | 指标告警、分布漂移检测 |
| 回滚 | 不需要 | 保留版本,支持快速切换 |
学习阶段最重要的目标是把整个链路跑通,验证条件注入机制确实在起作用。生产环境则要额外考虑数据管道的稳定性、模型版本管理、推理服务的延迟和异常输入处理。
6. 常见问题排查
6.1 显存不足
现象:训练刚开始就报CUDA out of memory。
原因通常是 token 数量过大或 batch 太大。Transformer 的注意力复杂度是 O(N²),N 是 token 数量。分辨率提高一倍,token 数量变四倍,显存占用可能增加十几倍。
排查顺序:
- 检查输入分辨率是否过高,先降到 1 度或 2.5 度。
- 检查 patch size 是否过小,增大 patch 能显著减少 token。
- 检查 batch size,先用 1 或 2 验证。
- 使用梯度累积模拟较大 batch,而不是直接放大 batch size。
- 启用混合精度训练。
| 现象 | 常见原因 | 检查方式 | 处理建议 |
|---|---|---|---|
| CUDA out of memory | token 过多或 batch 过大 | 输出模型的张量尺寸 | 降低分辨率、增大 patch、减小 batch |
| 训练很慢 | 注意力 O(N²) 计算量大 | 观察单步耗时 | 换用更小的 patch 数或使用窗口注意力 |
6.2 Timestep 条件没有起作用
现象:模型对不同 τ 的预测结果几乎一样,条件注入看起来无效。
可能原因有两个。第一,FiLM 的gamma和beta初始值接近零,导致初始阶段条件调制很弱,需要更长的训练时间才能显现。第二,timestep embedding 的 MLP 容量太小,无法编码足够丰富的条件信息。
检查方式:把不同 τ 的 embedding 向量打印出来,观察它们是否有区分度;或者在推理时固定输入 X_t,只改变 τ,看输出是否明显变化。
解决方式:给gamma和beta做特殊初始化,比如让gamma初始化为 1,beta初始化为 0;增大 timestep embedding 的隐藏层宽度;或者把 FiLM 应用到多个 Transformer Block 而不是只应用在最后。
6.3 预测结果过于平滑
现象:预测场的空间结构比真实场模糊,极端天气的强度偏低,RMSE 还可以但 ACC 在中长期掉得很快。
原因几乎总是 MSE 损失函数带来的均值回归倾向。模型发现输出气候平均态能降低损失风险,于是选择平滑预测。
解决方式:
- 加入谱域损失,强制模型学习高频结构。
- 使用对抗训练或扩散模型思路生成更锐利的场。
- 评估时把 RMSE 和 ACC 和真实场的标准差一起看,不要只盯 RMSE。
6.4 训练损失下降但评估指标不涨
现象:训练集损失正常下降,验证集 RMSE 也在下降,但 ACC 始终偏低。
可能原因是气候态参考场选得不好。ACC 计算的是异常场相关性,如果气候态里的季节循环没有被去掉,异常场里还残留大量季节信号,模型预测能力会被高估或掩盖真实问题。
检查方式:画出预测异常场和真实异常场的空间分布,看是否结构上接近;检查气候态是否按照训练期逐年逐日统计。
解决方式:重新计算气候态,按每个日历日取多年平均;或者对数据做季节循环去除再训练。
7. 最佳实践与可复用清单
7.1 数据质量检查清单
在开始训练前,逐项确认以下内容:
- 样本对是否严格按时间顺序构造,X_t 和 X_{t+τ} 来自同一条时间序列。
- 训练、验证、测试集是否按时间切分,没有随机混入相邻时间点。
- 归一化统计量是否只从训练集计算,没有偷看验证集和测试集。
- 各变量尺度是否差异过大,是否需要按通道分别归一化。
- 纬度权重是否已经加入损失函数和评估指标。
- 数据里是否存在缺失值或 NaN,是否已经做插值或掩码处理。
7.2 模型训练与评估检查清单
模型侧同样有一份可以复用的清单:
- 先用小规模合成数据验证模型能跑通,再切换真实数据。
- 固定 X_t,改变 τ 做一次推理,确认条件注入确实改变输出。
- 每个超前时间单独评估 RMSE 和 ACC,不要只报告平均值。
- 监测输出场的标准差,防止平滑退化。
- 保存训练中的最佳检查点,按验证集 ACC 而不是训练损失选择模型。
- 记录实验配置、数据版本和随机种子,保证实验可复现。
7.3 可以继续扩展的方向
Timestep conditioning 本身是一个灵活的机制,可以往多个方向扩展:
- 把 τ 条件替换成“起报时刻 + 超前时间”的联合条件,让模型感知季节。
- 在解码器中也注入时间条件,让最后一层同样感知预报时长。
- 结合集合预报,输入多个扰动初始场,输出概率分布。
- 用扩散模型或 flow matching 替代直接回归输出头,改善长期预测的平滑问题。
- 把地理位置、地形高度等静态场作为额外条件输入,增强局部特征。
7.4 几点工程建议
真实项目里最值得投入时间的是数据处理管道,而不是模型结构。天气数据动辄 TB 级,如果数据管道不稳定,训练过程会被反复打断。建议先把样本采样、归一化、切分做完整,再开始调模型。
关于训练成本,先用低分辨率、小模型跑通指标基线,确认逻辑无误后再逐步增加分辨率和模型深度。每次只改变一个变量,否则很难定位是数据问题、条件注入问题还是模型容量问题。
Timestep conditioning 的价值在于让一个模型平滑处理多个超前时间。它不是一个独立模型架构,而是一个可插拔的设计思想。理解了这套机制,再去看相关论文和其他条件生成模型,会发现很多结构是相通的。对于刚开始接触气象深度学习的开发者,最有效的练习是先在小规模数据上没有物理约束地复现这套流程,跑通后再加入真实气象数据和更复杂的损失函数。