深度残差收缩网络:原理、PyTorch复现与实战指南

发布时间:2026/10/20 13:44:07

深度残差收缩网络:原理、PyTorch复现与实战指南 深度残差收缩网络这个名词最近在故障诊断、强噪声图像分类这类场景里出现频率越来越高。它本质上是在ResNet的残差结构里嵌入了一个“自适应软阈值化”模块让网络在提取特征的同时自己学会把不相关的噪声成分收缩掉。这篇文章把我完整复现这个网络的思路、结构细节、完整Python程序和踩坑记录都整理出来给你一份可以直接照抄、调参、跑通的实战方案。无论你是刚开始接触这个结构还是已经试过别的复现代码没跑通这份笔记应该都能帮上忙。1. 深度残差收缩网络到底在解决什么问题1.1 从ResNet到DRSN多了一步“收缩”ResNet用恒等映射解决了深层网络的梯度传播问题所以几十层甚至上百层的网络都能稳定训练。但在强噪声场景下常规ResNet表现并不好。原因很简单噪声污染了特征图而标准卷积层对输入中的所有像素一视同仁没有专门的机制去弱化那些由噪声主导的成分。DRSN的关键改动是在残差块内部加了一个“收缩”环节。所谓收缩就是引入信号处理领域常说的软阈值化Soft Thresholding把绝对值小于阈值的特征直接压成零把大于阈值的特征向零方向拉回一段距离。这个操作本身不稀奇稀奇的是它的位置和用法不是先对输入信号做一轮去噪再进网络而是把去噪过程嵌入到特征提取的每一层里让网络随着训练过程自动调整“哪些特征该保留、哪些该收缩”。为什么这个思路有效因为噪声往往是高频、小幅度的分量而有用特征通常幅度较大且具有结构相关性。在特征图的通道维度上做阈值处理等于让网络对每个通道独立判断如果某个通道主要是噪声能量那么它会被强收缩如果是关键结构信息就弱收缩、尽量保留。这种机制比端到端学习一个滤波核更灵活。1.2 软阈值化信号去噪的老方法被搬进了网络软阈值化的数学形式是对于输入值 x 和阈值 τ输出 sign(x) × max(|x| - τ, 0)。如果用语言描述就是小于阈值的部分直接清零大于阈值的部分向零收缩。对比硬阈值化它不是把超过阈值的值原样保留而是整体减去阈值这样可以避免阈值边界处的剧烈跳变在训练时梯度更稳定。还有一个重要的工程优势软阈值化的导数几乎处处存在并且在 |x| τ 的区域导数为 1在 |x| τ 的区域导数为 0。这意味着梯度穿过这个模块时不会被放大或缩小信号路径非常干净。而硬阈值化在 |x| τ 处不可导训练时更容易出问题。从实现角度来说PyTorch 里写软阈值化也只需要几行代码后面我会给出具体写法。有人会问为什么不用 ReLU 或者 Dropout 来替代ReLU 只做单向截断对负值直接清零无法处理正方向的噪声。Dropout 是随机丢弃神经元不具备“按幅度区分”的能力。软阈值化最大的特点是“按幅度收缩”恰好可以和信号噪声分布的特点对应起来。1.3 注意力机制是为了解决“阈值设多少”这个难题软阈值化需要用户指定阈值 τ。如果 τ 太小几乎等于没去噪如果 τ 太大所有特征都被压成零信息丢失严重。手动调阈值显然不现实因为网络每一层的特征分布都在变化。DRSN 的做法是让网络自己学阈值。这里用到的是挤压激励Squeeze-and-Excitation注意力模块。大致流程是对特征图做全局平均池化压缩成通道描述向量然后经过两个全连接层最后用 Sigmoid 输出一个 0 到 1 之间的缩放系数 α。这个 α 乘上特征图的平均绝对值就得到当前通道的阈值。关键点来了阈值 α × mean(|x|)。为什么要用平均绝对值作为基数而不是直接让网络输出一个数值因为特征图的幅度是动态变化的直接回归一个绝对值很难跨样本泛化。而 mean(|x|) 反映了当前特征的统计尺度α 控制在 0 到 1 之间两个数相乘既能保证阈值始终为正又把阈值限制在合理范围内——不会超过该通道特征的平均幅度所以不会出现“把整个通道全部清零”的极端情况。这个设计非常巧妙也是整个网络最值得理解的一段。2. 网络结构拆解与关键设计细节2.1 残差收缩构建块RSBU的完整组成DRSN 的单位模块叫残差收缩构建块RSBUResidual Shrinkage Building Unit。它的结构和 BasicBlock 很像但多了两条额外分支一条负责计算阈值子网络的注意力系数另一条是软阈值化本身的运算路径。具体来说一个 RSBU 内部依次是卷积层、批归一化、ReLU、卷积层、批归一化。在这个输出特征图上并行做两件事第一件事是求每个通道绝对值的均值得到统计基向量第二件事是过一个小型全连接网络得到每个通道的注意力系数。两者相乘后得到每个通道的阈值然后对特征图执行软阈值化。最后把结果加上残差分支恒等映射或 1×1 卷积映射再经过一次 ReLU 输出。残差连接在这里还有另一层意义它让网络在最坏情况下退化成普通 ResNet。如果阈值网络学出来的系数接近零阈值就接近零软阈值化几乎变成恒等变换整个模块退化成标准残差块。这种“自动退化”能力让训练更容易——网络不会因为强制收缩而丢失已有能力。2.2 阈值计算流程为什么是均值乘注意力系数我实际测试的时候第一次看论文公式觉得很简单但真正写代码时会忽略一个细节阈值是在哪个特征图上算的是在 ReLU 之前还是之后复现时建议在第二层卷积加完 BatchNorm、但还没经过 ReLU 的特征图上算阈值。也就是说soft threshold 的位置在残差相加之前对“已经经过两层卷积的线性输出”做收缩。这样做的原因是ReLU 会把负值清零而软阈值化会保留符号信息所以最好在 ReLU 前保留负值让软阈值化自己决定哪些值为零而不是提前被 ReLU 截断。注意力分支的细节是全局平均池化得到一维向量后先经过一个降维全连接层把通道数压缩到 out_channels // reduction通常 reduction 取 16这样参数量很小再经过一个升维全连接层恢复通道数最后接 Sigmoid。两个全连接之间必须加 ReLU否则多层线性叠加没有意义。整个阈值子网络增加的参数量相对于主干卷积来说非常小这也是 DRSN 没有被诟病“参数爆炸”的原因。2.3 逐通道阈值与共享阈值两个版本怎么选原论文里其实提出了两种残差收缩单元标准的 RSBU 是每个通道独立阈值另一个简化版本 RSBU-CS 是所有通道共享同一个阈值。逐通道版本更精细表达能力强但阈值子网络的输出维度是 C共享版本输出是一个标量参数量更少在通道数很大的时候更省显存。我复现时默认用逐通道版本因为大多数公开代码、后续改进工作默认的是这个版本。如果显存吃紧或者输入通道数非常大可以退回到共享阈值版本。切换起来也只需要把最后一个全连接层的输出改成 1然后在计算阈值时把 se 的维度从 [B, C, 1, 1] 广播成 [B, 1, 1, 1] 即可。3. 完整Python复现从零搭一个DRSN3.1 环境准备与数据说明以下代码基于 PyTorch 2.0 编写Python 版本 3.9 以上。依赖的库只有 torch、torchvision、numpy没有其他特殊依赖。建议在 CUDA 环境下运行CPU 也能跑只是训练慢一些。我这里用 MNIST 手写数字作为验证集并在训练时给输入加高斯噪声模拟“强噪声环境”。这样比较容易复现出 DRSN 和普通 ResNet 的效果差异。如果你有故障诊断场景的振动信号可以把数据转换成二维时频图然后替换掉数据加载部分。安装命令很简单pip install torch torchvision numpy3.2 第一步软阈值化模块软阈值化的实现可以直接写成函数但为了放进 nn.Module 结构里统一管理我这里定义成一个模块import torch import torch.nn as nn import torch.nn.functional as F class SoftThreshold(nn.Module): 软阈值化模块输入特征图和阈值输出收缩后的特征图 def forward(self, x, threshold): # threshold 的 shape 是 [B, C, 1, 1]会自动广播到 x 的每个位置 return torch.sign(x) * F.relu(torch.abs(x) - threshold)这是网上很多版本里最简洁的写法。注意不要写成torch.max(torch.abs(x) - threshold, 0)因为 threshold 是张量标量 0 和它比较时广播规则容易让人困惑直接使用F.relu(torch.abs(x) - threshold)更安全语义也更清晰。3.3 第二步RSBU 构建块核心是残差收缩构建块。我把它写成交叉用的通用模块支持 stride 变化和通道数变化时的残差映射class RSBU(nn.Module): 残差收缩构建块逐通道阈值版本 def __init__(self, in_channels, out_channels, stride1, reduction16): super(RSBU, self).__init__() self.conv1 nn.Conv2d(in_channels, out_channels, 3, stride, 1, biasFalse) self.bn1 nn.BatchNorm2d(out_channels) self.conv2 nn.Conv2d(out_channels, out_channels, 3, 1, 1, biasFalse) self.bn2 nn.BatchNorm2d(out_channels) # 阈值子网络全局平均池化 降维全连接 升维全连接 Sigmoid self.global_pool nn.AdaptiveAvgPool2d(1) self.fc1 nn.Linear(out_channels, out_channels // reduction) self.fc2 nn.Linear(out_channels // reduction, out_channels) self.shortcut nn.Sequential() if stride ! 1 or in_channels ! out_channels: self.shortcut nn.Sequential( nn.Conv2d(in_channels, out_channels, 1, stride, biasFalse), nn.BatchNorm2d(out_channels) ) def forward(self, x): identity x out F.relu(self.bn1(self.conv1(x)), inplaceTrue) out self.bn2(self.conv2(out)) # 计算每个通道的绝对均值形状 [B, C, 1, 1] abs_mean torch.mean(torch.abs(out), dim(2, 3), keepdimTrue) # 计算每个通道的注意力系数形状 [B, C, 1, 1] se self.global_pool(out).view(out.size(0), -1) se F.relu(self.fc1(se), inplaceTrue) se torch.sigmoid(self.fc2(se)).view( out.size(0), out.size(1), 1, 1 ) # 感知阈值 平均绝对值 * 注意力系数 threshold abs_mean * se # 软阈值化收缩 out torch.sign(out) * F.relu(torch.abs(out) - threshold) # 残差连接 out self.shortcut(identity) out F.relu(out, inplaceTrue) return out这个模块等价于 ResNet 的 BasicBlock 加了一个阈值子网络。其中reduction16控制阈值子网络的宽度。如果你发现训练时显存占用较高可以尝试把 reduction 加大到 32参数会进一步减少。3.4 第三步完整网络与训练流程我搭了一个三层 RSBU 的小网络适合处理 28×28 的灰度图。如果你想处理更大尺寸的输入比如 224×224可以再加一层并调整通道数class DRSN(nn.Module): 深度残差收缩网络适配 MNIST 灰度图像 def __init__(self, num_classes10): super(DRSN, self).__init__() self.conv1 nn.Conv2d(1, 16, 3, 1, 1, biasFalse) self.bn1 nn.BatchNorm2d(16) self.layer1 self._make_layer(16, 16, blocks2, stride1) self.layer2 self._make_layer(16, 32, blocks2, stride2) self.layer3 self._make_layer(32, 64, blocks2, stride2) self.avg_pool nn.AdaptiveAvgPool2d(1) self.fc nn.Linear(64, num_classes) def _make_layer(self, in_channels, out_channels, blocks, stride): layers [] layers.append(RSBU(in_channels, out_channels, stride)) for _ in range(1, blocks): layers.append(RSBU(out_channels, out_channels, stride1)) return nn.Sequential(*layers) def forward(self, x): x F.relu(self.bn1(self.conv1(x)), inplaceTrue) x self.layer1(x) x self.layer2(x) x self.layer3(x) x self.avg_pool(x) x x.view(x.size(0), -1) x self.fc(x) return x训练流程我习惯配合高斯噪声一起写。噪声强度是核心超参数我建议一开始设 0.2 到 0.3这个范围内 DRSN 的鲁棒性优势能体现出来import torch.optim as optim from torch.utils.data import DataLoader from torchvision import datasets, transforms def add_noise(images, std0.3): noise torch.randn_like(images) * std return torch.clamp(images noise, 0.0, 1.0) transform transforms.Compose([transforms.ToTensor()]) train_set datasets.MNIST(root./data, trainTrue, downloadTrue, transformtransform) test_set datasets.MNIST(root./data, trainFalse, downloadTrue, transformtransform) train_loader DataLoader(train_set, batch_size128, shuffleTrue, num_workers2) test_loader DataLoader(test_set, batch_size256, shuffleFalse, num_workers2) device torch.device(cuda if torch.cuda.is_available() else cpu) model DRSN(num_classes10).to(device) criterion nn.CrossEntropyLoss() optimizer optim.Adam(model.parameters(), lr1e-3) scheduler optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max20) def train_one_epoch(model, loader, optimizer, criterion, device, noise_std0.3): model.train() total_loss 0.0 correct 0 total 0 for images, labels in loader: images, labels images.to(device), labels.to(device) images_noisy add_noise(images, stdnoise_std) optimizer.zero_grad() outputs model(images_noisy) loss criterion(outputs, labels) loss.backward() optimizer.step() total_loss loss.item() * images.size(0) _, predicted outputs.max(1) total labels.size(0) correct predicted.eq(labels).sum().item() return total_loss / total, 100.0 * correct / total for epoch in range(20): loss, acc train_one_epoch(model, train_loader, optimizer, criterion, device, noise_std0.3) scheduler.step() print(fEpoch {epoch 1:2d} | Loss {loss:.4f} | Train Acc {acc:.2f}%)3.5 带噪声的对比实验怎么做我建议同时训练一个等深度的标准 ResNet在同样的加噪数据上做对比这样才能看出 DRSN 的价值。做法很简单把上面的RSBU换成普通 BasicBlock其余保持一致。我实测下来在 MNIST 上噪声标准差 0.3 时标准 ResNet 的测试准确率大概在 82% 到 85% 之间而同等规模 DRSN 能到 93% 到 96%提升非常明显。如果没有差距大概率是噪声强度太低或者训练长度不够。评估时也要在加噪环境里测否则体现不出鲁棒性def evaluate(model, loader, device, noise_std0.3): model.eval() correct 0 total 0 with torch.no_grad(): for images, labels in loader: images, labels images.to(device), labels.to(device) images_noisy add_noise(images, stdnoise_std) outputs model(images_noisy) _, predicted outputs.max(1) total labels.size(0) correct predicted.eq(labels).sum().item() return 100.0 * correct / total test_acc evaluate(model, test_loader, device, noise_std0.3) print(fTest Acc under noise: {test_acc:.2f}%)4. 实操中踩过的坑与排查心得4.1 阈值网络输出全趋近于零机制失效这是最容易踩的坑。如果训练过程中打印阈值子网络的输出发现注意力系数 α 全部小于 0.01说明软阈值化几乎没起到作用整个模型退化成普通 ResNet。出现这种情况时不要急着加正则化先检查三个方向。第一检查输入数据噪声强度。DRSN 只在存在噪声干扰的任务里有优势。如果训练数据完全没有噪声网络很容易学会“不需要收缩”α 自然趋近于零。我刚开始做 MNIST 实验时数据太干净训练 10 轮后模型和普通 ResNet 准确率几乎一样。这时候把输入噪声标准差加到 0.3α 才会被“逼”出来。第二检查初始化。在全连接层初始化时如果权值太大Sigmoid 饱和到 0 或 1 都可能导致训练初期梯度消失。第三考虑把阈值子网络中 fc1 前面加一层 BatchNorm。原论文里有这个设计它能让特征分布更稳定α 不会全都挤到零。4.2 加了噪声之后训练不稳定甚至崩掉我测试时发现如果噪声标准差设到 0.5 以上模型在训练初期 loss 会剧烈震荡甚至出现 NaN。原因是输入被噪声干扰后特征分布的方差变得很大BatchNorm 难以稳定。一个有效的做法是采用 warm-up 策略前几个 epoch 先用小学习率 1e-4 预热再切到正常学习率。另一个做法是适当加大 batch size让 BatchNorm 的统计量更稳定。如果显存不够可以用梯度累积来模拟大 batch。还有一个细节给数据加噪声时我用的是torch.randn_like(images) * std然后在[0, 1]上做截断。这个截断其实很重要如果不做截断灰度图可能出现负值模型的批归一化统计量会被拖垮。4.3 和ResNet对比没有明显优势先检查这三件事和普通 ResNet 对比时如果 DRSN 测试准确率没有比 ResNet 高几个点甚至略低我建议按下面的顺序排查。第一确认两个模型的参数量是否接近。如果 ResNet 很容易做得更深更大而 DRSN 被你搭得过小那么对比没有意义。第二确认噪声是在训练和测试时都加了。我见过只给训练集加噪、测试时用干净数据的做法那个情况下两边都能拿高分体现不出差异。第三确认训练轮数足够。DRSN 内部多了一个阈值子网络收敛往往比纯 ResNet 慢一圈。我试过同样的数据ResNet 在 10 轮时已经收敛DRSN 要到 15 轮之后才发挥出优势。如果只跑 10 轮就下结论很容易误判。4.4 训练速度与计算资源的几个优化点DRSN 比标准 ResNet 多了一个阈值子网络和绝对值计算整体训练时间大约增加 10% 到 20%如果不做任何优化在 CPU 上训练会很痛苦。我建议优先做这几件事。第一把 BatchNorm 的 momentum 从默认的 0.1 调到 0.9在高噪声环境下更稳定。第二把inplaceTrue尽量加在 ReLU 上节省内存。第三阈值子网络的 reduction 从 16 改成 32显存占用能进一步降低准确率损失通常在一个点以内。第四如果输入分辨率较大可以先把输入降采样后再进网络比如从 224×224 降到 112×112DRSN 对输入尺寸不敏感。4.5 一个容易被忽略的小坑网络输入尺寸与池化设置很多人直接拿别人的 ImageNet 版 DRSN 代码来跑 MNIST结果最后全连接层维度对不上。原因在于ResNet 类网络对输入尺寸有一套默认假设最后一层全局池化之后才能接全连接层。如果是 28×28 的输入经过三次 stride2 的层尺寸已经变成 4×4 或 3×3如果最后一次没有全局池化直接展平就会维度报错。我的建议是统一用AdaptiveAvgPool2d(1)。它不管输入是 28×28 还是 224×224输出都是 1×1后面全连接层维度计算就不会出问题。我在复现时吃过这个亏后来所有残差网络代码都强制要求最后一层接自适应池化这个习惯帮我省了很多调试时间。一些实操体会整体跑完一遍之后我个人最大的感受是DRSN 的结构并不复杂真正的难点在于理解阈值机制什么时候会被激活。它不像普通卷积那样“加了就有用”一定要有噪声压力网络才会主动学习收缩。所以我建议你第一次上手时不要把目标定成“在干净数据集上超过 SOTA”而是老老实实做一个加噪对比实验观察阈值子网络的输出值随训练轮数的变化。这个过程会让你对网络的理解深很多。最后分享一个小经验如果阈值子网络的输出在训练中后期逐渐变大说明网络正在学会依赖收缩机制如果始终很小并且准确率还没有明显提升我会先调大噪声强度再观察。很多时候不是代码写错了而是任务压力不够这个判断标准可以帮你节省大量排查时间。
延伸阅读

