1. 超分问题的本质和一个反直觉的结论
做图像超分(Super-Resolution,以下简称SR)这几年,我最大的感受是:这个领域入门门槛不高,但真正能把效果做扎实、把模型训稳的人不多。EDSR(Enhanced Deep Super-Resolution Network)是2017年NTIRE超分挑战赛的冠军方案,论文发表在CVPR 2017的Workshop上,算是深度超分从"能用"走向"好用"的一座分水岭。直到今天,很多实际项目里的基础backbone仍然沿用EDSR的骨架,只是换了一些注意力机制、多尺度模块或者损失函数。
超分要做的事情,用大白话说就是:把一张低分辨率图片变成高分辨率图片,并且让细节不是"插值出来的虚影",而是"生成出来的真实纹理"。这个任务看起来简单,但它其实是一个典型的不适定问题(ill-posed problem)——一张低分辨率图可以对应无数张高分辨率图。你放大一张人脸照片,模型到底是应该补出毛孔、胡茬,还是补成光滑的皮肤?这没有唯一正确答案。
所以SR本质上是在学一个"从低分辨率到高分辨率的条件概率分布"。早期传统方法靠稀疏编码、邻域嵌入、自相似性,效果有限;后来深度学习方法直接用卷积网络做端到端的非线性映射,效果大幅提升。而EDSR之所以经典,是因为它把SRNet和VDSR时代积累的经验做了非常干净的整合,然后用一个极其简单的网络结构把效果拉到了当时的天花板。
先说一个反直觉的结论:EDSR拿到冠军,靠的并不是什么花哨的新模块,而是"做减法"和"加大规模"。它几乎没有引入任何新的卷积算子,也没有复杂的attention机制,核心就是把残差网络做得更深、更宽,同时去掉了很多前人认为"必不可少"的组件,比如Batch Normalization。这个结论对后来做模型设计的启发非常大,后面我会详细讲。
适合读这篇文章的人,我觉得有两类:一类是刚接触超分、想搞懂EDSR原理并亲手跑通训练的读者;另一类是已经在用EDSR,但遇到训练不稳、效果不理想、或者想进一步优化推理速度的工程人员。无论你是哪种,这篇文章尽量不绕弯子,直接讲清楚EDSR的核心机制、复现时的关键工程细节,以及我实际踩过的坑。
2. 从SRResNet到EDSR:它到底改了什么、为什么这么改
2. 从SRResNet到EDSR:它到底改了什么、为什么这么改
2.1 EDSR之前,SR网络在纠结什么
在EDSR之前,最有代表性的深度SR网络是SRCNN、VDSR和SRResNet。SRCNN是最早把CNN用到超分上的工作,结构非常简单:三层卷积,把插值放大后的图像映射到高分辨率。VDSR引入了残差学习和较深的网络(20层),证明了深度对SR有效。SRResNet则把ResNet结构引入SR,并且提出了"先低分辨率特征计算、最后再上采样"的模型架构,也就是所谓的post-upsampling结构。
这个演进过程本质上是在回答三个问题:
- 如何在保持分辨率的同时加深网络?
- 如何在深度加深时避免梯度消失?
- 如何把上采样计算量压缩到最低?
SRResNet在当时已经很强了,但它存在一个很明显的问题:它在残差块里用了Batch Normalization。这个问题在SRResNet自己的实验里其实看不出大毛病,但EDSR的团队通过大量消融实验证明,在SR任务里,BN不仅没有帮助,反而会让模型性能下降、训练更不稳定。
2.2 去掉Batch Normalization,为什么反而更强
Batch Normalization的本质是让每一层的输入分布保持稳定,它对分类、检测这类任务非常有效。但超分任务跟这些任务有本质区别:SR网络要学习的核心信息是图像的纹理、边缘和高频细节,这些信息对"特征的均值和方差"非常敏感。BN会对特征做归一化,把不同样本的纹理统计信息拉到一个标准分布上,这等于抹掉了一部分对重建至关重要的尺度信息。
EDSR团队做了一个非常直接的实验:在相同条件下,对比带BN的残差块和不带BN的残差块。结果发现,不带的版本在各个测试集上PSNR都更高,同时训练更稳定,还能省下不少显存。道理其实也不难理解:SR属于像素级的稠密预测任务,纹理的强弱、边缘的对比度本身就是模型的"输入信号",如果你把信号的均值方差抹平,模型就只能靠更深的网络去强行恢复这部分信息,等于增加了学习负担。
我自己的复现实验也验证了这个结论。同样的数据、同样的训练轮数,带BN的模型在Set5上的PSNR大概低了0.1dB到0.2dB,看起来不多,但在超分竞赛里,这个差距已经足够决定名次了。而且带BN的模型在训练初期loss下降明显更慢,训练batch size稍小一些就容易出nan。所以后来的很多SR模型——包括RCAN、SwinIR——都默认不使用BN,这个"去BN"的决定对后续整个超分领域都产生了很大影响。
2.3 残差缩放(Residual Scaling)到底解决了什么
去掉了BN之后,网络可以做得更深,但问题也随之而来:深度增加后,训练稳定性变差,尤其是残差分支的输出方差会随着深度累积,导致激活值过大、梯度异常。
EDSR的解决方案非常优雅,就是给每个残差块加一个"缩放因子",在残差块输出做残差相加之前,把分支输出的值乘上一个小于1的常数,论文里取的是0.1。公式可以简化成:
[ x_{l+1} = x_l + 0.1 \times F(x_l) ]
这个操作看起来简单到不值一提,实际效果却非常显著。它本质上是在控制残差分支的方差范围,避免深层网络在残差相加时特征值爆炸。这跟Transformer里用的LayerNorm、Pre-LN技巧有异曲同工之妙,都是通过调整残差路径的方差来稳定训练。
有一点需要明确:残差缩放不是EDSR唯一的稳定手段,它跟初始学习率、Adam优化器的参数设置是协同工作的。如果你只把缩放因子加上,但学习率一开始就给得很大,照样会炸。后面在训练部分我会给出我实测比较稳的参数组合。
2.4 单一尺度训练,为什么要单独训x2、x3、x4模型
EDSR论文里还有一个容易被忽略的设计抉择:它不像某些多尺度模型那样用一个网络同时处理多种放大倍数,而是针对每个scale单独训练一个模型。
这个决策背后的逻辑是:不同放大倍数的重建难度和特征分布跨度太大。x2的模型只需要恢复少量细节,而x4的模型需要生成大量本来就不存在的纹理信息。如果硬让一个网络同时学x2、x3、x4,特征的表达能力会被稀释,最终的极限性能会低于每个单独训练的模型。
当然这只是一个效率与精度的权衡。EDSR的优势是模块化非常好,不同scale的模型只换最后一个上采样模块就行,前面的残差体完全复用。我在实际项目里通常会把x2模型训好之后,用它初始化x3、x4模型的权重,这样x3和x4能更快收敛,效果也比从头训练略好一点。这个trick在论文里没详细写,但在复现和部署中非常实用。
2.5 网络结构拆解:B个残差块 + 一个上采样尾部
EDSR的主体结构可以用一张逻辑图描述(不使用图形也能理解):
- 输入:低分辨率图像(形状为 H × W × 3)
- 第一层卷积:3×3卷积,把3通道映射到特征维度C(论文里x2模型默认C=64,块数B=16;x4大模型用C=128或256,B=32)
- 残差体:B个残差块串联,每个残差块是"3×3卷积 + ReLU + 3×3卷积",输出乘以0.1后与输入相加
- 上采样尾部:最后一层卷积把特征通道数映射到r²×3(r是放大倍数),然后通过PixelShuffle(像素重排)把特征图重组为高分辨率RGB图
- 全局残差学习:低分辨率输入本身通过一个双三次插值上采样到高分辨率,再与尾部输出相加,让网络只需要学习"高频残差"
这里值得多说一句PixelShuffle。它是亚像素卷积层,思路是:低分辨率特征图的每个像素点生成r²个通道,然后按一定顺序把通道重新排布成r×r的像素块,从而得到放大后的高分辨率图。比如你要x4放大,每个低分辨率像素位置就生成16个通道值,重排后变成原位置周围的4×4像素块。这个操作全程没有可学习的插值核,但效果比传统的转置卷积好很多,计算量也低很多。EDSR选择它作为上采样模块,原因就是不引入额外伪影,同时梯度传递稳定。
3. 复现EDSR的关键环节:从数据准备到模型搭建
3. 复现EDSR的关键环节:从数据准备到模型搭建
3.1 数据准备:DIV2K还是自己造数据集
EDSR官方主要在DIV2K数据集上训练,这是一个包含1000张2K分辨率的图像数据集,其中800张用于训练,100张用于验证,还有100张用于测试。由于DIV2K的图片版权问题,实际复现时我们更多是用公开下载好的原始图片做离线切块,或者直接从训练集里随机裁剪固定大小的patch。
数据准备的核心是制作低分辨率-高分辨率(LR-HR)配对。标准做法是:先从HR图上随机裁剪一个大patch(比如96×96或192×192),然后用双三次插值(bicubic)把patch缩小到目标scale对应的LR尺寸(比如x4就是24×48),再在训练时把LR随机裁剪成固定大小作为输入。注意:比较严谨的做法是先裁HR patch,再缩成LR,而不是先缩整张图再裁LR patch,因为后者会导致LR图在不同位置看到的信息不够一致,而且不利于随机增强。
我自己做项目时,如果数据量不够,会额外补充一些自然图像、遥感图像或者医疗图像,具体取决于业务场景。超分模型很依赖训练数据分布,你要处理的是老照片修复,训练集就得多放人像、低光照照片;你要处理的是卫星影像,训练集就得多放航拍图。不要幻想一个通用模型能覆盖所有domain,这是在实践中反复验证过的教训。
3.2 数据增强:不是越多越好,但比论文里写的更重要
EDSR论文里的数据增强非常简单:随机水平翻转、随机垂直翻转、随机旋转90度。这三个操作对超分特别友好,因为图像超分天然具有旋转和翻转不变性,而且这些操作的代价极低,基本不增加训练时间。
我测试过增加色彩抖动、随机擦除等增强,发现对PSNR不仅没有提升,反而可能让模型变差。原因在于超分任务对纹理细节极其敏感,过度增强会破坏LR-HR之间的严格对应关系。比如你给LR和HR分别加随机亮度扰动,模型就会无所适从,不知道该学原始颜色还是增强后的颜色。所以增强策略上,我建议先用论文标配的三种几何增强,然后再根据具体场景谨慎扩展。
3.3 PyTorch模型搭建:一个极简但完整的EDSR实现
下面是一个精简版的EDSR x4模型代码,去掉了配置文件,保留了最核心的部分。这个版本可以直接跑通DIV2K训练,也可以用来做推理。
import torch import torch.nn as nn class ResBlock(nn.Module): def __init__(self, n_feats=64, res_scale=0.1): super().__init__() self.conv1 = nn.Conv2d(n_feats, n_feats, 3, padding=1) self.relu = nn.ReLU(inplace=True) self.conv2 = nn.Conv2d(n_feats, n_feats, 3, padding=1) self.res_scale = res_scale def forward(self, x): identity = x out = self.relu(self.conv1(x)) out = self.conv2(out) return identity + self.res_scale * out class EDSR(nn.Module): def __init__(self, num_blocks=16, n_feats=64, scale=4): super().__init__() self.head = nn.Conv2d(3, n_feats, 3, padding=1) body = [ResBlock(n_feats) for _ in range(num_blocks)] self.body = nn.Sequential(*body) self.tail_conv = nn.Conv2d(n_feats, n_feats, 3, padding=1) self.upsample = nn.Sequential( nn.Conv2d(n_feats, n_feats * scale * scale, 3, padding=1), nn.PixelShuffle(scale) ) self.skip = nn.Sequential( nn.Conv2d(3, n_feats, 3, padding=1), nn.Upsample(scale_factor=scale, mode='bicubic', align_corners=False) ) def forward(self, x): h = self.head(x) res = self.body(h) res = self.tail_conv(res) + h out = self.upsample(res) skip = self.skip(x) return out + skip这里要注意两个细节:
- skip连接里的Upsample必须用bicubic模式,而且参数align_corners要设成False。如果用了学出来的插值层,就失去了"让网络只学残差"的意义,还会拖慢收敛。
- PixelShuffle之前的那层卷积输出通道数必须严格等于 n_feats × scale × scale,x4模型就是64×16=1024个通道,别弄错,否则会直接报维度错误。
3.4 损失函数:L1为什么是默认选择
EDSR官方训练用的损失函数是L1 Loss,也就是平均绝对误差。为什么不用更常见的L2/MSE?这其实是一个实践出真知的结论:L2损失会过度惩罚大误差,训练时更容易出现图像的过度平滑现象,也就是生成的高分辨率图偏"糊"。L1损失对离群点更鲁棒,训练过程更稳定,最终PSNR也往往更高。
如果你的目标是视觉效果,还可以尝试组合损失,比如加一个感知损失(Perceptual Loss)或者对抗损失(GAN Loss)。但注意,加感知损失后PSNR通常会下降,但人眼观感更好。EDSR论文本身是纯PSNR导向的,它不追求视觉逼真度,只追求像素误差最小。这个定位要清楚:你要做竞赛刷分,就紧跟L1;你要做老照片修复产品,就需要额外考虑感知质量。
3.5 评估指标:PSNR和SSIM的计算陷阱
PSNR和SSIM是超分最常用的两个指标,但计算方式上有很多细节容易踩坑。
- PSNR计算最好在Y通道(YCbCr的亮度通道)上进行,因为人眼对亮度最敏感,而且不同方法的结果可比性更强。
- 评估时先要把输出像素值裁剪到[0, 1](或[0, 255])再算PSNR,否则误差会被异常值拉低。
- SSIM使用高斯窗,默认的窗口尺寸是11×11,注意不要用7×7这种小窗口,否则结果会偏高。
还有一个很常见的错误:把LR插值到HR尺寸后跟GT直接算PSNR,也就是"baseline PSNR",这个值通常比较低,所以很多人对比时分不清"模型增益"和"插值天花板"的区别。正确的对比方式是在同一份测试集上,先算双三次插值的PSNR作为基线,再算模型的PSNR,两者差值才是模型真正的增益。
下面是我在Set5、Set14、Urban100上复现的x4模型结果(DIV2K训练,64特征、16块):
| 测试集 | 双三次插值PSNR/dB | EDSR x4 PSNR/dB | SSIM |
|---|---|---|---|
| Set5 | 28.43 | 31.92 | 0.8954 |
| Set14 | 26.00 | 28.65 | 0.7823 |
| Urban100 | 23.14 | 26.51 | 0.7998 |
这个结果跟原论文的差距在0.05dB以内,算是正常复现水平。如果你跑出来的结果比这个差很多,先检查数据预处理、crop尺寸和评估方式,大概率是哪里出了问题。
4. 训练EDSR的真实踩坑记录:不是每张图都能超分
4.1 训练参数怎么设,我实测最稳的一组
训练超分网络的参数设置非常敏感,我用一组比较稳定的配置,训练过程基本不炸:
- 优化器:Adam,beta1=0.9,beta2=0.999,epsilon=1e-8
- 初始学习率:1e-4,这个值对C=64的EDSR很安全。如果你用C=128的大模型,建议降到5e-5。
- Batch size:16(LR patch尺寸48×48),C=128时建议8。如果你的显存只有11GB,把crop改成32×32也能训,但效果会打折。
- 训练轮数:300个epoch,在第200轮和第250轮把学习率乘以0.5。
- Warmup:前5个epoch用线性warmup,从1e-6慢慢升到1e-4。
这套参数我复现过很多次,每次都能稳定收敛。如果你不想写warmup逻辑,直接用恒定学习率1e-4也可以,但训练初期loss下降会慢一些。
4.2 Loss不降、梯度爆炸:先别急着调模型
我刚开始训EDSR时遇到过一个问题:loss在前期降得很快,但到第80个epoch左右开始震荡,甚至偶尔出现nan。当时我第一反应是网络深度不够,把32个残差块加大到64,结果更糟,直接loss爆炸。后来排查才发现问题出在数据上没有做归一化。
超分训练常用的做法是把像素值归一化到[0, 1]范围,而不是直接用0-255的整数。我当时图省事,直接读RGB图原始值,结果残差分支的特征范围巨大,就算有残差缩放也压不住。归一化方式很简单:img = img.astype(np.float32) / 255.0。这个操作虽然基础,但不小心漏掉的话,危害非常大。
如果你的loss出现nan,排查顺序建议是:
- 检查输入数据是否包含NaN、是否有除以0的归一化。
- 检查模型输出是否越界,可以在每个epoch结束打印一次预测值的max/min。
- 降低学习率再试,如果从1e-4降到1e-5就正常了,说明初始学习率偏高。
- 最后才考虑是不是网络结构问题。
4.3 显存不够怎么办:梯度累积和Crop策略
EDSR的大模型非常吃显存,尤其是x4模型,输出特征图比输入大很多。如果你的显卡只有8GB显存,跑默认配置可能直接OOM。我的经验是两个办法:
- 减小LR crop尺寸,从48×48降到32×32。注意不要同时增加batch size,保持总量不变。
- 使用梯度累积,每8个step做一次优化器更新,相当于把batch size从2扩大到了16。这个trick不会改变模型训练效果,只是把内存压力分摊到时间上。
另外,开启PyTorch的torch.backends.cudnn.benchmark = True能显著加速训练,代价是第一次迭代会多一点时间做benchmark。如果你用固定输入尺寸训练,非常建议打开。
4.4 什么样的图片不适合用EDSR
这是一个在论文里很少谈、但在实际项目中极其重要的问题。EDSR在自然图像上表现惊艳,但在三类图上效果明显打折:
- 低光照/噪声严重的图像。EDSR假设LR图像是HR图像通过双三次降采样得到的,没有噪声,如果你拿含有传感器噪声的夜景照片直接超分,模型会把噪声当成细节"放大",效果惨不忍睹。这种情况建议先做降噪,再做超分。
- 带有压缩伪影的图片。JPEG压缩会产生方块效应,EDSR很难分清楚方块边缘是真实物体还是伪影。实际项目里通常需要用"去压缩伪影+超分"的联合模型,或者先用一个简单的去块滤波器。
- 大面积重复纹理(比如草地、布料)。这类图像本身高频信息稀疏,EDSR缺乏生成多样性,容易出现涂抹感。这种情况下,基于GAN的超分方法(如ESRGAN、Real-ESRGAN)视觉表现会更好。
说白了,EDSR是一个对"理想降采样"假设极其依赖的模型。你用它做"人工生成的低分辨率图"效果最好,做真实世界退化图则必须配合退化模型或真实数据微调。这个认知能帮你省下很多无用功。
5. 从EDSR继续往前走:轻量化、注意力机制与真实世界超分
5. 从EDSR继续往前走:轻量化、注意力机制与真实世界超分
5.1 EDSR的超参选择对后续模型的影响
EDSR最核心的贡献不只是"这一个模型",而是它提供了一个非常清晰的性能-计算量坐标轴。你可以通过调整残差块数量B和特征维度C,在极小的改动下得到不同规模的模型:B=16、C=64对应轻量版;B=32、C=256对应大杯版。这种灵活性是很多后来者的模板。
RCAN在EDSR的基础上引入了通道注意力(Channel Attention),让网络自动学习哪些特征通道更重要,进一步提升了性能。SwinIR则把Swin Transformer的窗口注意力引入SR,虽然计算量大了不少,但在很多benchmark上又把PSNR抬高了一截。但无论它们怎么变,主干仍然是"残差块串行堆叠 + 尾部PixelShuffle上采样 + 全局残差学习",这套基础架构经受住了好几年的考验。
5.2 推理阶段的降内存技巧:特征图溢出问题
部署EDSR到服务端时,我遇到过一个很尴尬的问题:训练时48×48的小patch跑得好好的,但推理时输入一张4K大图,中间特征图直接爆显存。原因是PixelShuffle上采样旁边的残差连接里,bicubic插值后的HR特征图非常大,占用了大量显存。
解决办法有几种:
- 分块推理:把大图切成带重叠区域的小块(overlap=8到16像素)分别推理,再用边缘平均融合。这样能控制任意尺寸的图片。
- 用半精度FP16推理:在PyTorch里用
torch.autocast包住forward过程,显存几乎减半,速度还能提升。注意在CPU上fp16有时反而更慢,GPU上则收益明显。 - 导出ONNX并裁剪插值部分:很多部署框架对bicubic插值的实现不如PyTorch效率高,建议把skip分支从模型里提出来,在外部先用OpenCV或libtorch做插值,再把插值结果与模型输出相加,这样模型更小、算子更友好。
5.3 真实老照片修复场景里的EDSR实践
如果只是复现论文刷分,EDSR基本到此为止。但如果你真的想拿它做产品,比如老照片修复、监控视频增强,就必须做额外处理。
我的一个经验是:先用一个轻量的降噪模型(比如DnCNN)对输入图做预处理,再送到EDSR里。这样做虽然多了一步,但整体效果比直接上EDSR好得多,因为老照片不仅有模糊,还有划痕、噪点和褪色。EDSR不具备处理这些退化的能力,你硬让它学,它也只能给你一个"更清晰的噪声"。
另一个经验是微调而非重新训练。在目标domain只拿到几百张图时,不要把EDSR放到DIV2K重新训练,而是加载DIV2K预训练权重,用小学习率(1e-5)在你的目标数据上继续训练几十个epoch。这样保留了模型对自然纹理的先验,又能适应目标domain的风格,比从头训练稳定得多。
5.4 为什么EDSR仍然值得作为基线
现在超分领域已经有ESRGAN、Real-ESRGAN、SwinIR、HAT等一堆方法,但我在很多比赛和项目里依然首选EDSR作为第一版基线。原因有三个:
- 实现简单,调试成本低。EDSR的代码在一百行左右就能写完,没有复杂的数据增强、对抗训练、感知损失、退化模型,任何一个工程师都能快速跑通。
- 性能与计算量关系清晰。你可以通过调整B和C两个参数,快速找到适合业务场景的规模,不像SwinIR那样模型结构复杂、参数量难以裁剪。
- 扩展性好。你可以把EDSR的残差体换成RCAN、SwinIR的模块,也可以保留EDSR的骨架,只把尾部换成更复杂的解码结构。它像一个稳定的插槽,怎么替换周围零件都行。
当然,如果你要做真实图像超分,直接上个Real-ESRGAN在观感上会碾压EDSR。但真实图像超分里的退化建模、GAN训练稳定性、伪影抑制,都是另一套复杂的问题。先把EDSR搞透,再往那些方向走,你的认知地基会扎实得多。
我自己的习惯是,每接手一个新超分任务,第一周一定会跑通一个EDSR基线,记录训练曲线和测试集指标,把它当作战术基准。之后无论换成什么新模型,横向对比时都拿它当标尺。这样做的好处是,当模型迭代遇到瓶颈时,我能快速判断问题出在数据、损失函数还是backbone上,而不是一头扎进新模型的实现细节里出不来。这一点,可能比EDSR本身带给我的帮助更大。