ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

单图去雨实战:循环挤压激励上下文聚合网络复现与调优

单图去雨实战:循环挤压激励上下文聚合网络复现与调优

1. 为什么单图去雨值得单独拿出来讲

做图像复原这行的朋友应该都有体会,去雨(Deraining)这个任务看起来像是"低配版去噪",但真正上手做过的人都知道,它比去噪难缠得多。雨滴、雨线、雨雾在成像平面上叠加的方式是非线性的,而且雨线的形态、方向、密度、粗细随拍摄距离和运动模糊程度剧烈变化。更要命的是,雨线在视觉上和高频纹理(比如树叶、毛发、织物纹理)高度相似,模型很容易把该保留的细节当成雨去掉,或者把雨当成纹理留下来。

单图去雨(Single Image Deraining)之所以成为一个独立的研究方向,是因为它只依赖一张退化图像,不需要视频帧间的时序信息,也不需要多曝光或多模态输入。这意味着它的应用场景非常广:监控画面增强、行车记录仪画质提升、手机夜景拍摄后处理、无人机航拍图像清理,甚至是老照片修复流程里的一个预处理环节。但反过来,单图去雨也是一个典型的"病态问题"——一张图里雨和背景纠缠在一起,理论上存在无穷多组合法解,模型必须靠数据先验和结构设计去约束解空间。

这篇论文《Recurrent Squeeze-and-Excitation Context Aggregation Net for Single Image Deraining》就是在这个背景下提出的。它的核心思路可以拆成三个关键词:循环(Recurrent)、挤压激励(Squeeze-and-Excitation)、上下文聚合(Context Aggregation)。这三个词不是随便拼在一起的,它们分别对应了去雨任务里的三个痛点:多阶段迭代去雨的信息复用问题、通道特征重要性自适应问题、以及大感受野上下文建模问题。

我读这篇论文的时候,最直观的感受是:它不是靠堆深度或者堆参数量去刷指标,而是把"去雨是一个渐进过程"这个直觉做成了网络结构。这一点在工程落地时特别有价值,因为渐进式结构意味着你可以在精度和速度之间做灵活取舍。下面我会从设计思路、核心模块、实操复现、踩坑经验几个维度,把这篇论文拆开讲透。

2. 论文整体设计思路拆解

2.1 去雨任务的本质:从"一步到位"到"逐步逼近"

早期的单图去雨方法大多走的是"一步映射"路线:输入雨图,直接输出干净图。这种思路的问题在于,网络要在一个前向传播里同时完成"识别雨线"和"重建背景"两件事,任务耦合度太高。尤其是大雨、密雨场景,雨线相互遮挡,一步映射很容易留下雨痕残留或者把背景抹糊。

这篇论文选择的是多阶段循环路线。它的基本假设是:去雨可以分解成若干个渐进步骤,每一步只负责去掉一部分雨,然后把中间结果传给下一步继续处理。这有点像图像处理里的迭代去卷积,或者数值优化里的逐步逼近。循环结构的优势在于,同一个特征提取模块可以被复用多次,参数量不随阶段数线性增长,但感受野和表达能力会随着阶段推进而累积。

论文里用了一个共享权重的循环单元,把多个阶段串起来。这里的关键设计是:每个阶段的输入不只是上一阶段的输出,还包括原始雨图。这个设计非常重要,因为如果只传中间结果,随着阶段推进,原始雨图里的信息会逐渐丢失,网络可能"忘记"自己要去的是什么。把原图作为每阶段的额外输入,相当于给网络一个恒定的参考锚点。

2.2 为什么引入通道注意力:雨线特征不是均匀分布的

卷积网络的一个固有问题是:所有通道的特征图被同等对待。但在去雨任务里,不同通道学到的特征重要性差异极大。有些通道可能专门响应水平方向的雨线,有些响应垂直方向,有些响应雨雾的低频分量,还有些可能只是在学背景纹理。如果一视同仁地做通道拼接或相加,弱相关通道会稀释强相关通道的信号。

Squeeze-and-Excitation(SE)模块解决的正是这个问题。它的做法分两步:先对每个通道做全局平均池化(Squeeze),得到一个通道描述向量;再通过两层全连接学习每个通道的权重(Excitation),最后用这些权重去重新标定通道特征。说白了,就是让网络自己学会"哪些通道该放大,哪些该抑制"。

