我要提问
ARTICLE DETAIL

资讯详情

前沿编程新知与开发实战干货的深度解读。

DRRN精读:递归与残差如何让图像超分辨网络又深又省参数

DRRN精读:递归与残差如何让图像超分辨网络又深又省参数 文献精读系列写到第二十五篇我把目光放在图像超分辨方向一篇值得反复咀嚼的结构上基于递归残差网络的图像超分辨DRRN。在2017年那批超分辨论文里它的位置很特殊——既没有像后来的EDSR那样拼命堆参数也没有像GAN类方法那样追求视觉上的以假乱真而是用一个很古典的问题切入能不能在参数总量基本不变的前提下把网络做得足够深从而获得更大的感受野和更强的表达能力DRRN给出的答案是递归加残差。这个组合听起来简单真正拆开看里面有很多和直觉相悖的东西值得记录。不管你是想快速理清SR的发展脉络还是准备复现这个结构做对比实验这篇精读都会提供一些论文原文里看不出来的细节和经验。1. 为什么超分辨研究偏偏在DRRN这里停了一下1.1 从SRCNN到VDSR再到DRCN深度是怎么变成主线的图像超分辨的任务一句话说清楚给一张低分辨率图像想办法还原出清晰的高分辨率图像。问题在于这是一个典型的不适定逆问题——一张低分辨率图可以对应无数张高分辨率图所以算法真正在学的是在多种可能的答案里挑一个最合理的。早期方法靠稀疏编码、邻域嵌入这类手工特征效果有限。到了SRCNN出现人们第一次用三层卷积网络做LR到HR的端到端映射虽然结构简单但已经验证了深度学习这条路的价值。接下来的事情就变得很自然既然卷积有效那就把网络加深。VDSR用20层网络靠残差学习把PSNR抬上了一个台阶。到这一步社区形成了一个共识——深度是超分辨性能的胜负手。更深意味着更大的感受野、更复杂的非线性映射、更强的特征表达能力。但问题也跟着来了每一层卷积都要一组独立的权重深度增加参数像滚雪球一样膨胀训练越来越难模型文件越来越大部署越来越痛苦。DRCN开始尝试递归结构让一部分网络层共享参数思路是对的但整体设计还是绕梯度流动也不够顺畅。DRRN就是在这样的背景下出场的。它的核心主张非常直接与其每一层都配一套新权重不如让同一组残差单元在多个阶段反复出场用递归换深度再用残差保稳定。一句话概括就是要把网络做深但不想因此付出参数爆炸的代价。1.2 深网络要的大感受野和超分辨任务有什么关系要理解DRRN为什么执着于深度得先弄清楚感受野对超分辨具体意味着什么。低分辨率图像里的一个像素点在高分辨率空间里对应一个区域想要把这个区域的纹理、边缘、重复图案还原出来网络必须能“看到”足够大的上下文。一个3x3卷积的感受野只有3x3堆两层也就5x5网络看到的范围太窄就只能做短视的局部猜测遇到大面积结构纹理很容易翻车。层数每增加一层感受野就能往外扩一圈。VDSR做到20层感受野大概能覆盖41x41的区域已经能处理不少中等尺度的结构关系。DRRN通过递归把有效深度推到比VDSR更高的档位获取更大的感受野。但这个更深的网络不是靠独立参数堆出来的而是同一组卷积核在不同阶段反复处理特征——这就像一个人带着同一把刻刀在画布的不同层次上反复精修而不是每画一层就换一把新刀。感受野实质变大了参数却没怎么涨这是DRRN在策略上最聪明的地方。不过递归带来的深度是有代价的。同一组权重被反复使用梯度在反向传播时要沿着这条共享路径走很多趟容易指数级放大或衰减。所以DRRN必须配合残差学习来对冲这种不稳定。这就引出了它结构设计的真正核心——三种残差学习策略的组合。2. 拆开DRRN的门道三种残差加一套递归是怎么咬合的2.1 全局残差、多路径残差、局部残差各管一段DRRN把残差学习拆成三个层面这是它和VDSR这类只做简单残差学习的网络最大的不同。第一层是局部残差学习local residual learning发生在网络内部最基本的残差单元里。每个残差单元的构成是输入先过一个3x3卷积接ReLU再过一个3x3卷积得到的结果和原始输入相加。这个单元里的恒等连接最短作用也最直接——它保证即使网络内部有几十层递归信号依然可以绕过任意一个单元把梯度从后向前顺利传回去。没有这层局部保护深层递归根本训不动。第二层是多路径残差学习multi-path residual learning。因为每个残差单元都有恒等连接网络内部天然形成了很多条从输入特征到输出特征的通路信号可以跳过部分递归块直接往前走。这等于给梯度提供了一堆近道。VDSR只有一条主路径加一个全局跳连而DRRN在递归链路的每一段都留了门梯度衰减的路径被大大压缩。第三层是全局残差学习global residual learning也是最直观的一层网络输入的低分辨率图像特征经过前面所有递归处理后再和原始输入相加。这样网络从头到尾要学的就不是整张高分辨率图像而是高分辨率图像和低分辨率图像之间的残差。超分辨中LR和HR共享绝大部分低频信息网络只需要集中精力补高频细节学习压力小很多。这个设计和VDSR一脉相承但DRRN把它和局部、多路径残差叠加使用稳定性更好。这三层残差各管一段配合起来的效果是局部残差保证单个单元内部梯度通畅多路径残差保证递归块之间的信息不衰减全局残差从整体上降低学习目标难度。很多人初看DRRN会觉得它就是残差网络加了递归其实真正的设计点在于三种不同范围的残差连接如何各司其职。2.2 递归块的参数共享机制以及它带来的两面性DRRN的主体结构可以简化为输入卷积提取浅层特征然后进入递归主体多个递归块串联最后输出卷积重建高分辨率残差。递归主体的关键特点是参数共享——所有的递归块用的其实是同一组残差单元权重。在训练框架里这体现为同一个模块实例被循环调用多次每次调用产生新的输出特征但卷积核参数始终是同一份。参数共享带来的第一个好处是参数量的压缩。一个25层有效深度的DRRN它的实际可训练参数量和一个十来层的小网络差不多却拥有比VDSR更深的处理链路。这也是DRRN最常被引用的卖点之一用更少的参数超过VDSR的效果。但参数共享也有它的两面性。同一组卷积核要在不同阶段反复处理特征意味着每个残差单元必须同时适应浅层特征和深层特征两类分布——前几轮递归输入的是相对粗糙的低级特征后几轮输入的是被反复加工过的高级特征。这有点像一个人既要做粗加工又要做精修招式是同一套但面对的材料状态完全不同。这个矛盾限制了递归网络的有效深度——堆到一定程度再继续加递归次数性能反而不升不降甚至下降因为共享的那组参数已经很难在更多阶段上都保持有效了。DRRN的工作里专门对比了不同递归次数和不同结构配置结论也是这个方向。3. B22和B25论文靠哪两套结构论证深度本身有价值3.1 两套代表性配置的差异与实测趋势DRRN论文里有一组非常关键的消融实验在控制参数共享方式的前提下调整递归的结构观察性能变化。最终的推荐配置被总结为两个标签——B22和B25。所谓B代表递归主体的某种深度档位B22意思是网络有效卷积路径为22层左右的配置B25则是更深一档的25层左右配置。两者共享参数的策略基本一致差别主要体现在递归块内部的组织方式和总递归次数上。我能确定的趋势是B25在几个公开基准集上整体优于B22但领先幅度并不夸张大约在0.1到0.2dB这个量级。在Set5的2倍超分辨任务上DRRN比VDSR的PSNR高出约0.5dB这个提升在当时属于肉眼可感知的层次尤其放大来看纹理边缘的锯齿明显减少直线结构更干净。而在Urban100这类充满重复纹理、几何结构的测试集上DRRN的优势更明显——因为更大的感受野让它能更好地建模重复图案的长程相关性这正是以前的小感受野网络最吃力的地方。B25比B22好这件事论文想论证的不仅仅是性能随深度单调上升而是另一种更有价值的结论在参数共享的约束下网络依然可以通过增加递归深度获益说明深度本身对超分辨的价值大于单纯堆叠独立参数。这一结论后来被很多工作引用成为结构效率路线的重要基石。3.2 为什么同一个网络不能无限制加深如果递归深度越多越好那为什么不把递归次数调到100甚至更多这里就涉及递归结构的一个硬约束梯度在时间方向上会被反复乘上共享权重。简单解释一下这个机制。假设一个残差单元内部的有效权重矩阵谱半径为r递归T次反向传播时梯度和r^T相关。r接近1时梯度勉强保持稳定r大于1就会指数爆炸小于1则会指数消失。残差连接的存在让r在多数情况下被拉向1但不可能保证在所有情况下都等于1。所以当T变得过大网络对初始化极其敏感训练过程容易剧烈震荡性能不但不提升反而退化。DRRN论文在结构分析部分也做了类似的实验递归块数量增加到一定程度后继续增加层数PSNR的增益基本饱和再往后甚至下降。B22到B25之间还有微弱的正增益说明这个范围还在合理区间但再往上就开始踩梯度稳定性的红线。这也解释了为什么后来很多工作选择不共享参数但用更复杂的局部连接来继续加深网络——因为它们不想承担递归展开带来的训练稳定性风险愿意用更多参数去换更平缓的梯度路径。4. 复现DRRN时真正要下的功夫训练配置与工程细节4.1 数据准备、增强与Y通道训练这套标准动作复现DRRN的第一步不是写模型而是把数据流程走对。当时的超分辨训练范式很统一从高分辨率训练图像上随机裁剪子图先用双三次插值下采样生成低分辨率版本再把低分辨率子图作为输入、原始高分辨率子图作为标签喂给网络做回归。训练集规模不大通常是一两百张高质量图像同时配合数据增强——旋转90度、180度、270度以及水平翻转把数据量膨胀大约8倍。这套标准动作在SRCNN时期就形成了DRRN沿用得很顺手。色彩空间的处理是另一个容易忽略的细节。超分辨训练几乎都只在YCbCr空间的Y通道亮度上进行因为人眼对亮度细节最敏感而且颜色通道的结构相对简单直接用插值放大就能获得不错的结果。具体操作是把高分辨率图像转到YCbCr取Y通道做训练标签低分辨率图像同样转YCbCr对Y通道做输入Cb、Cr通道则在测试时用双三次插值直接放大最后再合并回RGB。我见过不少新人在复现时忘记这一步直接把RGB三个通道都塞进去训练效果差一截不说训练成本还高三倍。模型的输出层要处理的和VDSR类似网络输入是低分辨率图像本身或者它的浅层特征输出是在全局残差连接之后额外叠加的一个残差图最终和目标高分辨率图算MSE损失。损失函数用MSE而不是别的原因很朴素——PSNR和MSE直接关联SR领域的评价指标就是PSNRMSE在数学上最匹配这个优化目标。虽然MSE的感知质量后来被批评得很惨但放在DRRN的年代它是唯一让分数跑起来的合理选择。4.2 PyTorch里实现参数共享的两种写法DRRN的递归部分在PyTorch里实现起来有几种写法但核心就一个原则多次递归调用必须指向同一个模块实例而不是各自复制一份。用nn.ModuleList复制多个ResidualUnit然后让每个递归块各用一个副本这是最常见的错误——看起来结构差不多其实参数完全被分开训练了递归的意义直接消失。正确的写法是把残差单元定义成一个模块然后在循环里反复调用同一个实例。示意代码如下import torch import torch.nn as nn import torch.nn.functional as F class ResidualUnit(nn.Module): def __init__(self, channels64): super().__init__() self.conv1 nn.Conv2d(channels, channels, 3, padding1) self.conv2 nn.Conv2d(channels, channels, 3, padding1) self.relu nn.ReLU(inplaceTrue) def forward(self, x): out self.relu(self.conv1(x)) out self.conv2(out) return x out class DRRNBody(nn.Module): def __init__(self, unit, recursions): super().__init__() self.unit unit self.recursions recursions def forward(self, x): # 同一个 unit 被反复调用自动共享参数并建立多层计算图 for _ in range(self.recursions): x self.unit(x) return xPyTorch的自动求导会把每一次循环调用都记录到计算图里反向传播时会沿着这些路径自动累积梯度所以代码层面不需要额外的技巧。但要注意inplaceTrue在某些跳跃连接里可能引发版本相关的梯度报错如果遇到one of the variables needed for gradient computation has been modified by an inplace operation这类错误把inplace改成False就好。另一种实现方式是直接用递归函数加参数绑定或者把递归次数写进前向函数里循环展开。本质上和上面的写法一样。真正要留神的是显存——递归次数越多计算图上被保存的中间激活越多显存占用随递归次数线性上涨而不是随参数量上涨。这个特性直接影响训练配置后面会详细说。4.3 训练稳定性的关键梯度裁剪与残差缩放的实际作用DRRN训练里最折磨人的问题是震荡。原因是共享参数在递归方向上重复使用梯度累积到一定程度就会出现尖峰。一个非常有效的缓解手段是梯度裁剪把梯度的模长限制在一个范围内。VDSR时代社区就在用这个手法默认阈值0.4左右是很多复现者验证过的经验值。到了DRRN这里梯度裁剪几乎不是要不要的问题而是阈值该设多大的问题——设太大没效果设太小训练像蜗牛爬。我复现时的做法是先设0.5跑几个epoch观察loss曲线如果出现突然跳高再往下降的情况就逐步降到0.3、0.2。DRRN的梯度尖峰不像普通网络那样偶发它更接近一种周期性的累积释放因为递归展开会让某些权重组合在特定输入模式下触发梯度共振。另外很多后来复现DRRN的开源代码里给残差单元的残差分支乘了一个0.1的缩放系数也就是所谓的残差缩放residual scaling。这个技巧后来在EDSR里被广泛宣传但在DRRN这种共享参数的递归结构里它的价值更明显——相当于人为压低局部残差路径的有效增益让共享权重的更新更保守。严格说这是复现社区的经验扩展不是论文原始设置但实测下来训练稳定性确实提升明显。如果你自己调参建议优先尝试残差缩放加梯度裁剪的组合。5. 论文之外的经验账计算量、显存与训练时间5.1 参数少和算得快是两件事DRRN最容易被误解的地方就是把参数少等同于跑得快。实际完全不是这样。参数共享省的是存储和理论参数量但前向计算量一点没省——递归块每执行一次都要完整跑一遍卷积、激活、卷积和残差相加。25层有效深度的网络哪怕实际只有一套权重卷积运算仍然要执行25次。所以在推理速度上DRRN和一个普通的25层残差网络基本处在同一水平线上甚至因为结构更复杂还稍微慢一点。这个反直觉的点直接解释了为什么后来超分辨领域的主流没有沿着递归这条线狂奔。递归换取的是参数效率代价是计算效率和训练稳定性。对于学术benchmark来说参数少是很漂亮的卖点但对于实际部署推理延迟才是痛点。DRRN之后我们看到的大趋势反而是反过来EDSR直接把网络做宽做到52米层RCAN用残差嵌套都不再纠结参数总量而是更看重计算效率和梯度路径的干净。如果你要在论文里用DRRN做对比基线千万别只写参数量更少就完事一定要补充FLOPs和推理时间否则审稿人会默认参数少就是算得快然后拿这个假象去质疑你的实验安排。5.2 显存为什么被递归深度拖走以及怎么压缩训练DRRN时显存占用高的原因前面提到过PyTorch的反向传播需要保存前向过程中的中间激活。普通网络层数固定显存占用是确定的递归网络每一层都是同一个模块但每一层的激活都必须保留在自己的时间步上所以递归次数直接决定显存需求。假设一个残差单元的输出特征图是64通道的递归25次显存消耗大约和一个25层、每层64通道的普通网络持平。这带来一个很现实的问题想把B25跑出效果显存不够怎么办几个思路。第一减小训练patch尺寸这是最直接的显存杠杆。DRRN训练用的低分辨率子图尺寸并不大从40x40降到32x32显存能省接近四成。副作用是batch size可能因为梯度方差变大需要微调loss收敛曲线会抖一些。第二用梯度检查点也就是把部分中间激活在前向时释放反向时重新计算。这个技巧在PyTorch里封装成一个装饰器单行改动但训练时间会多出30%到50%。第三混合精度训练。DRRN的卷积和残差连接对数值精度要求不算苛刻用FP16配合缩放损失显存能再砍掉接近一半同时训练速度还更快。我的建议是如果你只是为了验证DRRN在某个数据集上的效果直接砍patch到32x32加混合精度是最省事的组合。如果你是想研究它的内部行为比如分析每个递归阶段输出了什么特征那就别开梯度检查点保持最完整的计算图否则分析工具拿到的中间量不完整。6. 定量效果之外DRRN给后续网络留下了什么6.1 从RCAN到RDN不共享参数的递归化表达DRRN之后超分辨领域没有大规模拥抱递归结构但它的思想被拆成了两块分别在后续工作里开花。一块是多尺度残差堆叠的思路。RCAN提出的残差中的残差结构本质上是把DRRN里的递归块换成了不共享参数的残差组每个残差组内部再用多个残差块叠加组间保持全局恒等连接。它保留了DRRN在深层次上反复精修的架构直觉但放弃了参数共享换来了更平滑的梯度路径和更好的可训练性。RDN则把稠密连接引入残差块让每个局部残差单元都能直接接触前面所有单元的输出来特征复用更充分。这些工作的共同点是都在追求同一件事让信息在网络深部的流动更畅通。另一块是参数效率这个概念被保留了下来。后续很多工作开始明确对比参数量和PSNR的关系比如超分辨领域后来出现的轻量化网络追求的目标就是用远小于DRRN的参数达到接近或超过DRRN的效果。回头去看DRRN算是把这条赛道的起跑线画清楚了。6.2 文献精读里更值得带走的东西读DRRN这篇工作技术细节当然重要但更值得带走的是它提出问题的方式。当年的超分辨研究不像现在这样动辄几十亿参数大家普遍面临的是显存有限、数据有限、训练时间有限的约束。在这个约束下DRRN问了一个好问题能不能不增加参数只通过改变参数的使用方式来提升性能这个问题本身比答案更珍贵。对做研究的人来说DRRN也是一份很清晰的消融实验范本。它对三种残差、递归次数、共享策略都做了逐步对比每一步都有明确的实验支撑。现在很多论文堆了一堆模块却说不清每个模块的作用回头看DRRN这种扎实的消融风格反而显得稀缺。我个人的阅读体会是复现DRRN最大的收获不在网络本身而在对深度的理解。深度不是目的感受野、表达能力、梯度路径才是目的。DRRN用递归证明了在参数受限的情况下可以通过重复使用结构来扩充有效深度但它也暴露了这个策略在训练稳定性和计算效率上的天花板。后来的网络选择用参数换稳定不是否定DRRN而是站在它的肩膀上做了取舍。如果你也想在自己的任务里尝试结构复用这种思路建议先想想你的瓶颈到底是参数太多、训练太慢还是显存不足想清楚这个才知道递归这条路适不适合你。
返回列表