從梯度消失到百層網絡: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.pswp.cn/pingmian/78587.shtml
繁體地址,請注明出處:http://hk.pswp.cn/pingmian/78587.shtml
英文地址,請注明出處:http://en.pswp.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;導…