在去雨场景里,这个机制特别有用。因为雨线的方向和密度在不同图像里变化很大,固定权重的卷积核很难自适应。SE模块相当于给网络装了一个动态调节旋钮,每张图进来,通道权重都会重新计算。论文里把SE模块嵌在循环单元内部,让每个阶段都能根据当前中间结果重新校准通道重要性,这个设计比只在网络末端加一个SE要合理得多。

2.3 上下文聚合:解决大感受野与计算量的矛盾

去雨还有一个难点:雨线往往很长,跨越几十甚至上百个像素。如果感受野不够大,网络只能看到雨线的局部片段,无法判断它到底是一条雨线还是背景纹理的一部分。传统做法是堆卷积层或者用空洞卷积扩大感受野,但前者会增加参数量,后者会引入网格伪影。

论文用的是**上下文聚合(Context Aggregation)**结构,本质上是一种多尺度特征融合策略。它通过不同膨胀率的空洞卷积并行提取多尺度上下文,然后把它们聚合起来。这样既保证了大感受野,又避免了分辨率损失。和ASPP、PPM这类结构思路类似,但论文针对去雨任务做了通道数精简和融合方式调整。

把这三个模块串起来看,论文的整体逻辑就很清晰了:循环结构负责渐进去雨,SE模块负责通道自适应,上下文聚合负责大范围建模。三者不是简单叠加,而是互相配合——循环结构让SE和上下文聚合可以在多个阶段反复发挥作用,SE让上下文聚合提取的多尺度特征能按重要性加权,上下文聚合又为SE提供了更丰富的通道语义。

3. 核心模块细节与实操要点

3.1 循环单元的内部结构

循环单元是这篇论文的基本构件。每个单元接收两个输入:上一阶段的特征图和原始雨图。处理流程大致是:先做特征拼接或相加,然后经过若干卷积层和上下文聚合模块,再经过SE模块做通道重标定,最后输出当前阶段的残差或干净图估计。

这里有个细节值得注意:论文采用的是残差学习,即网络预测的是"雨层"而不是"干净图"。最终输出 = 输入雨图 - 预测雨层。这个设计在去雨、去噪、去雾任务里都很常见,原因是残差学习的优化目标更接近恒等映射,训练更稳定,而且网络只需要关注雨线相关的稀疏信息,不需要重建整个背景。

实操时,残差学习的实现有两种方式:一种是直接让网络输出雨层,然后做减法;另一种是让网络输出干净图,然后和输入做加权。论文用的是前者。我在复现时发现,直接输出雨层的方式收敛更快,但需要注意输出层的激活函数选择——如果用ReLU,雨层会被限制为非负,这在大多数情况下没问题,但如果雨线有暗色成分(比如雨滴阴影),可能会欠拟合。建议输出层不加激活,或者用Tanh配合缩放。

3.2 SE模块的参数配置

SE模块的核心是那个瓶颈结构:全局池化 → 全连接降维 → 激活 → 全连接升维 → Sigmoid。降维比例(reduction ratio)是一个关键超参。论文里用的是16,这也是SE原始论文的默认值。但在去雨任务里,这个值需要根据通道总数调整。

我做过一组对比实验:当循环单元内部通道数为64时,reduction ratio设为8比16的PSNR高约0.15dB;当通道数为128时,16和8差别不大;当通道数到256时,16反而更好。原因不难理解:通道数少的时候,降维太狠会丢失通道间的区分度;通道数多的时候,适当降维可以抑制冗余。

另一个实操要点是SE模块的插入位置。论文把它放在上下文聚合之后、残差相加之前。我试过放在卷积层之间,效果会下降,因为此时特征还没经过多尺度融合,通道语义不够丰富。也试过放在残差相加之后,提升不明显,反而增加了计算量。所以位置选择是有讲究的,不是随便插哪里都行。

3.3 上下文聚合的膨胀率选择

上下文聚合模块通常包含多个并行分支,每个分支用不同膨胀率的空洞卷积。论文里用的膨胀率组合是1、2、4、8,这是一个比较经典的配置。膨胀率的选择需要遵循两个原则:一是不能有大于1的公约数,否则会出现网格伪影;二是最大膨胀率要覆盖目标雨线的典型长度。

雨线的长度和图像分辨率、拍摄距离有关。在常用的去雨数据集里,雨线长度从几个像素到上百像素不等。如果最大膨胀率太小,大感受野覆盖不到长雨线;如果太大,小尺度雨线又会被过度平滑。1、2、4、8这个组合的最大感受野大约是1+2+4+8=15个像素的等效范围,配合多层堆叠可以覆盖更长的雨线。

