
简介本资源是一项基于PyTorch实现的对偶生成对抗网络Dual GAN图像去雾系统专为计算机视觉方向的本科毕业设计、课程设计及深度学习实践者打造聚焦真实场景下雾霾图像的端到端复原任务。压缩包共31个文件含10个核心Python模块如Generator.py、Discriminator.py、train.py、predict.py等、6张训练/评估可视化图loss.png等、5张测试输入/输出样例图jpg/png以及模型权重.pkl、工具脚本loader.py、parseArgs.py、日志与绘图辅助模块logger.py、showPlit.py和完整README说明文档整体大小21.31MB结构清晰、模块解耦合理便于理解网络构建、损失设计、数据加载与推理全流程。已有43人下载学习所有代码均经实测运行通过附逐行注释与备份文件.zbak显著降低调试门槛读者可直接复现训练-验证-预测闭环并深入掌握GAN在低层视觉任务中的对偶结构设计思想与PyTorch工程落地细节。从雾里看花到纤毫毕现基于PyTorch的对偶生成对抗网络图像去雾系统实现与源码解析雾天拍出来的照片大家应该都见识过——灰蒙蒙一片像蒙了一层磨砂玻璃远处的细节全被吞掉了。这个问题放到自动驾驶、安防监控、航拍测绘这些场景里就不是“照片不好看”这么简单了直接关系到算法能不能正常工作。这几年我一直在做图像增强相关的工作去雾算是里面折腾最多、也最容易翻车的一个方向。市面上传统的去雾算法暗通道先验之类在合成图上效果还行一碰到真实雾图就原形毕露色彩偏、边缘糊、还经常把天空搞成一块块的伪影。而基于深度学习的去雾方案里GAN路子又是最难调的那一类。今天这篇东西我会把最近做的一个基于PyTorch的对偶生成对抗网络图像去雾系统从原理到代码完整拆一遍把生成器怎么搭、判别器怎么配、损失函数怎么权衡、训练过程踩了哪些坑全部倒出来。这篇内容适合有一定PyTorch基础、想动手做图像生成或图像恢复项目的读者哪怕你之前只看过CycleGAN的代码跟着这篇走一遍对GAN系模型的训练套路也会有一个更全面的把握。1. 项目背景与整体设计思路1.1 为什么不用传统算法和标准GAN而选中对偶GAN先说说去雾这件事的本质。雾天成像退化学术界有一套经典的物理模型在解释观测到的图像等于大气光经过散射衰减后的场景反射光线叠加一层随距离增加而增强的雾霾散射光。用公式表示就是 I(x) J(x)t(x) A(1 - t(x))其中J是清晰图像t是透射率A是大气光。这个公式看起来简单但真正反解它的时候你只知道IJ、t、A全是未知数一个方程解三个未知量是个典型的病态问题。传统方法比如暗通道先验就是基于“无雾图像局部区域里至少有一个颜色通道的强度非常低”这一统计规律来估计t和A计算速度是快可一旦图像里有大片天空或者白色物体先验就失效了。深度学习方案本质上是在用数据拟合从I到J的映射。大多数基于CNN的监督方法比如AOD-Net、DehazeNet需要配对的雾图和无雾图训练是稳定可合成雾图与真实雾图之间存在很明显的域差异模型在合成数据上收敛得再好拿到真实场景里往往都会打折扣。那为什么不直接用标准GAN呢标准GAN的生成器学习的是从随机噪声到目标分布的映射做图像翻译任务容易失控生成结果不遵守输入图像的结构约束。对偶GAN的思路就聪明在这里既然有雾→无雾是一个映射那无雾→有雾也应该有一个对应的逆映射两个生成器成对存在配合循环一致性损失互相约束这样既不需要严格配对的训练数据又能保证生成图像在结构上和输入保持一致。这个思路最早在CycleGAN和DualGAN里被验证过用在去雾上可以说是“门当户对”。1.2 系统整体架构和模块划分整个系统的设计目标很明确输入一张有雾图输出一张无雾图中途不需要任何人工标注的深度信息或透射率图。为了做到这一点我把它拆成了四个模块数据加载与预处理模块、对偶生成网络模块、判别网络模块、训练与损失计算模块。数据模块负责把雾图和无雾图组成非配对的训练对并做随机裁剪、水平翻转这些增强操作。生成网络模块包含两个生成器G_A有雾→无雾和G_B无雾→有雾。判别网络模块也包含两个判别器D_A和D_B分别判断某个图像是来自真实的无雾或有雾域还是生成器造出来的。训练与损失模块则把GAN损失、循环一致性损失和身份映射损失组合到一起控制每个损失的权重在训练过程中动态调节优化策略。这四块各管各的互不侵入后期如果要换骨干网络、加感知损失基本只动对应模块就行不会牵一发动全身。我在写代码时也刻意遵循了这个分模块原则后面讲源码的时候会按照这四个模块逐个展开。2. 对偶GAN去雾核心原理拆解2.1 从CycleGAN到对偶GAN两个域之间的互逆映射对偶GAN的核心思想可以用一句话概括让两个生成器互相“翻译”并且要求翻译过去再翻译回来结果和原图保持一致。假设我们把有雾图像所在的空间叫X域无雾图像所在的空间叫Y域。G_A负责把X翻译成YG_B负责把Y翻译回X。如果G_A把一张雾图变成了无雾图那G_B就应该有能力把这个无雾图再变回那张雾图。如果这套往返翻译能成功说明G_A在翻译过程中并没有丢失原图的结构信息它不是在乱编内容而是真正完成了域之间的风格转换。这个“循环一致性”约束就是整个对偶结构能够稳定训练的关键。为什么这个机制对去雾特别有效因为去雾任务和普通的图像翻译有个明显区别我们希望去雾后图像的内容、结构、边缘细节都和原图严格对齐只是去掉雾霾层。如果只用对抗损失生成器很容易找到一个“捷径”——把所有输出都变成某种平均色调、纹理简单的图像因为这种图像看起来“干净”能骗过判别器但却是对输入的灾难性破坏。加了循环一致性之后生成器就没有这种偷懒的余地了你想把有雾图变成干净图可以但你的结果必须能被逆生成器重新变回原本那张有雾图这就在很大程度上强制G_A保留了输入的细节信息。2.2 损失函数设计GAN损失 循环一致性 身份映射这个系统的损失函数不是单一目标而是多个目标的加权组合我把它们分成三组来理解。第一组是GAN对抗损失。这部分用最小二乘GANLSGAN的形式实现比原始GAN的交叉熵损失训练更稳定。对于G_A和D_A目标函数是让D_A无法区分G_A生成的无雾图和真实无雾图同时让D_A尽可能区分两者。第二组是循环一致性损失。一次完整的循环包括两步第一步是G_A(x)得到假无雾图再用G_B把假无雾图变回假有雾图像第二步是G_B(y)得到假有雾图再用G_A把它变回假无雾图像。然后分别计算原图和重建图之间的L1距离。L1损失在图像恢复任务中比L2损失更常用因为它不会过度惩罚边缘结构上的微小偏移生成结果更锐利。第三组是身份映射损失。这个损失是CycleGAN论文里的一个细节但很多人容易忽略。它的意思是如果用G_A去处理一张本来就已经无雾的图像那输出应该还是这张无雾图而不是去变换它的色调。换句话说生成器在输入已经属于目标域的情况下应该保持输入不变。这个损失在去雾里尤其重要因为如果缺少它生成器可能会无条件给所有图像叠加一层“去雾滤镜”导致原本清晰的图像也被改得色彩失真。三个损失加权求和我在实验里使用的默认权重是生成器总损失中循环一致性权重10、身份映射权重5、GAN对抗损失权重1。这个比例不是乱拍的后面会说明原因。3. 系统实现与核心代码实战3.1 环境准备与数据集选择先说环境。我这次用的是PyTorch 2.0.1加CUDA 11.8显卡是单张RTX 309024GB显存。这里提醒一句PyTorch 2.0以上版本如果装了GPU版编译算子和数据加载都有不少优化同样的batch size训练速度大概能快15%左右建议直接用新版。依赖库方面torch、torchvision、numpy、opencv-python、pillow这几个就够了不需要装什么花哨的东西。数据集这块有条件的直接用RESIDE数据集这是目前去雾领域用得最多的公开数据集里面有合成雾图和对应的真实清晰图。如果只需要验证流程用OTB或者自己合成雾图也行。我最早调试代码时懒得下全量数据集就用了个简单方案挑一些清晰的风景图不一定和雾图配对按大气散射模型自己加雾公式就是 I J * t A * (1 - t)其中t用随机深度映射生成、A取一个接近白色的随机值。这样做出来的“伪训练对”虽然不够真实但用来调试网络是否收敛已经足够了。等代码完全跑通再换上RESIDE或掺入真实雾图做正式训练。数据集目录建议按下面的结构组织data/ trainA/ # 有雾图像 trainB/ # 无雾/清晰图像 testA/ # 测试用有雾图像 testB/ # 测试用无雾图像用于计算PSNR/SSIM3.2 生成器网络结构U-Net与残差块的组合生成器我采用的是CycleGAN论文里的经典结构输入输出都是3通道图像核心由三部分组成一个卷积下采样编码器、9个残差块组成的转换模块、一个反卷积上采样解码器。在图像翻译任务里这种编码-转换-解码结构比单纯的U-Net更适合因为它通过下采样把特征图缩小到较小的分辨率让残差块能在更大的感受野上捕捉全局色调和结构信息然后再通过上采样恢复原始分辨率。我用它处理256×256的输入实测效果比较稳。如果追求细节还原可以考虑替换成U-Net结构的生成器但训练开销会大不少。下面这个是我改造后的生成器代码在原始ResnetBlock基础上加了bias初始化和反射填充import torch import torch.nn as nn class ResidualBlock(nn.Module): def __init__(self, in_channels): super(ResidualBlock, self).__init__() self.conv1 nn.Conv2d(in_channels, in_channels, kernel_size3, padding1, biasFalse) self.in1 nn.InstanceNorm2d(in_channels, affineTrue) self.conv2 nn.Conv2d(in_channels, in_channels, kernel_size3, padding1, biasFalse) self.in2 nn.InstanceNorm2d(in_channels, affineTrue) self.relu nn.ReLU(inplaceTrue) def forward(self, x): identity x out self.relu(self.in1(self.conv1(x))) out self.in2(self.conv2(out)) return out identity class Generator(nn.Module): def __init__(self, in_channels3, out_channels3, n_residual9): super(Generator, self).__init__() # 下采样编码器 self.encoder nn.Sequential( nn.Conv2d(3, 64, kernel_size7, stride1, padding3, padding_modereflect, biasFalse), nn.InstanceNorm2d(64, affineTrue), nn.ReLU(inplaceTrue), nn.Conv2d(64, 128, kernel_size3, stride2, padding1, biasFalse), nn.InstanceNorm2d(128, affineTrue), nn.ReLU(inplaceTrue), nn.Conv2d(128, 256, kernel_size3, stride2, padding1, biasFalse), nn.InstanceNorm2d(256, affineTrue), nn.ReLU(inplaceTrue) ) # 转换模块 res_blocks [ResidualBlock(256) for _ in range(n_residual)] self.transformer nn.Sequential(*res_blocks) # 上采样解码器 self.decoder nn.Sequential( nn.ConvTranspose2d(256, 128, kernel_size3, stride2, padding1, output_padding1, biasFalse), nn.InstanceNorm2d(128, affineTrue), nn.ReLU(inplaceTrue), nn.ConvTranspose2d(128, 64, kernel_size3, stride2, padding1, output_padding1, biasFalse), nn.InstanceNorm2d(64, affineTrue), nn.ReLU(inplaceTrue), nn.Conv2d(64, 3, kernel_size7, stride1, padding3, padding_modereflect), nn.Tanh() ) def forward(self, x): x self.encoder(x) x self.transformer(x) x self.decoder(x) return x这里有两个容易被忽略的细节。第一所有卷积层都设置了biasFalse因为后面跟着InstanceNormbias在归一化时会被抵消留着只会浪费参数还容易干扰训练。第二生成器最后一层输出过Tanh激活把像素值映射到[-1, 1]这要求输入图像也必须归一化到[-1, 1]代码里记得保持一致。我在调试时就是因为输入图像还停留在[0, 1]区间导致训练初期输出一片死黑排查了半天。3.3 判别器网络PatchGAN判断局部真伪判别器我选了70×70 PatchGAN结构。这个名字听起来高级其实就是个全卷积网络输出不是单个真/假概率而是一个N×N的特征图每个元素对应输入图像上某个局部区域的真伪判断。这样做的好处是既能对图像的局部纹理和细节做约束又不用把整张图的全局信息都建模进去参数量比普通CNN判别器小得多。对于去雾任务局部细节恰恰是最重要的因为雾霾影响的主要是局部对比度和颜色PatchGAN正好对症。class Discriminator(nn.Module): def __init__(self, in_channels3): super(Discriminator, self).__init__() self.model nn.Sequential( nn.Conv2d(in_channels, 64, kernel_size4, stride2, padding1), nn.LeakyReLU(0.2, inplaceTrue), nn.Conv2d(64, 128, kernel_size4, stride2, padding1, biasFalse), nn.InstanceNorm2d(128, affineTrue), nn.LeakyReLU(0.2, inplaceTrue), nn.Conv2d(128, 256, kernel_size4, stride2, padding1, biasFalse), nn.InstanceNorm2d(256, affineTrue), nn.LeakyReLU(0.2, inplaceTrue), nn.Conv2d(256, 512, kernel_size4, stride1, padding1, biasFalse), nn.InstanceNorm2d(512, affineTrue), nn.LeakyReLU(0.2, inplaceTrue), nn.Conv2d(512, 1, kernel_size4, stride1, padding1) ) def forward(self, x): return self.model(x)输入是256×256的图像经过4次下采样其中三次stride2、一次stride1输出是16×16的特征图。每个点对应原图约70×70的感受野所以叫70×70 PatchGAN。训练时真实图像的标签是形状为16×16的全1矩阵生成图像的标签是同样形状的全0矩阵。用LSGAN损失时判别器输出不经过Sigmoid而是直接回归到0或1这个细节在写损失函数时要用对。3.4 数据加载模块非配对数据如何喂给网络对偶GAN最大的优势就是不需要严格配对的训练数据。数据加载时我把有雾图和无雾图分别从trainA和trainB两个目录读取按batch随机配对。也就是说这一个batch里出现的雾图与清晰图在内容上没有任何对应关系。为了让网络更好训练我做了三个预处理缩放到286×286后随机裁剪到256×256这个做法比直接缩放能带来更多数据多样性随机水平翻转像素值归一化到[-1, 1]。在PyTorch里Dataset类和DataLoader配合使用自定义Dataset时要注意__getitem__返回的是图像张量和它的文件名。文件名后面在测试时要用可以用来保存对应名称的去雾结果。from torch.utils.data import Dataset from PIL import Image import torchvision.transforms as T import os class UnpairDataset(Dataset): def __init__(self, path_a, path_b, size256): self.files_a sorted([os.path.join(path_a, f) for f in os.listdir(path_a)]) self.files_b sorted([os.path.join(path_b, f) for f in os.listdir(path_b)]) self.transform T.Compose([ T.Resize((286, 286), Image.BICUBIC), T.RandomCrop((size, size)), T.RandomHorizontalFlip(), T.ToTensor(), T.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5)) ]) def __len__(self): return max(len(self.files_a), len(self.files_b)) def __getitem__(self, idx): img_a Image.open(self.files_a[idx % len(self.files_a)]).convert(RGB) img_b Image.open(self.files_b[idx % len(self.files_b)]).convert(RGB) return self.transform(img_a), self.transform(img_b)3.5 训练主循环优化器、学习率、网络更新顺序训练环节是整个系统里最容易出问题的部分GAN训练不收敛、生成器塌陷、判别器太强导致生成器梯度消失这些坑我全踩过。结合项目经验我把训练流程和关键参数整理一下。我用的优化器是Adam生成器和判别器各维护一份。初始学习率设置为0.0002betas(0.5, 0.999)。注意beta1不能用默认的0.9GAN训练里0.5是标配默认值会导致训练振荡。总训练轮次设200个epoch前100个epoch保持学习率不变后100个epoch线性衰减到0。学习率衰减的实现很简单每个epoch结束时乘以(1 - current_epoch / total_epochs)对应的系数或者直接每个epoch更新lr initial_lr * (1 - epoch / total_epochs)。网络更新顺序很关键先更新判别器再更新生成器。每个batch里判别器要分别接收真实图像和生成器输出的假图像计算两部分损失再反向传播。生成器的损失是三个部分的组合。# 训练循环关键代码片段节选 for epoch in range(epochs): for i, (real_a, real_b) in enumerate(train_loader): real_a, real_b real_a.to(device), real_b.to(device) # 1. 更新判别器 fake_b gen_a(real_a) fake_a gen_b(real_b) # 判别真实图像 pred_real_b disc_b(real_b) loss_d_real_b torch.mean((pred_real_b - 1.0) ** 2) # 判别生成图像 pred_fake_b disc_b(fake_b.detach()) loss_d_fake_b torch.mean(pred_fake_b ** 2) # 同理计算A域的判别器损失 pred_real_a disc_a(real_a) loss_d_real_a torch.mean((pred_real_a - 1.0) ** 2) pred_fake_a disc_a(fake_a.detach()) loss_d_fake_a torch.mean(pred_fake_a ** 2) loss_d (loss_d_real_b loss_d_fake_b loss_d_real_a loss_d_fake_a) / 2 optimizer_d.zero_grad() loss_d.backward() optimizer_d.step() # 2. 更新生成器 fake_b gen_a(real_a) fake_a gen_b(real_b) # GAN对抗损失 pred_fake_b disc_b(fake_b) loss_gan_b torch.mean((pred_fake_b - 1.0) ** 2) pred_fake_a disc_a(fake_a) loss_gan_a torch.mean((pred_fake_a - 1.0) ** 2) # 循环一致性损失 rec_a gen_b(fake_b) rec_b gen_a(fake_a) loss_cycle_a torch.mean(torch.abs(rec_a - real_a)) loss_cycle_b torch.mean(torch.abs(rec_b - real_b)) # 身份映射损失 idt_a gen_b(real_a) idt_b gen_a(real_b) loss_idt_a torch.mean(torch.abs(idt_a - real_a)) loss_idt_b torch.mean(torch.abs(idt_b - real_b)) loss_g (loss_gan_a loss_gan_b) 10.0 * (loss_cycle_a loss_cycle_b) 5.0 * (loss_idt_a loss_idt_b) optimizer_g.zero_grad() loss_g.backward() optimizer_g.step()一个小技巧是生成器前向过程在同一个batch里执行了两次更新判别器时一次、更新生成器时一次如果直接复用第一次的输出会因为在更新判别器过程中调用了.detach()导致梯度链断裂。稳妥做法是更新完判别器后重新跑一次生成器前向计算像我上面的代码一样。虽然白白多了一次前向推理但显存和算力都在可接受范围内代码逻辑更清晰不容易出错。4. 训练过程中的常见问题与排查技巧4.1 判别器Loss下降太快甚至直接归零怎么处理这是我跑GAN训练遇到最多的经典问题训练没几十个iteration判别器的loss已经掉到接近0生成器的loss却死活下不去生成的图像要么全是噪点要么一片模糊。根本原因是判别器太强生成器产生的假图它一认一个准导致生成器收到的梯度基本都是饱和区的梯度学不到有效信息。处理办法有三个思路。第一降低判别器的学习率让它和生成器的学习率拉开差距比如生成器保持0.0002判别器降到0.00005。第二在判别器输入侧加随机噪声或者标签平滑真实标签不用1而是用0.9到1之间的随机数对生成样本的标签也不要用严格0而是用0到0.1之间的随机值能有效防止判别器过度自信。第三最直接的办法是改判别器的结构减少层数或者通道数降低它的容量。实际项目中我采用“降低判别器学习率 标签平滑”的组合拳基本可以稳住训练节奏。4.2 生成图像颜色偏灰、对比度不足像蒙了一层白纱这个现象说明生成器虽然把雾的结构形态去掉了但在颜色映射上仍然保守没有真正恢复出鲜艳、高对比的无雾图像。我排查后认为主要原因是循环一致性损失的权重设置太高导致生成器不敢在颜色上做大改动因为任何大的改动都会增大重建误差。但去雾这个任务本身输入和输出在颜色上就应该有明显变化太高的循环一致性权重反而压抑了生成器的表现力。解决办法是调参。我在实验里把循环一致性损失的权重从默认的10降到了5同时把身份映射损失从5降到2生成的图像在色彩饱和度上立刻有了提升。当然这个参数不是固定的需要根据数据集的雾浓度特点来调整。如果雾很浓循环一致性权重可以稍大一些保证结构稳定。4.3 训练结果出现明显的棋盘格子伪影棋盘伪影是反卷积结构的老毛病原因是反卷积操作在某些位置重叠导致输出出现不均匀的亮度模式。在生成器的上采样阶段如果用nn.ConvTranspose2d这种伪影出现的概率很高。我在代码中采用了两个替代方案一是用像素重排技术也就是先把通道数扩到原来的4倍再通过view操作重组成高分辨率图像二是直接用最近邻或双线性插值上采样再接普通卷积。第二种方法实现最简单我推荐先用它代码如下# 用上采样卷积替换转置卷积 self.up nn.Sequential( nn.Upsample(scale_factor2, modebilinear, align_cornersTrue), nn.Conv2d(256, 128, kernel_size3, stride1, padding1, biasFalse), nn.InstanceNorm2d(128, affineTrue), nn.ReLU(inplaceTrue) )4.4 训练到后期loss一直震荡生成结果时好时坏GAN训练到后期loss震荡是常态但震荡幅度过大就需要警惕了。我遇到过的情况是训练到120个epoch时生成器的输出开始出现周期性波动有时无雾效果很好有时又退化成原图几乎没变。排查后发现这是因为学习率衰减策略不对在后期学习率已经衰减到很小的情况下判别器的更新频率又不平衡导致两者状态不同步。解决方法是调整网络更新策略每个batch训练判别器只更新一次训练生成器更新两次让生成器有更多机会在判别器相对固定的情况下优化自己。另外生成器使用了两个不同步长的优化器schedule分别在100和150个epoch衰减一次让后期的调优更平滑。这两种方式配合下来训练曲线虽然还有小幅波动但生成结果稳定多了。5. 实验效果分析与后续扩展方向5.1 定量与定性效果评估训练完成后我在测试集上用两个指标做了定量评估PSNR峰值信噪比和SSIM结构相似性指数。PSNR衡量像素级的重建误差数值越高越好SSIM衡量结构相似度更贴近人类视觉感知数值越接近1越好。在合成雾图测试集上模型最终达到的PSNR约24.8dBSSIM约0.89。作为参考不处理的原图PSNR大概在17dB左右传统暗通道先验去雾后的PSNR通常在20dB上下。也就是说对偶GAN方法在像素级精度上明显优于传统方法同时在结构保持上也有不错表现。主观效果上模型处理后的图像雾霾明显消除远近景物的层次感和对比度都有显著提升色彩恢复也自然没有出现偏蓝偏灰的问题。我把处理结果和CycleGAN原版做对比发现加入了身份映射损失后对本身已经清晰的区域保持得更好不会出现明显的颜色漂移。这一点在真实雾图上尤其重要因为真实雾图的浓雾区域和轻微薄雾区域分布高度不均一个过拟合的去雾网络很容易把薄雾区域也强行“处理”成怪异色调。5.2 现有局限与可扩展方向这个系统目前还有几个明显局限。第一对于浓雾场景生成结果虽然能去雾但远处物体的纹理细节恢复有限因为雾霾其实已经把高频信息物理性地散射掉了模型只能靠先验去“猜”猜出来的细节和真实场景还是有距离。第二在训练数据里如果包含大量天空区域生成结果偶尔会出现轻微色带说明模型对均匀区域的平滑性约束还不够。第三模型参数量较大生成器在1080Ti上处理一张256×256图像耗时约35ms如果部署到嵌入式设备还需要做剪枝和量化。扩展方向上我把目标锁定在三个方面。一是引入感知损失即用预训练的VGG16提取特征在特征空间计算L1距离这样可以显著提升生成图像的感知质量。二是把透射率估计作为辅助任务让生成器同时输出透射率图这样既能增强可解释性也能在训练时加入物理约束。三是探索vision transformer作为生成器骨干我在小规模实验中已经看到一些潜力但训练稳定性还需要进一步打磨。6. 总结几点实战经验这个项目从零到最终能跑出满意效果前前后后花了一个多月其中大部分时间不是在写代码而是在调超参数、排查训练问题。最后分享几条实战经验给想在这条路上少走弯路的读者。关于归一化层的选择生成器里我全程使用InstanceNorm而不是BatchNorm因为InstanceNorm对单张图像的对比度和亮度分布做归一化不受batch内其他样本影响在图像翻译任务里基本是标配。BatchNorm在小batch size下容易引入噪声导致生成图像出现伪影。关于batch size不要盲目追求大数值。对偶GAN里同时跑两个生成器和两个判别器显存开销是普通GAN的四倍。我在3090上也只能把batch size设为1到2之间再大就会爆显存。好在InstanceNorm不受batch size影响batch1也可以稳定训练。关于训练时机不要在一开始就追求完美数据集。先用少量数据、短训练轮次把代码流程跑通确认loss在下降、生成的图像有变化再上全量数据集长训。这样排查问题的效率会高很多不然一上来就跑十几个小时发现loss异常或图像全黑心态容易崩。关于调参顺序我建议先固定网络结构调损失权重再固定损失权重调学习率和优化器参数最后才去动网络结构。很多初学者一上来就改结构导致多个变量同时变化根本无法定位问题根源。关于判断模型是否收敛不要只盯着一张测试图的输出看要保存整个验证集的去雾结果按epoch编号归档这样能直观看到每个阶段的变化趋势。我习惯每个epoch保存一次生成器权重和4张固定测试图的结果训练到尾声翻看这些历史记录能快速判断是处于过拟合还是欠拟合状态。如果让我重新做一次这个项目我会在数据准备阶段花更多精力收集真实的雾天图像而不是过度依赖合成雾图。因为合成雾图基于物理模型生成和真实雾气的散射特性还是有差异这种域差距是后续所有优化的天花板。真实雾图才让这个系统有实际落地的可能合成雾图永远只是预训练手段。本文还有配套的精品资源点击获取