从梯度消失到百层网络:ResNet 是如何改变深度学习成为经典的?

自AlexNet赢得2012年ImageNet竞赛以来,每个新的获胜架构通常都会增加更多层数以降低错误率。一段时间内,增加层数确实有效,但随着网络深度的增加,深度学习中一个常见的问题——梯度消失或梯度爆炸开始出现。

梯度消失问题会导致梯度值变得非常小,几乎趋近于零;而梯度爆炸问题则会导致梯度值变得非常大。这两种情况都会增加训练难度,并导致错误率上升,随着层数的增加,模型在训练和测试数据上的性能都会受到影响。

从下图可以看出,20层CNN 架构在训练和测试数据集上的表现均优于56层CNN架构。作者进一步分析了错误率,认为错误率是由梯度消失/爆炸引起的。

screenshot_2025-04-24_14-28-10.png

2015 年,微软研究院提出了一个划时代的网络结构——ResNet(残差网络),并提出了一个非常简单却极其有效的思想:

“如果某些层学不到什么有用特征,那不如直接跳过它们。”


一、ResNet简介

ResNet 的突破源于其使用了跳跃(或残差)连接,解决了长期存在的梯度消失和爆炸问题。这些连接使 ResNet 成为第一个成功训练超过 100 层的模型的网络,并在 ImageNet 和COCO目标检测任务上取得了最佳效果。

  • 深度网络的挑战

在 ResNet 之前,非常深的神经网络面临两大挑战:

  • 梯度消失:随着网络深度增加,反向传播过程中的梯度值趋于减小。这会减慢前几层的学习速度,从而限制网络在深度增加时学习有用特征的能力。

  • 梯度爆炸:有时,在非常深的网络中,梯度会呈指数增长,导致数值不稳定,权重变得太大,从而导致模型失败。

这些问题导致深层模型的性能不如浅层模型。这种现象被称为“退化”,意味着添加更多层并不一定能提高准确率,反而往往会导致性能下降。


二、ResNet的创新点:跳过(残差)连接

跳过连接(或残差连接)的工作原理是,将较早层(例如,第 n-1 层)的输出直接添加到较晚层(例如,第 n+1 层)的输出。添加后,对结果应用 ReLU 激活函数。这意味着第 n 层实际上被“跳过”,从而使信息更容易在网络中流动。

这里 f(Xn-1) 表示卷积层 (n-1) 的输出被传递给 ReLU 激活函数

screenshot_2025-04-24_14-28-48.png

跳过连接的作用是确保即使第 n 层没有学到任何有用的信息(或输出为零),我们也不会丢失重要信息。相反,第 (n-1) 层的输出会向前传递,并与第 (n+1) 层的输出合并。

如果第 n 层没有增加价值,网络可以“跳过”它,从而保持一致的性能。如果两层都提供了有用的信息,那么将它们结合起来,就能利用两种信息源来提升网络的整体性能。


三、Resnet 的架构

