
图像超分辨这个方向里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-UltraB2-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, padding1) self.relu nn.ReLU(inplaceTrue) self.conv2 nn.Conv2d(channels, channels, 3, padding1) 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, channels64, recursions25): super().__init__() self.conv_in nn.Conv2d(1, channels, 3, padding1) self.block RecursiveBlock(channels) self.conv_out nn.Conv2d(channels, 1, 3, padding1) self.recursions recursions self.relu nn.ReLU(inplaceTrue) 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的小实验能省下不少时间。