目錄
什么是 DenseNet?
?稠密塊(Dense Block)詳解
一、稠密塊的核心思想
二、稠密塊的結構組成
1. 卷積單元(的結構)
2. 密集連接的具體方式
3. 關鍵參數:增長率(Growth Rate, k)
4. 過渡層(Transition Layer)
三、稠密塊的優勢
四、與 ResNet 殘差塊的對比
過渡層(Transition Layer)詳解
一、什么是過渡層?
二、過渡層的核心作用
三、過渡層的典型結構
四、設計細節與參數
五、與 ResNet 中對應組件的對比
DenseNet 的作用
DenseNet 與 ResNet 的核心區別
ResNet和DenseNet的結構示意圖
為啥卷積塊中采用?“批量歸一化(BN)→ 激活函數 → 卷積層”?的順序?
1. 批量歸一化(BN)放在最前:從根源穩定輸入分布
2. 激活函數放在 BN 之后:讓激活更 “有效”
3. 卷積層放在最后:利用穩定輸入提升參數學習效率
?完整代碼
實驗結果
什么是 DenseNet?
稠密連接網絡(Dense Convolutional Network,簡稱 DenseNet)是 2017 年由 Huang 等人提出的一種深層卷積神經網絡,其核心創新是“稠密連接(Dense Connection)”:網絡中的每個層都會與前面所有層直接連接,即第l層的輸入是前l-1層的輸出的拼接(而非簡單相加)。
核心結構:
稠密塊(Dense Block):由多個卷積層組成,層間通過稠密連接融合特征。設第i層的輸出為
,則第l層的輸入為前所有層輸出的拼接:
其中
表示通道維度上的拼接,
是第l層的卷積操作(含 BN、ReLU、卷積核)。
過渡層(Transition Layer):用于連接不同的稠密塊,通過 1×1 卷積降維和 2×2 平均池化減小特征圖尺寸,防止網絡參數爆炸。
稠密塊(dense block)和過渡層(transition layer)。 前者定義如何連接輸入和輸出,而后者則控制通道數量,使其不會太復雜。
?稠密塊(Dense Block)詳解
稠密塊(Dense Block)是深度學習網絡?DenseNet(Densely Connected Convolutional Networks)?的核心組成單元,其設計核心是通過密集連接(Dense Connection)?實現極致的特征重用,是對傳統卷積網絡和 ResNet 中 “跳躍連接” 的進一步升級。
一、稠密塊的核心思想
傳統卷積網絡中,每一層的輸入僅來自上一層;ResNet 通過 “跳躍連接” 讓層的輸入包含上一層的輸出(元素相加);而稠密塊則更進一步 ——每一層的輸入是前面所有層的輸出的拼接(Concatenation)。
具體來說,若稠密塊包含?L?層,第?l?層(記為
)的輸入是第?
?層的輸出的拼接,輸出為?
,其中:
?是稠密塊的初始輸入特征圖;
?表示將這些特征圖在通道維度上拼接;
是第?l?層的卷積操作(通常包含 BN、ReLU、卷積等子操作)。
這種 “密集連接” 使得特征在網絡中能夠被充分傳遞和復用,從根本上解決了深層網絡的 “特征衰減” 問題。
二、稠密塊的結構組成
一個典型的稠密塊由多個 “卷積單元” 和 “密集連接” 組成,同時為了控制計算量,通常會包含瓶頸層(Bottleneck)?和過渡層(Transition Layer)(過渡層用于連接不同稠密塊,非稠密塊內部結構,但需結合理解)。
1. 卷積單元(
的結構)
稠密塊中的每一層?\(H_l\)?通常由以下子操作組成(順序固定):
- BN(Batch Normalization):標準化特征,加速訓練;
- ReLU:非線性激活,增強表達能力;
- 1x1 卷積(可選,瓶頸層):減少輸入特征的通道數(如將通道數壓縮至原來的 1/4),降低計算量;
- 3x3 卷積:提取局部特征,輸出固定數量的特征圖(由 “增長率” 決定)。
其中,1x1 卷積作為 “瓶頸層” 是 DenseNet 的重要優化:若直接對拼接后的高維特征做 3x3 卷積,計算量會爆炸;而 1x1 卷積可先將通道數降至較低維度(如 4k,k 為增長率),再用 3x3 卷積輸出 k 個特征圖,既保證特征提取效率,又控制了參數規模。
2. 密集連接的具體方式
假設稠密塊的初始輸入特征圖為
(通道數為?
),每一層的輸出特征圖通道數為?k(即 “增長率”),則:
- 第 1 層輸入:
,輸出:
(通道數?k);
- 第 2 層輸入:
(通道數
),輸出:
(通道數?k);
- 第 3 層輸入:
(通道數?
),輸出:
(通道數?k);
- ...
- 第?L?層輸入:
(通道數?
),輸出:
(通道數?k)。
最終,整個稠密塊的輸出是所有層輸出的拼接:
(通道數?
)。
3. 關鍵參數:增長率(Growth Rate, k)
增長率?k?是稠密塊的核心參數,定義為每一層輸出的特征圖通道數。它控制了特征圖的增長速度:
- 若?k?較小(如 12、24),即使層數較多,拼接后的總通道數也不會過大,保證計算效率;
- 實驗表明,較小的?k(如 k=12)即可讓 DenseNet 達到優異性能,說明密集連接對特征的利用率極高。
4. 過渡層(Transition Layer)
稠密塊之間通過 “過渡層” 連接,作用是壓縮特征圖通道數并降低尺寸,避免網絡冗余:
- 過渡層通常由 “BN + 1x1 卷積 + 2x2 平均池化” 組成;
- 1x1 卷積將前一個稠密塊的輸出通道數壓縮(如壓縮至原來的 θ 倍,θ∈(0,1],稱為 “壓縮因子”);
- 平均池化將特征圖尺寸減半(如從 32x32 變為 16x16),控制網絡深度和計算量。
三、稠密塊的優勢
極致的特征重用 每一層都能直接訪問前面所有層的特征,特征在網絡中被反復利用,避免了傳統網絡中 “特征隨層數增加而衰減” 的問題。
緩解梯度消失 反向傳播時,梯度可通過密集連接直接從深層傳遞到淺層,大幅緩解深層網絡的梯度消失問題,使訓練更深的網絡成為可能。
參數效率更高 由于特征重用充分,DenseNet 無需像其他網絡(如 ResNet)那樣通過增加通道數提升性能,小增長率?k?即可實現高準確率,參數規模通常小于 ResNet。
正則化效果 密集連接增加了層之間的相互依賴,一定程度上減少了過擬合,提升模型泛化能力。
四、與 ResNet 殘差塊的對比
對比維度 稠密塊(Dense Block) 殘差塊(Residual Block) 連接方式 特征圖拼接(Concatenation) 特征圖元素相加(Element-wise Add) 輸入來源 前面所有層的輸出 僅上一層的輸出(跨層相加) 特征利用效率 極高(所有歷史特征直接參與當前層) 較高(僅上一層特征與當前層特征融合) 特征維度變化 隨層數線性增長(由增長率 k 控制) 基本不變(相加不改變通道數) 計算量控制 依賴瓶頸層(1x1 卷積)和過渡層 依賴殘差連接的 “恒等映射”
過渡層(Transition Layer)詳解
過渡層(Transition Layer)是稠密連接網絡(DenseNet)?中的關鍵組件,主要用于連接相鄰的稠密塊(Dense Block),并實現特征圖的降維和壓縮,在保證網絡效率的同時控制模型復雜度。以下從定義、作用、結構細節及設計意義展開詳解:
一、什么是過渡層?
過渡層是 DenseNet 中位于兩個稠密塊之間的連接模塊,其核心功能是對前一個稠密塊輸出的特征圖進行處理,為下一個稠密塊提供合適維度的輸入。由于稠密塊會通過密集連接生成大量特征圖(如一個包含 12 層的稠密塊可能輸出數百個特征圖),過渡層的作用類似于 “橋梁”,通過降維減少特征數量,避免網絡參數和計算量過度膨脹。
二、過渡層的核心作用
特征降維 稠密塊的輸出特征圖數量通常較多(例如,每個稠密塊會累計生成?
?個特征圖,其中?k?是增長率,L?是塊內層數)。過渡層通過卷積操作將特征圖數量按比例壓縮(通常壓縮至原來的?
?倍,
稱為壓縮因子,一般取 0.5),減少后續計算量。
空間尺寸縮減 通過池化操作(通常是 2×2 的平均池化)將特征圖的空間尺寸(高和寬)縮小一半,與傳統卷積網絡中 “下采樣→增大感受野” 的設計思路一致,幫助網絡捕捉更全局的特征。
特征融合與平滑 過渡層中的卷積操作(通常是 1×1 卷積)可以對稠密塊輸出的多通道特征進行融合,減少冗余信息,同時保持特征的連續性,為下一個稠密塊的輸入提供更精煉的特征。
三、過渡層的典型結構
過渡層的結構簡潔,通常由兩個操作組成,按順序執行:
1×1 卷積
- 作用:對輸入特征圖進行通道壓縮(降維),并融合通道間的信息。
- 細節:卷積核數量為前一個稠密塊輸出特征數的
倍(
),當
時不壓縮),步長為 1, padding 為 0。
- 示例:若前一個稠密塊輸出 200 個特征圖,
,則 1×1 卷積后輸出 100 個特征圖。
2×2 平均池化
- 作用:將特征圖的空間尺寸(如
)縮減為?
,實現下采樣。
- 細節:池化核大小 2×2,步長 2,無 padding,確保尺寸減半。
四、設計細節與參數
- 壓縮因子
:是 DenseNet 的重要超參數,控制特征壓縮比例。當?
時,DenseNet 稱為 “壓縮 DenseNet”(如 DenseNet-C),若?
則不壓縮。實驗中
?是常用設置,可在精度和效率間取得平衡。
- 激活函數與歸一化:過渡層的 1×1 卷積后通常會跟隨批量歸一化(BN)和 ReLU 激活函數,確保特征分布穩定并引入非線性。
五、與 ResNet 中對應組件的對比
在 ResNet 中,連接不同殘差塊的下采樣通常通過 “stride=2 的 3×3 卷積” 或 “單獨的池化層 + 卷積” 實現,目的是縮減尺寸但不刻意壓縮通道數。而過渡層的核心差異在于:
- 主動降維:通過 1×1 卷積和壓縮因子主動減少通道數,更注重控制模型參數。
- 與稠密塊的配合:由于稠密塊會累計大量特征,過渡層的降維是 DenseNet 控制復雜度的關鍵,而 ResNet 的殘差塊不會累計特征,因此無需專門的壓縮機制。
DenseNet 的作用
緩解梯度消失問題 稠密連接讓每個層都能直接接收前面所有層的梯度(反向傳播時,梯度無需經過多層累積),確保深層網絡的梯度能有效傳遞到淺層,解決了深層網絡訓練困難的問題。
促進特征復用 每個層的輸入包含前面所有層的特征(而非僅前一層),特征在網絡中被多次復用,減少了冗余特征的學習,提升了特征利用效率。例如,淺層的邊緣特征和深層的語義特征可直接融合,增強模型表達能力。
減少參數數量 由于特征復用充分,DenseNet 無需像 ResNet 那樣通過增加通道數來提升性能(通道數通常遠小于 ResNet),因此參數總量更少(例如 DenseNet-121 的參數約 800 萬,僅為 ResNet-50 的 1/3)。
抑制過擬合 特征的多次復用相當于給網絡引入了 “正則化” 效果,尤其在小數據集上,過擬合風險更低,泛化能力更強。
DenseNet 與 ResNet 的核心區別
對比維度 ResNet(殘差網絡) DenseNet(稠密連接網絡) 連接方式 每個層僅與前一層連接(“鏈式”):\(x_l = H_l(x_{l-1}) + x_{l-1}\) 每個層與前面所有層連接(“稠密”):\(x_l = H_l([x_0, ..., x_{l-1}])\) 特征融合方式 殘差相加(元素級加法,要求通道數相同) 特征拼接(通道級拼接,通道數隨層數累積) 通道數變化 隨深度翻倍(如 64→128→256→512),通過升維提升能力 單一層通道數固定(如 32),通過拼接累積總通道數(復用特征) 參數效率 較低(通道數大,參數多) 較高(通道數小,特征復用減少冗余參數) 計算復雜度 中等(加法操作輕量,但通道數大) 較高(拼接導致總通道數大,需通過過渡層控制) 梯度傳播 梯度通過殘差連接間接傳遞(需經過前一層) 梯度直接傳遞到所有淺層(無中間層阻礙) 適用場景 超深網絡(如 ResNet-152)、大數據集(需強表達能力) 中小數據集(泛化能力強)、對參數敏感的場景
ResNet和DenseNet的結構示意圖
為啥卷積塊中采用?“批量歸一化(BN)→ 激活函數 → 卷積層”?的順序?
1. 批量歸一化(BN)放在最前:從根源穩定輸入分布
BN 的核心功能是標準化輸入數據的分布(將每個通道的輸入調整為均值 0、方差 1 的標準分布),從而解決 “內部協變量偏移”。
如果將 BN 放在卷積層之后(即 “卷積→BN→激活”),則 BN 只能標準化卷積層的輸出;而放在卷積層之前(即 “BN→激活→卷積”),BN 可以直接標準化進入卷積層的原始輸入,從更早期穩定數據分布,效果更徹底:
- 卷積層的輸入分布更穩定,參數更新時輸出變化更平緩,訓練更穩定;
- 避免卷積層因輸入分布劇烈波動而陷入 “參數震蕩”(例如卷積核反復調整以適應突變的輸入)。
2. 激活函數放在 BN 之后:讓激活更 “有效”
激活函數(如 ReLU)的作用是引入非線性,但其效果高度依賴輸入分布:
- ReLU 對負數輸入會 “截斷”(輸出 0),若輸入分布不穩定(例如均值偏移到負數區域),會導致大量神經元 “死亡”(輸出恒為 0);
- BN 將輸入標準化為 “均值 0、方差 1” 的分布后,ReLU 的輸入會均勻分布在正負區間,既能保留足夠多的正輸入(激活有效神經元),又能通過負輸入的截斷引入非線性,避免激活函數失效。
3. 卷積層放在最后:利用穩定輸入提升參數學習效率
卷積層是特征提取的核心,其參數學習(通過梯度下降更新?W?和?b)需要穩定的輸入分布:
- 經過 BN 標準化和激活函數非線性變換后,輸入到卷積層的數據分布已非常穩定,卷積核可以更高效地學習 “有意義的特征模式”(例如邊緣、紋理);
- 若卷積層放在最前,其輸出分布會因參數更新而劇烈變化,后續的 BN 和激活函數需要不斷 “適配” 這種變化,降低訓練效率。
先穩定分布,再引入非線性,最后高效提取特征
?完整代碼
"""
文件名: 7.7 稠密連接網絡(DenseNet)
作者: 墨塵
日期: 2025/7/14
項目名: dl_env
備注: 實現完整的DenseNet網絡,包含稠密塊、過渡層及端到端訓練流程,用于Fashion-MNIST分類
"""import torch
from torch import nn
from d2l import torch as d2l
# 手動顯示圖像相關庫
import matplotlib.pyplot as plt # 繪圖庫
import matplotlib.text as text # 用于修改文本繪制(解決符號顯示問題)# -------------------------- 核心解決方案:解決文本顯示問題 --------------------------
# 定義替換函數:將Unicode減號(U+2212,可能導致顯示異常)替換為普通減號(-)
def replace_minus(s):"""解決Matplotlib中Unicode減號顯示異常的問題參數:s: 待處理的字符串或其他類型對象返回:處理后的字符串或原始對象(非字符串類型)"""if isinstance(s, str): # 判斷輸入是否為字符串return s.replace('\u2212', '-') # 替換特殊減號為普通減號return s # 非字符串直接返回# 重寫matplotlib的Text類的set_text方法,解決減號顯示異常
original_set_text = text.Text.set_text # 保存原始的set_text方法
def new_set_text(self, s):"""重寫后的文本設置方法,在設置文本前先處理減號顯示問題"""s = replace_minus(s) # 調用替換函數處理文本中的減號return original_set_text(self, s) # 調用原始方法設置文本
text.Text.set_text = new_set_text # 應用重寫后的方法# -------------------------- 字體配置(確保中文和數學符號正常顯示)--------------------------
plt.rcParams["font.family"] = ["SimHei"] # 設置中文字體(支持中文顯示)
plt.rcParams["text.usetex"] = True # 使用LaTeX渲染文本(提升數學符號顯示效果)
plt.rcParams["axes.unicode_minus"] = True # 確保負號正確顯示(避免顯示為方塊)
plt.rcParams["mathtext.fontset"] = "cm" # 設置數學符號字體為Computer Modern(更美觀)
d2l.plt.rcParams.update(plt.rcParams) # 讓d2l庫的繪圖工具繼承上述字體配置# 定義卷積塊:BN + ReLU + 3x3卷積(DenseNet的基礎單元)
def conv_block(input_channels, num_channels):"""構建DenseNet中的基礎卷積單元,實現特征提取參數:input_channels: 輸入特征圖的通道數num_channels: 輸出特征圖的通道數(即增長率的一部分)返回:nn.Sequential: 包含批量歸一化、激活函數和卷積層的序列模塊"""return nn.Sequential(nn.BatchNorm2d(input_channels), # 批量歸一化:穩定輸入分布,加速訓練nn.ReLU(), # ReLU激活:引入非線性,增強特征表達能力# 3x3卷積:提取局部空間特征,padding=1確保輸出尺寸與輸入相同nn.Conv2d(input_channels, num_channels, kernel_size=3, padding=1))class DenseBlock(nn.Module):"""稠密塊(DenseBlock):DenseNet的核心組件,通過密集連接實現特征復用參數:num_convs: 塊內包含的卷積塊數量input_channels: 初始輸入通道數num_channels: 每個卷積塊的輸出通道數(即"增長率",控制特征增長速度)"""def __init__(self, num_convs, input_channels, num_channels):super(DenseBlock, self).__init__()layer = []# 逐個添加卷積塊,每個卷積塊的輸入通道數隨層數遞增for i in range(num_convs):# 第i個卷積塊的輸入通道數 = 初始通道數 + i×增長率# 例如:第0個卷積塊輸入=input_channels,第1個=input_channels+num_channels,以此類推layer.append(conv_block(input_channels + i * num_channels, # 動態計算輸入通道數num_channels # 固定輸出通道數(增長率)))self.net = nn.Sequential(*layer) # 將所有卷積塊組合成序列def forward(self, X):"""前向傳播:通過密集連接拼接所有卷積塊的輸出參數:X: 輸入特征圖,形狀為(batch_size, input_channels, height, width)返回:拼接后的特征圖,形狀為(batch_size, final_channels, height, width)其中final_channels = input_channels + num_convs×num_channels"""for blk in self.net:Y = blk(X) # 當前卷積塊的輸出# 在通道維度(dim=1)上拼接原始輸入X和當前輸出Y(密集連接的核心)X = torch.cat((X, Y), dim=1)return X# 過渡層:連接兩個稠密塊,實現特征降維和尺寸縮減
def transition_block(input_channels, num_channels):"""過渡層:壓縮特征通道數并減小空間尺寸,避免網絡參數爆炸參數:input_channels: 輸入特征圖的通道數(前一個稠密塊的輸出)num_channels: 輸出特征圖的通道數(通常為輸入的1/2,即壓縮因子0.5)返回:nn.Sequential: 包含批量歸一化、激活、卷積和池化的序列模塊"""return nn.Sequential(nn.BatchNorm2d(input_channels), # 批量歸一化:穩定過渡層輸入分布nn.ReLU(), # 激活函數:引入非線性# 1x1卷積:減少通道數(核心降維操作,參數少且計算高效)nn.Conv2d(input_channels, num_channels, kernel_size=1),# 2x2平均池化:將空間尺寸減半(height和width各除以2)nn.AvgPool2d(kernel_size=2, stride=2))if __name__ == '__main__':# -------------------------- 測試核心組件功能 --------------------------# 測試1:稠密塊(2個卷積塊,輸入3通道,增長率10)dense_blk = DenseBlock(num_convs=2, input_channels=3, num_channels=10)X = torch.randn(4, 3, 8, 8) # 隨機輸入:4個樣本,3通道,8x8尺寸Y = dense_blk(X)print(f"稠密塊輸出形狀: {Y.shape}") # 預期:(4, 3+2×10=23, 8, 8)# 測試2:過渡層(輸入23通道,輸出10通道)trans_blk = transition_block(input_channels=23, num_channels=10)Z = trans_blk(Y)print(f"過渡層輸出形狀: {Z.shape}") # 預期:(4, 10, 4, 4)(尺寸減半)# -------------------------- 構建完整DenseNet網絡 --------------------------# 第一個模塊:初始卷積+池化(預處理輸入圖像)b1 = nn.Sequential(# 7x7大卷積:初步提取全局特征,步長2壓縮尺寸nn.Conv2d(1, 64, kernel_size=7, stride=2, padding=3),nn.BatchNorm2d(64), # 批量歸一化nn.ReLU(), # 激活# 3x3最大池化:進一步將尺寸減半(為后續稠密塊做準備)nn.MaxPool2d(kernel_size=3, stride=2, padding=1))# 配置稠密塊參數num_channels = 64 # 初始通道數(與b1輸出通道一致)growth_rate = 32 # 增長率:每個卷積塊的輸出通道數(控制特征增長速度)num_convs_in_dense_blocks = [4, 4, 4, 4] # 每個稠密塊包含的卷積塊數量(總4個稠密塊)# 構建后續模塊(稠密塊+過渡層)blks = []for i, num_convs in enumerate(num_convs_in_dense_blocks):# 添加稠密塊blks.append(DenseBlock(num_convs, num_channels, growth_rate))# 更新當前通道數:稠密塊輸出通道 = 輸入通道 + 卷積塊數量×增長率num_channels += num_convs * growth_rate# 除最后一個稠密塊外,添加過渡層(通道數減半)if i != len(num_convs_in_dense_blocks) - 1:blks.append(transition_block(num_channels, num_channels // 2))num_channels = num_channels // 2 # 更新通道數為過渡層輸出# 組裝完整網絡net = nn.Sequential(b1, # 初始模塊*blks, # 所有稠密塊和過渡層nn.BatchNorm2d(num_channels), # 最終批量歸一化nn.ReLU(), # 激活nn.AdaptiveAvgPool2d((1, 1)), # 全局平均池化:將特征圖壓縮為1x1nn.Flatten(), # 展平為一維向量nn.Linear(num_channels, 10) # 全連接層:輸出10類(Fashion-MNIST))# -------------------------- 訓練DenseNet模型 --------------------------# 訓練參數lr = 0.1 # 學習率(DenseNet對學習率較魯棒)num_epochs = 10 # 訓練輪數batch_size = 256 # 批量大小# 加載Fashion-MNIST數據集(調整圖像大小為96x96適配網絡)train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size, resize=96)# 啟動訓練(使用d2l庫的訓練函數,自動支持GPU)print("\n開始訓練DenseNet模型...")d2l.train_ch6(net, train_iter, test_iter,num_epochs, lr,device=d2l.try_gpu() # 自動選擇GPU(如有))# 顯示訓練曲線(損失+準確率)plt.show(block=True)