完整程式碼及其資料,請移步小編的GitHub地址
傳送門:請點選我
如果點選有誤:https://github.com/LeBron-Jian/DeepLearningNote
這裡結合網路的資料和DenseNet論文,捋一遍DenseNet,基本程式碼和圖片都是來自網路,這裡表示感謝,參考連結均在後文。下面開始。
DenseNet 論文寫的很好,有想法的可以去看一下,我這裡提供翻譯地址:
深度學習論文翻譯解析(十五):Densely Connected Convolutional Networks
自ResNet提出以後,ResNet的變種網路層出不窮,都各有其特點,網路效能也有一定的提升。本文學習CVPR 2017最佳論文 DenseNet,論文中提出的 DenseNet(Dense Convolutional Network)主要還是和ResNet以及Inception網路做對比,思想上有所借鑑,但是卻是全新的結構,網路結構並不複雜,卻非常有效,在CIFAR指標上全面超越ResNet,可以說是DenseNet吸收了ResNet 最精華的部分,並在此上做了更加創新的工作,使得網路效能進一步提升。
1,ResNet VS DenseNet
首先,我們通過對ResNet的對比來大概瞭解一下 DenseNet。
下圖為ResNet網路的短路連線機制(其中+代表的是元素級相加操作)。
可以看出ResNet是每個層與前面的某層(一般是2~3層)短路連線到一起,連線方式是通過元素級相加。
DenseNet的基本思路與ResNet一致,但是它建立的是前面所有層與後面層的密集連線(dense connection),它的名稱也是由此而來。DenseNet的另外一大特色是通過特徵在channel上的連線來實現特徵重用(feature reuse)。這些特點讓DenseNet在引數和計算成本更少的情形下實現比ResNet更優的效能。
相比ResNet,DenseNet提出了一個更激進的密集連線機制:即互相連線所有的層,具體來說就是每個層都會接受其前面所有層作為其額外的輸入。ResNet是每個層與前面的某層(一般是2~3層)短路連線在一起,連線方式是通過元素級相加。而在DenseNet中,每個層都會與前面所有層在 channel維度上連線(Concat)在一起(這裡各個層的特徵圖大小是相同的,後面會說明),並作為下一層的輸入。對於一個 L 層的網路,DenseNet共包含L*(L+1)/2 個連線,相比ResNet,這是一種密集連線。而且DenseNet是直接Concat來自不同層的特徵圖,這可以實現特徵重用,提高效率,這一特點是DenseNet和ResNet最主要的區別。
需要明確一點,Dense connectivity 僅僅是在一個 Dense Block 裡的,不同 Dense Block 之間是沒有Dense Connectivity的。
下圖為DenseNet網路的密集連線機制(其中C代表的是 channel級連線操作),在DenseNet中直接 concat來自不同層的特徵圖,這可以實現特徵重用,提升效率,這一特點是DenseNet與ResNet最主要的區別。
如果用公式表示的話,傳統的網路在 l 層的輸出為:
而對於ResNet,增加了來自上一層輸入的 identity函式:
在DenseNet中,會連線前面所有層作為輸入:
其中,上面的 Ht(*) 代表是非線性轉換函式(non-linear-transformation),它是一個組合操作,其可能包括一系列的 BN(Batch Normalization),ReLU,Pooling及其Conv操作。注意這裡的 l 層與 l-1 層之間實際上包含多個卷積層。
1.1 Keras中add和 concatenate 操作的不同
說起ResNet和DenseNet的區別了,就不得不說一下程式碼層面了,畢竟我們的目的是實現它。
首先說結論,ResNet的使用都是 add 操作,而DenseNet和InceptionNet使用的都是 concatenate操作。
關於 Concatenate 操作
拼接,H,W 都不改變,但是通道數增加。
網路結構設計中很重要的一種操作,經常用於將特徵聯合,多個卷積提取框架提取的特徵融合或者是將輸出層的資訊進行融合。Densenet 是做通道的合併,而Concatnate 是通道數的合併,也就是說描述影像本身的特徵增加了,而每一特徵下的資訊是沒有增加的。
Keras 中 Concatnate 函式與 concatnate 函式
這裡直接分析原始碼,不多分析只看區別:
首先是 Concatenate()函式:
class Concatenate(_Merge): """Layer that concatenates a list of inputs. It takes as input a list of tensors, all of the same shape except for the concatenation axis, and returns a single tensor, the concatenation of all inputs. # Arguments axis: Axis along which to concatenate. **kwargs: standard layer keyword arguments. """
再來是 concatenate()函式:
def concatenate(inputs, axis=-1, **kwargs): """Functional interface to the `Concatenate` layer. # Arguments inputs: A list of input tensors (at least 2). axis: Concatenation axis. **kwargs: Standard layer keyword arguments. # Returns A tensor, the concatenation of the inputs alongside axis `axis`. """ return Concatenate(axis=axis, **kwargs)(inputs)
concatenate() 函式 是 Concatenate() 函式的介面函式,我們可以使用兩個中的任意一個,但是方法要寫正確。後面我們會做程式碼驗證。
關於 Add 操作
加,H,W,C 都不改變,只是相應元素的值會改變。
資訊之間的疊加,ResNet是做值的疊加,通道數是不變的。add是描述影像的特徵下的資訊量增多了,但是描述影像的維度本身沒有增加,只是在每一維度下資訊量在增加。
Keras 中 Add 函式與 add 函式
這裡直接分析原始碼,不多分析只看區別:
首先是 Add()函式:
class Add(_Merge): """Layer that adds a list of inputs. It takes as input a list of tensors, all of the same shape, and returns a single tensor (also of the same shape). # Examples ```python import keras input1 = keras.layers.Input(shape=(16,)) x1 = keras.layers.Dense(8, activation='relu')(input1) input2 = keras.layers.Input(shape=(32,)) x2 = keras.layers.Dense(8, activation='relu')(input2) # equivalent to added = keras.layers.add([x1, x2]) added = keras.layers.Add()([x1, x2]) out = keras.layers.Dense(4)(added) model = keras.models.Model(inputs=[input1, input2], outputs=out) ``` """
再來是 add()函式:
def add(inputs, **kwargs): """Functional interface to the `Add` layer. # Arguments inputs: A list of input tensors (at least 2). **kwargs: Standard layer keyword arguments. # Returns A tensor, the sum of the inputs. # Examples ```python import keras input1 = keras.layers.Input(shape=(16,)) x1 = keras.layers.Dense(8, activation='relu')(input1) input2 = keras.layers.Input(shape=(32,)) x2 = keras.layers.Dense(8, activation='relu')(input2) added = keras.layers.add([x1, x2]) out = keras.layers.Dense(4)(added) model = keras.models.Model(inputs=[input1, input2], outputs=out) ``` """ return Add(**kwargs)(inputs)
add() 函式 是 Add() 函式的介面函式,我們可以使用兩個中的任意一個,但是方法要寫正確。後面我們會做程式碼驗證。
程式碼展示Keras中四個函式的區別
程式碼如下:
from keras.layers import Concatenate, Add, add, concatenate import numpy as np import tensorflow as tf matrix1 = np.array([[1,2,3], [4,5,6]]) matrix2 = np.array([[11,22,33], [44,55,66]]) # 將一個numpy資料轉換為tensor t1 = tf.convert_to_tensor(matrix1) t2 = tf.convert_to_tensor(matrix2) print(t1) print(t2) ''' [[1 2 3] [4 5 6]] [[11 22 33] [44 55 66]] Tensor("Const:0", shape=(2, 3), dtype=int32) Tensor("Const_1:0", shape=(2, 3), dtype=int32) ''' exp_Add = Add()([t1, t2]) exp_Concatenate = Concatenate()([t1, t2]) print(exp_Add) print(exp_Concatenate) # 要對tensor進行操作,需要先啟動一個Session with tf.Session() as sess: print('exp_Concatenate is ', exp_Add.eval()) print('exp_Concatenate is ', exp_Concatenate.eval()) ''' exp_Concatenate is [[12 24 36] [48 60 72]] exp_Concatenate is [[ 1 2 3 11 22 33] [ 4 5 6 44 55 66]] Tensor("add_1/add:0", shape=(2, 3), dtype=int32) Tensor("concatenate_1/concat:0", shape=(2, 6), dtype=int32) ''' exp_Add1 = add([t1, t2]) exp_Concatenate1 = concatenate([t1, t2]) print(exp_Add1) print(exp_Concatenate1) ''' Tensor("add_2/add:0", shape=(2, 3), dtype=int32) Tensor("concatenate_2/concat:0", shape=(2, 6), dtype=int32) ''' with tf.Session() as sess: print(exp_Add1.eval() == exp_Add.eval()) print(exp_Concatenate1.eval() == exp_Concatenate.eval()) ''' [[ True True True] [ True True True]] [[ True True True True True True] [ True True True True True True]] '''
2,DenseNet網路架構
當CNNs增加深度的時候,就會出現一個緊要的問題:當輸入或者梯度的資訊通過很多層之後,它可能會消失或過度膨脹。研究表明,如果卷積網路在接近輸入和接近輸出地層之間包含較短地連線,那麼,該網路可以顯著地加深,變得更精確並且能夠更有效的訓練。在論文中提出的架構為了確保網路層之間的最大資訊流,將所有層直接彼此連線。為了保持前饋特性,每個層從前面的所有層獲得額外的輸入,並將自己的特徵對映傳遞到後面的所有層。該論文基於這個觀察提出了以前饋的方式將每個層與其他層連線的密集卷積網路(DenseNet)。
原作者通過觀察目前深度網路的一個重要特點就是都加入了 shorter connections,能夠讓網路更深,更準確,更高效。作者充分利用了 skip connections ,設計了一種稠密卷積神經網路(Dense Convolutional Network),讓每一層都接受它前面所有層的輸出。對於傳統卷積結構,L層一共有L個 connections,但DenseNet,L層一共有L(L-1)/2 個 Connection。
2.1 DenseNet閃光點
- 1,相比ResNet 擁有更少的引數數量
- 2,旁路加強了特徵的重用
- 3,網路更易於訓練,並具有一定的正則效果
- 4,緩解了梯度消失(gradient vanishing)和模型退化(model degradation)的問題
2.2 DenseNet 網路分析
DenseNet 是一種具有密集連線的卷積神經網路。在該網路中,任何兩層之間都有直接的連線,也就是說,網路每一層的輸入都是前面所有層輸出的並集,而該層所學習的特徵圖也會被直接傳給其後面所有層作為輸入。
下圖是一個五層的密集塊:
DenseNet 的前向過程如上圖所示,可以更直觀地理解其密集連線方式,比如 h3 的輸入不僅包括來自 h2 的 x2,還包括前面兩層的 x1 和 x2,他們是在 channel 維度上連線在一起的。
下圖給出了DenseNet的網路結構,它共包含 4個 DenseBlock,各個 DenseBlock之間通過Transition 連線在一起。
CNN網路一般要經過Pooling或者 stride>1 的Conv 來降低特徵圖的大小,而DenseNet的密集連線方式需要特徵圖大小保持一致。為了解決這個問題,DenseNet網路中使用 DenseBlock + Transition 的結構,其中 DenseBlock 是包含很多層的模組,每個層的特徵圖大小相同,層與層之間採用密集連線方式。而 Transition模組是連線兩個相鄰的 DenseBlock ,並且通過 Pooling使特徵圖大小降低。
2.3 Dense Block
首先展示一下Dense Block網路結構:
Dense Block模組:BN + ReLU + Conv(3*3) + dropout
transition layer模組:BN + ReLU + Conv(1*1)(filter_num:m) + dropout + Pooling(2*2)
我們知道DenseNet的網路結構主要由DenseBlock和 Transition組成,下面具體來學習網路的實現細節,首先看網路結構:
在DenseBlock中,各個層的特徵圖大小一致,可以在channel維度上連線。DenseBlock中的非線性組合 H(*) 採用的是 BN+ReLU+3*3Conv 的結構,如下圖所示:
該架構與ResNet相比,在將特性傳遞到層之前,沒有通過求和來組合特性,而是通過連線他們的方式來組合特性。因此第 x 層(輸入層不算在內)將由 x個輸出的特徵圖,這些輸入是之前所有層提取出的特徵資訊,或者說採用 x 個卷積核, x 在DenseNet稱為 growth rate,這是一個超引數,一般情況下使用較小的 k(比如12),就可以得到較佳的效能。假定輸入層的特徵圖的 channels 數為 k0,那麼 l 層的輸入的 channel 數為 k0 + k(l - 1),因此隨著層數的增加,儘管k設定的較小,DenseBlock的輸入會非常多,不過這是由於特徵重用所造成的,每個層僅有 K 個特徵是自己獨有的,因為它的密集連線特性,研究人員將其稱為 Dense Convolutional Network(DenseNet)。
因為不需要重新學習冗餘特徵圖,這種密集連線模式相對於傳統的卷積網路只需要更少的引數。傳統的前饋體系結構可以看做是具有一種狀態的演算法,這種狀態從一個層傳遞到另一個層。每個層從其前一層讀取狀態並將其寫入後續層。它改變狀態,但也傳遞需要保留的資訊。研究提出的密集網路體系結構明確區分了新增到網路的資訊和保留的資訊。密集網層非常窄(例如:每層12個過濾器),僅向網路的“集體知識”新增一小組特徵對映,並且保持其餘特徵對映不變,並且最終分類器基於網路中的所有特徵對映做出決策。
除了引數更少,另一個DenseNets 的優點是改進了整個網路的資訊流和梯度,這使得他們易於訓練。每個層直接訪問來自損失函式和原始輸入訊號的梯度,帶來了隱式深度監控。這使得訓練深層網路變得更簡單。此外,研究人員觀察到密集連線具有規則化效果,這減少了對訓練集較小的任務的過擬合。
2.4 DenseNet-B
首先展示一下 DenseNet-B 網路結構:
Dense Block模組:BN + ReLU + Conv(1*1)(filter_num:4K) + dropout + BN + ReLU + Conv(3*3) + dropout
transition layer模組:BN + ReLU + Conv(1*1)(filter_num:m) + dropout + Pooling(2*2)
密集連線不會帶來冗餘嗎?不會!密集連線這個詞給人的第一感覺就是極大地增加了網路的引數量和計算量。但是實際上DenseNet比其他網路效率更高,其關鍵就在於網路每層計算量的減少以及特徵的重複利用。DenseNet 則是讓 l 層的輸入直接影響到之後的所有層,它的輸出為:xl = H1([X0, X1, ... Xl-1]),其中 [X0, x1, ...Xl-1] 就是將之前的 feature map 以通道的維度進行合併。並且由於每一層都包含之前所有層的輸出資訊,因此其只需要很少的特徵圖就夠了,這也是為什麼 DenseNet的引數量較其他模型大大減少的原因。這種Dense Connection 相當於每一層都直接連線 input 和 loss,因此就可以減輕梯度消失現象,這樣更深網路不是問題。需要明確一點,Dense Connectivity 僅僅是在一個 Dense Block裡的,不同Dense Block 之間是沒有Dense Connectivity的,比如下圖所示:
天底下沒有免費的午餐,網路自然也不例外。在同層深度下獲得更好的收斂率,自然是由額外代價的,其代價之一就是其恐怖如斯的記憶體佔用。
每一個DenseBlock模組的輸出維度有多大呢?
假設一個L層的Dense Block模組中輸出K個 feature map,即網路增長率為K,其中已經加入了Bottleneck單元,那麼第L層的輸入為K0 + K*(L-1)(其中第K0為輸入層的維度),而總共輸出的維度為:第一層的維度 + 第二層的維度 + 第三層的維度 + ... + 第L層的維度,加入Bottleneck單元后每層的輸出維度為4K,那麼最終 Dense Block模組的輸出維度為4K*L。也就是說隨著Dense Block 模組深度的加深,即隨著層數L的增加,最終輸出的 feature map 的維度也是一個很大的數,為了解決這個問題,在transition layer模組中加入了1*1卷積做降維。
為了解決這個問題,在Dense Block模組中加入了 Bottleneck單元,如下圖所示,即 BN + ReLU + 1*1Conv + BN + ReLU + 3*3 Conv,稱為 DenseNet-B結構。其中1*1Conv降維得到 4k 個特徵圖它起到的作用是降低特徵數量,從而提升計算效率(K為增長率)。
2.5 DenseNet-BC
首先展示一下 DenseNet-BC 網路結構:
Dense Block模組:BN + ReLU + Conv(1*1)(filter_num:4K) + dropout + BN + ReLU + Conv(3*3) + dropout
transition layer模組:BN + ReLU + Conv(1*1)(filter_num:θm,其中 0<θ<1,文章取θ=0.5) + dropout + Pooling(2*2)
對於 Transition層,它主要是連線兩個相鄰的 DenseBlock,並且降低特徵圖大小。Transition層包括一個1*1的卷積和 2*2 的 AvgPooling,結構為:BN + ReLU + 1*1 Conv + 2*2 AvgPooling。另外,Transition層可以起到壓縮模型的作用。假定 Transition的上接 DenseBlock得到的特徵圖 channels 數為 m,Transition可以產生 |θm| 個特徵(通過卷積層),其中 θ € (0, 1] 是壓縮係數(compression rate),當 θ = 1 時,特徵個數經過 Transition層沒有變換,即無壓縮,而當壓縮係數小於1時,這種結構稱為 DenseNet-C,文章使用 θ=0.5 。對於使用 Bottleneck 層的 DenseBlock結構和壓縮係數小於1的Transition 組合結構稱為 DenseNet-BC。
3,DenseNet的優缺點分析
參考地址:https://blog.csdn.net/comway_Li/article/details/82055229
DenseNet 的核心思想在於建立了不同層之間的連線關係,充分利用了feature,進一步減輕了梯度消失問題,加深網路不是問題,而且訓練效果非常好。另外利用bottleneck layer,Transition layer 以及較小的 growth rate 使得網路變窄,引數減少,有效抑制了過擬合,同時計算量也減少了。DenseNet優點很多,而且在和ResNet的對比中優勢還是非常明顯的。
3.1 作者提出演算法的出發點
目前來看,深度卷積網路挑戰主要有:
- 1,Underfitting(欠擬合):一般來說,模型越為複雜,表現能力越強,越不融合欠擬合。但是深度網路不一樣,表現表達能力夠,但是演算法不能達到那個全域性最優(ResNet基本解決)
- 2,Overfitting(過擬合):泛化能力下降
- 3,實際系統的部署問題,如何提升效率和減少記憶體,能量消耗
那麼如何消除上述的冗餘性?得到更緊湊的結構?更好的泛化效能?由隨機網路深度,我們就得知訓練時扔掉大部分層卻效果不錯,說明冗餘性很多,每一層乾的事情很少,只學一點東西。
所以目的就是減少不必要的計算,提高泛化效能。
3.2 演算法優點
綜合來看,DenseNet的優勢主要體現在以下幾個方面:
- 抗過擬合,由於密集連線方式,DenseNet提升了梯度的反向傳播,使得網路更容易訓練。由於每層可以直達最後的誤差訊號,實現了隱式的“deep supervision”;所以DenseNet具有非常好的抗過擬合效能,尤其適合於訓練資料相對匱乏的應用。
- 引數更小且計算更高效,這有點違反直覺,由於DenseNet是通過concat特徵來實現短路連線,實現了特徵重用,並且採用較小的growth rate,每個層所獨有的特徵圖是比較小的;達到了與ResNet相當的精度,DenseNet所需的計算量也只有ResNet的一半左右。計算效率在深度學習實際應用中的需求非常強烈。
- 泛化性更強,如果沒有 data augmention,CIFAR-100下,ResNet表現下降很多,DenseNet下降不多,說明DenseNet泛化效能更強。
要注意的一點是,如果實現方式不當的話,DenseNet可能耗費很多GPU視訊記憶體,一種高效的實現如下圖所示,更多細節可以見這篇論文Memory-Efficient Implementation of DenseNets。不過我們下面使用Pytorch框架可以自動實現這種優化。
3.3 如何對DenseNet的模型做改進
1,每層開始的瓶頸層(1*1 卷積)對於減少引數量和計算量非常有用
2,像VGG和ResNet那樣每做一次下采樣(down-sampling)之後都把層寬度(growth rate)增加一倍,可以提高 DenseNet 的計算效率(FLOPS efficiency)
3,與其他網路一樣,DenseNet的深度和寬度應該均衡的變化,當然DenseNet 每層的寬度要遠小於其他模型
4,每一層設計得較窄會降低 DenseNet 在GPU 上的執行效率,但可能會提高再 CPU 上的執行效率
3.4 DenseNet 是否耗費視訊記憶體
如果出現DenseNet在訓練時對記憶體消耗非常厲害。這個問題其實是演算法實現不優帶來的。當前的深度學習框架對 DenseNet 的密集連線沒有很好的支援,我們只能藉助於反覆的拼接(Concatenation)操作,將之前層的輸出與當前層的輸出拼接在一起,然後傳給下一層。對於大多數框架(如 Torch 和 TensorFlow),每次拼接操作都會開闢新的記憶體來儲存拼接後的特徵。這樣就導致一個L層的網路,要消耗相當於L(L+1)/2 層網路的記憶體(第i 層的輸出在記憶體裡被存了 (L-i+1)份)。
解決這個問題的思路其實並不能,我們只需要預先分配一塊快取,供網路中所有的拼接村(Concatenation layer)共享使用,這樣DenseNet對記憶體的消耗便從平方級降到了線性級別。在梯度反傳過程中,我們再把相應卷積層的輸出複製到該快取,就可以重構每一層的輸入特徵,進而計算梯度。當然網路中由於Batch Normalization層的存在,實現起來還有一些需要注意的細節。
新的實現極大地減少了 DenseNet 在訓練時對視訊記憶體的消耗,比如論文中 190 層的 DenseNet 原來幾乎佔滿了 4塊 12G的記憶體的GPU,而優化後的程式碼僅需要 9G 的視訊記憶體,在單卡上就能訓練。
另外就是網路在推理(或測試)的時候對記憶體的消耗,這個是我們在實際產品中(尤其是在移動裝置上)部署深度學習模型時最關心的問題。不同於訓練,一般神經網路的推理過程不需要一直保留每一層的輸出,因此可以在每計算好一層的特徵後便將前面層特徵佔用的記憶體釋放掉,而DenseNet則需要始終儲存所有前面層的輸出。但是考慮到 DenseNet每一層產生的特徵圖很少,所以在推理的時候佔用記憶體不會多於其他網路。
4,DenseNet演算法總結分析
4.1,DenseNet網路三種結構的區分
文章同時提出了DenseNet,DenseNet-B,DenseNet-BC三種結構,上面也學習了,這裡再單獨提出來,具體區別如下:
DenseNet
- Dense Block模組:BN+Relu+Conv(3*3)+dropout
- transition layer模組:BN+Relu+Conv(1*1)(filternum:m)+dropout+Pooling(2*2)
DenseNet-B
- Dense Block模組:BN+Relu+Conv(1*1)(filternum:4K)+dropout+BN+Relu+Conv(3*3)+dropout
- transition layer模組:BN+Relu+Conv(1*1)(filternum:m)+dropout+Pooling(2*2)
DenseNet-BC
- Dense Block模組:BN+Relu+Conv(1*1)(filternum:4K)+dropout+BN+Relu+Conv(3*3)+dropout
- transition layer模組:BN+Relu+Conv(1*1)(filternum:θm,其中0<θ<1,文章取θ=0.5) +dropout +Pooling(2*2)
其中,DenseNet-B在原始DenseNet的基礎上,加入Bottleneck layers, 主要是在Dense Block模組中加入了1*1卷積,使得將每一個layer輸入的feature map都降為到4k的維度,大大的減少了計算量。
4.2 問題1:pooling
因為神經網路從輸入到輸出趨勢就是 channel 數逐漸增加, feature map逐漸縮小,而使 feature map 縮小的操作就是 pooling,pooling 前後 feature map 不一樣,這種情況下 concatenation 是沒有用的,這種情況下論文將一個大網路分成幾個 dense blocks,中間使用 transition layers(一個作用就是 pooling)進行連線。
transition block層的程式碼如下:
def transition_block(input,nb_filter,dropout_rate=None,pooltype=1,weight_decay=1e-4): x = BatchNormalization(axis=-1,epsilon=1.1e-5)(input) x = Activation('relu')(x) x = Conv2D(nb_filter,(1,1),kernel_initializer='he_normal', padding='same', use_bias=False, kernel_regularizer=l2(weight_decay))(x) if(dropout_rate): x = Dropout(dropout_rate)(x) if(pooltype==2): x = AveragePooling2D((2,2),strides=(2,2))(x) elif(pooltype==1): x = ZeroPadding2D(padding=(0,1))(x) x = AveragePooling2D((2,2),strides=(2,1))(x) elif(pooltype==3): x = AveragePooling2D((2,2),strides=(2,1))(x) return x,nb_filter
4.3 問題2:指數增長的通道數
當看到DenseNet的公式的時候,我們肯定會想到通道數增長速度的問題。
輸入通道數為 c0 = k0,卷積不改變通道數,那麼第一層通道數為 c1 = c0 + k0 = 2k0,第二層通道數為 c2 = c0+c1+k0 = K0+2K0+k0 =4K0,...,很輕鬆證明:Cn = 2n-1k0,這種指數級別的通道數是不允許存在的,過多的通道數會極大的增加引數量,從而降低執行速度。
所以論文首先給出了一個限定條件,即方程 Hl(*) 輸出的通道數不是 Cn = 2n-1k0,而是一個固定的k(論文使用了一個專門的術語growth rate 表示這個引數),即每一層都是固定的通道數,但是輸入 [x0, x1, ....xn] 的通道數為 k0+k(l-1),這種通道數的差別表明方程 Hl(*) 有一個維度壓縮過程。
growth rate 靠卷積得到固定通道數:
def conv_block(input,growth_rate,dropout_rate=None,weight_decay=1e-4): x = BatchNormalization(axis=-1,epsilon=1.1e-5)(input) x = Activation('relu')(x) x = Conv2D(growth_rate,(3,3),kernel_initializer='he_normal', padding = 'same')(x) if(dropout_rate): x = Dropout(dropout_rate)(x) return x
論文認為固定為 k 的通道數表明了網路的全域性狀態,而隨著 feature map 的逐步縮小,表示資訊越來越集中了,越來越成為高階特徵了。
但是論文研究又發現L和k的增加會使得網路表現更好,注意,k=8的結果會比k=32的結果要差幾個百分點,但是 k=32會讓網路的視訊記憶體佔用超級大。
也可以使用 1*1 的卷積網路,所以總的網路就變成了 BN-ReLU-Conv(1*1)-BN-ReLU-Conv(3*3),輸出的通道數為 4k 能夠利用 1*1 卷積核先將通道數降低(畢竟 k 為32顯得有點小,實際專案還可能設定為 8)。
dense_block的程式碼:
def dense_block(x,nb_layers,nb_filter,growth_rate,droput_rate=0.2,weight_decay=1e-4): for i in range(nb_layers): cb = conv_block(x,growth_rate,droput_rate,weight_decay) x = concatenate([x,cb],axis=-1) nb_filter +=growth_rate return x ,nb_filter
4.4 問題3:引數過多
DenseNet不斷堆積卷積網路,引數增長是很明顯的,所以一般使用卷積網路壓縮輸入:The initial convolution layer comprises 2k convolutions of size 7*7 with stride 2;
對應的程式碼為:
_nb_filter = 64 # conv 64 5*5 s=2 x = Conv2D(_nb_filter ,(5,5),strides=(2,2),kernel_initializer='he_normal', padding='same', use_bias=False, kernel_regularizer=l2(_weight_decay))(input)
為了防止 dense block 將卷積通道數線性增加,使得後期通道數過多,transition layers 一般會對通道數進行縮減:to further improve model compactness, we can reduce the number of feature-maps at transition layers.
對應的程式碼為:
# 64 + 8 * 8 = 128 x ,_nb_filter = dense_block(x,8,_nb_filter,8,None,_weight_decay) #128 x,_nb_filter = transition_block(x,128,_dropout_rate,2,_weight_decay) #128 + 8 * 8 = 192 x ,_nb_filter = dense_block(x,8,_nb_filter,8,None,_weight_decay) #192->128 x,_nb_filter = transition_block(x,128,_dropout_rate,2,_weight_decay) #128 + 8 * 8 = 192 x ,_nb_filter = dense_block(x,8,_nb_filter,8,None,_weight_decay)
可以看到 transition block 的通道數都是 128,實際上後一個 transition block 輸入的通道數為 192,進行了通道的縮減。
4.5 熱力圖分析
在設計初,DenseNet便被設計成讓一層網路可以使用所有值錢層網路 feature map 的網路結構,為了探索 feature 的複用情況,作者進行了相關實驗,作者訓練的 L=40,K=12 的DenseNet,對於任意Dense Block中的所有卷積層,計算之前某層 feature map 在該層權重的絕對值平均數,這一平均數表明了這一層對於之前某一層 feature 的利用率,下圖為由該平均數繪製出的熱力圖:
從圖中我們可以得出以下結論:
- 1,一些較早層提取出的特徵仍可能被較深層直接使用
- 2,即使是 Transition Layer也會使用到之前 DenseBlock中所有層的特徵
- 3,第2~3個DenseBlock中的層對之前Transition Layer利用率很低,說明 transition layer 輸出大量冗餘特徵,這也為DenseNet-BC提供了證據支援,即Compression的必要性
- 4,最後的分類層雖然使用了之前DenseBlock的多層資訊,但是更偏向於使用最後幾個 feature map 的特徵,說明在網路的最後幾層,某些 high-level 的特徵可能被產生。
5,Keras實現
訓練的話:對於沒有使用資料增強的資料集,在卷積層後使用了 dropout ,dropout的比例為 0.2。
DenseNet 的一個版本的Keras實現如下(如果想看其他版本,請去我的GitHub拿,地址在上面):
''' this script is DenseNet model for Keras link: https://www.jianshu.com/p/274d050d517e ''' from __future__ import print_function from __future__ import absolute_import from __future__ import division from keras.models import Model, Sequential from keras.layers import Flatten, Dense, Input, Conv2D, MaxPooling2D, BatchNormalization, Dropout from keras.optimizers import Adam from keras.layers import Input, Activation from keras.layers.pooling import GlobalAveragePooling2D, AveragePooling2D, MaxPooling2D from keras.regularizers import l2 from keras.layers import Concatenate, Add, add, concatenate from keras.layers.normalization import BatchNormalization import keras.backend as K def conv_block(input_shape, nb_filter, bottleneck=False, dropout_rate=None, weight_decay=1e-4): ''' Apply BatchNorm, Relu, 3*3 Conv2D, optional bottleneck block and dropout Args: input_shape: Input keras tensor nb_filter: number of filters bottleneck: add bottleneck block dropout_rate: dropout rate weight_decay: weight decay factor returns: keras tensor with batch_norm, relu and convolution2d added (optional bottleneck) ''' # 表示特徵軸,因為連線和BN都是對特徵軸來說 concat_axis = 1 if K.image_data_format() == 'channel_first' else -1 x = BatchNormalization(axis=concat_axis, epsilon=1.1e-5)(input_shape) x = Activation('relu')(x) # bottleneck 表示是否使用瓶頸層,也就是使用1*1的卷積層將特徵圖的通道數進行壓縮 if bottleneck: inter_channel = nb_filter * 4 # He正態分佈初始化方法,引數由0均值,標準差為sqrt(2 / fan_in) 的正態分佈產生,其中fan_in權重張量的扇入 x = Conv2D(inter_channel, (1, 1), kernel_initializer='he_normal', padding='same', use_bias=False, kernel_regularizer=l2(weight_decay))(x) x = BatchNormalization(axis=concat_axis, epsilon=1.1e-5)(x) x = Activation('relu')(x) x = Conv2D(nb_filter, (3, 3), kernel_initializer='he_normal', padding='same', use_bias=False)(x) if dropout_rate: x = Dropout(dropout_rate)(x) return x def transition_block(input_shape, nb_filter, compression=1.0, weight_decay=1e-4, is_max=False): ''' 過渡層是用來連線兩個 dense block,同時在最後一個dense block的尾部不需要使用過渡層 按照論文的說法:過渡層由四部分組成: BatchNormalization ReLU 1*1Conv 2*2Maxpooling Apply BatchNorm, ReLU , Conv2d, optional compression, dropout and Maxpooling2D Args: input_shape: keras tensor nb_filter: number of filters compression: caculated as 1-reduction, reduces the number of features maps in the transition block (compression_rate 表示壓縮率,將通道數進行調整) dropout_rate: dropout rate weight_decay: weight decay factor return : keras tensor, after applying batch_norm, relu-conv, dropout maxpool ''' # 表示特徵軸,因為連線和BN都是對特徵軸來說 concat_axis = 1 if K.image_data_format() == 'channel_first' else -1 x = BatchNormalization(axis=concat_axis, epsilon=1.1e-5)(input_shape) x = Activation('relu')(x) x = Conv2D(int(nb_filter * compression), (1, 1), kernel_initializer='he_normal', padding='same', use_bias=False, kernel_regularizer=l2(weight_decay))(x) # 論文提出使用均值池化層來做下采樣,不過在邊緣提取方面,最大池化層效果應該更好,可以加上介面 if is_max: x = Maxpooling2D((2, 2), strides=(2, 2))(x) else: x = AveragePooling2D((2, 2), strides=(2, 2))(x) return x def dense_block(input_shape, nb_layers, nb_filter, growth_rate, bottleneck=False, dropout_rate=None, weight_decay=1e-4, grow_nb_filters=True, return_concat_list=False): ''' Bulid a dense_block where the output of each conv_block is fed to subsequent ones 此處使用迴圈實現了Dense Block 的密集連線 Args: input_shape: keras tensor nb_layers: the number of layers of conv_block to append to the model nb_filter: number of filters growth_rate: growth rate weight_decay: weight decay factor grow_nv_filters: flag to decode to allow number of filters to grow return_concat_list: return the list of feature maps along with the actual output returns: keras tensor with nv_layers of conv_block append 其中 x=concatenate([x, cb], axis=concat_axis)操作使得x在每次迴圈中始終維護一個全域性狀態 第一次迴圈輸入為 x, 輸出為 cb1,第二次輸入為 cb=[x,cb1],輸出為cb2,第三次輸入為cb=[x,cb1,cb2],輸出為cb3 以此類推,增長率為growth_rate 其實就是每次卷積時使用的卷積核個數,也就是最後輸出的通道數。 ''' concat_axis = 1 if K.image_data_format() == 'channel_first' else -1 x_list = [input_shape] for i in range(nb_layers): cb = conv_block(input_shape, growth_rate, bottleneck, dropout_rate, weight_decay) x_list.append(cb) x = concatenate([input_shape, cb], axis=concat_axis) if grow_nb_filters: nb_filter += growth_rate if return_concat_list: return x, nb_filter, x_list else: return x, nb_filter def DenseNet_model(input_shape, classes, depth=40, nb_dense_block=3, growth_rate=12, include_top=True, nb_filter=-1, nb_layers_per_block=[6, 12, 32, 32], bottleneck=False, reduction=0.0, dropout_rate=None, weight_decay=1e-4, subsample_initial_block=False, activation='softmax'): ''' Build the DenseNet model Args: classes: number of classes input_shape: tuple of shape (channels, rows, columns) or (rows, columns, channels) include_top: flag to include the final Dense layer depth: number or layers nb_dense_block: number of dense blocks to add to end (generally = 3) growth_rate: number of filters to add per dense block nb_filter: initial number of filters. Default -1 indicates initial number of filters is 2 * growth_rate nb_layers_per_block: number of layers in each dense block. Can be a -1, positive integer or a list. If -1, calculates nb_layer_per_block from the depth of the network. If positive integer, a set number of layers per dense block. If list, nb_layer is used as provided. Note that list size must be (nb_dense_block + 1) bottleneck: add bottleneck blocks reduction: reduction factor of transition blocks. Note : reduction value is inverted to compute compression dropout_rate: dropout rate weight_decay: weight decay rate subsample_initial_block: Set to True to subsample the initial convolution and add a MaxPool2D before the dense blocks are added. subsample_initial: activation: Type of activation at the top layer. Can be one of 'softmax' or 'sigmoid'. Note that if sigmoid is used, classes must be 1. Returns: keras tensor with nb_layers of conv_block appended ''' concat_axis = 1 if K.image_data_format() == 'channel_first' else -1 if type(nb_layers_per_block) is not list: print('nb_layers_per_block should be a list !!!') return 0 final_nb_layer = nb_layers_per_block[-1] nb_layers = nb_layers_per_block[:-1] # compute initial nb_filter if -1 else accept users initial nb_filter if nb_filter <= 0: nb_filter = 2 * growth_rate # compute compression factor compression = 1.0 - reduction # initial convolution if subsample_initial_block: initial_kernel = (7, 7) initial_strides = (2, 2) else: initial_kernel = (3, 3) initial_strides = (1, 1) Inp = Input(shape=input_shape) x =Conv2D(nb_filter, initial_kernel, kernel_initializer='he_normal', padding='same', strides=initial_strides, use_bias=False, kernel_regularizer=l2(weight_decay))(Inp) if subsample_initial_block: x = BatchNormalization(axis=concat_axis, epsilon=1.1e-5)(x) x = Activation('relu')(x) x = Maxpooling2D((3, 3), strides=(2, 2), padding='same')(x) # add dense blocks for block_index in range(nb_dense_block-1): x, nb_filter = dense_block(x, nb_layers[block_index], nb_filter, growth_rate, bottleneck=bottleneck, dropout_rate=dropout_rate, weight_decay=weight_decay) # add transition block x = transition_block(x, nb_filter, compression=compression, weight_decay=weight_decay) nb_filter = int(nb_filter * compression) # the last dense block does not have a transition_block x, nb_filter = dense_block(x, final_nb_layer, nb_filter, growth_rate, bottleneck=bottleneck, dropout_rate=dropout_rate, weight_decay=weight_decay) x = BatchNormalization(axis=concat_axis, epsilon=1.1e-5)(x) x = Activation('relu')(x) x = GlobalAveragePooling2D()(x) if include_top: x = Dense(classes, activation=activation)(x) model = Model(Inp, output=x) model.summary() return model if __name__ == '__main__': DenseNet_model(input_shape=(227, 227, 3), classes=1000, bottleneck=True, reduction=0.5)
Keras實現地址:https://github.com/titu1994/DenseNet
https://github.com/flyyufelix/DenseNet-Keras
實現:https://blog.csdn.net/shi2xian2wei2/article/details/84425777
參考文獻:https://www.cnblogs.com/skyfsm/p/8451834.html
https://www.cnblogs.com/zhhfan/p/10187634.html
https://blog.csdn.net/Gentleman_Qin/article/details/84638700
包含了pytorch 的實現:https://zhuanlan.zhihu.com/p/37189203
https://blog.csdn.net/u014380165/article/details/75142664