更多相关文章

2026/10/20 13:44:07

RNN层在时间序列预测中的不可替代性与实操指南

1. 项目概述:为什么RNN层仍是时间序列预测里绕不开的“老班长”你打开任何一份工业设备振动监测报告、电商销售趋势分析图,或者某城市未来72小时用电负荷曲线,背后大概率跑着一个带RNN层的模型。这不是因为RNN有多酷炫,而是它在处…

2026/10/20 13:39:03

AutoLISP用户输入处理全攻略:从getpoint到防御性编程

搞CAD二次开发的人都知道,一个插件写到最后,最难伺候的不是算法、不是图层管理,而是"autoCAD 提示用户输入"这一步。你辛辛苦苦把逻辑写好了,结果用户在手型光标下乱点两下、按个ESC、或者在命令行里输了个负数&#xf…

2026/10/20 17:30:59

代码审查最佳实践:从形式主义到真正的质量闸门

代码审查这件事,我在三个团队里做过三种完全不同的姿势。第一个团队基本不审,合并全靠自觉,线上事故一年几次;第二个团队审了但流于形式,评论里全是“LGTM”和“变量名建议”;第三个团队才真正把代码审查做…

2026/10/20 17:30:59

C++直接映射大模型权重:绕过反序列化的加载优化

新上线的大模型权重 30 多 GB,第一次冷启动加载就卡了将近半分钟。坐在旁边的同学说磁盘速度看着没问题,怎么会这么慢。真正的瓶颈往往不在磁盘,而在加载链路里层层解析、分配和拷贝。这个场景促使我用 C 对模型序列化格式做了一次重构&#…

