新闻详情

新闻详情

首页 / 资讯中心 / 详情

ResNet-18深度解析:从残差思想到PyTorch实现与部署

发布时间:2026/10/1 13:58:31来源:尧图网络
ResNet-18深度解析:从残差思想到PyTorch实现与部署
去年帮一个团队把分类模型从端到端的Transformer换回ResNet-18原因是边缘设备上的推理延迟始终压不下来而换回ResNet-18之后latency直接少了40%精度只掉0.3个百分点。这件事让我对“经典网络”有了更实际的认知ResNet-18不是过时的玩具而是很多业务场景下真正能落地的选择。这篇文章我想系统地把ResNet-18拆开讲一遍包括它为了解决什么问题而诞生、18这个数字怎么数出来的、每个stage在做什么、PyTorch里怎么从零实现以及我自己在实际训练和部署中踩过的坑。无论你是刚入门深度学习想搞懂第一个真正好用的CNN结构还是做工程落地需要快速跑一个稳健的baseline这篇内容应该都能派上用场。1. 退化现象网络堆到一定深度后为什么反而变差了1.1 一个反直觉的实验结论在ResNet出现之前业界普遍觉得“网络越深表达能力越强效果应该越好”。毕竟深度网络理论上可以拟合更复杂的函数。但何恺明团队在CIFAR-10上做了一个非常直接的实验训练一个20层的CNN和一个56层的CNN结果发现56层网络的训练错误率和测试错误率都明显高于20层。关键点是“训练错误率也更高”。如果是过拟合训练集上的loss应该更低、测试集上更高才对。现在的情况是训练集上就输了说明问题不在泛化而在优化——深层网络在训练过程中本身就很难收敛到好的解。这个现象后来被称作退化问题Degradation Problem它不是梯度消失导致的完全不学习而是网络越深优化难度越大最后停在一个比较差的局部最优附近。1.2 根因排查梯度消失不是唯一答案很多人一听到“深层网络难训练”第一反应就是“梯度消失”。确实链式求导把很多小于1的梯度因子连乘起来传到浅层时梯度几乎为0浅层参数更新不动网络就废了。但ResNet论文那个时间点上Batch Normalization已经比较普及了BN能把每一层的激活值拉回相对正常的分布一定程度缓解了梯度消失。那为什么加了BN还是退化这就指向另一个问题深层网络的优化曲面极其复杂。权重初始化之后几十层非线性变换堆叠在一起信号在前向传播中会被不断放大或压缩。即使不爆炸各层之间的梯度尺度也可能差异巨大导致优化过程很不稳定。换句话说深层网络不是“学不会”而是“很难稳定地学会”。ResNet的残差结构恰恰是在这个方向上给出了解决方案。1.3 恒等映射的“默认值”思路退化问题最直接的推论是如果一个浅层网络已经能做到80%准确率那么在这个网络后面再加上若干层这些新增层最理想的行为就是什么都不做——保持输入等于输出这样深层网络至少不会比浅层差。问题在于让一堆带权重、带非线性激活的卷积层去“什么都不做”是非常困难的。你希望它们学到近似的恒等映射但卷积、ReLU这些操作天然不是恒等映射权重稍微偏一点输出就偏移了。ResNet的思路是把目标从“让层学习H(x)”改成“让层学习H(x)-x”或者说让层学习一个残差。如果最优映射确实是恒等映射那网络只需要把残差推到0就行——而把输出推向0比把权重推向一个精确的恒等映射要容易得多。这就是残差结构的核心哲学给网络一个简单的默认起点让每一层只需要学习相对于当前状态的增量变化。2. 残差单元的设计逻辑那个加号凭什么能拯救深层网络2.1 一个公式和一个加号残差单元的基本公式非常简单y F(x, {W_i}) xF就是堆叠的非线性层在ResNet-18里通常是两个3×3卷积。x是输入y是输出。加法是逐元素相加要求F的输出和x的shape完全一致。这个加号就是整个ResNet的灵魂。从梯度角度看这个加号给反向传播开了一条“高速公路”。误差信号从最后一层传回来时经过残差单元的加号有一路梯度可以直接通过恒等路径传到上一层。即使F内部两个卷积层的梯度很小也不影响梯度从恒等路径反向传播。这意味着深层网络也能保证梯度不消失浅层参数能收到有效更新。2.2 为什么优化残差比优化原始映射更容易我用一个类比来解释这件事。假设你要把一个数值从10调整到11原始的逐层映射方案是让网络直接输出11你需要同时调整几十个旋钮让它们的组合效果精确等于11。残差方案是让网络输出1然后把10和1加起来得到11。1不是一个绝对目标而是一个“增量”。训练初期网络权重接近初始化时F的输出自然接近0这时候残差块的输出趋近于x整个深层网络看起来就像一个浅层网络。优化的起点从“随机初始化的深层非线性变换”变成了“接近恒等映射的浅层结构”这大大降低了训练难度。随着训练进行F慢慢学到任务需要的增量。这也是为什么ResNet训练收敛比相同深度的Plain Network快得多。2.3 捷径分支的三种形态严格来说残差单元里“x”所在的路径被称为捷径连接Shortcut Connection或者跳跃连接Skip Connection。在实际网络的不同位置这条捷径有不同的实现方式恒等捷径当F(x)和x的通道数、尺寸完全一致时直接把x拿过来相加无参数、无计算量。1×1卷积投影当F(x)和x的尺寸或通道数不一致时通常是feature map尺寸减半、通道数翻倍的时刻用一个1×1卷积将x调整为和F(x)相同的形状这个1×1卷积的stride也要跟着变化。带可学习权重的捷径论文里讨论过给捷径加可学习的缩放参数但实验表明这样做没有明显收益甚至可能影响收敛所以标准ResNet用的都是前两种。在实际代码实现中只需要判断输入输出shape是否一致不一致就套一个downsample一致就直接相加。这就是残差连接的全部秘密。3. ResNet-18逐层解剖18这个数字怎么数出来每层在做什么3.1 完整数据流ResNet-18的输入默认是224×224×3的RGB图像数据流是这样走的输入: 224×224×3 conv1: 7×7卷积, stride2, 输出 112×112×64 bn1 relu maxpool: 3×3, stride2, 输出 56×56×64 layer1: 2个BasicBlock, 输出 56×56×64 layer2: 2个BasicBlock, 输出 28×28×128 layer3: 2个BasicBlock, 输出 14×14×256 layer4: 2个BasicBlock, 输出 7×7×512 avgpool: 全局平均池化, 输出 1×1×512 fc: 全连接层, 输出 类别数默认1000整个网络没有LayerNorm、没有Dropout主体就是“卷积BNReLU”和残差相加的反复堆叠。3.2 Feature Map尺寸与通道数的四次变化我把整个流程中关键阶段的张量变化整理成了一张表阶段输入尺寸操作输出尺寸通道数输入224×224-224×2243conv1pool224×2247×7 conv s2 3×3 maxpool s256×5664layer156×562×BasicBlock, 无下采样56×5664layer256×56第一个Block的首个卷积 stride228×28128layer328×28第一个Block的首个卷积 stride214×14256layer414×14第一个Block的首个卷积 stride27×7512空间分辨率从224一路降到7总共做了5次2倍下采样conv1一次、maxpool一次、layer2到layer4各一次其中conv1和maxpool连续执行所以进入layer1时已经是56×56。通道数则从64涨到512而且每下降一次分辨率通道数就翻倍。这个设计的意图很明显空间信息逐渐压缩语义信息逐渐增强同时计算量保持相对平稳——因为分辨率减半会让计算量降为1/4通道数翻倍会让计算量升为2倍整体算下来每个stage的计算量基本差不多。3.3 有参数层的统计口径ResNet-18的“18”到底是怎么数出来的很多人第一次看会很困惑因为如果加上BN、池化、ReLU层数远不止18。标准口径是只统计带有可学习权重的卷积层和全连接层。具体数一下输入层conv11个卷积层layer1到layer4每个stage有2个BasicBlock每个BasicBlock有2个卷积层一共4×2×216个卷积层最后的fc1个全连接层116118。这就是ResNet-18名称的由来。BN虽然也有可学习的缩放参数但在工程习惯里不算入层数。maxpool、avgpool、ReLU这些无参数或非学习型操作更不算。这个统计口径适用于整个ResNet家族ResNet-34就是13463×2134ResNet-50则因为每个stage的block数量不同、且使用了Bottleneck结构但计数逻辑同理。4. 为什么是ResNet-18与ResNet-34/50/101的取舍对比4.1 BasicBlock与Bottleneck的结构差异ResNet家族中ResNet-18和ResNet-34用的是BasicBlockResNet-50及以上用的是Bottleneck。BasicBlock我们已经见过了就是两个3×3卷积串联输入输出通道一致。而Bottleneck的结构是三段式1×1卷积: 降维如256→64 3×3卷积: 在低维空间做特征提取64→64 1×1卷积: 升维64→256Bottleneck的设计动机很明确当网络够深时直接堆两个3×3卷积的参数量太大。用1×1卷积先把通道数降下来在低维空间做3×3卷积最后再升回去总参数量大幅减少。这个设计让ResNet-50以上能堆到50层、101层甚至152层而计算负担可控。4.2 参数量、FLOPs与真实性能不同版本ResNet的参数量和计算量差异很大我把常用数据整理成了一张表模型参数量FLOPs(224×224)ImageNet Top-1参考ResNet-18约11.7M约1.8G约69.8%ResNet-34约21.8M约3.6G约73.3%ResNet-50约25.6M约4.1G约76.2%ResNet-101约44.5M约7.6G约77.4%注意这里是参考值不同训练策略和数据增强下会有波动。但从数据能看出一个重要趋势ResNet-34到ResNet-50参数量只增加不到4Mtop-1却能涨约3个百分点性价比非常高。而从ResNet-18到ResNet-34参数量增加了10M只涨了约3.5个百分点。4.3 什么任务该选ResNet-18根据我自己在不同项目里的体感选型逻辑大概是这样的数据集不大几千到几万张直接用ResNet-18深网络很容易过拟合。边缘设备实时推理ResNet-18是首选量化后模型不到12MB延迟很低。快速验证baselineResNet-18训练时间短先跑通完整pipeline再说。大规模数据百万级 追求精度选ResNet-50以上或者直接上ViT系列。特征提取/迁移学习ResNet-18提取的通用特征已经够用而且内存占用小。视频流/多帧处理ResNet-18单帧算力低适合做逐帧或轻量级时空模型。ResNet-18的核心价值不是“最强”而是“最稳”。你几乎不需要为它调什么特殊的超参数它能以最小的成本给你一个可靠的性能下限。5. 从零手写ResNet-18PyTorch实现与容易被忽略的细节5.1 核心代码结构ResNet-18的代码实现非常有规律核心组件只有两个BasicBlock和ResNet主体。先看BasicBlockimport torch import torch.nn as nn class BasicBlock(nn.Module): def __init__(self, in_channels, out_channels, stride1, downsampleNone): super().__init__() self.conv1 nn.Conv2d(in_channels, out_channels, kernel_size3, stridestride, padding1, biasFalse) self.bn1 nn.BatchNorm2d(out_channels) self.relu nn.ReLU(inplaceTrue) self.conv2 nn.Conv2d(out_channels, out_channels, kernel_size3, stride1, padding1, biasFalse) self.bn2 nn.BatchNorm2d(out_channels) self.downsample downsample def forward(self, x): identity x out self.conv1(x) out self.bn1(out) out self.relu(out) out self.conv2(out) out self.bn2(out) if self.downsample is not None: identity self.downsample(x) out identity out self.relu(out) return out注意这里biasFalse因为后面接BN卷积的偏置是冗余的省掉还能减少一点参数量。然后看ResNet主体和_make_layer方法class ResNet18(nn.Module): def __init__(self, num_classes1000): super().__init__() self.conv1 nn.Conv2d(3, 64, kernel_size7, stride2, padding3, biasFalse) self.bn1 nn.BatchNorm2d(64) self.relu nn.ReLU(inplaceTrue) self.maxpool nn.MaxPool2d(kernel_size3, stride2, padding1) self.layer1 self._make_layer(64, 64, blocks2, stride1) self.layer2 self._make_layer(64, 128, blocks2, stride2) self.layer3 self._make_layer(128, 256, blocks2, stride2) self.layer4 self._make_layer(256, 512, blocks2, stride2) self.avgpool nn.AdaptiveAvgPool2d((1, 1)) self.fc nn.Linear(512, num_classes) def _make_layer(self, in_channels, out_channels, blocks, stride1): downsample None if stride ! 1 or in_channels ! out_channels: downsample nn.Sequential( nn.Conv2d(in_channels, out_channels, kernel_size1, stridestride, biasFalse), nn.BatchNorm2d(out_channels), ) layers [] layers.append(BasicBlock(in_channels, out_channels, stride, downsample)) for _ in range(1, blocks): layers.append(BasicBlock(out_channels, out_channels)) return nn.Sequential(*layers) def forward(self, x): x self.conv1(x) x self.bn1(x) x self.relu(x) x self.maxpool(x) x self.layer1(x) x self.layer2(x) x self.layer3(x) x self.layer4(x) x self.avgpool(x) x torch.flatten(x, 1) x self.fc(x) return x这套代码已经能直接在ImageNet或自定义数据集上训练了。如果只想跑通验证可以先把num_classes改成你的类别数然后在CIFAR-10上小规模试一下。5.2 下采样与维度匹配的完整逻辑很多人第一次看_make_layer会问为什么stride要传进来downsample的判断条件又是什么关键逻辑是这样在layer2、layer3、layer4的第一个BasicBlock里stride2。这个stride作用在BasicBlock的conv1上所以conv1输出的feature map尺寸直接减半。同时由于输入x还是56×56×64以layer2为例而F(x)已经是28×28×128x和F(x)的shape不一致无法直接相加。所以必须先在shortcut路径上做一次1×1卷积stride也设为2把x也变成28×28×128。这个1×1卷积就放进downsample里。_ make_layer里的判断条件写得比较严谨只要stride不等于1或者输入输出通道数不一致就一定要做投影。stride1且通道一致时downsample保持为NoneBasicBlock就直接走恒等捷径。这也是为什么stage内后续的block都不传downsample——它们的输入输出shape相同。5.3 代码实现里容易踩的坑第一个坑是卷积stride和padding算错导致shape对不上。比如conv1如果用7×7 stride2padding要设为3这样224的输入才能得到112的输出。若padding设为2输出尺寸变成111.5——在PyTorch里会直接报错或者输出非预期尺寸。第二个坑是CIFAR-10这种小尺寸输入不能直接套ImageNet结构。32×32的图像经过7×7 stride2卷积再经过maxpoolfeature map就剩8×8了信息损失非常严重。常见的做法是把首层换成3×3卷积、stride1、不要maxpool或者干脆直接用别人在CIFAR上预定义好的ResNet变体。第三个坑是BN的train/eval模式切换。模型训练时要在model.train()模式下BN会用当前batch的均值和方差推理时必须在model.eval()模式下BN才会用训练阶段累积的running_mean和running_var。如果忘了切换推理结果会飘得很离谱。对新手来说这个现象很隐蔽因为loss看起来是正常的。6. 训练与部署ResNet-18的实用经验6.1 从零训练的超参与数据增强ResNet-18从零训练时我常用的配置是这样的优化器SGDmomentum0.9weight_decay1e-4batch size128或256取决于显存初始学习率0.1batch size 256时如果batch size小就降到0.01~0.05学习率策略cosine退火或者step decay比如第30、60、90个epoch各除以10训练轮数CIFAR-10上200~300 epochImageNet上90~120 epoch数据增强RandomResizedCrop RandomHorizontalFlipImageNet训练还会加ColorJitter和lighting augmentationResNet对数据增强的敏感度没有ViT那么高但基本的RandomResizedCrop不能少。换掉这个增强方式top-1精度可能会掉2~3个点。6.2 迁移学习的正确打开方式大部分实际项目不会从零训练而是加载ImageNet预训练权重再微调。用torchvision加载很简单import torchvision.models as models model models.resnet18(pretrainedTrue) num_classes 10 # 你的任务类别数 model.fc nn.Linear(model.fc.in_features, num_classes)微调策略取决于数据量数据非常少每类几十张冻结所有卷积层只训练fc。此时ResNet-18相当于一个固定的特征提取器输入经过forward到avgpool输出512维特征再接一个新分类头。数据中等每类几百到几千张冻结前几个stage只微调layer3、layer4和fc学习率设小一点比如1e-4。数据充足每类上万张全量微调用SGD或AdamW都行AdamW的初始学习率通常设1e-4~3e-4。还有一个实用技巧可以不替换fc而是把最后一层换成其他分类器比如LightGBM。做法是先把训练集过一遍模型取出fc之前的512维特征然后用逻辑回归或GBDT去拟合分类目标。有些场景下这种做法比直接微调fc更稳特别适合几千张样本的小数据集。6.3 部署时的轻量化手段ResNet-18本身已经不算大FP32权重约47MB11.7M参数乘以4字节但部署到边缘设备时还是可以做进一步压缩ONNX导出用torch.onnx.export导出为ONNX配合ONNX Runtime或TensorRT做推理加速。导出时要固定输入尺寸避免动态shape带来的额外开销。INT8量化训练后量化Post-Training Quantization在ResNet-18上通常能保持97%以上的精度模型体积从47MB降到约12MB。如果精度掉得多用量化感知训练QAT再微调几个epoch就能拉回来。算子融合BN和卷积可以融合成一个卷积推理时额外省一些计算。ONNX导出时很多工具会自动做。ResNet-18量化后的推理速度在Jetson Nano这类设备上跑224×224输入单帧耗时大约能控制在10毫秒以内完全可以做实时视频流处理。6.4 我实际使用中的体感总结最后分享一点我自己的经验ResNet-18最大的优点是确定性。我遇到过很多次新模型在某个数据集上效果不稳定换seed结果波动很大但ResNet-18很少出这种问题。如果你在做一个新任务第一版baseline用ResNet-18结果突然很差那大概率不是模型的锅而是数据pipeline有问题。用它来“排除法”定位问题是我觉得它最有价值的用途之一。另外如果你是刚入门的学生强烈建议不要直接用torchvision的现成模型而是自己照着结构手写一遍。只有自己把BasicBlock、downsample、stride变化这些细节全部理清楚才算真正理解ResNet。等手写跑通了再回到torchvision你会发现自己看代码的速度完全不一样。
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

