Dropout技术之随机神经元与随机深度
1. 寫在前面
在學習復現EfficientNet網絡的時候,里面有一個MBConv模塊長下面這個樣子:
當然,這個結構本身并不是很新奇,從resNet開始,幾乎后面很多網絡,比如DenseNet, MobileNet系列,ShuffleNet系列以及EfficientNet系列都會發現這樣的殘差結構。 但這次探索里面發現了Dropout這個點, 之前在實現殘差結構的時候, 如果碰到Dropout, 我一直以為是之前學習到的隨機失活神經元的Dropout,但直到在這里看到源碼才發現,不是我想象的那么簡單!
這種殘差結構里面使用的Dropout,是一種叫做隨機深度的Dropout技術。這個是2016年ECCV上發表的一篇paper,論文叫做《Deep Network with Stochastic depth》, 說的是訓練過程中,不是隨機失活每一層的神經元了,而是隨機去掉很多層,這樣能減少冗余,還能加速訓練。
出于好奇,我讀了下這篇paper, 又學習到了一種訓練帶有殘差網絡的騷操作,所以,這篇文章想統一把這兩種Dropout放一塊整理下。
2. Dropout之隨機神經元
這個技術就是普通的Dropout技術了,Dropout隨機失活神經元,就是我們給出一個概率,讓神經網絡層的某個神經元權重為0(失活)
就是每一層,讓某些神經元不起作用,這樣就就相當于把網絡進行簡化了(左邊和右邊可以對比),我們有時候之所以會出現過擬合現象,就是因為我們的網絡太復雜了,參數太多了,并且我們后面層的網絡也可能太過于依賴前層的某個神經元。
加入Dropout之后, 首先網絡會變得簡單,減少一些參數,并且由于不知道淺層的哪些神經元會失活,導致后面的網絡不敢放太多的權重在前層的某個神經元,這樣就減輕了一個過渡依賴的現象, 對特征少了依賴, 從而有利于緩解過擬合。
這個類似于我們期末考試的時候有沒有,老師總是會給我們畫出一個重點,但是由于我們不知道這些重點哪些會真的出現在試卷上,所以就得把精力分的均勻一些,都得看看, 這樣保險一些,也能泛化一點,至少只要是這些類型的題都會做。 而如果我們不把精力分的均勻一些,只關注某種題型, 那么準糊一波
所以這種Dropout技術可以幫助網絡緩解過擬合。不太難理解, 但使用的時候有幾個注意問題:
數據尺度變化
我們用Dropout的時候是這樣用的: 只在訓練的時候開啟Dropout,而測試的時候是不用Dropout的,也就是說模型訓練的時候會隨機失活一部分神經元, 而測試的時候我們用所有的神經元,那么這時候就會出現這個數據尺度的問題, 所以測試的時候,所有權重都乘以1-drop_prob, 以保證訓練和測試時尺度變化一致。 怎么理解? 依然拿上面的圖來說:
假設我們的輸入是100個特征, 那么第一層的第一個神經元的表達式應該是這樣, 這里先假設不失活:
Z11=∑i=1100wixiZ_{1}^{1}=\sum_{i=1}^{100} w_{i} x_{i} Z11?=i=1∑100?wi?xi?
假設我們這里的wixi=1w_ix_i=1wi?xi?=1, 那么第一層第1個神經元Z11=100Z_1^1=100Z11?=100, 注意這是不失活的情況,那么如果失活呢? 假設失活率drop_prob=0.3, 也就是我們的輸入大約有30%是不起作用的,也就是會有30個不起作用, 當然這里是大約哈,因為失活率%30指的是每個神經元的失活率。換在整體上差不多可以理解成30個不起作用,那么我們的Z11Z_1^1Z11?相當于
Z11train=∑i=170wixi=70{Z_1^1}_{train} = \sum_{i=1}^{70} w_ix_i = 70Z11?train?=i=1∑70?wi?xi?=70
我們發現,如果使用Dropout之后,我們的Z11Z_1^1Z11?成了70, 比起不失活來少了30, 這就是一個尺度的變化, 所以我們就發現如果訓練的時候用Dropout, 我們每個神經元取值的一個尺度是會縮小的,比如這里的70, 而測試的時候我們用的是全部的神經元,尺度會變成100,這就導致了模型在數值上有了一個差異。因此,我們在測試的時候,需要所有的權重乘以1-drop_prob這一項, 這時候我們在測試的時候就相當于:
Z11test=∑i=1100(0.7×wi)xi=0.7×100=70{Z_1^1}_{test} = \sum_{i=1}^{100}(0.7\times w_i)x_i = 0.7 \times100 = 70Z11?test?=i=1∑100?(0.7×wi?)xi?=0.7×100=70
這樣采用Dropout的訓練集和不采用Dropout的測試集的尺度就變成一致了。 Pytorch在實現Dropout的時候, 是權重乘以11?p\frac{1}{1-p}1?p1?的,也就是除以1-p, 這樣就不用再測試的時候權重乘以1-p了, 也沒有改變原來數據的尺度。 也就是上面公式中的
Z11train=∑i=170(700.7wi)xi=100Z11test=∑i=1100wixi=100{Z_1^1}_{train} = \sum_{i=1}^{70} (\frac{70}{0.7}w_i)x_i = 100 \\ {Z_1^1}_{test} = \sum_{i=1}^{100} w_ix_i = 100Z11?train?=i=1∑70?(0.770?wi?)xi?=100Z11?test?=i=1∑100?wi?xi?=100
這個細節要注意下。
Dropout層放置的位置
比如,我們寫下面這段代碼
這里注意看MLP網絡里面Dropout層的位置,一般是放在需要Dropout的層的前面。輸入層不需要dropout,最后一個輸出層一般也不需要。就是由于Dropout操作,模型訓練和測試是不一樣的,上面我們說了,訓練的時候采用Dropout而測試的時候不用Dropout, 那么我們在迭代的時候,就得告訴網絡目前是什么狀態,如果要測試,就得先用.eval()函數告訴網絡一下子,訓練的時候就用.train()函數告訴網絡一下子。
這就是我們之前熟知的Dropout隨機神經元技術了, 之前我的學習認知也停留在這里為止,直到又見識到了隨機深度技術, 所以下面重點整理下這個是怎么玩的。
3. Dropout之隨機深度
隨機深度是黃高博士在2016年提出來的一種針對網絡高效訓練的技術, 談到黃高博士,可能大家更熟悉他提出的DenseNet網絡, 這個網絡要比隨機深度晚一些,但也受到隨機深度的一些啟發。
3.1 背景
深的網絡在現在表現出了十分強大的能力,但是也存在許多問題。即使在現代計算機上,梯度會消散、前向傳播中信息的不斷衰減、訓練時間也會非常緩慢等問題。
ResNet的強大性能在很多應用中已經得到了證實,盡管如此,ResNet還是有一個不可忽視的缺陷——更深層的網絡通常需要進行數周的訓練——因此,把它應用在實際場景下的成本非常高。為了解決這個問題,作者們引入了一個“反直覺”的方法,即在我們可以在訓練過程中任意地丟棄一些層,并在測試過程中使用完整的網絡。
在EfficientNet中也逐漸發現了這個現象, 之前的一些研究, 主要是關注網絡的準確率和參數數量,比如設計更加復雜的網絡結構,更深,更寬,分辨率更大等,去提高網絡的準確率,但后來逐漸發現,這些網絡在實際場景中可能不太好落地。 所以后續的一些研究,又開始關注與網絡的訓練速度,推理速度等,所以一些輕量級的網絡慢慢誕生。 比如MobileNet系列,ShuffleNet系列以及EfficientNet系列。 當然也有可能是精度慢慢的到了瓶頸了。
這篇paper也是想提高網絡的訓練速度或者效率,所以思路就是提出隨機深度,在訓練時使用較淺的深度(隨機在resnet的基礎上pass掉一些層),在測試時使用較深的深度,較少訓練時間,提高訓練性能,最終在四個數據集上都超過了resnet原有的性能(cifar-10, cifar-100, SVHN, imageNet)。其訓練過程中采用隨機dropout一些中間層的方法改進ResNet,發現可以顯著提高ResNet的泛化能力。
那么怎么做到呢?
3.2 網絡基本思想
作者用了殘差塊作為他們網絡的構件,因此,在訓練中,如果一個特定的殘差塊被啟用了,那么它的輸入就會同時流經恒等表換shortcut(identity shortcut)和權重層;否則輸入就只會流經恒等變換shortcut。
在訓練的過程中,每一個層都有一個“生存概率”,并且都會被任意丟棄。在測試過程中,所有的block都將保持被激活狀態,而且block都將根據其在訓練中的生存概率進行調整。
假設HlH_lHl?是第lll個殘差塊的輸出結果, flf_lfl?是由第lll個殘差塊的主分支輸出。blb_lbl?是一個隨機變量(只有1或者0,反映一個block是否是被激活的,或者是否啟用當前主分支)。那么加了隨機深度的Dropout之后的殘差塊輸出公式計算如下:
H?=ReLU?(b?f?(H??1)+id?(H??1))H_{\ell}=\operatorname{ReLU}\left(b_{\ell} f_{\ell}\left(H_{\ell-1}\right)+\operatorname{id}\left(H_{\ell-1}\right)\right) H??=ReLU(b??f??(H??1?)+id(H??1?))
這個其實也非常好理解, 原先的殘差結構,就是跳遠連接+主分支然后非線性激活,只不過這里多了一個blb_lbl?來控制主分支是否有效。 如果bl=0b_l=0bl?=0, 那么
Hl=ReLU?(id(Hl?1))H_{l}=\operatorname{ReLU}\left(i d\left(H_{l-1}\right)\right) Hl?=ReLU(id(Hl?1?))
直走跳遠連接,而這個是恒等映射,相當于當前的殘差塊不起作用,否則當前的殘差塊就被啟用。
那么這個blb_lbl?是怎么得到的呢? 這個和普通Dropout差不多,我們對于每個殘差塊,都指定一個是主分支激活的概率ppp,即每個殘差塊都有1?p1-p1?p可能性被dropout掉,即bl=0b_l=0bl?=0。
當然,在實際操作的時候,作者是將“線性衰減規律”應用到了每一層的生存概率,因為他們覺得較早的層會提取低級特征,而這些基礎特征對后面的層很重要,所以這些層不應該頻繁的丟棄主分支。 而隨著后面層提取的特征越來越抽象,冗余度可能更高,所以越到后面,這個丟棄主分支的概率就增加,具體計算公式如下:
p?=1??L(1?pL)p_{\ell}=1-\frac{\ell}{L}\left(1-p_{L}\right) p??=1?L??(1?pL?)
這里的plp_lpl?表示lll層訓練中主分支的保留概率,LLL是block塊的總數量, pLp_LpL?是我們給出的dropout_rate。lll是表示lll層的殘差塊。
實驗表明,同樣是訓練一個110層的ResNet,以任意深度進行訓練的性能,比以固定深度進行訓練的性能要好。這就意味著ResNet中的一些層(路徑)可能是冗余的。
所以這種訓練方式的優點:
當然,這里的原理不是很難, 下面主要是從代碼層面看看具體是怎么實現的。
這里拿EfficientNet網絡里面的代碼進行說明,其他的也都類似:
# kernel_size, in_channel, out_channel, exp_ratio, strides, use_SE, drop_connect_rate, repeats default_cnf = [[3, 32, 16, 1, 1, True, drop_connect_rate, 1],[3, 16, 24, 6, 2, True, drop_connect_rate, 2],[5, 24, 40, 6, 2, True, drop_connect_rate, 2],[3, 40, 80, 6, 2, True, drop_connect_rate, 3],[5, 80, 112, 6, 1, True, drop_connect_rate, 3],[5, 112, 192, 6, 2, True, drop_connect_rate, 4],[3, 192, 320, 6, 1, True, drop_connect_rate, 1]]這里給出每個stage的配置, 這個具體不用管,這個看EfficientNet的網絡結構就知道。
這里是修改配置的代碼,也就是會遍歷上面的每個stage,然后根據重復次數建立殘差塊,這里的殘差塊是倒殘差模塊,開頭的那個圖里面的結構。 主要是框出來的這句話,就是“線性衰減規律”的那個公式, 這里的cnf[-1]表示的當前殘差塊的dropout_rate, 而args[-2]是我們指定的dropout_rate, bbb表示當前lll層, num_blocks就是總的blocks數, 和上面公式一一對應。
這里就會發現,搭建網絡的時候,每個殘差塊都會指定一個dropout_rate, 那么在每個殘差塊里面,我們搭建的dropout層如下, 這里直接拿EfficientNetV1來看,重點關注self.dropout即可,上面的那些是主分支上的擴張卷積,dw卷積以及降維卷積,不是這篇文章的重點:
class InvertedResidualEfficientNetV1(nn.Module):def __init__(self,cnf: InvertedResidualConfigEfficientNet,norm_layer: Callable[..., nn.Module]):super(InvertedResidualEfficientNetV1, self).__init__()self.use_res_connect = (cnf.stride == 1 and cnf.input_c == cnf.out_c)layers = OrderedDict()activation_layer = nn.SiLU # alias Swish# expandif cnf.expanded_c != cnf.input_c:layers.update({"expand_conv": ConvBNActivation(cnf.input_c,cnf.expanded_c,kernel_size=1,norm_layer=norm_layer,activation_layer=activation_layer)})# depthwiselayers.update({"dwconv": ConvBNActivation(cnf.expanded_c,cnf.expanded_c,kernel_size=cnf.kernel,stride=cnf.stride,groups=cnf.expanded_c,norm_layer=norm_layer,activation_layer=activation_layer)})if cnf.use_se:layers.update({"se": SqueezeExcitationV2(cnf.input_c,cnf.expanded_c)})# projectlayers.update({"project_conv": ConvBNActivation(cnf.expanded_c,cnf.out_c,kernel_size=1,norm_layer=norm_layer,activation_layer=nn.Identity)})self.block = nn.Sequential(layers)self.out_channels = cnf.out_cself.is_strided = cnf.stride > 1# 只有在使用shortcut連接時才使用dropout層if self.use_res_connect and cnf.drop_rate > 0:self.dropout = DropPath(cnf.drop_rate)else:self.dropout = nn.Identity()def forward(self, x: Tensor) -> Tensor:result = self.block(x)result = self.dropout(result)if self.use_res_connect:result += x這里的代碼細節不用多說, 其實就是開頭的那個殘差網絡結構, 我們主要看看啥時候使用Dropout, 只有使用跳遠連接,以及當前的dropout_rate大于0的時候, 我們的Dropout層會走一個DropPath, 否則不是殘差結構,或者沒有dropout_rate, 那么我們就恒等過去,所以DropoutPath只用于殘差結構。
那么DropPath是怎么實現呢?
class DropPath(nn.Module):def __init__(self, drop_prob=None):super(DropPath, self).__init__()self.drop_prob = drop_probdef forward(self, x):return drop_path(x, self.drop_prob, self.training)這里是建了一個DropPath層, 這里的核心實現是drop_path函數,在這里面,實現的就是根據給定的dropout_rate概率隨機失活主分支。所以重點看看這個的實現邏輯:
def drop_path(x, drop_prob: float = 0, training: bool = False):if drop_prob == 0. or not training:return xkeep_prob = 1 - drop_prob# ndim是維度個數 x.shape[0] 是樣本個數, shape: (x.shape[0], 1, 1, 1) 維度可以用+拼接shape = (x.shape[0], ) + (1, ) * (x.ndim - 1)# 為每個樣本生成一個隨機數 torch.rand[0, 1), keep_prob (0, 1], 兩者之和是[0, 2) 形狀是(x.shape[0], 1, 1, 1)random_tensor = keep_prob + torch.rand(shape, dtype=x.dtype, device=x.device) # torch.rand 均勻分布抽取的隨機數([0,1))# 下取整,即random_tensor非0即1 形狀(x.shape[0], 1, 1, 1)random_tensor.floor_() # 下取整# 這里隨機失活主分支, 除以keep_prob是為了保持訓練和測試的尺度一致,普通dropout思路output = x.div(keep_prob) * random_tensorreturn output這里為了弄明白,我每一行代碼就加了注釋。 其實邏輯很簡單, 對于我們一個batch里面的樣本,比如nnn個, 那么輸入x的形狀就是(n,channelsize,h,w)(n, channel_{size}, h, w)(n,channelsize?,h,w), 我們首先會每個樣本,都會生成一個[0,2)之間的隨機數, 然后下取整,就得到了非0即1的random_tensor, 這個其實就是我們的blb_lbl?, 每個樣本對應一個,所以每個樣本訓練的時候,都會看看是否激活主分支。 然后具體是否激活,就是最后一行代碼做的事情, 這里除以keep_prob是為了保證訓練集和測試集的尺度范圍一致,和普通的dropout一樣。
這樣,就實現了dropout技術隨機丟棄某些殘差層。
之所以整理, 我覺得這個技術在網絡的訓練中還是非常實用的,并且是一種通用技術,可以用到帶有殘差網絡的很多模型,比如resnet, densenet, efficientnet等等,既能加快訓練速度,也能增加網絡精度,非常powerful的東西。
參考:
- 深度學習模型之——Stochastic depth(隨機深度)
總結
以上是生活随笔為你收集整理的Dropout技术之随机神经元与随机深度的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: mysql case when用法
- 下一篇: ARM开发板系统移植-----rootf