简介:SRGAN 是首个能将自然图像放大 4 倍并生成照片级逼真纹理的生成对抗网络框架,面向从事图像超分辨率、深度学习与图像处理的开发者与研究者。它通过对抗损失与内容损失构成的感知损失函数,解决了传统均方误差优化导致高频细节缺失、感知保真度不足的问题,适合需要复现或改进超分模型的中高级学习者。资源包共 1198 个文件,约 156MB,以 TensorBoard 事件日志为主,另含 Python 源码、XML 配置、JPG 样例图、PTH 模型权重及 PYC 编译文件,覆盖训练记录、网络定义与推理脚本等环节。目前已有 2544 人学习下载。借助完整的训练日志与权重文件,读者可复盘 SRGAN 的对抗训练过程、观察损失收敛曲线,并基于现有代码快速开展 4 倍超分实验与感知质量对比。
1. 从一张糊图说起:SRGAN 到底能帮你把图救回多少
手里有一批低分辨率素材,放大后边缘发虚、纹理糊成一团,传统双三次插值怎么调都像蒙了层雾——这是很多做图像处理的人都会遇到的场景。SRGAN 就是冲着这个痛点来的:它把生成对抗网络引入单图像超分辨率任务,不再只优化像素误差,而是用一个判别器去逼生成器产出更接近真实高分辨率图像的纹理和边缘。和 SRCNN、ESRGAN 这类同赛道方案比,SRGAN 的特点是结构清晰、论文复现成本低,适合想搞懂 GAN 超分原理、又需要一套能跑通的训练与推理流程的人。它不承诺把马赛克变清晰,但在 4 倍放大、人脸和自然纹理场景下,视觉观感通常比纯插值方案更耐看。下面按「原理选型 → 数据准备 → 训练推理 → 避坑 → 进阶验证」的顺序拆一遍。
2. SRGAN 的结构账:生成器、判别器与损失函数怎么配
2.1 生成器为什么用残差块堆,而不是直接上卷积
SRGAN 的生成器主体是一串残差块(Residual Block),每个块里是卷积、批归一化、PReLU 的固定组合,最后接亚像素卷积(PixelShuffle)做上采样。选残差结构的原因很直接:超分任务输入和输出高度相关,网络只需要学「高频残差」,不用从零重建整张图,训练更稳、收敛更快。常见做法是堆 16 个残差块,每个块通道数 64,这个配置在 4 倍放大下显存占用和效果比较平衡。
亚像素卷积是另一个关键点。它先通过卷积把通道数扩到通道数 × 放大倍数²,再按空间重排成高分辨率特征图。相比反卷积,它没有重叠伪影,计算也更规整。下面是一段生成器核心结构的参考实现:
import torch import torch.nn as nn class ResidualBlock(nn.Module): def __init__(self, channels=64): super().__init__() # 两个 3x3 卷积,中间夹 BN 和 PReLU,这是 SRGAN 论文的标准块 self.conv1 = nn.Conv2d(channels, channels, 3, padding=1) self.bn1 = nn.BatchNorm2d(channels) self.prelu = nn.PReLU() self.conv2 = nn.Conv2d(channels, channels, 3, padding=1) self.bn2 = nn.BatchNorm2d(channels) def forward(self, x): residual = x out = self.prelu(self.bn1(self.conv1(x))) out = self.bn2(self.conv2(out)) return out + residual # 残差直连,保证低频信息不丢 class Generator(nn.Module): def __init__(self, scale=4, num_blocks=16): super().__init__() self.head = nn.Conv2d(3, 64, 9, padding=4) self.blocks = nn.Sequential(*[ResidualBlock(64) for _ in range(num_blocks)]) self.mid = nn.Conv2d(64, 64, 3, padding=1) # 每个 PixelShuffle 放大 2 倍,scale=4 时叠两个 upsample = [] for _ in range(scale // 2): upsample += [nn.Conv2d(64, 256, 3, padding=1), nn.PixelShuffle(2), nn.PReLU()] self.upsample = nn.Sequential(*upsample) self.tail = nn.Conv2d(64, 3, 9, padding=4) def forward(self, x): feat = self.head(x) out = self.mid(self.blocks(feat)) out = out + feat # 全局残差,把低频直接送到输出前 out = self.upsample(out) return self.tail(out)逻辑上分三段:头部大卷积提特征,中部残差块学高频,尾部上采样加重建。参数上num_blocks控制容量,显存吃紧就降到 8;scale只支持 2 的幂次组合,写 3 会在scale // 2处丢一次上采样,这是新手常翻车的地方。
2.2 判别器与感知损失:为什么不能只看 PSNR
判别器是一个二分类网络,输入是高分辨率图,输出是真假概率。它逐层卷积、LeakyReLU、最后全连接加 Sigmoid,结构比生成器简单得多。训练时生成器和判别器交替更新:先固定生成器训判别器分辨真假,再固定判别器训生成器骗过它。
真正让 SRGAN 区别于普通超分的是损失函数。它由内容损失和对抗损失加权组成,内容损失又常用 VGG 感知损失替代纯 MSE。感知损失比较的是生成图和高清图在 VGG 特征空间的距离,而不是逐像素差,所以纹理更自然。代价是 PSNR 可能反而比 MSE 方案低——这是选型时必须接受的取舍:要指标还是要观感。
class Discriminator(nn.Module): def __init__(self): super().__init__() # 输入 96x96 的 HR patch,逐层下采样到 6x6 layers = [] in_ch = 3 for out_ch in [64, 64, 128, 128, 256, 256, 512, 512]: layers += [nn.Conv2d(in_ch, out_ch, 3, stride=1 if out_ch == 64 else 2, padding=1), nn.BatchNorm2d(out_ch), nn.LeakyReLU(0.2, inplace=True)] in_ch = out_ch self.features = nn.Sequential(*layers) self.classifier = nn.Sequential( nn.AdaptiveAvgPool2d(1), nn.Conv2d(512, 1024, 1), nn.LeakyReLU(0.2, inplace=True), nn.Conv2d(1024, 1, 1)) def forward(self, x): return self.classifier(self.features(x)).view(-1)判别器输出用view(-1)拉平,配合BCEWithLogitsLoss使用,不要自己再加 Sigmoid,否则数值不稳。这是我在多个 GAN 项目里反复确认过的细节。
3. 数据与训练流程:从 DIV2K 到可复现的 4 倍放大
3.1 数据准备:HR 切块与 LR 降采样必须成对
SRGAN 训练需要成对的 HR/LR 图像。常见做法是用 DIV2K 或自建高清图库,先随机裁 HR patch(比如 96×96),再用双三次插值降采样得到对应 LR patch(24×24,对应 4 倍)。关键点是降采样核要和推理时一致,否则训练和测试分布对不上,效果会明显打折。
import cv2 import numpy as np import random def make_pairs(hr_path, patch=96, scale=4): hr = cv2.imread(hr_path) hr = cv2.cvtColor(hr, cv2.COLOR_BGR2RGB) h, w, _ = hr.shape if h < patch or w < patch: return None # 随机裁剪,增加样本多样性 top = random.randint(0, h - patch) left = random.randint(0, w - patch) hr_patch = hr[top:top + patch, left:left + patch] # 双三次降采样,核必须与推理一致 lr_patch = cv2.resize(hr_patch, (patch // scale, patch // scale), interpolation=cv2.INTER_CUBIC) return lr_patch, hr_patch参数说明:patch决定显存占用,96 是 8G 显存下的稳妥值;scale要和生成器配置一致。返回的 LR 是 24×24,HR 是 96×96,喂给网络前统一转 float 并归一化到 [0,1] 或 [-1,1],全流程保持一致即可。
3.2 训练循环:两个优化器、两套梯度
训练时生成器和判别器各有一个优化器,交替更新。下面是一个最小可跑的循环骨架:
import torch from torch import nn, optim device = torch.device("cuda" if torch.cuda.is_available() else "cpu") G = Generator(scale=4).to(device) D = Discriminator().to(device) opt_G = optim.Adam(G.parameters(), lr=1e-4, betas=(0.9, 0.999)) opt_D = optim.Adam(D.parameters(), lr=1e-4, betas=(0.9, 0.999)) bce = nn.BCEWithLogitsLoss() mse = nn.MSELoss() for lr_img, hr_img in dataloader: lr_img, hr_img = lr_img.to(device), hr_img.to(device) # ---- 更新判别器 ---- opt_D.zero_grad() fake = G(lr_img).detach() # detach 防止梯度回传到 G real_logit = D(hr_img) fake_logit = D(fake) loss_D = bce(real_logit, torch.ones_like(real_logit)) + \ bce(fake_logit, torch.zeros_like(fake_logit)) loss_D.backward() opt_D.step() # ---- 更新生成器 ---- opt_G.zero_grad() fake = G(lr_img) fake_logit = D(fake) # 内容损失用 MSE 起步,稳定后可换 VGG 感知损失 loss_content = mse(fake, hr_img) loss_adv = bce(fake_logit, torch.ones_like(fake_logit)) loss_G = loss_content + 1e-3 * loss_adv # 对抗项权重先给小 loss_G.backward() opt_G.step()逻辑说明:判别器更新时对生成图detach(),避免梯度污染生成器;生成器损失里对抗项权重1e-3是常见起点,太大容易训练发散、出现棋盘伪影。参数上lr=1e-4对两个网络都适用,若判别器 loss 迅速掉到 0,说明它太强,要降低判别器学习率或提高对抗项权重。
3.3 推理与保存:别在训练图上验证
推理时把 LR 图送进生成器即可,注意输入尺寸要能被放大倍数整除,否则 PixelShuffle 会报维度错误。保存模型建议同时存state_dict和优化器状态,方便断点续训。
G.eval() with torch.no_grad(): sr = G(lr_tensor.to(device)).clamp(0, 1) torch.save({"G": G.state_dict(), "opt_G": opt_G.state_dict()}, "srgan_ckpt.pth")验证一定要用训练时没见过的图,否则你看到的清晰度是过拟合的假象。我一般会留 10% 数据做验证集,每 5 个 epoch 跑一次,肉眼对比再决定要不要继续训。
4. 避坑与排查:SRGAN 训练里最容易翻车的五件事
4.1 生成图出现规则网格伪影
现象:放大后图像上浮着一层规律的方格纹。原因:PixelShuffle 前的卷积核尺寸和通道数不匹配,或上采样层叠放顺序有误。解决:确认每个上采样单元是「卷积到通道×4→ PixelShuffle(2) → 激活」,卷积核用 3×3,别用 1×1。
4.2 判别器 loss 迅速归零
现象:训练几个 batch 后判别器 loss 接近 0,生成器输出越来越糊。原因:判别器太强或学习率过高,生成器梯度消失。解决:把判别器学习率降到生成器的 1/2,或给判别器加 dropout、降低更新频率(每两步更新一次)。
4.3 PSNR 比双三次插值还低
现象:算指标发现 SRGAN 的 PSNR 不如插值。原因:感知损失和对抗损失优化的是观感,不是逐像素误差,指标低是正常现象。解决:别用 PSNR 当唯一标准,补一个 LPIPS 或人工主观评分;如果业务硬性要求 PSNR,就退回 MSE 主导的损失配置。
4.4 显存溢出(OOM)
现象:训练中途报 CUDA out of memory。原因:patch 太大、残差块太多或 batch size 过高。解决:把 patch 从 96 降到 64,num_blocks从 16 降到 8,batch size 降到 4,并开启torch.cuda.amp混合精度。
4.5 推理结果偏色或过曝
现象:生成图整体发灰或高光溢出。原因:训练时归一化范围是 [-1,1],推理却按 [0,1] 处理,或忘了clamp。解决:统一归一化约定,推理输出后clamp(0,1)再转 uint8,别直接乘 255。
5. 进阶验证:用 LPIPS 和切片对比判断 SRGAN 是否真的可用
训完模型别急着下结论,先做两件事:一是换指标,二是做切片对比。PSNR/SSIM 对 GAN 类超分不友好,建议加 LPIPS(感知距离)和一组人工盲评。LPIPS 越低表示感知越接近真实,通常 SRGAN 在这一项上明显优于双三次插值。
import lpips import torch loss_fn = lpips.LPIPS(net="vgg").to(device) # 输入需归一化到 [-1,1] score = loss_fn(sr_tensor * 2 - 1, hr_tensor * 2 - 1) print("LPIPS:", score.mean().item())参数说明:net="vgg"是常用后端,换成alex结果会略有差异,同一批对比要固定后端。切片对比则是把同一区域(比如人脸眼睛、建筑边缘)从 LR、双三次、SRGAN 三种结果里裁出来并排看,重点看纹理是否自然、有没有伪影。我一般会挑 5 到 10 个典型 patch,肉眼过一遍再决定模型能不能上。
还有一个容易被忽略的验证点:跨域泛化。用 DIV2K 训出来的模型直接跑手机截图或监控画面,效果往往掉一截,因为降采样核和噪声分布不同。常见做法是拿目标域的低清图做少量微调,或者训练时加入随机模糊和噪声增强。这一步不做,上线后大概率被真实数据打脸。
从那以后我每次训完 SRGAN,都强制走一遍「LPIPS + 切片盲评 + 跨域抽测」三件套,不再只看 loss 曲线。希望帮到你。
本文还有配套的精品资源,点击获取