Python抓取东京证券交易所历史行情:从API认证到量化分析实战 2026/10/1 14:36:34

Python抓取东京证券交易所历史行情:从API认证到量化分析实战

1. 项目概述与实现的整体思路先说结论:这个项目的核心,是把“看着新闻猜股市”变成“拿数据算市场”。我去年底接到一个技术验证任务——需要把东京证券交易所的日经指数和几只重点股票的十年历史行情抓下来,做成一个可复用的数据分析基线&am…

阅读更多 →
Codex × 短视频变现:全景分析——从 Seedance 多模态生成到 AI 编程智能体落地 2026/10/1 14:36:27

Codex × 短视频变现:全景分析——从 Seedance 多模态生成到 AI 编程智能体落地

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

阅读更多 →
100万Token上下文到底有多大?一文读懂GPT-5.4与TaoToken的API调用实践 2026/10/1 14:36:27

100万Token上下文到底有多大?一文读懂GPT-5.4与TaoToken的API调用实践

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

阅读更多 →
Agent 结构化输出工程:别让下游解析“看起来像 JSON“的自由文本 2026/10/1 14:36:27

Agent 结构化输出工程:别让下游解析“看起来像 JSON“的自由文本

Agent 结构化输出工程:别让下游解析"看起来像 JSON"的自由文本 摘要:当 Agent 开始承接真实业务——抽取、分类、编排、跨系统操作——"模型说了什么"远没有"模型输出的东西能不能被机器可靠地消费"重要。本文从真实开发者…

阅读更多 →
2026 秋招财务数字化校招工具栈拆解|JD 与面经复盘 2026/10/1 14:36:27

2026 秋招财务数字化校招工具栈拆解|JD 与面经复盘

一、2026 秋招财务数字化岗位核心工具清单,结合岗位日常工作任务说明2026 秋招财务数字化岗位,应届生核心必备工具包含 Excel、SQL、Power BI,加分工具为 ERP 系统、RPA、Python,这是从 BOSS 直聘、应届生求职网 2026 届校招 JD 提…

阅读更多 →
2026国内大模型API聚合平台横评:TaoToken统一Key接入四大平台核心优势解析 2026/10/1 14:36:27

2026国内大模型API聚合平台横评:TaoToken统一Key接入四大平台核心优势解析

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

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞 ✉