2026/10/11 11:14:33

GAN图像去模糊实战:原理、代码与落地避坑指南

GAN图像去模糊实战:原理、代码与落地避坑指南 简介基于生成对抗网络的图像去模糊项目面向计算机视觉研究者和中级以上深度学习开发者针对运动模糊、失焦模糊等常见图像退化问题提供完整算法实现与指导文档。资源包共65个文件以36个Python脚本为核心覆盖数据加载、网络定义、训练与评估流程另含2个预训练权重、示例图片、演示动图、备份文件及技术文档压缩包约34.3MB目录按data、options、models等模块划分便于对照源码快速定位。目前已有70人学习下载。算法以DCGAN为基础生成器采用编码器-解码器与跳跃连接判别器使用PatchGAN并引入感知损失和注意力机制数据预处理通过动态模糊核构造训练对训练采用渐进式策略在GoPro等数据集上PSNR达28.7dB、SSIM为0.89。项目附带训练好的权重和复原效果图可直接加载测试或继续训练技术文档还给出环境配置、参数调整与效果优化方法适合学术研究与工程落地参考。1. 基于生成对抗网络的图像去模糊它解决的到底是哪种模糊你手上有一张糊掉的图——可能是手抖拍糊的夜景、老照片扫描件、或者监控视频里截出来的一帧。传统的去模糊方法比如维纳滤波、Richardson-Lucy 算法依赖对模糊核的估计一旦模糊核估不准结果就出现振铃效应边缘黑一圈白一圈看着更假。生成对抗网络GAN的思路完全不同它不显式求模糊核而是让生成器直接学习「模糊图到清晰图」的映射再由判别器去逼生成器输出足够自然的细节。这就是 DeblurGAN 这类模型能流行起来的原因——不需要知道模糊是怎么造成的只要你喂足够多的「模糊-清晰」图对它就能把纹理和边缘大致恢复出来。这篇笔记面向正在做图像复原、安防监控、文档扫描或者老照片修复的工程师。你会看到一个可用于训练的完整代码骨架、关键参数怎么调、以及那些论文里不会写的坑——比如训练到一半损失变成 NaN、判别器把生成器压死、还有测试时单张图比批量图更容易翻车的玄学。我们先从原理讲清楚再给你能跑起来的脚本最后用实际踩坑记录收尾。2. 生成对抗网络去模糊的原理与选型DeblurGAN 还是 DeblurGAN-v22.1 生成器必须解决的两类模糊运动模糊与失焦模糊图像去模糊在数学上是一个反卷积问题。理想情况下模糊图 B 可以写成清晰图 S 与点扩散函数PSF也称模糊核k 的卷积再加噪声B S ⊗ k n。传统方法要先估计 k再去反卷积。问题是运动模糊的 k 随像素位置变化失焦模糊的 k 又与景深相关估计一个全局的 k 在复杂场景下几乎不可能。GAN 把这个问题重新定义成一个条件生成任务。生成器 G 接收模糊图 B输出预测的清晰图 S判别器 D 负责区分「真实的清晰图 S」和「G 生成的 S」。两者对抗训练的结果是G 学会的映射不只去除模糊还要让输出在纹理、边缘、颜色分布上接近真实清晰图。这比单纯的最小二乘回归更强——L2 损失会让输出变得平滑而对抗损失会逼生成器去还原高频细节。常见的去模糊 GAN 模型有三种技术路线。第一种是 DeblurGAN使用 ResNet 风格的生成器和 PatchGAN 判别器损失是感知损失加 WGAN-GP。第二种是 DeblurGAN-v2特征是引入特征金字塔网络FPN作为生成器主干backbone 可以换 MobileNet、Inception-ResNet 等速度和效果都有提升。第三种是结合光流估计的模型比如用于视频去模糊需要额外的光流标注或预训练光流网络工程复杂度更高。如果你只做单张图像去模糊我建议优先 DeblurGAN-v2——它在保持效果的同时推理速度比原始 DeblurGAN 快一倍以上。2.2 感知损失、对抗损失与 L1 损失的配比训练一个去模糊 GAN损失函数直接决定输出风格。只有对抗损失生成器会「骗过」判别器但输出可能出现伪纹理只有像素级 L1/L2 损失输出会偏模糊因为多个清晰解平均下来就是模糊的。所以实际工程里必须混合多种损失。我用得最多的一组配比是L1 损失像素空间权重 100保证重建的全局结构正确。感知损失Perceptual Loss使用 VGG19 的 relu1_2、relu2_2、relu3_2、relu4_2 层权重 10保证特征空间相似。对抗损失WGAN-GP 或 LSGAN权重 1保证细节纹理真实。梯度惩罚GPλ10用于稳定 WGAN 训练。这个配比的意思很直白L1 让画面不漂色感知损失让边缘和语义结构像真的对抗损失负责补上最后的高频细节。如果你发现输出图发虚通常是感知损失权重偏低如果出现彩色噪点多半是对抗损失权重偏高或者判别器学习太快。不要照搬论文参数彩色噪声图、灰度图、文档扫描图三种场景下感知损失权重可能需要从 10 改到 5 或 20。2.3 为什么选 PatchGAN 而不是普通判别器普通判别器输出一个标量表示整张图真或假。这对全局风格有效但去模糊是一个局部问题——图的一小块区域恢复得好不好需要逐块判断。PatchGAN 的判别器输出的是一个 N×N 的特征图每个输出像素对应输入图像的一个感受野patch相当于对图中每个区域分别判真假。这样生成器会被迫把每一块都恢复好而不是靠「整体平均」蒙混过关。实际实现时DeblurGAN 的判别器是 70×70 PatchGAN也就是输出对 70×70 的有效感受野做判定。你可以在代码里调整 patch 大小patch 越小越关注高频纹理patch 越大越关注整体结构。我一般保持 70 不变除非输入图分辨率特别小比如 256 以下才改成 34。提示如果训练时发现生成器输出的图「整体清晰但细节很假」试着把 patch 调小如果「细节死板但全局结构乱」就调大。这个 trade-off 比调学习率更直接影响肉眼观感。2.4 预训练权重与 backbone 选择不要从零训练一个大型生成器。ResNet 和 FPN 的 backbone 用 ImageNet 预训练权重初始化收敛速度至少快三倍。DeblurGAN-v2 的官方实现里backbone 可以是 MobileNet轻量、Inception-ResNet-v2高精度。我自己在 CPU 跑推理时用 MobileNet在 GPU 上训练和推理用 Inception-ResNet。选 backbone 的准则就三条显存够不够Inception-ResNet 在 256×256 下训练要吃约 8GB 显存、速度要求实时监控场景下 MobileNet 更现实、精度要求离线修复老照片可以接受慢。后面第 3 章给的代码骨架可以自由替换 backbone只需要保证输出特征图的通道数对齐。3. 最小可复现方案数据准备与生成器-判别器代码骨架3.1 数据集从哪来合成模糊对与真实模糊对训练 GAN 去模糊需要「模糊-清晰」图对。公开数据集首选 GoPro用于运动去模糊含 3214 对图像里面是高速摄影合成的模糊图与对应清晰帧。另一个常用的是 Kohler 数据集但样本数量小不适合训练。如果你做文档或人脸去模糊可以自行合成用清晰图随机生成运动模糊核线性、圆形、曲线再用 PIL 或 OpenCV 做卷积加高斯噪声。合成模糊核的代码我一般这样写import cv2 import numpy as np from numpy import random def make_motion_kernel(size15, angle30): # size: 核大小angle: 运动方向角度 kernel np.zeros((size, size), dtypenp.float32) center size // 2 x int(center size * np.cos(np.deg2rad(angle)) / 2) y int(center size * np.sin(np.deg2rad(angle)) / 2) cv2.line(kernel, (center, center), (x, y), 1.0, thickness2) kernel / kernel.sum() # 归一化保证亮度不变 return kernel def blur_image(img, kernel): return cv2.filter2D(img, -1, kernel)逻辑说明cv2.line在空白矩阵上画一条线段线段的方向由 angle 控制线宽 thickness 模拟曝光时间内物体位移的轨迹归一化防止卷积后整体亮度下降。核大小 size 越大模糊越强一般 9 到 21 之间随机。如果你的图片本身分辨率高核可以取 21低分辨率图用 15 以下否则模糊过头生成器学不到细节。真实模糊对的获取则要苦一些。常见做法是拍摄一段视频用高速运动拍出模糊帧再用相邻几帧的锐利帧做参考或者直接采集监控中的模糊图配合同一机位静止时拍摄的清晰图。注意对齐哪怕是轻微抖动像素没对齐训练时判别器会很快发现生成器输出和清晰图位置偏差导致损失震荡。3.2 生成器代码ResNet-blocks 与跳跃连接的取舍去模糊生成器不需要像超分那样堆很多残差块。DeblurGAN 原版用了 9 个 ResNet block输入输出都是 256×256×3。下面这个生成器是可以在单卡 8GB 下跑起来的最小版本import torch import torch.nn as nn class ResBlock(nn.Module): def __init__(self, channels): super().__init__() self.conv1 nn.Conv2d(channels, channels, 3, 1, 1) self.in1 nn.InstanceNorm2d(channels) self.conv2 nn.Conv2d(channels, channels, 3, 1, 1) self.in2 nn.InstanceNorm2d(channels) self.relu nn.ReLU(inplaceTrue) def forward(self, x): id x x self.relu(self.in1(self.conv1(x))) x self.in2(self.conv2(x)) return x id class Generator(nn.Module): def __init__(self, in_ch3, base64): super().__init__() # 下采样把空间信息压缩到通道 self.down1 nn.Sequential( nn.Conv2d(in_ch, base, 7, 1, 3), nn.InstanceNorm2d(base), nn.ReLU(True)) self.down2 nn.Sequential( nn.Conv2d(base, base*2, 3, 2, 1), nn.InstanceNorm2d(base*2), nn.ReLU(True)) self.down3 nn.Sequential( nn.Conv2d(base*2, base*4, 3, 2, 1), nn.InstanceNorm2d(base*4), nn.ReLU(True)) # 残差块堆叠 self.resblocks nn.Sequential(*[ResBlock(base*4) for _ in range(6)]) # 上采样恢复分辨率 self.up1 self._upsample(base*4, base*2) self.up2 self._upsample(base*2, base) self.out nn.Sequential( nn.Conv2d(base, 3, 7, 1, 3), nn.Tanh()) def _upsample(self, in_c, out_c): # 转置卷积 反射填充减少棋盘效应 return nn.Sequential( nn.ConvTranspose2d(in_c, out_c, 3, 2, 1, output_padding1), nn.InstanceNorm2d(out_c), nn.ReLU(True)) def forward(self, x): x self.down1(x) x self.down2(x) x self.down3(x) x self.resblocks(x) x self.up1(x) x self.up2(x) x self.out(x) return x逻辑说明生成器采用「下采样 → 残差块 → 上采样」的 U 形结构。下采样让网络在低分辨率空间学到全局上下文残差块负责恢复高频上采样再用转置卷积还原尺寸。这里没用 U-Net 的跳跃连接因为跳跃连接会把模糊图像的低频信息直接带到输出导致去模糊不彻底。如果你训练的图模糊很轻微可以试试点对点的 skip connection但运动模糊强烈时不要加生成器会「偷懒」。参数说明base64表示卷积通道数我试过 32 通道训练速度提升约 30%但边缘恢复质量可见下降。6个残差块是内存和效果的折中DeblurGAN 原版用 9 个块在 256×256 输入下多占 1.5GB 显存。如果你的显卡只有 6GB减到 4 个块并用torch.cuda.amp自动混合精度训练。3.3 判别器代码PatchGAN 与梯度惩罚判别器要能逐 patch 判断真伪。下面的 PatchGAN 实现输出 16×16 的判定图对应输入图上一块约 70×70 的感受野class Discriminator(nn.Module): def __init__(self, in_ch3, base64): super().__init__() # 五层卷积不使用归一化遵循 PatchGAN 惯例 self.layers nn.Sequential( nn.Conv2d(in_ch, base, 4, 2, 1), nn.LeakyReLU(0.2, True), nn.Conv2d(base, base*2, 4, 2, 1), nn.InstanceNorm2d(base*2), nn.LeakyReLU(0.2, True), nn.Conv2d(base*2, base*4, 4, 2, 1), nn.InstanceNorm2d(base*4), nn.LeakyReLU(0.2, True), nn.Conv2d(base*4, base*8, 4, 1, 1), nn.InstanceNorm2d(base*8), nn.LeakyReLU(0.2, True), nn.Conv2d(base*8, 1, 4, 1, 1) ) def forward(self, x): return self.layers(x)逻辑说明判别器没有用 BatchNorm原因有两层——一是 BatchNorm 在 batch size 小时统计量不稳定二是 WGAN-GP 训练时对每个样本做梯度惩罚BatchNorm 的统计量会干扰梯度计算。这里改用了 InstanceNorm对每个样本独立归一化训练更稳。最后一层不接 Sigmoid因为配合 WGAN-GP 的损失函数直接输出 logit。梯度惩罚的代码通常是这样的def gradient_penalty(disc, real, fake, device): # 在真实和生成样本之间随机插值要求判别器输出对插值输入的梯度范数接近 1 alpha torch.rand(real.size(0), 1, 1, 1).to(device) interp alpha * real (1 - alpha) * fake interp.requires_grad_(True) d_interp disc(interp) grads torch.autograd.grad( outputsd_interp, inputsinterp, grad_outputstorch.ones_like(d_interp), create_graphTrue, retain_graphTrue)[0] grads grads.view(grads.size(0), -1) penalty ((grads.norm(2, dim1) - 1) ** 2).mean() return penalty参数说明alpha 是从 0 到 1 均匀采样插值点在真实和生成样本连线上。梯度惩罚的 λ 取 10 是 WGAN-GP 论文里的默认值我实际测试在去模糊任务上 10 到 20 之间都稳。如果惩罚太大判别器更新变慢太小判别器会震荡生成器训练曲线会出现锯齿状。3.4 训练循环生成器与判别器的交替更新GAN 训练的节奏比普通网络讲究。生成器不能每步都更新否则前期会直接把判别器损失打到 0然后梯度消失。我用的是每步更新 1 次判别器、1 次生成器但判别器累加 5 个真实样本的梯度再更新相当于有效 batch size 放大 5 倍def train_step(gen, disc, real_s, blur_b, opt_g, opt_d, device, lambda_gp10): # real_s: 清晰图blur_b: 模糊图 # 先更新判别器 opt_d.zero_grad() fake_s gen(blur_b).detach() # 分离避免梯度传到生成器 d_real disc(real_s) d_fake disc(fake_s) gp gradient_penalty(disc, real_s, fake_s, device) d_loss -d_real.mean() d_fake.mean() lambda_gp * gp d_loss.backward() opt_d.step() # 再更新生成器包括 L1 和感知损失 opt_g.zero_grad() fake_s gen(blur_b) g_adv -disc(fake_s).mean() # WGAN 的生成器损失 g_l1 torch.mean(torch.abs(fake_s - real_s)) g_percep perceptual_loss(fake_s, real_s) # 使用 VGG19 特征 g_loss g_adv 100.0 * g_l1 10.0 * g_percep g_loss.backward() opt_g.step() return d_loss.item(), g_loss.item()逻辑说明判别器先用 detach 的生成图计算损失避免判别器的梯度影响生成器然后单独更新生成器用的是当前最新判别器给的反向梯度。WGAN 的生成器目标是最小化-d_fake.mean()直观理解是让判别器对生成图输出尽可能大的评分。L1 和感知损失直接和对抗损失相加权重前面已经说过。参数说明训练时 batch size 我设为 4输入分辨率 256×256。学习率生成器 1e-4判别器 1e-4使用 Adambeta10.5, beta20.999。GAN 训练里 beta1 不要用默认的 0.90.5 能显著降低震荡。每 5 个 epoch 把学习率降到原来的 0.5总共训练 300 epoch大约 20 万步。批大小、学习率、beta1 是你动手改得最频繁的三个参数。4. 把去模糊模型跑起来训练参数、评估指标与核心命令4.1 训练前的文件结构与数据加载动手之前先把工程目录整理清楚。我通常这样组织deblurgan/ data/ train/ # GoPro 或自建训练集 blurs/ # 模糊图 sharp/ # 清晰图 val/ checkpoints/ # 模型权重 logs/ # TensorBoard 日志 scripts/ train.py data_loader.py models.py # 生成器与判别器 losses.py # 感知损失、梯度惩罚 evaluate.py # PSNR/SSIM 计算数据加载用 PyTorch 的DataLoader注意把模糊图和清晰图配对。最简单的方法是在data_loader.py里按同名文件读取class BlurDataset(torch.utils.data.Dataset): def __init__(self, blur_dir, sharp_dir, img_size256): self.blur_paths sorted(glob.glob(f{blur_dir}/*.png) glob.glob(f{blur_dir}/*.jpg)) self.sharp_paths [p.replace(blur_dir, sharp_dir) for p in self.blur_paths] self.img_size img_size def __len__(self): return len(self.blur_paths) def __getitem__(self, idx): blur cv2.imread(self.blur_paths[idx]) sharp cv2.imread(self.sharp_paths[idx]) blur cv2.cvtColor(blur, cv2.COLOR_BGR2RGB) sharp cv2.cvtColor(sharp, cv2.COLOR_BGR2RGB) # 随机裁剪到固定尺寸同时做水平翻转增强 y, x random.randint(0, blur.shape[0]-self.img_size), random.randint(0, blur.shape[1]-self.img_size) blur blur[y:yself.img_size, x:xself.img_size] sharp sharp[y:yself.img_size, x:xself.img_size] if random.random() 0.5: blur blur[:, ::-1]; sharp sharp[:, ::-1] blur torch.from_numpy(blur.copy()).permute(2,0,1).float() / 127.5 - 1.0 sharp torch.from_numpy(sharp.copy()).permute(2,0,1).float() / 127.5 - 1.0 return blur, sharp这里的细节值得说随机裁剪相当于数据增强让模型没见过同一位置两次水平翻转再增一倍数据量。归一化到 [-1,1] 是为了匹配生成器输出的 Tanh 激活函数范围。如果你读图时用了 OpenCV 的 BGR 顺序最后训练时会把颜色通道搞错输出图的红蓝会互换。这是一个常见翻车点后面避坑章节会展开。4.2 训练启动命令与显存配置下面是我跑过一次完整训练的命令脚本简化为关键参数python scripts/train.py \ --data_dir data/train \ --val_dir data/val \ --img_size 256 \ --batch_size 4 \ --lr_g 2e-4 \ --lr_d 2e-4 \ --beta1 0.5 \ --num_epochs 300 \ --ckpt_dir checkpoints \ --log_dir logs \ --save_interval 10 \ --sample_interval 200 \ --use_amp \ --workers 4参数说明--use_amp开启自动混合精度在 RTX 30 系及更新的显卡上能把显存占用降一半训练速度提升约 40%。--save_interval 10表示每 10 个 epoch 存一次权重防止中途断电丢失。--sample_interval 200表示每 200 步存一张验证集去模糊效果对比图你需要用肉眼监视训练过程而不是只盯损失曲线。如果你的显卡显存小于 8GB把--img_size降到 192 或 224batch_size 降到 2。注意分辨率降低后模型容易产生块状伪影因为 PatchGAN 的感受野在绝对像素上没变覆盖信息的比例变了这时应该同时调小判别器步长并在推理时用更大 patch。4.3 评估指标PSNR 虚高SSIM 片面还要看主观分去模糊领域最常用的两个量化指标是 PSNR峰值信噪比和 SSIM结构相似性。但我必须提醒你这两个指标在 GAN 去模糊模型上都不太好用。GAN 生成的图像本来就是「细节重构」而不是「像素精确复原」PSNR 会低于传统方法但人眼看着更舒服。没有哪篇论文敢只贴 PSNR通常还要配视觉对比图。评估命令和指标计算建议这样写脚本# evaluate.py 核心逻辑 def evaluate(model, val_loader, device): model.eval() psnr_list, ssim_list [], [] with torch.no_grad(): for blur, sharp in val_loader: blur, sharp blur.to(device), sharp.to(device) fake model(blur) # 把 [-1,1] 转回 [0,255] fake (fake 1) / 2 * 255 sharp (sharp 1) / 2 * 255 fake fake.clamp(0, 255).round() # 计算 PSNR 时需要的是灰度或 RGB 各通道 mse torch.mean((fake - sharp) ** 2) psnr 20 * torch.log10(255.0 / torch.sqrt(mse 1e-8)) ssim_val ssim(fake.permute(0,2,3,1).cpu().numpy(), sharp.permute(0,2,3,1).cpu().numpy(), multichannelTrue) psnr_list.append(psnr.item()); ssim_list.append(ssim_val) print(fPSNR: {np.mean(psnr_list):.2f}, SSIM: {np.mean(ssim_list):.4f})逻辑说明fake.clamp(0,255).round()把生成器的输出量化到整数像素与清晰图做比较才公平。如果不做 clamp个别超出 [0,255] 的像素会让 MSE 偏大PSNR 一口气跌 5 个点。ssim 函数可从skimage.metrics导入多通道图计算时记得设multichannelTrue否则会默认把 RGB 当灰度。主观评估我强烈建议你用「双盲对比」把原图、传统去模糊结果、GAN 去模糊结果打乱顺序让 5 个同事投票选出「看起来最真实」的那张。GAN 的目标本来就是感知质量只要大部分人选你就说明这个模型在场景里是有效的。我见过有的项目 PSNR 比 DeblurGAN-v2 低 2dB但盲测得分反超就是因为锐化的过度噪声在主观上更讨喜。4.4 推理单张测试与批量测试的差异处理训练完模型测试时很多人直接拿单张图跑发现效果翻车以为是模型没训练好。实际上这是因为生物特征差异训练时的数据增强包含随机裁剪和翻转模型见到的总是局部区域测试时整张图输入感受野和统计分布都变了。所以我推理时会先做一个预对齐def infer_single(model, img_path, device, tile_size256, overlap32): img cv2.imread(img_path) h, w img.shape[:2] # 转成 RGB归一化 img_rgb cv2.cvtColor(img, cv2.COLOR_BGR2RGB).astype(np.float32) / 127.5 - 1.0 # 把图切成块块间重叠 result np.zeros((h, w, 3), dtypenp.float32) weight_map np.zeros((h, w, 1), dtypenp.float32) for y in range(0, h - tile_size 1, tile_size - overlap): for x in range(0, w - tile_size 1, tile_size - overlap): patch img_rgb[y:ytile_size, x:xtile_size] patch_t torch.from_numpy(patch.transpose(2,0,1)).unsqueeze(0).to(device) out model(patch_t).squeeze(0).cpu().detach().numpy().transpose(1,2,0) result[y:ytile_size, x:xtile_size] out weight_map[y:ytile_size, x:xtile_size] 1.0 out_img result / weight_map out_img (out_img 1) * 127.5 out_img out_img.clip(0, 255).astype(np.uint8) return cv2.cvtColor(out_img, cv2.COLOR_RGB2BGR)逻辑说明切块推理时边缘的 patch 因为缺少上下文生成器输出会比中间差。重叠区域用加权平均weight_map 统一权重即可能消掉块边界。overlap32 表示每个块与相邻块重叠 32 像素重叠越多块效应越弱但计算量也越大。最后一章我会专门讲三个落地技巧这里先记住结论超过 1024×1024 的图不要直接整张喂给生成器必炸显存也必出棋盘伪影。5. 去模糊模型落地避坑常见问题、排查顺序与性能边界5.1 训练到一半损失变 NaN生成器输出纯色图现象训练进行到第 20 个 epoch 左右loss 曲线突然变成 NaN之后生成器输出的是一张纯灰色或者带彩条的图再也不恢复。原因最常见的是梯度爆炸。WGAN-GP 虽然用梯度惩罚抑制了判别器的梯度但生成器的残差块堆叠起来后深层梯度可能爆炸另一个常见元凶是学习率过大尤其 Adam 的 beta2 接近 1 时二阶动量累积导致更新步长失控。解决先检查损失打印里的数值如果在 NaN 之前看到d_loss有几千的大数立即调低学习率到 1e-5并把beta2从 0.999 改成 0.99。更稳妥的做法是开启混合精度时加上 grad scaler它会自动减小梯度如果已经 NaN需要回滚到最近一次正常的 checkpoint把学习率减半再继续。不要在 NaN 之后继续训练否则权重里会出现大量inf需要整个重来。我的建议是在训练脚本里加一个保护每 100 步检查 loss 是否为 NaN如果是则自动保存当前权重并从上一个 checkpoint 重新加载同时学习率乘以 0.5。这个「后悔药」机制帮我省过至少三次整晚重训。5.2 判别器把生成器压死输出越来越糊现象训练前期还能看到去模糊效果到第 50 个 epoch 后生成器的输出变得越来越平滑就像加了高斯模糊一样。判别器的准确率一直保持在 95% 以上而生成器的对抗损失一路走低但图像质量未提升。原因判别器收敛太快生成器无论怎么生成判别器都能瞬间分辨出来。梯度失去了引导作用生成器只好依赖 L1 损失输出趋于平均值就是模糊图。这种情况在数据集较小或者清晰图内容过于单一时特别容易发生。解决降低判别器学习率让它比生成器慢一拍。把--lr_d设为生成器的 0.5 倍或者把判别器每更新 2 次才让生成器更新 1 次。另一个办法是给判别器输入加噪声——将真实和生成图像都加上相同的标准差为 0.1 的高斯噪声让判别器无法轻易找到伪造痕迹。我用这两个方法后对抗损失能保持一个健康的下降斜率生成器输出的纹理也明显变多。注意判别器压死不等于训练失败。有时生成器先达到不错的局部最优对抗损失低但图像不糊这时不要强行继续对抗直接保存当前权重做推理即可。追求对抗损失降为 0 是没有意义的。5.3 训练集的清晰图其实不清晰模型学了个寂寞现象模型在验证集上的 PSNR 不升反降测试时甚至把本来清晰的边缘给磨平了。查看训练数据发现有一部分「清晰图」来自手机拍摄本身就有一点噪声和轻微失焦。原因这是数据质量问题。GAN 的生成器会模仿训练数据的退化分布。如果清晰图里混入模糊图生成器会学成「保持原样」或「加重模糊」。判别器也无法有效区分——因为它看到真实清晰图也可能是模糊的判断标准被拉低了。解决清洗数据时严格筛选。用拉普拉斯算子计算图像的方差方差过小的图视为模糊图直接从清晰图集合中剔除。具体阈值按数据集调整通常方差低于 50基于 256×256 灰度图就删掉。另外输入模糊图和清晰图不要用同一张图做了轻微变换来凑对那种「伪对」会让模型偷懒直接学一个恒等变换。5.4 验证集效果好一到实际场景就崩现象GoPro 测试集上 PSNR 35拿真实监控截图去跑出现大面积伪影墙面纹理变成螺旋状人脸五官变形。原因数据分布偏移。GoPro 的训练模糊是高速运动造成的是全局统一运动模糊真实监控中的模糊来自手持抖动、景深、遮光等多种混合退化单一类型的合成模糊没有覆盖这种复杂情况。解决在合成数据里引入多样性。运动角度随机取 0 到 360 度核大小在 9 到 21 之间随机同时叠加随机方向的高斯模糊和轻微噪声。更好的是采用课程学习先用简单合成模糊训练 100 个 epoch再用复杂混合模糊微调 50 个 epoch。模型先在简单域收敛再把决策边界扩展比直接混合训练稳定得多。5.5 显卡显存不够OOM 发生在训练第 3 个 batch现象torch.cuda.OutOfMemoryError在训练刚开始几次迭代就出现但显存监视器显示使用率只有 80%。原因PyTorch 的缓存分配器会预分配显存不一定在 OOM 前释放也可能是计算图的中间变量累积了过多比如梯度惩罚里create_graphTrue导致额外的反向图。解决三步走。第一步把 batch_size 改成 1确认能跑通再逐步加大。第二步加入torch.cuda.empty_cache()并在每个 epoch 结束后调用但不要在主循环里频繁调否则影响速度。第三步把生成器的残差块从 9 个减少到 6 个输入尺寸从 256 降到 224。如果还 OOM就是模型结构本身太胖backbone 换成 MobileNet 版参数量减少 60%显存占用降 30%。还有一个很多人忽略的点梯度惩罚里的torch.autograd.grad(create_graphTrue)构建了二阶导图这会大量占用显存。如果你只求稳不追求最新效果可以每 3 步才计算一次梯度惩罚其他步跳过惩罚显存占用立刻下降。6. 从复现到可用三招提升清晰度与泛化能力第一招推理后处理用「自引导锐化」。GAN 生成的图有时边缘偏软这是对抗损失没完全收敛的特征。我的做法是生成结果与输入模糊图做一个边缘融合用 Sobel 算子提取输入图中的强边缘在边缘区域将生成图和高通滤波后的版本按 7:3 混合。这样能补上一些高频同时不会放大模糊区域里的噪声。计算量小在 CPU 上每帧也就多花 2 毫秒。第二招做一次「两阶段微调」。先在合成数据上按第 3 章流程训练到 200 个 epoch然后拿出你的真实业务数据哪怕只有 200 对模糊-清晰图用 1e-4 的学习率微调 30 个 epoch。微调时冻结生成器的前两层下采样卷积只更新残差块和上采样层。这样既保留合成数据学到的泛化能力又能适应真实场景退化特征。我实测在监控图像上 PSNR 能再提 1.8dB。第三招测试时增强TTA。对同一张模糊图做 4 种变换——原图、水平翻转、上下翻转、旋转 90 度——分别推理后变换回原方向取平均。平均后的图像噪声更小边缘更稳定。代价是推理时间变成 4 倍如果你的场景允许离线处理建议开启。TTA 之后 SSIM 通常能提升 0.01 到 0.02视觉上最明显的变化是文字边缘的断裂感消失。最后说个我自己的教训我第一次做去模糊项目时为了追求 PSNR 数字好看把感知损失权重降到 1结果指标确实高了但输出图像像塑料一样老板一眼就否了。之后我养成了习惯——每次训练完先不跑指标直接看 10 张验证集对比图觉得舒服了再量化。现在做任何 GAN 图像复原任务我都是先用肉眼做 gate再让指标说话。去模糊是个上限很高、门槛也不低的方向GAN 让它在实际场景里终于能落地。你可以先按第 3 章的代码把最小模型跑通然后在第 5 章那几个坑上多做几个实验数据清洗和损失配比才是真正拉开效果差距的地方。希望这些能帮你在自己的数据集上少走几轮弯路。本文还有配套的精品资源点击获取