python手寫神經網絡之BatchNormalization實現

也是來源於《深度學習入門——基於Python的理論與實現》附加代碼,書中只是給了BN的對比結果,展示了BN的效果,沒有再贅述實現(可能因爲有點複雜),所以這裏研究一下BN的代碼。

之前我曾經使用過TensorFlow的BN,它提供了兩三種接口,透明程度和使用方法不相同,有的是透明到你可以自定義參數並傳給BN層,然後訓練參數,也有隻定義一個層,全自動使用的,但是都沒有自己純手寫一個python實現更透徹。而且TensorFlow有很多高級封裝和其他功能的整合,亂花漸欲迷人眼。不如來一個純版的實現更好。

關於理論基礎,這裏就不贅述了,先將x歸一化,再通過學習到的參數縮放平移x,達到分佈歸一與分佈還原擬合的trade-off,公式如下:
論文參考實現過程
可以看到,正向傳播有兩個計算參數mu和sigma,然後完成其餘歸一化計算,最後再通過兩個可學習參數beta和gamma進行分佈恢復。

反向傳播,反向傳播有兩個關注點,計算gamma和beta的梯度,以便進行更新;計算對輸入(x)的梯度,以便完成梯度的傳導工作。

如果是工程版的實現,根據Tensorflow接口的功能,還應該分訓練和測試階段,訓練階段應該進行一個mu(mean)和sigma(var)的總平均值的更新。這個好像沒懸疑,必須要有的,沒有就不完整(失去了降噪和降低過擬合的功能)

實現框架,類似本書其他網絡層,實現一個BN類封裝,包含初始化、正向傳播、反向傳播等函數。

其實難點就是把公式逐步拆解,再把所有的導數公式都找出來,這一步最好在紙上做。給每個中間變量都想好名字。
因爲考慮不一定周到,比如維度(convNet默認(N,C,H,W)),時間關係,我就不重新造輪子了,主要是先自己列個大綱和僞代碼,然後解讀實例代碼,看自己有哪些遺漏或者想不到的,他細節都是怎麼實現的,算一個學習過程,爲了直觀,解讀以註釋形式給出。
這個方法的收穫還是有很多的,反向傳播的逐個計算可能是最難的,或者叫繁瑣,我認爲本質上和sigmoid或者affine層或者softmax_with_loss是一樣的,如果有前邊的基礎,這個只是有空再練手,其實並不影響你理解,所以我跳過了,沒在紙面上推。

