搭建深度學習框架(四) Dropout與Batch Normalization

Dropout

網絡過於習慣訓練集的數據分佈, 尤其是如果訓練集規模還不夠大時, 網絡會出現嚴重的過擬合, 使得在訓練集上的準確率非常高, 但是測試集上的準確率很低. 而且這個過程還不可逆, 一旦網絡過擬合那網絡就廢了, 我們的努力全部木大.
神經網絡中的一種有效的對抗過擬合的方法是dropout, 它基於模型融合的思想, 讓當前隱層的一些神經元暫時死掉, 它的輸出將是0 ,從而不對本次前向和反向傳播產生任何影響. 這樣做的好處是, 如果這樣的模型仍然能被訓練到收斂, 那麼我們可以認爲我們從隱層中抽取出一定比例的神經元也仍然是收斂的. 然而這樣隨機採樣得到的很多小一些的子網絡與子網絡之間是有些微的差異的, 這些差異經過模型融合, 能讓總網絡變成一個不具有傾向性的模型(委員會思想). 從而模型的偏差可能會上升一些, 但是方差一般會降低更多.
在這裏插入圖片描述
它的前向和反向傳播非常簡單, 我們只需要給定一個概率, 然後它就會生成一個mask把一定比例的x置零再輸出. 反向傳播時, 因爲mask處的x被置零了, 就相當於點乘了一個0常數, 那麼它的反向傳播也是乘一個0, mask把對應位置的dz置零再反向傳播.

Batch-Normalization

接下來我們要講一下, 能讓訓練加速的一種技巧, 叫做batch norm. 相對於卷積和循環的網絡, 這種技術的出現是2015年, 是很年輕的技術. 這種方法針對訓練中(尤其是在深度網絡裏)遇到的困難提出一些解決方案, 之前我們的代碼實踐雖然是深度學習, 但並不算是非常deep的網絡, 真正很深的大型網絡在訓練時會面臨Internal Covariate Shift的問題. 簡單來說, 我們的反向傳播計算梯度是逐層進行的, 當我們更新了第l層的參數, 第l層的輸出也會改變, 從概率的角度解釋就是分佈發生了改變. 這樣l+1層就需要去適應新的輸入分佈, 這個過程需要時間. 可怕的是, 當網絡很深時, 這個適應過程就會非常複雜且困難, 爲了加速深度網絡的訓練, 有一種想法是, 我們人爲地改變數據分佈, 每一隱層無論何時, 分佈都是相似的. 一種方法就是規範化方法, 我們把數據分佈減去分佈的均值再除以標準差, 這樣分佈就會被規約到和標準正態分佈類似. 注意這裏的規範化是對每一維度獨立進行的, 而不是計算樣本集的協方差矩陣, 那樣會浪費大量算力.
μj=1mi=1mZji \mu_j = \frac{1}{m}\sum_{i=1}^m Z_{ji}
σj2=1mi=1m(Zjiμj)2 \sigma_j^2 = \frac{1}{m}\sum_{i=1}^m (Z_{ji}-\mu_j)^2
Z^ji=Zjiμjσj \hat Z_{ji} = \frac{Z_{ji}-\mu_j}{\sigma_j}
這個做法當然是可以的, 我們在數據科學中使用的一種數據預處理方式就是這種Normalization. 在神經網絡中當然也可以用, 而且很適合, 因爲大多數我們用到的激活函數都是在輸入x=0時斜率最大, 從而梯度最大, 學習速度最快. 如果我們把這種方法應用在隱層間, 就能解決上面Internal Covariate Shift的問題.
但是如果真的那麼做, 會出現新的問題. 我們知道神經網絡的一大特點就是越深層, 特徵就越抽象. 最明顯的例子是自編碼器, 如果我們用自編碼器處理MNIST數據, 並把隱層的特徵在2D平面做可視化, 會看見不同類別的數據會被編碼到獨立的區域. 這就是深度的網絡對數據處理後的結果. 但是這時我們用一個這樣的norm處理它, 很大可能會把網絡已經提取到的不同的特徵再次混合在一起, 網絡的表達能力遭到傷害. 那麼要怎樣既能保住底層網絡學習到的參數信息, 又能修正數據分佈, 方便網絡訓練呢?
BN的最大貢獻就是這部分, 我們再次加入兩個參數, 把數據分佈再做一個一維線性變換, 把分佈拉離0, 並修正方差. 也就是
yji=γjZ^ji+βj y_{ji} = \gamma_j\hat Z_{ji} + \beta_j
相當於和上面的規範化相對的一種方法, 如果mu和beta相等, gamma和sigma相等, 這就是一個沒有進行變換的變換. 通過上面的步驟,我們就在一定程度上保證了輸入數據的表達能力. 有時我們會爲了處理分母, 用一個eps來輔助運算, 總的運算方法如下.
在這裏插入圖片描述

BN的反向傳播

這個過程的反向傳播是怎麼做的呢? 其實再怎麼複雜也無非是代數求導, 只要有耐心推就怎麼都推得出來.
Lβj=SUMROWLyij \frac{\partial L}{\partial \beta_j} = SUMROW \frac{\partial L}{\partial y_{ij}}
Lγj=SUMROWLyijZij \frac{\partial L}{\partial \gamma_j} = SUMROW \frac{\partial L}{\partial y_{ij}}*Z_{ij}
下面的部分才比較複雜, 均值的偏導比較容易, 但是方差不但有分數形式還與均值有關, 我們分三步來看向量形式的求導
在這裏插入圖片描述
這裏抄一張圖, batchnorm的計算圖展開, 可視化的計算可能會更爲清晰
在這裏插入圖片描述

代碼實現

導入包, 還和之前一樣, 我們不使用torch.nn的模塊, 只使用張量運算工具, 核心的前向後向算法我們自己實現

import torch
import math
import numpy as np
import matplotlib.pyplot as plt
import torch.nn.functional as F

Dropout實現非常簡單, 設置隨機的mask屏蔽一些前向傳播和後向傳播即可

class Dropout:
    '''
    隨機把一些神經元的輸出變爲0
    從而前向和反向傳播中,這個神經元將不再發揮作用
    '''
    def __init__ (self, prob, train = True):
        self.p = prob # dropout 的概率
        self.train = train # 訓練模式,dropout層啓用
        
    def forward(self,X):
        if not self.train: return X
        self.mask = torch.rand(len(X))>self.p
        X[self.mask] = 0
        return X
    
    def backward(self,dz):
        if not self.train: return dz
        dz[self.mask] = 0
        return dz
    
    def __call__(self, X):
        return self.forward(X)

batch norm相對比較麻煩, 但是對着公式敲也能敲出來. 這裏只實現極簡的版本, 不設置eps參數, 不實現怎麼更新參數的update方法, 當然也不包括更新相關的動量參數.

class BatchNorm:
    def __init__(self, dim):
        self.w = torch.randn(dim)*math.sqrt(2/(dim))
        self.b = torch.randn(dim)*math.sqrt(2/(dim))
        self.dim = dim
        self.x = None
        self.mu = None
        self.sigma = None
        self.param  = True
    
    def forward(self, x):
        self.x = x
        self.mu = torch.mean(x,axis = 0)
        self.sigma = torch.sqrt(torch.var(x,axis = 0, unbiased=False))
        n,m = x.shape
        assert m==self.dim
        self.n = n
        self.xhat = (x-self.mu)/self.sigma
        self.out = self.w*(self.xhat)+self.b
        return self.out
    
    def backward(self, dz):
        self.db = torch.sum(dz, axis = 0)
        self.dw = torch.sum(self.xhat*dz, axis = 0)
        
        M_dx = self.x-self.mu
        dL_dxhat = dz*self.w
        
        dL_dx1 = dL_dxhat*(1/self.sigma).unsqueeze(0).repeat(self.n,1)
        
        dL_dsigma2 = dL_dxhat*(M_dx/(-2*self.sigma**3))
        dL_dsigma2 = torch.sum(dL_dsigma2,axis = 0)
        means = torch.mean(M_dx,axis = 0)
        dsigma2_dx = 2*(M_dx-means)/self.n
        dL_dx2 = dL_dsigma2*dsigma2_dx
        
        dL_dmu = dL_dsigma2*(-2*means)-torch.sum(dL_dx1,axis = 0)
        dL_dx3 = (1/self.n)*torch.ones((self.n,self.dim))*dL_dmu
        
        dx = dL_dx1+dL_dx2+dL_dx3
        return dx
        
    def __call__(self, X):
        return self.forward(X)

