图像超分辨这个方向里,DRRN是一篇绕不开的工作。我第一次读它,是被“递归”两个字吸引的:一个残差模块反复用很多次,参数量几乎不增加,效果还能超过当时很多重网络。这篇论文做的事情,本质上是在告诉你“深度”和“参数量”是可以解耦的。对于刚入门图像复原的人、准备复现超分实验的研究生,以及关心轻量化模型的算法工程师,它都值得精读。它没有特别复杂的数学,却能带来不少结构设计上的启发。按我的经验,读这篇文献的价值不在于背下网络层数,而在于理解作者在面对“更深的网络怎么训练”和“有限参数怎么发挥”这两个问题时,选了一条特别巧的路。
1. 这一篇论文到底在解决什么问题
1.1 图像超分辨:一个一眼望不到底的逆问题
图像超分辨(Super-Resolution)的目标很简单:给一张低分辨率图,恢复出清晰的高分辨率图。但“简单”只是表面。一张小图可以对应无数张大图,丢失的高频细节无法从单张图像中严格恢复,所以它天然是一个病态逆问题。你可以把它想象成拿到一张拼图的下半部分,却要猜出整张图的上半部分——猜得合理但不保证唯一。
早期的传统方法靠插值,比如双三次插值,速度快但边缘糊。后来有基于稀疏表示、自相似先验的方法,效果好了不少,但基本上都是在手工设计先验。到了深度学习阶段,SRCNN先把卷积网络引入超分,用三层卷积直接学“低分辨率到高分辨率”的映射,效果明显超过传统方法,但结构太浅,感受野有限,恢复出的纹理还是偏平滑。VDSR进一步把网络做到了二十层左右,效果提升很大,同时也带来了新问题:参数变多了,训练变难了,部署也不是那么轻快。
读论文的时候特别要留意这种“上一代方法留下了什么遗憾”,因为新工作往往就是冲着这个遗憾来的。DRRN并不是第一个做深度超分的,但它非常精准地回应了“网络够深但参数不能太多”这个需求。
1.2 当时的两难:加深网络还是控制参数量
在DRRN出现之前,超分网络的主流思路是“越深越好”。深网络有更大的感受野,能利用更大范围的上下文信息来重建细节,所以VDSR这类工作才会有效果。但加深网络不是没有代价的,最直接的问题有三个。
第一,参数变多。每一层独立的卷积核都要存下来,训练时要更新的梯度也更多。模型容量一大,在有限训练数据下容易过拟合。超分数据集的规模不像分类任务那样动辄上百万张,过拟合问题在超分里更明显。第二,训练变得不稳定。网络越深,梯度从最后一层传回第一层时越容易消失或爆炸,必须花更多心思调学习率、加梯度裁剪。第三,部署压力。当时的移动端设备性能有限,参数多意味着模型文件大、内存占用高,不太容易落地。
所以你看到的是一个两难:不加深度,效果上不去;加了深度,参数和训练成本都上去了。DRRN想做的事情,是找到一条中间路——网络可以很深,但参数量不能跟着线性涨。这个目标放到今天看依然很有现实意义,因为轻量化模型一直是工程落地里的刚需。
1.3 DRRN给出的答案:递归加残差
DRRN的全称是Deep Recursive Residual Network,翻译过来就是“深度递归残差网络”。名字已经把核心说清楚了:递归(Recursive)负责解决参数量问题,残差(Residual)负责解决训练稳定性和深度退化问题。
具体来说,它用同一个残差模块在网络里反复出现很多次。因为参数是共享的,网络整体深度虽然很大,但实际需要保存的参数只有一份。与此同时,它在两个尺度上加入了残差连接:一个是全局性的,从输入直接连接到输出;另一个是局部的,在每个残差单元内部做跳跃连接。全局残差让网络只需要学习“低分辨率到高分辨率之间的差异”,局部残差让梯度在小范围内可以顺畅回传。
这个设计给我最明显的感受是“朴素且有效”。它没有用特别花哨的模块,就是把两个已经被验证过的思想组合在一起,做出了一种非常契合超分任务特性的结构。论文里主推的B2-Ultra结构,我后面会详细拆。
2. 核心结构拆解:递归残差网络的设计逻辑
2.1 全局残差:先学会做减法
先看一个很容易被忽视的细节:超分任务的输入和输出,其实高度相似。把一张低分辨率图放大后,它已经具备了大致的轮廓、颜色、对比度,和真正的高分辨率图之间差的只是高频细节。如果你让网络直接从零开始“画”一张高分辨率图,任务很难;但如果让它只预测差值,任务就轻松很多。
DRRN的全局残差就是这个思路。输入图像在经过网络主体处理后,会在最后阶段与输出直接相加。网络主体学习的是残差信号,而不是完整的高分辨率图像。用生活里的话说,不是你让一个学生从头默写整篇文章,而是你给他一份草稿,让他只改出错的地方。改动范围小,任务难度自然降低。
为什么用加法而不是拼接(Concat)?加法不改变通道数,计算量小,而且超分任务本质上是像素级的数值回归,直接加一个恒等映射非常自然。如果用拼接,后续卷积层要处理双倍通道,参数和显存都会上涨,这和DRRN追求参数效率的目标是冲突的。
2.2 局部残差:不让梯度在深路上迷路
全局残差解决的是“网络学什么”,局部残差解决的是“梯度怎么传”。DRRN的递归单元内部同样是残差结构。每个子模块做的是“卷积 -> 激活 -> 卷积”,然后把输入再加回来。这样无论网络叠了多少层,每一小段都能确保梯度有一条“近路”可以走。
这其实是ResNet思想在超分里的延伸。深层网络在训练时会遇到退化问题:层数增加,训练误差反而上升,不是因为过拟合,而是因为优化困难。残差连接让每一层只需要学习一个增量,这个增量通常很小,网络整体偏向于先保持输入不变,再慢慢调整。对于超分这种输入输出差距小、但局部变化非常敏感的任务,这种学习方式非常合适。
我自己的理解是:全局残差让网络“在整张图层面做减法”,局部残差让网络“在特征层面不断微调”。两者配合起来,网络既不会在训练时迷路,也不会把大量能力浪费在重复记忆输入信息上。
2.3 递归共享:一份权重,反复精修
递归是整个DRRN里最特别的设计。传统深层网络每一层都有自己独立的权重,50层就是50份卷积核。DRRN的做法是定义好一个递归模块,然后让它在网络里被调用D次。D越大,网络前向传播经过的变换越多,但权重只有一份。
为什么这能work?关键在于图像超分本身有很强的自相似性。自然图像里的边缘、纹理、重复结构会在不同位置反复出现,同一个递归模块在逻辑上相当于一个“更新规则”,每次前向传播都把当前的特征往更精细的方向推一点。和“用一把刻刀反复雕刻”很类似:你不是每次换一把新刀,而是用同一把刀一遍遍打磨,越磨越细。
递归还有一个数学上的好处:共享参数等于给模型加了很强的正则。参数少,模型容量受限,反而抑制了过拟合。很多人在复现时会发现,DRRN用相对较小的训练数据就能训练得很稳,这是参数共享带来的隐含收益。但要注意,参数没涨不代表计算量没涨。递归D次,前向计算量大约就是D倍的模块计算量,所以网络推理速度并不像参数量看起来那么轻量,这一点在部署时特别重要。
2.4 B1-Ultra与B2-Ultra:从单层残差到双层残差
DRRN论文里设计了两种递归单元,B1-Ultra和B2-Ultra。B1-Ultra可以理解为一个带局部残差的小模块:输入经过两次卷积和激活,再与自身相加。B2-Ultra则是在这个基础上更进一层,在同一个递归单元内部堆叠两个残差小块。也就是说,单次递归内部会完成两次“精修”,信息处理粒度更细。
| 设计 | B1-Ultra | B2-Ultra |
|---|---|---|
| 递归单元内部结构 | 单个残差单元 | 两个残差单元级联 |
| 单次递归的特征修正 | 一次卷积块调整 | 连续两次调整 |
| 参数量趋势 | 低 | 仍然共享,不随递归次数增加 |
| 实际效果 | 已超过普通深层基线 | 论文主推,效果更好 |
论文实验显示B2-Ultra优于B1-Ultra。这个结果其实不难理解:同样一次递归,B2-Ultra内部的变换路径更长、非线性更强,能处理更复杂的特征变化。但代价是单次递归的计算量更大,训练时显存占用也随之增加。如果你要自己复现,我建议先用B2-Ultra结构,因为它是论文最终的主打方案。
3. 实验配置和训练细节:按这个思路怎么复现
3.1 数据怎么来,低分辨率图像怎么造
超分实验的数据准备有一个固定的套路,很多论文都这样做。
训练时,通常会从公开的自然图像数据集中取图。DRRN这类方法使用的退化模型很简单:对高分辨率图像做双三次下采样,得到低分辨率图像。这个做法看起来有点理想化,因为真实世界的退化往往更复杂,包含噪声、模糊、压缩伪影等,但它已经成为超分研究的标准起跑线。先在一个清晰可控的问题上把模型研究明白,再扩展到真实场景,这是合理的科研路径。
实际操作中,彩色图像会被转换到YCrCb颜色空间,网络只在Y通道(亮度通道)上做超分,Cr和Cb通道通常直接用双三次插值放大到目标尺寸。为什么这样处理?有两个原因。第一,人眼对亮度变化最敏感,恢复好Y通道就能带来明显的视觉提升。第二,三个通道一起算会白白增加三倍计算量,收益却很有限。这个预处理习惯从SRCNN时代一直延续到现在,读超分论文时如果看到评测指标是Y通道PSNR,指的就是这个意思。
数据增强方面,常见的做法是随机裁剪、旋转90度、180度、270度,以及水平翻转。超分训练数据量大,增强可以等价地扩大训练集覆盖,帮助网络学到更多纹理模式。
3.2 训练时的关键设定
训练配置方面,DRRN的通用设定和当时的主流超分网络是一致的,我按自己复现的习惯梳理一下。
损失函数一般用MSE,也就是像素级均方误差。超分任务的核心目标就是让预测的高分辨率图像和真实高分辨率图像逐像素接近,MSE和PSNR正好一一对应,所以早期工作基本都用它。后来很多工作换用L1损失,L1对离群点没那么敏感,训练往往更稳,但对于理解DRRN本身,MSE就够了。
优化器用Adam比较多,初始学习率通常在1e-4量级。训练过程中学习率会逐步衰减,一种常见的做法是训练到一定阶段把学习率除以10。这里有一个经验:超分训练对学习率特别敏感,学习率太大,loss震荡,PSNR上不去;学习率太小,收敛极慢。如果你发现训练半天PSNR纹丝不动,先别怀疑网络结构,先检查学习率。
一个值得注意的细节是,DRRN这类结构通常会刻意避开BatchNorm。BatchNorm在小批次上可以加速收敛,但超分网络做的是逐像素回归,很多设置是单图测试,BatchNorm在训练和测试时的统计量不一致容易出问题。更关键的是,递归模块的参数被多次复用,如果内部有BatchNorm,它统计的均值方差在不同递归步骤之间会被互相干扰。所以DRRN选择不用BatchNorm,用残差连接和较小的学习率来维持训练稳定。这个设计选择很值得学习:不是所有新潮模块都适合你的任务,要理解它为什么不适合。
3.3 评测指标和读表技巧
PSNR是超分论文里出现频率最高的指标。它由MSE换算而来,数值越大代表像素误差越小。SSIM则衡量结构相似度,更接近人的主观感知。两个指标都不是完美的,但它们的好处是可以量化对比,所以学术界一直沿用。
读实验结果表的时候有几点要留个心眼。第一,要看评测的是哪个颜色通道。很多论文报告的是Y通道上的PSNR,有些报告RGB三通道平均,两者数值会差一点,不能直接对比。第二,有些方法会使用自集成技巧:把输入图片做8种旋转和翻转,分别推理,再变换回去取平均。这个操作通常能稳定提升一点PSNR。看到结果时,要分清这到底是不是加了自集成的数字。第三,注意测试集。Set5、Set14、BSD100、Urban100这些是超分领域常用的公开测试集,内容特点不同。Urban100包含很多城市建筑图像,自相似结构多,递归类方法在它上面的优势往往更明显,这也是DRRN喜欢展示结果的一个测试集。
4. 从公式看DRRN的本质
4.1 输入输出关系:全局残差的数学形式
如果要用一个式子概括DRRN,可以写成:
y = F(x) + x
这里的x是经过双三次放大后的低分辨率图像,已经和目标高分辨率图像有相同的尺寸。F(·)是网络主体,它学习的是高分辨率图像和放大后低分辨率图像之间的残差。网络主体里包含特征提取、递归模块、特征重建三部分。
这个式子看起来很简单,但它说明了一个核心设计:网络的目标输出不是一张“从无到有”的高分辨率图,而是一张修正量。因为x本身已经包含了绝大部分低频信息,F只需要负责把高频细节补回来。这样做既降低了学习难度,也让深度网络更容易训练。如果你不用这个全局残差,把一个几十层网络直接用于像素回归,训练很容易陷入不收敛的困境。
4.2 递归单元的状态更新:共享参数下的迭代
把网络主体进一步展开。
设G(·)表示共享参数的递归单元,x经过一层特征提取后得到初始特征z0。然后网络开始反复执行:
z1 = G(z0) z2 = G(z1) … zD = G(z_{D-1})
D就是递归次数。注意,每一步的G是同一个函数,论文里只保存一份参数。这和普通残差网络完全不同,普通网络里z1和z2使用的可能是两组不同卷积核。
递归过程可以理解为一种迭代优化:初始特征z0包含的是低分辨率图像的基本信息,每一步G都会对特征做一次变换和精修。因为参数共享,网络不能在不同递归阶段使用不同的滤波器,它必须找到一组在所有阶段都有效的滤波器。这会迫使网络学到更通用、更本质的特征变换规则,而不是简单地为每一层堆叠专门化的滤波器。这也是DRRN在参数量很小的前提下依然保持强表达力的原因之一。
最后,将经过D次递归的特征送入重建卷积层,再与原始输入x相加,得到最终输出。整个流程可以用一句话概括:输入放大图,提取特征,同一模块迭代精修D次,重建残差,加回原图。
4.3 梯度为什么没消失:残差与递归的配合
深层网络训练困难,根源在于梯度在反向传播时是连乘形式,每一层的导数都小于1的话,乘到前几层就接近0了。ResNet的残差连接之所以有效,是因为它让每一小段的导数都包含一个恒等项,使得梯度在反向传播过程中天然有一条“高速公路”。
DRRN里的递归单元内部有局部残差,所以每次 G 对输入求导时,至少包含一项接近单位阵的分量。即使递归D次,梯度从最后一层传回第一层,也不会因为反复连乘而彻底消失。与此同时,全局残差保证网络整体只需要学习小规模的残差信号,梯度的量级也不容易被拉爆。
从这个角度看,DRRN不是简单地“很多层”,它是一个深度和残差高度绑定的结构。没有残差,递归加深会导致梯度问题;没有递归,残差结构就是普通的深度网络,参数效率会下降。两者互为补充,缺一不可。
5. 复现过程中的坑与解决记录
5.1 从零搭一个最小DRRN
下面我给一个可运行的PyTorch最小实现,对应B2-Ultra的近似结构。这里输入约定为已经插值放大的单通道Y图,输出同样尺寸的单通道残差重建结果。
import torch import torch.nn as nn class ResidualUnit(nn.Module): def __init__(self, channels): super().__init__() self.conv1 = nn.Conv2d(channels, channels, 3, padding=1) self.relu = nn.ReLU(inplace=True) self.conv2 = nn.Conv2d(channels, channels, 3, padding=1) def forward(self, x): out = self.relu(self.conv1(x)) out = self.conv2(out) return x + out class RecursiveBlock(nn.Module): def __init__(self, channels): super().__init__() self.unit1 = ResidualUnit(channels) self.unit2 = ResidualUnit(channels) def forward(self, x): return self.unit2(self.unit1(x)) class DRRN(nn.Module): def __init__(self, channels=64, recursions=25): super().__init__() self.conv_in = nn.Conv2d(1, channels, 3, padding=1) self.block = RecursiveBlock(channels) self.conv_out = nn.Conv2d(channels, 1, 3, padding=1) self.recursions = recursions self.relu = nn.ReLU(inplace=True) def forward(self, x): identity = x out = self.relu(self.conv_in(x)) for _ in range(self.recursions): out = self.block(out) out = self.conv_out(out) return out + identity这段代码只有几十行,但已经能体现DRRN的核心思想。你只需要注意一点:循环里反复调用的是同一个self.block模块,不是每次新建一个块。PyTorch里只要把同一个模块放进循环里,参数就是共享的,这一点正是递归残差的关键。
5.2 训练常见问题速查表
我在复现过程中踩过一些坑,整理成表,方便对照。
| 现象 | 可能原因 | 建议处理 |
|---|---|---|
| PSNR一直不涨 | 输入没有归一化,或学习率不合适 | 把输入归一化到0~1区间,尝试从1e-4调整 |
| loss震荡明显 | 学习率偏大,batch size偏小 | 降低学习率,适当增大batch |
| 显存不够 | 递归次数大,中间激活值多 | 减小patch尺寸,减小batch,或使用梯度检查点 |
| 复现不出论文效果 | 评测通道、自集成、clip范围不一致 | 统一在Y通道评测,注意输出clip到0~1 |
| 训练慢 | 递归导致计算量线性增大 | 先在较小递归次数上验证正确性,再加大 |
其中“没有归一化”是我见过最常见的低级错误。图像像素如果是0到255的整数,直接送进网络,损失会非常大,梯度也会不稳定。超分训练一般把输入除以255,或者更大范围除以255后再做标准化。别看这个细节小,它能直接影响最终PSNR零点几个dB。
5.3 递归结构在工程部署上的实际影响
DRRN虽然参数少,但它不是“免费的午餐”。递归D次意味着同一个模块要计算D次,推理时间大致随D线性增长。实际部署时,一个参数只有1M左右的模型,推理耗电和时延可能比一些独立层网络还高。所以如果要做移动端部署,光看参数量是不够的,必须把FLOPs和单帧耗时一起评估。
一种工程上的做法是把递归循环展开,也就是把同一份权重对应的多次计算写成顺序的多层调用,方便导出为静态推理图。还有一些量化手段也可以应用,因为网络绝大部分算力集中在卷积上,2.5倍或8倍量化会有比较明显的加速收益。不过要注意,量化和递归展开后,模型名义层数会变成“实际展开层数”,工具链对深度的支持程度需要提前确认。
还有一点是关于梯度检查点。如果你训练时递归次数很大,显存不够,可以打开PyTorch的梯度检查点功能,在递归模块前向时不保存中间激活,反向时重新计算。这样能大幅降低训练显存,代价是训练时间变长。对DRRN这种“深而参数少”的网络,这个技巧非常实用。
6. 这篇论文给我留下了什么
6.1 参数效率比参数数量更重要
DRRN对我最大的启发不是“递归好用”这个单一结论,而是一个更通用的观点:模型性能不一定靠堆参数来实现,结构设计本身就可以带来参数效率的飞跃。后来的很多超分工作里,我们能看到大量类似的思想:用更深的递归、更紧凑的残差、更高效的特征复用,在保持参数规模可控的同时提升效果。
在深度学习越来越强调大模型的今天,回头读一读DRRN这样的工作,反而会有一种“少即是多”的提醒。当你面对一个资源受限的任务,与其盲目加宽加深,不如想想能不能像DRRN一样,找到任务本身的规律,然后用一个共享的、可迭代的结构去匹配它。超分图像里的自相似性,就是这种规律的典型例子。
6.2 一个关于递归次数的个人经验
最后分享一个我复现时发现的细节。递归次数D从10增加到25时,PSNR提升是比较明显的;但从25再往上涨,收益开始变小,训练时间却成倍增加。每个数据集、每个放大倍数都有一个性价比比较高的递归次数,未必是越大越好。
所以我建议你在正式跑实验前,先在小测试集上做一组不同D的快速对比,找到PSNR提升开始放缓的那个点,再决定最终深度。这个经验不会出现在论文正文里,但你要是自己动手复现DRRN,先跑一遍不同D的小实验,能省下不少时间。