下面是代碼解讀

  • 前向傳播的外層封裝,卷積網絡被改造成N*D的模式,最後恢復(好像用了channel_first模式):
    def forward(self, x, train_flg=True):#封裝:修改卷積模式到FC模式,
        # BN應該是有一個channel一個平均值的模式?這裏channel被平鋪了,每個feature“像素點”一個平均值。
        # 所以這可能不算最好的conv實例,權當是個FC罷,就是遇到Conv強行轉FC的一個形式
        self.input_shape = x.shape
        if x.ndim != 2:
            N, C, H, W = x.shape#用了channel_first
            x = x.reshape(N, -1)

        out = self.__forward(x, train_flg)

        return out.reshape(*self.input_shape)#恢復原樣
  • 前向傳播內層實現:
    def __forward(self, x, train_flg):
        if self.running_mean is None:#全局平均值的初始化,作者的參考代碼全都是根據第一次前向傳播確定形狀的模式
            N, D = x.shape
            #這裏也可以看出,是根據每個feature一個平均值,所以好像ConvNet不友好?但是BN層又不該改變維度,做了平均值最終也是要恢復的,注意,主要是平均值數值本身的不同,一個channel一個平均值理論上更平均一些罷了。
            self.running_mean = np.zeros(D)
            self.running_var = np.zeros(D)

        if train_flg:#訓練時(在紙上做公式的分解)
            mu = x.mean(axis=0)#計算N個xi的平均值,N坍塌,D維持不變
            xc = x - mu#x零中心化(N,D)
            var = np.mean(xc ** 2, axis=0)#分子:計算標準差(簡化了,因爲xc=x-mu是零中心,所以xi-mu就成了xc,然後np.mean)
            std = np.sqrt(var + 10e-7)#分母,標準差,防零
            xn = xc / std#這是標準化後的輸出,恢復分佈的操作out= self.gamma*xn + self.beta提到了if外邊,爲了和else共享代碼

            self.batch_size = x.shape[0]
            self.xc = xc#這些都被記錄了下來,爲了反向傳播方便使用
            self.xn = xn
            self.std = std
            # 這兩個是記錄全局平均,我也好奇全局平均怎麼記錄,總不能一直累加,最後測試時記住數量除一次?更不能每次簡單對沖進去?因爲batch不一樣!
            # 就算除以batch再去更新,那麼權重也不太對,之前batch=10000被算成1份,現在batch=10也被算成一份,就沖淡了10000份?所以看到,他是用了momentum來做滑動平均的更新。
            # 這些細節不去實現真的很不容易想到!!!!
            self.running_mean = self.momentum * self.running_mean + (1 - self.momentum) * mu
            self.running_var = self.momentum * self.running_var + (1 - self.momentum) * var
        else:#這個簡單:測試時候直接用全局平均值套公式計算一下就行了
            xc = x - self.running_mean
            xn = xc / ((np.sqrt(self.running_var + 10e-7)))

        out = self.gamma * xn + self.beta#共享操作:恢復分佈
        return out
  • 反向傳播外層
    def backward(self, dout):#同樣的,封裝了一個ConvNet維度處理的過程
        if dout.ndim != 2:
            N, C, H, W = dout.shape
            dout = dout.reshape(N, -1)

        dx = self.__backward(dout)

        dx = dx.reshape(*self.input_shape)
        return dx
  • 反向傳播內層
    def __backward(self, dout):
        #beta和gamma是最外層的兩個操作,反向傳播最簡單,由xn*gamma+beta=out可知
        dbeta = dout.sum(axis=0)#細節:x的坍縮,一個batch合併成一個值。
        dgamma = np.sum(self.xn * dout, axis=0)#同上
        #下面的不墨跡了,其實就是前向傳播的過程,逐步反推回去
        dxn = self.gamma * dout
        dxc = dxn / self.std
        dstd = -np.sum((dxn * self.xc) / (self.std * self.std), axis=0)
        dvar = 0.5 * dstd / self.std
        dxc += (2.0 / self.batch_size) * self.xc * dvar
        dmu = np.sum(dxc, axis=0)
        dx = dxc - dmu / self.batch_size#用來傳遞給前一層

        self.dgamma = dgamma#本層需要的結果,保存,用於更新參數
        self.dbeta = dbeta#本層需要的結果,保存,用於更新參數

        return dx
  • 最後簡單帶過init
    def __init__(self, gamma, beta, momentum=0.9, running_mean=None, running_var=None):
        self.gamma = gamma
        self.beta = beta
        self.momentum = momentum#用來做全局平均的衰減
        self.input_shape = None  # Conv層的情況下爲4維,全連接層的情況下爲2維

        # 測試時使用的全局平均值和方差
        self.running_mean = running_mean
        self.running_var = running_var

        # backward時使用的中間數據
        self.batch_size = None
        self.xc = None
        self.std = None
        self.dgamma = None
        self.dbeta = None
  • 這樣,外邊加上class BatchNormalization就完成了。

  • 最後就是使用(需要在外部定義params傳給BN類進行初始化,這個類似TF給出的一種半透明的使用方法,可以看到gamma默認1,beta默認0,對xn的分佈不產生影響,之後具體什麼值就要學習了)

        # 初始化權重
        self.__init_weight(weight_init_std)

        # 生成層
        activation_layer = {'sigmoid': Sigmoid, 'relu': Relu}
        self.layers = OrderedDict()
        for idx in range(1, self.hidden_layer_num+1):
            self.layers['Affine' + str(idx)] = Affine(self.params['W' + str(idx)],
                                                      self.params['b' + str(idx)])
            if self.use_batchnorm:
                self.params['gamma' + str(idx)] = np.ones(hidden_size_list[idx-1])
                self.params['beta' + str(idx)] = np.zeros(hidden_size_list[idx-1])
                self.layers['BatchNorm' + str(idx)] = BatchNormalization(self.params['gamma' + str(idx)], self.params['beta' + str(idx)])
                
            self.layers['Activation_function' + str(idx)] = activation_layer[activation]()
            
            if self.use_dropout:
                self.layers['Dropout' + str(idx)] = Dropout(dropout_ration)

        idx = self.hidden_layer_num + 1
        self.layers['Affine' + str(idx)] = Affine(self.params['W' + str(idx)], self.params['b' + str(idx)])

        self.last_layer = SoftmaxWithLoss()
  • 求梯度
    def numerical_gradient(self, X, T):
        loss_W = lambda W: self.loss(X, T, train_flg=True)

        grads = {}
        for idx in range(1, self.hidden_layer_num+2):
            grads['W' + str(idx)] = numerical_gradient(loss_W, self.params['W' + str(idx)])
            grads['b' + str(idx)] = numerical_gradient(loss_W, self.params['b' + str(idx)])
            
            if self.use_batchnorm and idx != self.hidden_layer_num+1:
                grads['gamma' + str(idx)] = numerical_gradient(loss_W, self.params['gamma' + str(idx)])
                grads['beta' + str(idx)] = numerical_gradient(loss_W, self.params['beta' + str(idx)])

        return grads
  • 最後就是和其他一樣的

w-=lr*grad

  • 測試時注意一下參數要寫成False:

def predict(self, x, train_flg=False):

小結:

BN層分兩個階段
公式的分解稍微繁瑣

ConvNet的優化問題(私以爲,這個代碼不代表全部情況)

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