梯度測試

我們用torch的標準batchnorm1d和反向傳播工具計算梯度, 比較它和我們自己實現的batchnorm的前向後向得到的結果

x = torch.randn(5,2)
target = torch.randn(5,2)

bn_torch = torch.nn.BatchNorm1d(2)
bn_torch.eps = 0
bn_torch.momentum = 0
bn_torch.train()


x.requires_grad = True
y = bn_torch(x)
loss = F.mse_loss(y,target)
bn_torch.zero_grad()
x.grad.data.zero_()
loss.backward()
print("output:\n",y)
print("weight grad:\n",bn_torch.weight.grad)
print("bias grad:\n",bn_torch.bias.grad)
print("x grad:\n",x.grad)

output:
 tensor([[ 0.0093,  0.0303],
        [-0.4916,  0.3338],
        [ 0.1308, -1.7180],
        [-1.3539,  1.3910],
        [ 1.7053, -0.0372]], grad_fn=<NativeBatchNormBackward>)
weight grad:
 tensor([1.1435, 1.5440])
bias grad:
 tensor([-0.4674, -0.0078])
x grad:
 tensor([[ 0.0288,  0.3495],
        [ 0.0996,  0.1467],
        [-0.3124, -0.2545],
        [ 0.0733, -0.3541],
        [ 0.1107,  0.1124]])
bn = BatchNorm(2)
bn.w = bn_torch.weight.detach()
bn.b = bn_torch.bias.detach()
y = bn(x)
dx = bn.backward((y-target)/5)

print("output:\n",y)
print("weight grad:\n",bn.dw)
print("bias grad:\n",bn.db)
print("x grad:\n",dx)

output:
 tensor([[ 0.0093,  0.0303],
        [-0.4916,  0.3338],
        [ 0.1308, -1.7180],
        [-1.3539,  1.3910],
        [ 1.7053, -0.0372]], grad_fn=<AddBackward0>)
weight grad:
 tensor([1.1435, 1.5440], grad_fn=<SumBackward1>)
bias grad:
 tensor([-0.4674, -0.0078], grad_fn=<SumBackward1>)
x grad:
 tensor([[ 0.0288,  0.3495],
        [ 0.0996,  0.1467],
        [-0.3124, -0.2545],
        [ 0.0733, -0.3541],
        [ 0.1107,  0.1124]], grad_fn=<AddBackward0>)

工程細節

在實際使用batch norm時, 深度學習框架通常會把模塊定義成兩個模式, 一個是訓練模式一個是測試模式. 在訓練時, 我們的BN層會不斷吃數據, 並計算出一個對整個訓練集而言的均值和方差, 在測試時, 我們會用這個均值和方差進行計算. 從而讓訓練時的局部性不會對測試產生影響. 具體到實現上就是train模式和eval模式.
再者就是上面提到的epsilon避免方差過小. 更新batch norm的方法是梯度下降, 我們計算出w和b的梯度後會用梯度優化方法優化參數, 這時pytorch還設置了動量, 避免w和b產生過大的更新. 從實踐經驗上來看, 這一點的幫助還是相當大的.

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章