
一张雨天拍摄的街道照片和一张雾天拍摄的航拍图虽然都属于恶劣天气下的降质图像但它们的成像模型完全不同雨痕是高频重复纹理雾是全局大气光散射造成的对比度下降和颜色偏移。如果用一个固定公式去逆推几乎不可能同时兼顾两类退化。这就是 All-in-One Weather Restoration全天候统一图像恢复要解决的问题训练一个模型在不知道输入属于哪种天气的情况下一次性完成去雨、去雪、去雾、低光照增强等任务。光谱调和Spectral Harmonization是这一类方案里值得关注的设计思路把不同天气的特征放到频率域里去对齐而不是在像素空间里强行混合。这篇文章会从问题出发解释为什么单模型统一恢复会失败再拆解光谱调和的原理给出一个基于 PyTorch 的可复现实现最后补充训练、验证、排错和部署时需要留意的细节。1. 为什么 All-in-One 天气恢复难在特征调和1.1 单一天气恢复模型与统一模型的本质差异先看单一任务模型。去雾模型通常依赖大气散射模型把观测图像理解为场景辐射经过透射率衰减后叠加大气光模型要做的是估计透射率和大气光再反演清晰图像。去雨模型通常假设干净图像叠加了雨痕层模型要做的是分离或抑制雨痕层。这两类任务的先验完全不同一个关心全局低频信息一个关心局部高频纹理。如果用同一套网络同时处理这两种退化网络权重必须在两类任务之间做折中。问题在于网络不知道当前输入是雨天还是雾天。输入一张雾图网络既要抑制高频又要恢复低频对比度输入一张雨图网络需要保留背景边缘却要去除方向性条纹。一个不加设计的共享编码器很容易把两类特征的统计量混在一起最后得到“雨没去干净、雾也没去透”的结果。在单任务模型里模型对退化类型有确定性假设因此网络结构可以针对该假设设计。统一恢复模型等于把退化类型也变成了模型需要隐式推断的信息。这个信息无法从像素值直接看出来只能靠网络在特征空间中自己区分。而特征空间里的区分能力正是这类模型最容易翻车的地方。1.2 多天气数据集直接混训的两个典型问题很多人第一次尝试 All-in-One 模型时做法非常简单把多个天气数据集的配对数据合并到一个 DataLoader直接训一个 U-Net。一开始 loss 会下降但验证时会发现模型表现很怪。第一个问题是数据域差异。不同数据集来自不同采集设备图像分辨率、色偏、压缩噪声、标注方式都不一样。Rain100H 的雨图是合成雨线RESIDE 的雾图来自大气散射模拟LOL 的低光照图像来自真实的低曝光与长曝光配对标定。这些数据堆在一起网络先学到的是“数据集的拍照风格”而不是“天气退化的共同规律”。典型现象是训练集上 PSNR 很高换一个数据集测试立刻掉几个 dB。第二个问题是任务歧义。同一张特征图网络无法判断该走“去高频”还是“恢复低频”的路径。普通卷积层没有显式的频率选择能力所有通道都在同一套卷积核下计算结果是高频抑制与低频恢复互相干扰。更麻烦的是不同天气类型在数量上往往不平衡模型会偏向样本量大的天气类型另一个类型的指标会非常难看。这两个问题说明All-in-One 不能理解为“多数据集多任务一起训练”而是要先把不同天气任务的特征对齐到同一个可共享的表示空间。光谱调和解决的就是这一步。1.3 频率域天然适合做天气特征的统一不同天气退化在频率域里有明显的区分度这是光谱调和可行的基础。退化类型主要频率区域视觉表现处理需求雨痕中高频、方向性重复白色或半透明条纹抑制高频条纹但要保留背景边缘雪高频加中频、稀疏遮挡白色斑点和遮挡块修复被遮挡区域的纹理雾与霾低频、全局分布对比度下降、颜色偏灰白全局对比度恢复与颜色校正低光照低频为主、亮度通道暗部噪声明显、颜色偏暗亮度增强与噪声抑制既然不同退化在不同频率带上“各占一块”一个自然想法是在频率域里对特征做重整让不同天气类型共享低频全局信息同时保留各自特有的高频细节。光谱调和的思路可以概括为把编码器输出的特征图变换到频率域在频率域里通过可学习的模块重新分配幅值和相位信息使得来自不同天气条件的特征分布更加接近然后再变回空间域交给解码器恢复。这样做的好处有两个。其一频率域把“全局对比度”和“局部纹理”显式拆开了网络可以分别处理不必再用大量卷积层去隐式学习这种分离。其二FFT 的计算复杂度是 O(N log N)远低于自注意力机制的 O(N²) 复杂度在分辨率不太高的情况下非常适合嵌入到轻量级恢复网络里这也是标题里 Efficient 一词的来源。2. 光谱调和的原理与模块设计2.1 从空间特征到频谱特征在 PyTorch 里对特征图做频谱变换使用的是torch.fft.rfft2。与完整 FFT 不同rfft2 利用实信号频谱的共轭对称性只计算一半频率分量因此返回张量的最后一个维度是W // 2 1而不是W。import torch import torch.nn as nn import torch.nn.functional as F # x: [B, C, H, W] def inspect_spectrum(x): f torch.fft.rfft2(x, normortho) print(输入尺寸:, x.shape) print(频谱尺寸:, f.shape) # [B, C, H, W // 2 1] print(数据类型:, f.dtype) # torch.complex64 amp f.abs() phase f.angle() print(幅值形状:, amp.shape, 相位形状:, phase.shape)对一张[B, C, 256, 256]的特征图rfft2返回[B, C, 256, 129]。幅值f.abs()表示每个频率分量的能量大小相位f.angle()表示该分量的位置结构信息。在图像融合和图像增强的文献里通常认为幅值携带能量、对比度和颜色高低频分布信息相位携带边缘、轮廓和纹理位置信息。2.2 为什么直接用相位建模容易出问题相位张量看起来只是一个普通的实数张量直接在上面叠加卷积层似乎可行。但相位有一个特殊性质它的取值范围是[-π, π]并且具有环绕特性。π和-π表示同一个角度如果卷积输出把某个位置的相位从π - 0.1推到π 0.1计算结果会被解释成接近-π产生突变。这种环绕不连续会让训练很不稳定输出图像出现振铃或断裂纹理。更稳妥的做法是绕过相位直接对频谱的实部和虚部做操作。实部和虚部是对复数频率分量的正交分解它们在数值上是连续的没有环绕问题。把幅值缩放转嫁到实部和虚部上等价于对频率分量做统一增益而通过小卷积核在实部和虚部上做跨通道混合可以调整频率分量的结构关系。这样做既保留了频率域建模的能力又避开了相位角度带来的训练陷阱。2.3 推荐实现在实部和虚部上做调和光谱调和模块可以设计成三个步骤。第一步是幅值重标定。把频谱的幅值做通道全局池化用类似 SE-Net 的通道注意力机制计算每个通道的缩放因子。不同天气退化会影响不同通道的能量分布这个缩放因子让模型可以按通道调整频率能量从而对齐跨天气的特征分布。第二步是实部虚部调谐。用两个 1×1 卷积分别处理实部和虚部实现跨通道信息混合。为什么要用 1×1 卷积而不是 3×3因为频率分量之间天然存在全局关联这里做的是通道间的重组不需要在空间频率位置上做局部卷积1×1 卷积参数量小还能保持计算效率。第三步是残差输出。把处理后的频谱反变换回空间域通过一个可学习的缩放参数叠加到输入特征上。缩放参数初始化为 0意味着训练初期模块不改变输入网络先稳定收敛再逐步把频率域调整量加进来能显著提升训练稳定性。2.4 模块复杂度控制策略频谱调和模块本身很轻但 FFT 的计算量还是和特征图分辨率直接相关。实际设计时有一个重要原则不要在最高分辨率特征图上做频谱操作而是把调和模块放在编码器的最深层也就是分辨率最低、通道数最多的瓶颈位置。例如输入 256×256经过三次下采样后特征图是 32×32在这个分辨率上做rfft2代价很小。解码器从低频特征逐步恢复高频细节时已经携带了被调和的全局频率信息再结合编码器的跳连接补充局部细节效果上仍然保留了光谱调和的作用。这个取舍就是“高效”二字的工程来源。3. 环境准备与实验数据组织3.1 运行环境与依赖版本下面这份环境清单用于本地实验。如果原始项目没有给出固定版本落地之前要先确认自己机器上的 CUDA 和显卡驱动兼容性。组件建议版本说明Python3.8 或 3.93.10、3.11 也可行但部分旧扩展库可能不兼容PyTorch1.13 或 2.x需要包含torch.fft1.8 之后的版本都支持torchvision与 PyTorch 同版本用于数据集加载和图像变换CUDA11.7 或 12.x取决于显卡驱动和 PyTorch 安装方式GPU 显存8GB 以上256×256 输入、batch 8 左右8GB 勉强够用图像库opencv-python, pillow读图、裁剪、合成退化数据创建环境并安装依赖conda create -n weather_restore python3.9 -y conda activate weather_restore pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118 pip install numpy opencv-python pillow tensorboard没有 GPU 也可以跑通流程但要把 patch size 降到 128batch size 降到 2网络通道数减半。学习阶段优先确保流程正确而不是追求指标。3.2 天气数据集的两种组织方式第一种是使用公开的配对数据集把不同天气类型的数据放在同一个训练目录下。常用的研究基准包括 Rain100H、Snow100K、RESIDE雾天、LOL低光照等。这些数据集提供同一个场景的退化图和清晰图训练和评价协议相对标准。第二种是自行合成退化数据。对清晰图添加模拟雨线、雪点、雾效或亮度衰减生成配对样本。合成的优势是可以自由控制退化的强度、方向、数量和比例方便做类别均衡。缺点是合成退化与真实退化之间存在分布差异模型在真实数据上的效果需要单独验证。实际项目的常见做法是两者结合先用公开数据集快速验证模型结构再在目标场景数据上做合成退化微调最后用少量真实配对数据做最后的域适应。这个顺序能减少真实数据标定成本也能尽早发现结构性问题。3.3 数据加载与增广代码不管数据集来源是什么DataLoader 都要完成几件事随机裁剪、随机翻转旋转、归一化到[-1, 1]。裁剪不仅控制显存也相当于增广让模型看到更多局部结构。import random import torch class MixedWeatherDataset(torch.utils.data.Dataset): def __init__(self, pairs, patch_size256, trainTrue): self.pairs pairs # [(degraded_path, clean_path), ...] self.patch_size patch_size self.train train def __getitem__(self, idx): deg_path, clean_path self.pairs[idx] degraded read_image(deg_path) # [3, H, W], 范围 [0, 1] clean read_image(clean_path) if self.train: h, w self.patch_size, self.patch_size i random.randint(0, degraded.shape[1] - h) j random.randint(0, degraded.shape[2] - w) degraded degraded[:, i:i h, j:j w] clean clean[:, i:i h, j:j w] if random.random() 0.5: degraded torch.flip(degraded, dims[2]) clean torch.flip(clean, dims[2]) if random.random() 0.5: degraded torch.rot90(degraded, 1, dims[1, 2]) clean torch.rot90(clean, 1, dims[1, 2]) return degraded * 2 - 1, clean * 2 - 1 def read_image(path): from PIL import Image img Image.open(path).convert(RGB) t torch.from_numpy(np.array(img, dtypenp.float32) / 255.0) return t.permute(2, 0, 1)如果训练时希望让模型知道当前输入的天气类型可以在数据集里同时返回一个 one-hot 条件向量[rain, snow, haze, lowlight]。这个条件向量可以作为辅助信息控制频谱调解模块的参数。需要强调的是推理阶段如果不再输入条件向量模型结构里就必须有一个“无条件分支”否则训练和推理行为不一致。3.4 实验环境与部署环境的差别实验阶段关心的是迭代速度小 patch、小 batch、TensorBoard 可视化足够。部署阶段还要额外考虑三件事。第一输入预处理必须一致。训练时图像归一化到[-1, 1]部署时的推理管线也必须做同样的均值方差映射不能直接扔原始 0 到 255 的 uint8 张量进模型。第二尺寸对齐。FFT 对输入尺寸没有严格限制但推理时的长宽最好和训练时的 patch size 对齐避免模型在不同尺度特征上表现不一致。如果实际业务图很大先切块推理再拼接或者做一次缩放都要提前约定。第三算子导出。torch.fft.rfft2在不同推理框架里的导出支持程度差异很大。导出 ONNX 前先确认目标框架是否支持 FFT 类算子。如果支持不完整一个可行方案是把频谱调和模块限定在训练阶段部署时换成空间域的等效卷积分支另一个方案是做训练-推理异构训练用频谱模块推理用 JIT 编译后的模型。这个坑越早测越好不要等训练完才发现导不出去。4. 基于 PyTorch 的光谱调和恢复网络实现4.1 频谱工具函数先实现一个最关键的闭合检查函数对任意输入张量执行rfft2再执行irfft2必须能近似重建原始输入。这是后续调试所有频谱模块的基础。def fft2_reconstruct_check(x): import torch.fft as fft f fft.rfft2(x, normortho) y fft.irfft2(f, sx.shape[-2:], normortho) error (x - y).abs().max().item() print(重建最大误差:, error) return error 1e-5关键点是irfft2的s参数必须显式传入原始分辨率的(H, W)因为rfft2丢掉了最后一个频率分量反变换时如果不指定尺寸宽度的奇偶性可能对不上。这个检查和下面的频谱调和模块放在一起测试可以提前排除百分之八十的尺寸错误。4.2 光谱调和模块实现下面这个模块采用幅值重标定加实部虚部调谐的设计避免直接操作相位角。import torch import torch.fft as fft import torch.nn as nn class SpectralHarmonization(nn.Module): def __init__(self, channels, reduction8): super().__init__() # 幅值通道注意力计算每个通道的频率能量缩放系数 self.amp_se nn.Sequential( nn.AdaptiveAvgPool2d(1), nn.Conv2d(channels, channels // reduction, 1), nn.ReLU(inplaceTrue), nn.Conv2d(channels // reduction, channels, 1), nn.Sigmoid(), ) # 实部和虚部的跨通道调谐 self.real_conv nn.Conv2d(channels, channels, 1) self.imag_conv nn.Conv2d(channels, channels, 1) self.rescale nn.Parameter(torch.zeros(1)) def forward(self, x): f fft.rfft2(x, normortho) # [B, C, H, W/21] f_real f.real f_imag f.imag amp torch.sqrt(f_real ** 2 f_imag ** 2 1e-8) amp_scale self.amp_se(amp) # [B, C, 1, 1] f_real f_real * amp_scale f_imag f_imag * amp_scale f_real self.real_conv(f_real) f_imag self.imag_conv(f_imag) f_harm torch.complex(f_real, f_imag) out fft.irfft2(f_harm, sx.shape[-2:], normortho) return x self.rescale * out这段代码有三个设计点需要理解。第一f.real和f.imag是连续实数张量在上面做卷积、乘缩放不会触碰相位环绕问题。第二amp_se池化的是幅值而不是直接池化实部或虚部。幅值反映了该通道在频谱上的总体能量天气类型不同通道能量的分布就不同用通道注意力去调节每个通道的能量水平可以让不同天气的全局特征在幅值尺度上更接近。第三rescale初始化为 0模块开始时输出恒等于输入。这等价于给网络一个“先不调整频率”的起点避免随机初始化破坏已经稳定的特征表达训练到中后期网络自然会把rescale学到合理数值。4.3 轻量恢复网络主体网络采用编码器-解码器结构编码器三次下采样在最深层接入光谱调和模块然后逐级上采样通过跳连接补充高频细节。整体预测图像的残差而不是直接回归完整清晰图像。class DownBlock(nn.Module): def __init__(self, in_ch, out_ch): super().__init__() self.conv1 nn.Conv2d(in_ch, out_ch, 3, 2, 1) self.conv2 nn.Conv2d(out_ch, out_ch, 3, 1, 1) self.act nn.LeakyReLU(0.2, inplaceTrue) def forward(self, x): return self.act(self.conv2(self.act(self.conv1(x)))) class UpBlock(nn.Module): def __init__(self, in_ch, out_ch): super().__init__() self.up nn.ConvTranspose2d(in_ch, in_ch // 2, 2, 2) self.conv nn.Conv2d(in_ch // 2, out_ch, 3, 1, 1) self.act nn.LeakyReLU(0.2, inplaceTrue) def forward(self, x): x self.act(self.up(x)) return self.act(self.conv(x)) class AllInOneRestorationNet(nn.Module): def __init__(self, in_ch3, base32): super().__init__() self.head nn.Sequential( nn.Conv2d(in_ch, base, 3, 1, 1), nn.LeakyReLU(0.2, inplaceTrue), ) self.enc1 DownBlock(base, base * 2) self.enc2 DownBlock(base * 2, base * 4) self.enc3 DownBlock(base * 4, base * 8) self.harm SpectralHarmonization(base * 8) self.dec1 UpBlock(base * 8, base * 4) self.dec2 UpBlock(base * 4, base * 2) self.dec3 UpBlock(base * 2, base) self.tail nn.Conv2d(base, in_ch, 3, 1, 1) def forward(self, x): x0 self.head(x) # [B, 32, H, W] x1 self.enc1(x0) # [B, 64, H/2, W/2] x2 self.enc2(x1) # [B, 128, H/4, W/4] x3 self.enc3(x2) # [B, 256, H/8, W/8] xh self.harm(x3) # 光谱调和尺寸不变 x self.dec1(xh) x2 # [B, 128, H/4, W/4] x self.dec2(x) x1 # [B, 64, H/2, W/2] x self.dec3(x) x0 # [B, 32, H, W] delta self.tail(x) return x delta # 残差学习输出仍为 [B, 3, H, W]残差学习的价值在于清晰图与退化图的大部分低频信息是接近的网络只需要学习一个较小的修正量比直接回归整张图像更容易收敛。如果tail输出接近 0模型输出就约等于输入这就保证了训练初期不会出现大偏差。4.4 频域损失与整体训练损失像素域 L1 损失能约束整体亮度结构但对高频细节和频谱能量关系不敏感。加入一个频域损失让预测结果和目标清晰图在傅里叶变换后尽可能接近可以利用频谱分量对齐优化方向。def frequency_loss(pred, target): f_pred torch.view_as_real(torch.fft.rfft2(pred, normortho)) f_target torch.view_as_real(torch.fft.rfft2(target, normortho)) return F.l1_loss(f_pred, f_target)rfft2返回复张量view_as_real把它变成最后一维为[real, imag]的实数张量这样可以直接参与 L1 损失计算。整体损失为def total_loss(pred, clean): l1 F.l1_loss(pred, clean) freq frequency_loss(pred, clean) return l1 0.1 * freq频域损失权重从 0.05 到 0.2 之间都可以尝试。权重太大会让网络过度关注整体频率能量忽略局部纹理权重太小时频域约束几乎不起作用。建议先固定 L1 训练少量迭代确认模型稳定收敛后再加入频域损失更容易定位是哪部分损失导致的质量问题。5. 训练、验证与评价指标5.1 训练脚本与关键超参训练超参直接决定模型能不能收敛这里给出一组在单卡 8GB 显存下可运行的配置。超参数推荐值说明patch_size256显存不足时降到 128batch_size8与 patch_size 联动调整epochs100 到 200以验证指标为准防止死板optimizerAdamW权重衰减设 1e-4 防止过拟合learning_rate1e-4配合 cosine 衰减最后降到 1e-6warmup_epochs5避免初始化阶段大幅震荡ampTrue混合精度训练节省显存并加速训练循环核心部分from torch.cuda.amp import autocast, GradScaler model AllInOneRestorationNet().cuda() optimizer torch.optim.AdamW(model.parameters(), lr1e-4, weight_decay1e-4) scheduler torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max150, eta_min1e-6) scaler GradScaler() for epoch in range(epochs): model.train() for degraded, clean in loader: degraded, clean degraded.cuda(), clean.cuda() optimizer.zero_grad() with autocast(): pred model(degraded) loss total_loss(pred, clean) scaler.scale(loss).backward() scaler.step(optimizer) scaler.update() scheduler.step()训练时需要监控的不只是总 loss。建议每 100 个 iteration 打印一次 L1 损失和频域损失如果 L1 在下降但频域损失震荡说明频域约束和像素域约束在打架需要调小频域权重。5.2 PSNR 和 SSIM 的计算约定PSNR 和 SSIM 是全参考指标计算前必须约定数值范围。常见错误是在[-1, 1]的模型输出上直接算 PSNR又在[0, 255]的 ground truth 上算得到的结果没有意义。统一约定模型输出和 ground truth 都先映射到[0, 1]再计算指标。def calculate_psnr(pred, clean, max_pixel1.0): pred (pred.clamp(-1, 1) 1) / 2 clean (clean.clamp(-1, 1) 1) / 2 mse torch.mean((pred - clean) ** 2) return 10 * torch.log10(max_pixel ** 2 / (mse 1e-8))PSNR 对整体像素误差敏感SSIM 对局部结构相似性敏感。实际使用中两者都要看并且在柱状图上分别统计每个天气类型的均值和方差。如果某个天气类型的 PSNR 均值低且方差大说明模型对该类退化效果不稳定需要单独排查。5.3 怎么判断模型真的做到了“统一”训练完成后不能只看混合测试集上的平均指标那是欺骗自己。正确的验证方式是对每个天气类型单独计算指标再和单任务模型的指标做对比。统一模型每个类型的指标都不应明显低于对应单任务模型太多。除了数值指标还建议做两个可视化检查。第一个是残差可视化。把模型的输出减去输入得到恢复残差图。雨图的残差应该集中在雨痕位置雾图的残差应该是全局均匀的对比度变化而背景区域不应出现明显纹理改变。如果残差图里出现了原图没有的结构说明模型在伪造细节。第二个是频谱对比。对预测结果、退化输入、清晰图分别做rfft2把幅值谱画出来对比。正常的恢复结果应该在高频位置更接近清晰图低频能量分布也应该恢复到合理水平。这个检查虽然需要写一点绘图代码但能直观看到光谱调和到底有没有起作用。6. 常见问题与排查链路6.1 输出出现网格状或振铃伪影现象恢复图像细节区域出现周期性横纹、网格或者边缘振铃。原因链条第一irfft2没有指定s参数导致反变换尺寸和原图不一致。第二直接在相位角上做卷积相位环绕不连续造成频谱突变。第三频域损失权重过大的同时FFT 上的高频分量被过度放大。检查方式# 在模型 forward 里临时加一句 y fft.irfft2(f_harm, sx.shape[-2:], normortho) # 确认 s 始终等于输入尺寸不要用默认尺寸再做一个闭合测试随便初始化一个模块输入随机的[B, C, 256, 256]输出尺寸必须仍然是[B, C, 256, 256]。如果尺寸对不上先修尺寸问题再看损失权重。处理建议保持rescale初始为 0频域损失权重从 0.05 开始如果仍然振铃把实部虚部的 1×1 卷积换成 1×1 的组卷积减少跨通道过度混合。6.2 某一类天气的恢复效果明显变差现象去雨效果不错但去雾结果偏色严重或者低光照增强后噪点爆炸。原因数据集中各类天气样本数量不平衡或者不同数据集的退化强度差异太大。光谱调和模块把特征分布往样本量大的方向拉动小样本天气类型被“牺牲”了。检查方式在验证阶段按天气类型分组统计 PSNR、SSIM不看平均指标。如果某个类型只有另一种类型一半的样本量基本可以确认是数据不平衡。处理建议每个 batch 内按天气类型均匀采样保证每次迭代都能看到所有类型的数据。如果仍然不稳定可以先在单一类型上分别预训练几个分支再把共享权重加载到统一网络里微调。这种“先分后合”的策略比一步到位稳定得多。6.3 训练 loss 下降明显但验证指标不升现象训练 L1 损失从 0.1 降到 0.02但验证集 PSNR 没有同步上升甚至下降。原因过拟合特定数据集风格或者验证和训练的数据分布不一致。另一个常见原因是评价代码计算范围与训练归一化不一致比如训练用[-1, 1]验证却把输出直接当作[0, 1]计算 PSNR导致结果系统性偏低。检查方式对比训练集和验证集中同一场景的退化程度。如果训练集是合成雨线验证集是真实雨图指标不升是正常的这是域差距问题不是模型 bug。再检查验证代码里是否对模型输出做了和训练一致的 clamp 和归一化。处理建议统一评价代码确保训练、验证、测试使用同一套预处理。对真实场景部署加入少量真实配对数据做微调或者用真实退化图像做无监督域适应。6.4 训练出现 NaN 或 loss 剧烈震荡检查顺序如下表现象可能原因检查方式处理建议loss 出现 NaN学习率过高查看前几个 iteration 梯度范数降低学习率到 1e-5 测试loss 剧烈震荡AMP 精度不足关闭autocast对比对频域损失单独保持 float32梯度爆炸网络过深或损失尺度大打印梯度最大值加梯度裁剪clip_grad_norm_某类指标骤降数据集混入坏样本检查 DataLoader 输出范围和路径清洗数据增加异常检测6.5 实验记录清单训练这类多数据集模型时最怕的是复现不了自己的结果。建议每次实验都记录以下信息数据集组合方式每个天气类型用了哪些数据集、各多少张图。预处理版本patch size、归一化方式、增广开关。训练配置学习率、epoch、batch、损失权重、随机种子。每个天气类型单独的验证指标不是混合平均。模型结构版本是否包含光谱调和模块、通道数、瓶颈分辨率。可视化输出典型样本的输入、预测、残差、频谱对比图。有了这份记录换数据、改权重、调整结构后指标变化才能追根溯源。7. 最佳实践与扩展方向7.1 发布前可复用检查清单把下面这份清单贴在项目 README 里每次提交模型前逐项确认频谱闭合测试通过任意输入经过rfft2和irfft2重建误差小于 1e-5。模型输入输出尺寸与预处理一致训练和部署统一使用[-1, 1]归一化。验证脚本对每个天气类型单独输出 PSNR 和 SSIM不只看平均。训练配置包含随机种子、数据集版本、损失权重可复现。检查输出图像残差确认没有出现与退化无关的伪影。部署链路确认目标框架支持 FFT 算子或者在导出时替换为等效空间域分支。7.2 值得继续扩展的方向第一个方向是条件化调和。在训练时给模型输入的天气类型编码让频谱调和模块根据不同天气动态调整幅值与实虚部处理方式。推理时可以保留条件输入也可以训练一个轻量分类器自动判断天气类型实现真正的“未知道路输入也能自动适配”。第二个方向是跨尺度频谱注意力。当前模块只在最深层做一次调和可以尝试在多个尺度上都插入调和模块配合自注意力机制处理长距离依赖。代价是显存和参数量上升是否值得要由具体任务决定。第三个方向是视频天气恢复。单帧模型逐帧处理会产生闪烁需要在时间维度上约束调和模块的输出一致性。可以引入光流或时序注意力让频谱调和结果在相邻帧之间保持稳定。第四个方向是真实退化数据适配。合成数据训练的模型落到真实场景通常有落差。可以考虑用真实退化图做无监督微调或者在频谱域上做对抗性对齐让模型对真实天气的分布更鲁棒。对于刚接触这个方向的开发者建议先不要追求复现完整论文而是把本文的 4.2 和 4.3 节代码跑通做一次“雨雪雾低光四类数据混训”的完整实验对比有光谱调和模块和无该模块的指标差异。这个对比实验能直观展示频率域特征对齐的价值也是后续深入改进的基础。