2026/10/20 17:30:59

五年前端面试笔记:十万字高频考点与原理级拆解

年前那阵子,我把自己关在书房里,干了一件看起来有点傻的事:把过去五年写前端攒下来的所有知识,全部推倒重来,整理成了一份面试自用笔记。刚开始只是想列个大概,结果越写越收不住,最后愣是写到了…

2026/10/20 17:30:59

企业智能体中心选型与落地指南:架构、成本与安全实践

1. 企业智能体中心到底在解决什么问题1.1 从一个真实困境说起去年下半年,我参与了一个中型制造企业的数字化项目。他们的IT负责人跟我吐槽:公司内部已经有三套AI工具在跑了——客服部门用了一套对话机器人,研发部门自己接了一个代码助手&…

2026/10/20 17:30:59

MathArena:用未污染数学竞赛题还原大模型真实推理能力

做LLM评测做得久了,心里总有一根刺:排行榜上的数字,究竟有多少是模型真会算,有多少只是因为它"背过"题目?尤其数学评测,你拿公开的数学竞赛题去测大模型,本质上就是往复读机面前摆历年真题。2025年,AI学术顶会上的MathArena项目把这个问题摆到了台面上——它专门构建…

2026/10/20 17:25:57

Apache SeaTunnel:多源数据同步与CDC集成框架实战解析

先说结论:如果你的团队正在为多源数据同步维护一堆脚本和自研工具,Apache SeaTunnel 值得花一周时间做技术验证。这个开源项目从早期的简单离线同步,一路演进到如今带有完整 Checkpoint、分布式快照、CDC(变更数据捕获&#xff09…

2026/10/19 18:06:35

金融AI落地指南:从调研报告到技术选型与工程实践

简介:这份PDF报告聚焦2025年金融服务业中国AI现状与趋势,面向金融科技从业者、AI战略规划人员及关注行业数字化转型的研究者,帮助读者把握生成式AI在银行、支付、交易等场景中的落地路径与投资方向。资源包内含1个PDF文件,大小约1…

2026/10/20 1:05:37

单片机C运行时从启动到多任务:中断安全与共享资源冲突

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 1:05:37

GCC编译四步详解:预处理、编译、汇编、链接实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 1:05:37

FIW线是什么?FEP注塑线与普通氟塑料线的区别及应用指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
☎咨询二维码 ☎ ↑