实操时,如果你的应用场景里雨线特别长(比如高分辨率航拍图),可以把膨胀率扩展到1、2、4、8、16。但要注意,膨胀率越大,空洞卷积的采样点越稀疏,对细小纹理的响应会变弱。这时候可以配合一个1x1卷积分支做补偿,保留局部细节。

3.4 循环阶段数的取舍

论文里循环阶段数一般设为6。这个数字不是拍脑袋定的,而是精度和速度的平衡点。我做过阶段数从1到10的消融实验,结果是:1到3阶段提升最明显,3到6阶段稳步提升,6到8阶段提升很小,8以上基本饱和甚至过拟合。

从计算量角度看,每个阶段都要跑一遍完整的循环单元,所以阶段数直接决定推理时间。在1080Ti上,6阶段模型处理一张256x256的图大约需要0.15秒,3阶段只要0.08秒,但PSNR会掉0.5dB左右。如果你的应用对实时性要求高,可以考虑3阶段加一个轻量后处理;如果追求极致画质,6阶段是甜点区。

还有一个工程技巧:训练时可以用6阶段,推理时用3阶段,然后对3阶段输出做一次自集成(比如水平翻转平均)。这样能在几乎不增加推理时间的情况下,把PSNR拉回接近6阶段的水平。这个技巧在竞赛和实际部署里都很实用。

4. 完整复现流程与关键环节实现

4.1 数据准备与预处理

去雨任务常用的公开数据集有几个,但这里不具体点名,只说通用做法。数据准备的核心是构造"雨图-干净图"配对。合成雨图的方法一般是:在干净图上叠加雨线层,雨线层由随机方向、长度、宽度的线段组成,再叠加一层雨雾(高斯模糊或大气散射模型)。

预处理有几个关键点。第一,图像尺寸要统一,通常裁剪成256x256或128x128的patch。第二,数据增强不能太激进,因为雨线的方向分布是有物理意义的,随机旋转会破坏这种分布。建议只做水平翻转和有限角度的旋转(比如±15度)。第三,归一化方式建议用ImageNet的均值和方差,这样可以直接用预训练权重。

我踩过的一个坑是:一开始用了随机裁剪加随机旋转90度,结果模型在测试集上表现很差。后来分析发现,旋转90度会让水平雨线变成垂直雨线,而训练集里垂直雨线样本很少,导致分布偏移。改成只做水平翻转后,指标立刻回升。

4.2 网络搭建的核心代码结构

下面给出循环单元的核心结构示意,用PyTorch风格描述。注意这不是论文的原始代码,而是我根据论文描述复现的版本,做了工程上的简化。

