简介:SRResCycGAN是ECCVW AIM2020真实图像超分辨率挑战赛赛道3的官方PyTorch实现。项目针对真实场景中低分辨率图像退化过程不符合双三次降采样假设的问题,借用CycleGAN思路构建深度循环生成对抗网络,保持LR与HR域间分布一致性,实现x4放大系数的超分重建。代码包共28个文件,以Python脚本、Markdown文档和结果对比图片为主:py文件覆盖模型定义、训练与推理流程,png提供定量与定性对比图,README给出使用说明,Dockerfile可快速搭建GPU/CPU环境,压缩包整体大小45.21MB,目录分层清楚,其中还包含预训练权重目录及测试样例,可直接复现论文效果。已有510人学习。借助此代码仓库,研究者和开发者可以拿到论文配套的SRResCycGAN模型实现、模块化代码与演示样例,便于复现赛题方案,也可直接修改用于真实图像超分项目或进一步对比实验。 先把话说在前面:真实图像超分辨率(Real-World Super-Resolution)这个方向,大多数从合成数据上训练出来的模型,一拿到真实照片上基本就露馅。边缘发虚、纹理糊成一团、伪影比放大前还明显——这些问题我在做实际项目时一个都没少踩。这次要展开聊的SRResCycGAN,是ECCVW AIM2020上针对真实图像超高分辨率任务提出的一个开源方案,思路很直接:既然真实低分辨率图像的退化过程没法用简单的双三次下采样建模,那就用CycleGAN那套循环一致性思路,让网络在无配对数据下自己学双向映射,配合深度残差卷积网络把细节重建出来。整个项目以代码回购的形式公开,适合正在做盲超分、真实图像增强、生成对抗网络应用落地的同学参考。
这篇文章我不打算把论文复述一遍,而是从工程复现的角度,把SRResCycGAN的模型设计、代码结构、训练细节和我在复现过程中踩过的坑一次讲清楚。
1. 真实图像超分为什么难:合成数据那套玩法在真实场景中失灵了
1.1 传统超分模型的“理想假设”和现实之间的矛盾
传统监督超分模型的训练套路是:拿高分辨率图像 I_HR 做双三次下采样,得到低分辨率图像 I_LR,然后训练网络学习 I_LR → I_HR 的映射,用L1或L2损失约束输出。这套做法在评测集上PSNR很高,因为训练和测试的退化方式完全一致,模型等于在“背题”。
但真实场景中,一张照片从传感器到最终保存到手机里,经历的退化过程大致可以写成:
I_LR = (I_HR ⊗ k) ↓_s + n
其中 k 是模糊核(镜头散焦、运动模糊、大气扰动等),↓_s 是下采样,n 是传感器噪声和压缩伪影。问题在于:真实应用里 k 和 n 都是未知的,而且每张图都可能不一样。你拿双三次下采样训练出来的模型去处理一张真的带噪声、带模糊的夜景照片,输入分布就完全偏离了训练分布,网络看到的东西“不认识了”,输出自然就奇怪。
这就是盲超分(Blind Super-Resolution)要解决的核心问题:在退化模型未知的前提下,怎么从一张低分辨率输入里重建出高分辨率图像。
1.2 AIM2020真实超分赛道给SRResCycGAN提出了什么要求
ECCVW AIM2020(Advances in Image Manipulation Workshop)在2020年设置了真实世界超分辨率挑战赛,赛道使用的数据是真实拍摄得到的图像对:用不同焦距或不同相机拍同一场景,获得真实对应的LR-HR图像对。但即便有真实图像对,直接训练也有问题——LR和HR之间的对应关系仍然受到拍摄视角、景深、对焦差异的影响,像素级对齐并不完美,模型很难学到稳定的映射。
SRResCycGAN的思路是绕过“严格配对”的限制:既然LR和HR之间的映射不确定,那就让两个方向的生成器自己去学。模型名拆开来看:SR(超分辨率)+ Res(残差卷积网络)+ Cyc(循环一致性)+ GAN(生成对抗网络)。它把CycleGAN的双向循环结构搬到了超分任务上:一个生成器把LR变成HR,另一个生成器把HR退回LR空间,两者互相约束,不需要训练数据严格配对也能学。
1.3 SRResCycGAN整体解题思路
整个模型包含两个生成器 G_LR→HR 和 G_HR→LR,以及两个判别器 D_HR 和 D_LR。训练时,输入一张真实LR图 x,G_LR→HR 生成超分结果 G(x),判别器 D_HR 判断它和真实HR图是否同分布;同时 G_HR→LR 把 G(x) 退回低分辨率空间,要求能还原回 x,这就是循环一致性约束。反过来,对真实HR图 y 也做一遍对称操作。
这个设计的妙处在于:即使训练数据里没有像素级对齐的LR-HR对,只要LR集合和HR集合各自是独立真实采集的,循环一致性就能让网络学会“放大后还能缩回去”的稳定映射,从而避免生成器随意编造高频细节。
2. SRResCycGAN的网络结构:残差生成器 + PatchGAN判别器 + 双向循环
2.1 生成器:深度残差卷积网络是主体
SRResCycGAN的生成器主体遵循SRResNet的设计范式:卷积层提取特征,中间堆叠若干残差块(Residual Block),最后用像素重排(PixelShuffle)完成上采样。我在复现时实际搭建的结构是:
- 入口一个 3×3 卷积,把输入从RGB 3通道映射到64通道特征空间;
- 中间堆叠16个残差块,每个残差块由两个 3×3 卷积和ReLU激活组成,卷积通道数64,内部使用跳跃连接做恒等映射;
- 根据放大倍数,末尾接若干个 PixelShuffle 上采样模块,每个模块把特征图通道数减少为原来的1/4,同时空间分辨率翻倍,实现2倍上采样;
- 出口一个 3×3 卷积,把64通道映射回3通道RGB,输出超分结果。
这里“残差卷积网络”体现在两个层面:残差块内部有恒等映射,网络整体结构本身就是深度残差卷积架构,这也是SRResCycGAN能堆到足够深而不梯度消失的原因。上采样时用PixelShuffle而不是转置卷积,是因为转置卷积容易产生棋盘格伪影,PixelShuffle把特征重排成高分辨率图,感受野更连续,输出更干净。
2.2 判别器:PatchGAN的局部判别逻辑
判别器采用PatchGAN结构,这也是CycleGAN系列的标配。普通判别器输出一个概率标量,判断整张图真/假;PatchGAN输出一个 N×N 的矩阵,每一个值代表原图对应局部区域(感受野内)的真/假概率。
举个例子:输入一张256×256的图像,经过几层下采样卷积后,输出可能是16×16×1的特征图,每个点感受野大约是70×70像素。这等价于把原图切成16×16个小块,逐块判断真假。用PatchGAN的好处是:它逼着生成器把局部纹理做得足够真实,而不是只追求整体分布接近——这对超分任务来说非常关键,因为超分判优的恰恰是局部细节。
2.3 循环一致性损失:SRResCycGAN的“定海神针”
循环一致性是SRResCycGAN和普通SRGAN最本质的区别。SRGAN只需要生成器骗过判别器,生成结果在感知上接近真实HR即可,不需要能变回原LR;SRResCycGAN强制要求:
L_cyc = E_{x~P_LR} [ || G_HR→LR( G_LR→HR(x) ) - x ||1 ] + E{y~P_HR} [ || G_LR→HR( G_HR→LR(y) ) - y ||_1 ]
第一项的意思是:真实LR图经过超分再降质回来,应该还是原来那张图。这个约束让 G_LR→HR 不敢随意“放飞自我”——如果放大的结果是编造出来的,那它不可能被另一个生成器完美还原回原图。L1范数比L2更适合做这个约束,因为L1对异常值不敏感,得到的重建边缘更锐利。
标题里“深度循环”的“循环”指的就是这个双向映射约束,它让两个生成器成为彼此的监督信号。在无配对数据条件下,这是比监督学习更可行的约束方式。
2.4 总损失函数:对抗、循环、感知三管齐下
SRResCycGAN的完整训练目标可以写成:
L_total = L_GAN + λ_cyc * L_cyc + λ_per * L_per
其中 L_GAN 是对抗损失,采用LSGAN形式(用最小二乘损失替代二分类交叉熵),我在复现时用的权重参考如下:
| 损失项 | 权重 | 说明 |
|---|---|---|
| 对抗损失 L_GAN | 1.0 | 让超分结果在分布上接近真实HR |
| 循环一致性 L_cyc | 10.0 | 保证LR→HR→LR可还原 |
| 感知损失 L_per | 0.1 ~ 1.0 | 用VGG16中间层特征约束感知相似度 |
感知损失(Perceptual Loss)是可选的,它提取真实HR和超分结果在VGG网络中间层的特征图,计算特征层面而不是像素层面的距离。加上它之后,重建出的纹理在人类视觉上更自然,但训练时显存开销会明显增加。如果是第一次跑这个项目,我建议先不加感知损失,把主流程跑通再逐步加。
3. 从代码回购看工程实现:仓库结构和数据流梳理
3.1 仓库目录:典型PyTorch训练项目的布局
SRResCycGAN的代码仓库结构比较清晰,是典型的PyTorch训练项目布局:
SRResCycGAN/ ├── data/ │ ├── dataset.py # 数据集加载与预处理 │ └── augment.py # 数据增强 ├── models/ │ ├── generator.py # 双向生成器定义 │ ├── discriminator.py # 双向判别器定义 │ └── losses.py # 循环一致性损失、对抗损失、感知损失 ├── train.py # 训练入口 ├── test.py # 推理入口 ├── options.py # 命令行参数配置 └── checkpoints/ # 模型权重保存这种按模块拆分的结构对二次开发很友好。如果想换生成器结构,只改 generator.py;想调整损失函数配置,只动 losses.py;数据预处理相关全在 dataset.py。很多开源超分项目的代码把训练逻辑、模型定义和数据处理全堆在一个文件里,改起来特别痛苦,这个项目的拆法算得上讲究。
3.2 数据加载环节:无配对真实图像的读取方式
dataset.py 的核心逻辑是同时维护两个独立的数据集列表:一个是真实LR图像文件路径列表,另一个是真实HR图像文件路径列表。训练时每个iteration分别从两个列表中随机抽一个批次,LR和HR不需要有对应关系。
这里有一个细节值得注意:真实HR图像的分辨率各不相同,而训练时需要统一尺寸。常用的做法是随机裁剪固定大小的图像块(比如256×256),先把所有HR图像缩放到2倍目标尺寸再裁剪。比如要训练4倍超分,生成器输入是64×64的LR块,目标输出是256×256——那就从HR图上随机裁剪256×256区域,然后通过高斯模糊加下采样合成对应的低分辨率输入。
等等,不是说不用双三次下采样吗?这里需要区分清楚:合成低分辨率输入只是作为网络输入的形式,并不是作为监督训练的退化模型。训练时真正约束 G_LR→HR 输出质量的,是判别器(判断是否属于真实HR分布)和循环一致性损失(超分后能退回原LR)。合成下采样只负责给生成器一个合理尺寸的输入,模型并不依赖像素级对应关系来做监督。
3.3 数据增强:平移裁剪就能大大提升稳定性
augment.py 里的数据增强包括随机水平翻转、随机旋转90度倍数、随机裁剪。这些几何增强对GAN训练稳定性帮助很大,因为PatchGAN判别器对局部纹理分布极其敏感,如果训练数据里同类纹理总是出现在同一位置,判别器就容易过拟合到像素位置信息,导致训练后期G和D互相打架。
我在复现时还额外加了一个小技巧:对LR和HR图像同步做RandomResizedCrop。原因是很多真实照片的画面主体不在正中心,固定中心裁剪会让模型看到的内容过于单一,随机缩放裁剪相当于引入了尺度多样性,能让生成器更鲁棒。此外我没有做颜色抖动类的增强,因为超分任务是颜色保持任务,颜色偏移会被判别器识别为伪影,干扰训练。
3.4 训练主循环:两个生成器交替更新
train.py 里的训练循环是所有代码里最值得细读的部分。单个iteration的大致逻辑如下:
# 生成器更新 fake_hr = gen_lr_to_hr(lr_real) fake_lr = gen_hr_to_lr(hr_real) # 循环重建 rec_lr = gen_hr_to_lr(fake_hr) rec_hr = gen_lr_to_hr(fake_lr) # 判别器对生成结果的判断 d_fake_hr = dis_hr(fake_hr) d_fake_lr = dis_lr(fake_lr) # 感知损失(可选) per_loss = vgg_loss(fake_hr, hr_real) # 总损失 g_loss = lsgan_loss_g(d_fake_hr) + lsgan_loss_g(d_fake_lr) \ + cycle_weight * (l1_loss(rec_lr, lr_real) + l1_loss(rec_hr, hr_real)) \ + per_weight * per_loss # 判别器更新 d_hr_loss = lsgan_loss_d(dis_hr(hr_real), dis_hr(fake_hr.detach())) d_lr_loss = lsgan_loss_d(dis_lr(lr_real), dis_lr(fake_lr.detach())) d_loss = (d_hr_loss + d_lr_loss) * 0.5值得注意的两个细节:
第一,更新判别器时,fake图像必须用 detach() 把梯度截断,否则梯度会回流到生成器,导致更新方向混乱。这是GAN训练的入门坑,但在真实项目里经常看到有人因为这个问题训练不收敛。
第二,生产器和判别器的更新频率最好保持1:1,但判别器学习率设为生成器的一半。原因是PatchGAN判别器比生成器更容易过拟合,把D的学习率压低一些,可以让G有时间追上D的判别能力,而不是被D牵着鼻子走。
4. 训练过程中的坑:损失震荡、模式崩塌和过拟合
4.1 对抗损失震荡:LSGAN也不能完全免疫
即使是LSGAN,训练过程中仍然会看到生成器损失和判别器损失来回拉锯,这是正常现象。真正要警惕的是梯度爆炸——如果你观察到损失值突然跳到正常值的几十倍,然后训练彻底崩掉,大概率是判别器过强,生成的假图被判得太惨,梯度回流时爆炸了。
我的经验做法是:
- 给对抗损失一个梯度裁剪,clip值设置为 max_norm=1.0,保证回传梯度幅度可控;
- 限制判别器更新幅度,在判别器优化器上设置 grad_clip;
- 如果连续多个epoch判别器准确率接近100%,临时降低判别器学习率到十分之一,让它“喘口气”。
这些操作不会在论文里写,但对训练稳定性的帮助远远超过调一两个损失权重。SRResCycGAN本质上是一个两生成器两判别器的四网络对抗训练系统,稳定性的优先级高于收敛速度。
4.2 循环一致性损失的权重大小怎么定
cycle loss weight 设为10,这是CycleGAN原始论文的标准配置。为什么是10而不是1?因为循环一致性损失的数值范围通常远小于对抗损失:L1 loss在图像值域0-1上,初始阶段大约在0.2-0.5之间,而LSGAN loss的数值可以到2-4。如果不把cycle loss权重提上去,对抗损失会完全支配梯度方向,生成器的输出只追求“像HR”,但完全没有约束它是否还保留原始LR的内容结构。
我在调参时试过把cycle weight降到1.0,结果生成器在训练中期开始“改图”——把输入图像的结构都改了,只为了让判别器觉得好看。把权重拉回10之后,结构保持才恢复正常。循环一致性损失就是那个把生成器“摁住”的约束,权重太低等于没有。
4.3 过拟合的迹象:训练集上重建效果好,测试集上一塌糊涂
真实超分任务里过拟合的表现非常隐蔽:训练集上的循环重建损失持续下降,对抗损失也正常,但把模型放到一张没见过的照片上,输出纹理怪异、颜色发灰。这是因为生成器把训练集LR图的某些退化特征(比如特定相机的噪声模式、特定压缩伪影)当作规律记住了,遇到新的退化分布就崩。
我验证过两个有效对策:
- 加大随机退化增强:对LR输入随机叠加高斯噪声、高斯模糊(模糊核大小随机),相当于扩大退化空间的覆盖范围,让生成器见过更多退化类型;
- 提前停止训练:不需要等完整epoch数跑完,用验证集上FID或LPIPS指标监控,一旦指标连续5个epoch不再下降就保存当前权重退出。
其中第二点特别关键。在GAN类的超分模型里,训练时间越长不代表效果越好,很多情况下最优权重出现在训练中段,后期反而因为生成器和判别器过度拟合而退化。
4.4 评价指标怎么选:PSNR在真实超分上并不可靠
传统超分评价用PSNR和SSIM,这两个指标在真实超分场景下并不完全适用。原因很简单:真实图像对没有像素级对齐,你算PSNR时比对的是“模型超分结果”和“真实HR图”的像素差异,但两者本来就没有严格对应关系,PSNR值低不代表模型做得差。
我在项目评估时主要看三个指标:
| 指标 | 类型 | 适用场景 |
|---|---|---|
| PSNR / SSIM | 全参考 | 只能作为参考,用于确认保真度没有明显下降 |
| LPIPS | 全参考 | 衡量感知相似度,值越低意味着人眼看起来越接近真实HR |
| NIQE / FID | 无参考 | 在完全无GT时测图像自然度,值越低越好 |
最终判断模型好坏,最快的办法还是直接看放大图对比纹理细节和边缘锐利度,AI画质再好也替代不了人眼验收。
5. 如果要复现SRResCycGAN,我的建议和坑位提醒
5.1 硬件与训练成本:一张24G显存显卡是基线
SRResCycGAN双生成器双判别器的结构,加上16个残差块,参数量并不小。我实测过:
- 输入64×64 LR图,输出256×256 HR图,batch size设为4时,显存占用约16G;
- 如果把batch size提到8,显存接近22G;
- 加入感知损失后,batch size 4会导致显存溢出,只能降到2。
所以如果要用高质量的配置完整训练,建议最少一张RTX 3090/4090级别的显卡,或者用A100跑。没有大显存的话,可以先把残差块数量从16减到8,生成器能力会打折扣,但整体流程可以跑通。
训练时长方面,在自己的数据集上从零训练,300个epoch大概需要3到5天(单卡)。如果想快速验证,强烈建议先用小尺寸数据(比如64×64 → 128×128的2倍超分)跑一版,确认损失曲线正常了再切换到目标分辨率。
5.2 从零训练还是加预训练:我的做法
我复现时先做了一步“预训练引导”:先用L1损失单独训练 G_LR→HR 生成器,让它先学会基本的光照和颜色还原,然后再整体加载这个权重做GAN训练。
做法很简单:
# 第一阶段:只优化生成器的L1重建损失 optimizer.zero_grad() loss_l1 = l1_loss(gen_lr_to_hr(lr), hr) loss_l1.backward() optimizer.step()这个阶段跑10-20个epoch,把循环重建的底子打好。然后正式进入GAN训练,加载第一阶段的生成器权重,再让判别器从零开始学习。这样做的好处非常明显:判别器一开始面对的已经是一个基本合格的超分结果,不会出现生成器初始输出太烂、判别器瞬间过拟合到“所有假图直接判负”、然后整个训练崩溃的问题。
我在一开始没做这个预训练,直接启动GAN训练,结果前50个epoch里生成器几乎学不到任何有效信息,对抗损失一直下不去。加入L1引导之后,训练曲线明显平滑很多。
5.3 推理阶段:灵活选择输出尺寸
test.py 的推理逻辑比训练逻辑简单得多,加载 G_LR→HR 权重,对输入图像做归一化、进网络、反归一化就可以。但有一点要注意:输入图像尺寸不要求必须是固定值,生成器是全卷积网络,可以对任意尺寸的输入做超分。只不过如果分辨率太高,一次性推理会爆显存,这时可以用切片缝合(overlap-tile)策略:把输入切块分别推理,再拼接回去。
拼接处要注意做重叠区和线性融合,否则接缝处会出现一条明显的纹理断裂。我在做4K图像全图超分时,用256×256重叠、线性融合的方式处理,效果很好,接缝完全看不出。
5.4 和改进版的对比:ESRGAN、SwinIR和Real-ESRGAN
SRResCycGAN毕竟来自2020年的工作,和后来出现的Real-ESRGAN、SwinIR等方法相比,它在效率上不一定占优。Real-ESRGAN用高阶退化模型模拟真实退化过程,SwinIR用Transformer结构做特征提取,在高倍超分任务上的纹理重建能力都更强。但SRResCycGAN的循环一致性思想至今没有过时——在训练数据无法严格配对的场景下,比如用老照片修复、视频监控画面增强,CycleGAN框架的可解释性和稳定性仍然相当能打。
如果你是想快速落地生产环境,我建议把SRResCycGAN作为基线,跑通流程后再替换更强的生成器骨架;如果你是想研究无配对超分,SRResCycGAN的代码结构比Real-ESRGAN那种高度耦合的实现更适合入门学习。我自己后来做一个医学影像增强项目时,就沿用了SRResCycGAN的双向循环框架,只不过把生成器换成了轻量化的MobileNet风格,效果很稳定。
最后再分享一个调参的小心得:GAN类型的超分模型,损失权重和学习率的设置在第一周训练里决定成败。每次调整参数后,我都坚持把生成器和判别器的损失单独打点记录,而不是只看总损失。如果循环重建损失在下降、但生成器对抗损失停滞不动,问题多半在学习率;如果判别器损失掉到接近0而生成器损失激增,那就要检查是不是判别器过强或cycle loss权重太小。盯着这两个信号做调整,比盲目堆算力高效得多。
本文还有配套的精品资源,点击获取