以下是 Resnet-18 的架构和层配置,取自研究论文《图像识别的深度残差学习》(论文地址:https://arxiv.org/abs/1512.03385)

screenshot_2025-04-24_14-29-18.png

让我们选择 Conv3_x 块,并尝试了解其内部发生的情况。让我们使用卷积块和恒等块来理解这一点。

  • 卷积块

目的:当输入和输出的尺寸(形状)不同时,使用卷积块,原因如下:

  • 空间大小(特征图的高度和宽度)的变化。

  • 频道数量的变化。

  • 身份区块

目的:当输入和输出的尺寸(形状)相同时,使用身份块,允许将输入直接添加到输出而无需任何转换。

通过示例理解卷积和身份块,使用卷积和身份块的 Conv3_x 块数据流

screenshot_2025-04-24_15-03-53.png

上图告诉我们 56x56 图像如何通过 Conv3_x 块传播的细节,现在我们将看看图像在这些块内的每个步骤中是如何转换的。

  • 代码

class ResNet18(nn.Module):def __init__(self, n_classes):super(ResNet18, self).__init__()self.dropout_percentage = 0.5self.relu = nn.ReLU()# BLOCK-1 (starting block) input=(224x224) output=(56x56)self.conv1 = nn.Conv2d(in_channels=3, out_channels=64, kernel_size=(7,7), stride=(2,2), padding=(3,3))self.batchnorm1 = nn.BatchNorm2d(64)self.maxpool1 = nn.MaxPool2d(kernel_size=(3,3), stride=(2,2), padding=(1,1))# BLOCK-2 (1) input=(56x56) output = (56x56)self.conv2_1_1 = nn.Conv2d(in_channels=64, out_channels=64, kernel_size=(3,3), stride=(1,1), padding=(1,1))self.batchnorm2_1_1 = nn.BatchNorm2d(64)self.conv2_1_2 = nn.Conv2d(in_channels=64, out_channels=64, kernel_size=(3,3), stride=(1,1), padding=(1,1))self.batchnorm2_1_2 = nn.BatchNorm2d(64)self.dropout2_1 = nn.Dropout(p=self.dropout_percentage)# BLOCK-2 (2)self.conv2_2_1 = nn.Conv2d(in_channels=64, out_channels=64, kernel_size=(3,3), stride=(1,1), padding=(1,1))self.batchnorm2_2_1 = nn.BatchNorm2d(64)self.conv2_2_2 = nn.Conv2d(in_channels=64, out_channels=64, kernel_size=(3,3), stride=(1,1), padding=(1,1))self.batchnorm2_2_2 = nn.BatchNorm2d(64)self.dropout2_2 = nn.Dropout(p=self.dropout_percentage)# BLOCK-3 (1) input=(56x56) output = (28x28)self.conv3_1_1 = nn.Conv2d(in_channels=64, out_channels=128, kernel_size=(3,3), stride=(2,2), padding=(1,1))self.batchnorm3_1_1 = nn.BatchNorm2d(128)self.conv3_1_2 = nn.Conv2d(in_channels=128, out_channels=128, kernel_size=(3,3), stride=(1,1), padding=(1,1))self.batchnorm3_1_2 = nn.BatchNorm2d(128)self.concat_adjust_3 = nn.Conv2d(in_channels=64, out_channels=128, kernel_size=(1,1), stride=(2,2), padding=(0,0))self.dropout3_1 = nn.Dropout(p=self.dropout_percentage)# BLOCK-3 (2)self.conv3_2_1 = nn.Conv2d(in_channels=128, out_channels=128, kernel_size=(3,3), stride=(1,1), padding=(1,1))self.batchnorm3_2_1 = nn.BatchNorm2d(128)self.conv3_2_2 = nn.Conv2d(in_channels=128, out_channels=128, kernel_size=(3,3), stride=(1,1), padding=(1,1))self.batchnorm3_2_2 = nn.BatchNorm2d(128)self.dropout3_2 = nn.Dropout(p=self.dropout_percentage)# BLOCK-4 (1) input=(28x28) output = (14x14)self.conv4_1_1 = nn.Conv2d(in_channels=128, out_channels=256, kernel_size=(3,3), stride=(2,2), padding=(1,1))self.batchnorm4_1_1 = nn.BatchNorm2d(256)self.conv4_1_2 = nn.Conv2d(in_channels=256, out_channels=256, kernel_size=(3,3), stride=(1,1), padding=(1,1))self.batchnorm4_1_2 = nn.BatchNorm2d(256)self.concat_adjust_4 = nn.Conv2d(in_channels=128, out_channels=256, kernel_size=(1,1), stride=(2,2), padding=(0,0))self.dropout4_1 = nn.Dropout(p=self.dropout_percentage)# BLOCK-4 (2)self.conv4_2_1 = nn.Conv2d(in_channels=256, out_channels=256, kernel_size=(3,3), stride=(1,1), padding=(1,1))self.batchnorm4_2_1 = nn.BatchNorm2d(256)self.conv4_2_2 = nn.Conv2d(in_channels=256, out_channels=256, kernel_size=(3,3), stride=(1,1), padding=(1,1))self.batchnorm4_2_2 = nn.BatchNorm2d(256)self.dropout4_2 = nn.Dropout(p=self.dropout_percentage)# BLOCK-5 (1) input=(14x14) output = (7x7)self.conv5_1_1 = nn.Conv2d(in_channels=256, out_channels=512, kernel_size=(3,3), stride=(2,2), padding=(1,1))self.batchnorm5_1_1 = nn.BatchNorm2d(512)self.conv5_1_2 = nn.Conv2d(in_channels=512, out_channels=512, kernel_size=(3,3), stride=(1,1), padding=(1,1))self.batchnorm5_1_2 = nn.BatchNorm2d(512)self.concat_adjust_5 = nn.Conv2d(in_channels=256, out_channels=512, kernel_size=(1,1), stride=(2,2), padding=(0,0))self.dropout5_1 = nn.Dropout(p=self.dropout_percentage)# BLOCK-5 (2)self.conv5_2_1 = nn.Conv2d(in_channels=512, out_channels=512, kernel_size=(3,3), stride=(1,1), padding=(1,1))self.batchnorm5_2_1 = nn.BatchNorm2d(512)self.conv5_2_2 = nn.Conv2d(in_channels=512, out_channels=512, kernel_size=(3,3), stride=(1,1), padding=(1,1))self.batchnorm5_2_2 = nn.BatchNorm2d(512)self.dropout5_2 = nn.Dropout(p=self.dropout_percentage)# Final Block input=(7x7) self.avgpool = nn.AvgPool2d(kernel_size=(7,7), stride=(1,1))self.fc = nn.Linear(in_features=1*1*512, out_features=1000)self.out = nn.Linear(in_features=1000, out_features=n_classes)# ENDdef forward(self, x):# block 1 --> Starting blockx = self.relu(self.batchnorm1(self.conv1(x)))op1 = self.maxpool1(x)# block2 - 1x = self.relu(self.batchnorm2_1_1(self.conv2_1_1(op1)))    # conv2_1 x = self.batchnorm2_1_2(self.conv2_1_2(x))                 # conv2_1x = self.dropout2_1(x)# block2 - Adjust - No adjust in this layer as dimensions are already same# block2 - Concatenate 1op2_1 = self.relu(x + op1)# block2 - 2x = self.relu(self.batchnorm2_2_1(self.conv2_2_1(op2_1)))  # conv2_2 x = self.batchnorm2_2_2(self.conv2_2_2(x))                 # conv2_2x = self.dropout2_2(x)# op - block2op2 = self.relu(x + op2_1)# block3 - 1[Convolution block]x = self.relu(self.batchnorm3_1_1(self.conv3_1_1(op2)))    # conv3_1x = self.batchnorm3_1_2(self.conv3_1_2(x))                 # conv3_1x = self.dropout3_1(x)# block3 - Adjustop2 = self.concat_adjust_3(op2) # SKIP CONNECTION# block3 - Concatenate 1op3_1 = self.relu(x + op2)# block3 - 2[Identity Block]x = self.relu(self.batchnorm3_2_1(self.conv3_2_1(op3_1)))  # conv3_2x = self.batchnorm3_2_2(self.conv3_2_2(x))                 # conv3_2 x = self.dropout3_2(x)# op - block3op3 = self.relu(x + op3_1)# block4 - 1[Convolition block]x = self.relu(self.batchnorm4_1_1(self.conv4_1_1(op3)))    # conv4_1x = self.batchnorm4_1_2(self.conv4_1_2(x))                 # conv4_1x = self.dropout4_1(x)# block4 - Adjustop3 = self.concat_adjust_4(op3) # SKIP CONNECTION# block4 - Concatenate 1op4_1 = self.relu(x + op3)# block4 - 2[Identity Block]x = self.relu(self.batchnorm4_2_1(self.conv4_2_1(op4_1)))  # conv4_2x = self.batchnorm4_2_2(self.conv4_2_2(x))                 # conv4_2x = self.dropout4_2(x)# op - block4op4 = self.relu(x + op4_1)# block5 - 1[Convolution Block]x = self.relu(self.batchnorm5_1_1(self.conv5_1_1(op4)))    # conv5_1x = self.batchnorm5_1_2(self.conv5_1_2(x))                 # conv5_1x = self.dropout5_1(x)# block5 - Adjustop4 = self.concat_adjust_5(op4) # SKIP CONNECTION# block5 - Concatenate 1op5_1 = self.relu(x + op4)# block5 - 2[Identity Block]x = self.relu(self.batchnorm5_2_1(self.conv5_2_1(op5_1)))  # conv5_2x = self.batchnorm5_2_1(self.conv5_2_1(x))                 # conv5_2x = self.dropout5_2(x)# op - block5op5 = self.relu(x + op5_1)# FINAL BLOCK - classifier x = self.avgpool(op5)x = x.reshape(x.shape[0], -1)x = self.relu(self.fc(x))x = self.out(x)return x

实现后,我们可以直接创建此类的对象并传递数据集的输出类的数量,并使用它在任何图像数据上训练我们的网络。

  • 这些块为什么有用?

  • 卷积块处理空间分辨率或通道数量的变化,同时保留残差连接。

  • 身份块专注于在不改变输入维度的情况下学习附加特征。

  • 它们共同作用,允许梯度流,即使某些层不能有效学习,也能使深度网络有效地训练


四、ResNet为何成为经典

ResNet的成功,不在于它堆了多少层,而在于它对“深层神经网络如何训练”这个根本问题给出了一个优雅解法:如果学不会,就跳过去!

这种看似简单的思想,却释放了深度学习的潜力,也为后续模型设计开辟了全新路径,DenseNet、Mask R-CNN、HRNet、Swin Transformer……都离不开它的残差思想。

所以,ResNet 不只是一种网络架构,更是一种范式的转变——这,正是它成为经典的原因。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/pingmian/78587.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

JVM——引入

什么是JVM?它与JDK、JRE的关系? JVM、JRE 和 JDK 是 Java 平台的三个核心组件,各自承担着不同的职责,它们之间的关系密不可分。理解它们的区别和联系有助于更好地开发、部署和运行 Java 应用程序。对于 Java 开发者来说&#xff…

PyCharm 2023升级2024 版本

windows下把老版本卸载之后,需要把环境变量,注册表信息删除。 并且把C:\Users\用户\AppData 文件夹下的 Local\JetBrains和Roaming\JetBrains 都删除,再重新安装 原旧项目升级的方式: 1.2023虚拟机的文件夹是venv 改为.venv…

从外卖大战看O2O新趋势:上门私厨平台系统架构设计解析

京东高调进军外卖市场,美团全力防守,两大巨头的竞争让整个行业风起云涌。但在这场外卖大战之外,一个更具潜力的细分市场正在悄然兴起——上门私厨服务。 与标准化外卖不同,上门私厨提供的是个性化定制服务。厨师带着新鲜食材上门现…

驱动开发系列53 - 一个OpenGL应用程序是如何调用到驱动厂商GL库的

一:概述 一个 OpenGL 应用程序调用 GPU 驱动的过程,主要是通过动态链接库(libGL.so)来完成的。本文从上到下梳理一下整个调用链,包含 GLVND、Mesa 或厂商驱动之间的关系。 二:调用关系 1. 首先一个 OpenGL 应用程序(比如游戏或图形渲染软件)在运行时会调用 OpenGL 提供…

springboot3 声明式 HTTP 接口

1 介绍 在 Spring 6 和 Spring Boot 3 中,我们可以使用 Java 接口来定义声明式的远程 HTTP 服务。这种方法受到 Feign 等流行 HTTP 客户端库的启发,与在 Spring Data 中定义 Repository 的方法类似。 声明式 HTTP 接口包括用于 HTTP exchange 的注解方法…

多级缓存架构设计与实践经验

多级缓存架构设计与实践经验 在互联网大厂Java求职者的面试中,经常会被问到关于多级缓存的架构设计和实践经验。本文通过一个故事场景来展示这些问题的实际解决方案。 第一轮提问 面试官:马架构,欢迎来到我们公司的面试现场。请问您对多级…

Mac「brew」快速安装Redis

安装Redis 步骤 1:安装 Redis 打开终端(Terminal)。 运行以下命令安装 Redis: brew install redis步骤 2:启动 Redis 安装完成后,可以使用以下命令启动 Redis 服务: brew services start redis…

文献阅读(一)植物应对干旱的生理学反应 | The physiology of plant responses to drought

分享一篇Science上的综述文章,主要探讨了植物应对干旱的生理机制,强调通过调控激素信号提升植物耐旱性、保障粮食安全的重要性。 摘要 干旱每年致使农作物产量的损失,比所有病原体造成损失的总和还要多。为适应土壤中的湿度梯度变化&#x…

if consteval

if consteval 是 C23 引入的新特性,该特性是关于immediate function 的,即consteval function。用于在编译时检查当前是否处于 立即函数上下文(即常量求值环境),并根据结果选择执行不同的代码路径。它是对 std::is_con…

MANIPTRANS:通过残差学习实现高效的灵巧双手操作迁移

25年3月来自北京通用 AI 国家重点实验室、清华大学和北大的论文“ManipTrans: Efficient Dexterous Bimanual Manipulation Transfer via Residual Learning”。 人手在交互中起着核心作用,推动着灵巧机器人操作研究的不断深入。数据驱动的具身智能算法需要精确、大…

Field访问对象int字段,对象访问int字段,通过openjdk17 C++源码看对象字段访问原理

在Java反射机制中,访问对象的int类型字段值(如field.getInt(object))的底层实现涉及JVM对内存偏移量的计算与直接内存访问。本文通过分析OpenJDK 17源码,揭示这一过程的核心实现逻辑。 一、字段偏移量计算 1. Java层初始化偏移量…

Java查询数据库表信息导出Word

参考: POI生成Word多级标题格式_poi设置word标题-CSDN博客 1.概述 使用jdbc查询数据库把表信息导出为word文档, 导出为word时需要下载word模板文件。 已实现数据库: KingbaseES, 实现代码: 点击跳转 2.效果图 2.1.生成word内容 所有数据库合并 数据库不合并 2.2.生成文件…

Qt中的全局函数讲解集合(全)

在头文件<QtGlobal>中包含了Qt的全局函数&#xff0c;现在就这些全局函数一一详解。 1.qAbs 原型&#xff1a; template <typename T> T qAbs(const T &t)一个用于计算绝对值的函数。它可以用于计算各种数值类型的绝对值&#xff0c;包括整数、浮点数等 示…

AI与IT协同的典型案例

简介 本篇代码示例展示了IT从业者如何与AI协同工作&#xff0c;发挥各自优势。这些案例均来自2025年的最新企业实践&#xff0c;涵盖了不同IT岗位的应用场景。 一、GitHub Copilot生成代码框架 开发工程师AI协作示例&#xff1a;利用GitHub Copilot生成代码框架&#xff0c;…

三网通电玩城平台系统结构与源码工程详解(二):Node.js 服务端核心逻辑实现

本篇文章将聚焦服务端游戏逻辑实现&#xff0c;以 Node.js Socket.io 作为主要通信与逻辑处理框架&#xff0c;展开用户登录验证、房间分配、子游戏调度与事件广播机制的剖析&#xff0c;并附上多个核心代码段。 一、服务端文件结构概览 /server/├── index.js …

【prompt是什么?有哪些技巧?】

Prompt&#xff08;提示词&#xff09;是什么&#xff1f; Prompt 是用户输入给AI模型&#xff08;如ChatGPT、GPT-4等&#xff09;的指令或问题&#xff0c;用于引导模型生成符合预期的回答。它的质量直接影响AI的输出效果。 Prompt 的核心技巧 1. 明确目标&#xff08;Clar…

堆和二叉树--数据结构初阶(3)(C/C++)

文章目录 前言理论部分堆的模拟实现:(这里举的大根堆)堆的创建二叉树的遍历二叉树的一些其他功能实现 作业部分 前言 这期的话讲解的是堆和二叉树的理论部分和习题部分 理论部分 二叉树的几个性质:1.对于任意一个二叉树&#xff0c;度为0的节点比度为2的节点多一个 2.对于完全…

Dockerfile讲解与示例汇总

容器化技术已经成为应用开发和部署的标准方式,而Docker作为其中的佼佼者,以其轻量、高效、可移植的特性,深受开发者和运维人员的喜爱。本文将从实用角度出发,分享各类常用服务的Docker部署脚本与最佳实践,希望能帮助各位在容器化之路上少走弯路。 无论你是刚接触Docker的…

在QGraphicsView中精确地以鼠标为锚缩放图片

在pyqt中以鼠标所在位置为锚点缩放图片-CSDN博客中的第一个示例中&#xff0c;通过简单设置&#xff1a; self.setTransformationAnchor(QGraphicsView.AnchorUnderMouse) 使得QGraphicsView具有了以鼠标为锚进行缩放的功能。但是&#xff0c;其内部应当是利用了滚动条的移动来…

制造工厂如何借助电子看板实现高效生产管控

在当今高度竞争的制造业环境中&#xff0c;许多企业正面临着严峻的管理和生产挑战。首先&#xff0c;管理流程落后&#xff0c;大量工作仍依赖"人治"方式&#xff0c;高层管理者理论知识薄弱且不愿听取专业意见。其次&#xff0c;生产过程控制能力不足&#xff0c;导…