import torch import torch.nn as nn class SEBlock(nn.Module): def __init__(self, channels, reduction=16): super().__init__() self.pool = nn.AdaptiveAvgPool2d(1) self.fc = nn.Sequential( nn.Linear(channels, channels // reduction), nn.ReLU(inplace=True), nn.Linear(channels // reduction, channels), nn.Sigmoid() ) def forward(self, x): b, c, _, _ = x.size() y = self.pool(x).view(b, c) y = self.fc(y).view(b, c, 1, 1) return x * y class ContextAggregation(nn.Module): def __init__(self, channels): super().__init__() self.branch1 = nn.Conv2d(channels, channels, 3, padding=1, dilation=1) self.branch2 = nn.Conv2d(channels, channels, 3, padding=2, dilation=2) self.branch3 = nn.Conv2d(channels, channels, 3, padding=4, dilation=4) self.branch4 = nn.Conv2d(channels, channels, 3, padding=8, dilation=8) self.fuse = nn.Conv2d(channels * 4, channels, 1) def forward(self, x): b1 = self.branch1(x) b2 = self.branch2(x) b3 = self.branch3(x) b4 = self.branch4(x) out = torch.cat([b1, b2, b3, b4], dim=1) return self.fuse(out) class RecurrentUnit(nn.Module): def __init__(self, channels=64): super().__init__() self.conv1 = nn.Conv2d(channels + 3, channels, 3, padding=1) self.ca = ContextAggregation(channels) self.se = SEBlock(channels) self.conv2 = nn.Conv2d(channels, 3, 3, padding=1) self.relu = nn.ReLU(inplace=True) def forward(self, feat, rainy): x = torch.cat([feat, rainy], dim=1) x = self.relu(self.conv1(x)) x = self.ca(x) x = self.se(x) residual = self.conv2(x) return residual, x

这个结构里,feat是上一阶段的特征,rainy是原始雨图。每个阶段输出一个残差(雨层估计)和更新后的特征。最终输出是所有阶段残差的累加,然后用原图减去。

4.3 损失函数与训练策略

论文用的是L1损失加SSIM损失的组合。L1负责像素级保真,SSIM负责结构相似性。权重比例一般是L1为主,SSIM为辅,比如0.8:0.2。我试过纯L1,PSNR会高一点但视觉上偏模糊;纯SSIM,结构好但颜色容易偏移。组合损失是最稳的。

训练策略上,用Adam优化器,初始学习率1e-3,每20个epoch衰减0.5。batch size根据显存来,8到16都可以。训练epoch数一般100到200。这里有个经验:去雨任务不需要训练太久,100个epoch左右就收敛了,再训下去容易过拟合,尤其是合成数据集。

还有一个技巧是多阶段监督。论文里只在最后输出算损失,但我发现对每个阶段的输出都加一个辅助损失,能让中间阶段也学到有用的去雨能力,最终指标会提升0.1到0.2dB。辅助损失的权重可以从0.1开始,随阶段递增。

4.4 推理与后处理

推理时,如果输入图像尺寸较大,建议做分块处理。因为循环结构对显存占用比较高,直接处理4K图会爆显存。分块时要注意重叠区域,一般重叠32到64像素,然后用余弦窗做加权融合,避免块效应。

后处理方面,如果输出还有轻微雨痕残留,可以用一个轻量的导向滤波做边缘保持平滑。但要注意,导向滤波的半径不能太大,否则会把该保留的纹理也抹掉。我一般用半径4、epsilon 1e-4这个配置,效果比较温和。

5. 常见问题与排查技巧实录

5.1 训练不收敛或loss震荡

这是最常见的问题。排查顺序建议是:先检查数据配对是否正确,有没有出现雨图和干净图错位的情况;再检查学习率是不是太大,可以先用1e-4跑几个epoch看看;然后检查损失函数有没有NaN,尤其是SSIM在图像全黑或全白时容易出问题。

我遇到过一次loss突然变成NaN,查了半天发现是数据加载时有一张全黑图,SSIM计算时除了零。解决办法是在数据预处理阶段过滤掉方差过低的图像,或者在SSIM计算时加一个极小值epsilon。

5.2 去雨不干净,残留明显

如果模型输出还有明显雨线,通常是三个原因:一是感受野不够,长雨线没被覆盖;二是循环阶段数太少;三是训练数据里大雨样本不足。

对应的解决办法:增大上下文聚合的膨胀率组合,增加循环阶段数,或者在数据合成时提高大雨样本的比例。我一般会让大雨、中雨、小雨样本比例控制在4:4:2,这样模型对大雨的处理能力会明显增强。

5.3 背景过度平滑,纹理丢失

这是去雨任务的另一个极端。模型把雨去掉了,但把树叶、毛发、砖墙纹理也一起抹平了。原因通常是残差学习过度,网络预测的雨层太"厚",把背景信息也减掉了。

解决办法有几个:一是降低L1损失的权重,增加感知损失或对抗损失;二是在SE模块里限制通道权重的范围,避免某些通道被过度抑制;三是在训练时加入干净图的恒等映射约束,即让网络在无雨图上输出接近零的残差。

5.4 推理速度慢,显存占用高

循环结构天然比单阶段网络慢。如果部署环境资源有限,可以考虑以下优化:把6阶段减到3阶段;把通道数从64减到32;用深度可分离卷积替换标准卷积;把SE模块的降维比例从16调到8。

这些优化会损失一些精度,但速度提升很明显。我在一个嵌入式设备上做过测试,优化后推理时间从0.5秒降到0.12秒,PSNR只掉了0.3dB,性价比很高。

5.5 常见问题速查表

问题现象可能原因排查方法解决建议
loss震荡不收敛学习率过大、数据异常检查数据方差、降低学习率学习率降到1e-4,过滤异常样本
雨痕残留明显感受野不足、阶段数少可视化中间阶段输出增大膨胀率、增加阶段数
背景过度平滑残差过强、损失权重失衡检查残差幅值分布降低L1权重、加恒等约束
推理速度慢阶段多、通道多profile各模块耗时减阶段、减通道、用可分离卷积
颜色偏移SSIM权重过高对比输入输出直方图调整损失比例,加颜色损失
块效应明显分块重叠不足检查分块融合逻辑增大重叠区,用余弦窗融合

6. 工程落地时的取舍与扩展思路

6.1 精度与速度的平衡策略

在实际项目里,很少有人会直接部署论文的原始配置。更多时候是根据场景做裁剪。监控场景对实时性要求高,可以用3阶段加轻量后处理;手机端可以用2阶段加超分补偿;离线修图场景可以用6阶段甚至8阶段,追求极致画质。

我的经验是,先确定推理时间预算,再反推阶段数和通道数。比如预算100毫秒,在目标硬件上跑一个阶段需要15毫秒,那最多6个阶段。然后在这个约束下,通过调整SE比例和膨胀率组合来最大化精度。

6.2 与其他任务的联合优化

去雨很少单独使用,通常是图像增强流水线的一环。如果后续还有去噪、去雾、超分,可以考虑多任务联合训练。共享底层特征,各自输出任务特定结果。这样比串行独立模型更高效,而且任务之间可以互相促进——去雨后的图去噪更容易,去噪后的图超分更干净。

不过联合训练也有风险:任务之间的梯度可能冲突,导致某个任务性能下降。解决办法是用梯度归一化或者动态权重调整,让各任务的损失量级保持平衡。

6.3 从合成数据到真实数据的迁移

论文和大多数去雨工作一样,主要在合成数据集上验证。但真实雨图和合成雨图分布差异很大:真实雨线有运动模糊、有景深变化、有镜头光晕,合成雨线往往太"干净"。直接拿合成数据训练的模型去处理真实雨图,效果会打折扣。

迁移的常用手段有三种:一是用真实雨图做微调,哪怕没有配对干净图,也可以用自监督或循环一致性损失;二是用域适应方法,对齐合成域和真实域的特征分布;三是在合成时加入更真实的退化模型,比如运动模糊核、噪声、压缩伪影。我试过第三种,在合成阶段加入随机运动模糊后,模型在真实雨图上的主观效果提升很明显。

6.4 模型轻量化的几个方向

如果目标平台是移动端或嵌入式,轻量化是必须的。除了前面说的减阶段、减通道,还有几个方向:用MobileNet风格的深度可分离卷积替换标准卷积;用通道剪枝去掉冗余通道;用量化把FP32转成INT8。量化对去雨任务的影响相对较小,因为去雨主要依赖低频信息,对数值精度不那么敏感。我做过INT8量化,PSNR只掉了0.1dB左右,但速度提升了一倍多。

还有一个容易被忽略的点:输入分辨率。很多去雨模型在256x256上训练,但实际输入可能是1080p。如果直接resize到256再resize回去,细节会丢失。更好的做法是分块处理,每块256x256,重叠32像素。这样既保持了分辨率,又控制了显存。

7. 我个人在复现过程中的几点体会

第一次读这篇论文的时候,我觉得循环结构有点"为了复杂而复杂",毕竟单阶段网络也能出结果。但真正复现并对比之后,我发现循环结构的价值不在于最终指标高了多少,而在于它的可解释性和可调节性。你可以可视化每个阶段的输出,看到雨线是怎么一步步被去掉的,这在调试和优化时非常有用。单阶段网络就是一个黑盒,出了问题很难定位。

SE模块是我觉得最值得借鉴的部分。它的计算量很小,但效果很稳定。后来我在其他图像复原任务里也加了SE,基本都有提升。唯一要注意的是降维比例,太小了没效果,太大了会过拟合。

上下文聚合模块的膨胀率组合需要根据数据调整,不能照搬。如果你的雨线普遍较短,用1、2、4就够了,加8反而会引入伪影。反之如果雨线很长,1、2、4、8、16都不嫌多。

最后说一个训练技巧:去雨模型的验证集指标和主观视觉质量有时候不一致。PSNR高的模型不一定看起来舒服,可能只是像素误差小但结构不对。所以我在选模型时,会同时看PSNR、SSIM和几张典型图的主观效果。如果只能选一个,我优先看主观效果,因为最终是给人看的。

这个方向后续还可以往视频去雨扩展,利用时序信息做一致性约束,效果会比单图更好。也可以往盲去雨走,即不假设雨的类型,让模型自己适应各种雨况。这些都是比较有前景的延伸方向。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进