由darknet框架源碼窺探CNN中的batch normalization(批次歸一化)的實現

batch normalization用於卷積層後的歸一化。卷積神經網絡每一層的參數更新後會導致數據分佈的變化,使得網絡學習更加困難,損失變化也更加振盪。通過歸一化後,將每一層的輸出數據歸一化在均值爲0方差爲1的高斯分佈中。

批次歸一化的方法於z-score數據標準化的方法是一致的,計算方法如下:

設數據集A={v_{1},v_{2},\cdots ,v_{n}},數據集的均值爲\bar{A},數據集的標準差爲\sigma _{A}

對A中的所有數據進行z-score標準化,計算過程如下:

                                                                                            v_{i}^{'}=\frac{v_i-\bar{A}}{\sigma _{A}}

對於batch normalization也是如此,對於卷積神經網絡,數據集A爲每一層的輸出數據(三維數據h*w*c),即對於該三維數據中的每一個元素都要進行z-score標準化。那麼問題在於,對於卷積神經網絡層輸出的三維數據,他的均值,方差是什麼。

我們先從均值和方差的存儲空間的創建看起,在src/convolutional_layer.c中的make_convolutional_layer函數中可以看到存儲空間的創建過程:

l.mean = calloc(n, sizeof(float));
l.variance = calloc(n, sizeof(float));

他們的大小都是n個float,而n代表該層的卷積核個數,也是該卷積層輸出數據的通道數。那麼可以猜想,是對輸出數據的每一個通道計算一個均值。

在src/batchnorm_layer.c中的forward_batchnorm_layer函數可以看到,具體的實現過程,代碼如下:

void forward_batchnorm_layer(layer l, network net)
{
    if(l.type == BATCHNORM) copy_cpu(l.outputs*l.batch, net.input, 1, l.output, 1);
    copy_cpu(l.outputs*l.batch, l.output, 1, l.x, 1);
    if(net.train){//訓練過程
        mean_cpu(l.output, l.batch, l.out_c, l.out_h*l.out_w, l.mean);//計算均值
        variance_cpu(l.output, l.mean, l.batch, l.out_c, l.out_h*l.out_w, l.variance);//計算方差
        
        //計算滾動平均和滾動方差
        scal_cpu(l.out_c, .99, l.rolling_mean, 1);
        axpy_cpu(l.out_c, .01, l.mean, 1, l.rolling_mean, 1);
        scal_cpu(l.out_c, .99, l.rolling_variance, 1);
        axpy_cpu(l.out_c, .01, l.variance, 1, l.rolling_variance, 1);

        normalize_cpu(l.output, l.mean, l.variance, l.batch, l.out_c, l.out_h*l.out_w);   
        copy_cpu(l.outputs*l.batch, l.output, 1, l.x_norm, 1);
    } else {
        normalize_cpu(l.output, l.rolling_mean, l.rolling_variance, l.batch, l.out_c, l.out_h*l.out_w);
    }
    scale_bias(l.output, l.scales, l.batch, l.out_c, l.out_h*l.out_w);
    add_bias(l.output, l.biases, l.batch, l.out_c, l.out_h*l.out_w);
}

第一句判斷條件不用看,那是batch normalization作爲單獨一層時,而一般情況下是直接在卷積層中直接調用。

if(net.train)判斷是否爲訓練過程,其中提到rolling_mean和rolling_variance,滾動平均值和滾動方差,這個稍後再說。

我們先來看均值和方差的計算過程,兩個函數:

mean_cpu(l.output, l.batch, l.out_c, l.out_h*l.out_w, l.mean);//計算均值
variance_cpu(l.output, l.mean, l.batch, l.out_c, l.out_h*l.out_w, l.variance);//計算方差

函數實現在src/blas.c中

我們先來看均值計算mean_cpu,我將調用語句作爲註釋寫在最上面一行,方便對照:

//mean_cpu(l.output, l.batch, l.out_c, l.out_h*l.out_w, l.mean);
void mean_cpu(float *x, int batch, int filters, int spatial, float *mean)
{
    float scale = 1./(batch * spatial);
    int i,j,k;
    for(i = 0; i < filters; ++i){
        mean[i] = 0;
        for(j = 0; j < batch; ++j){
            for(k = 0; k < spatial; ++k){
                int index = j*filters*spatial + i*spatial + k;
                mean[i] += x[index];
            }
        }
        mean[i] *= scale;
    }
}

scale=1/(batch*spatial),scale縮放因子即1除數據集元素個數,那麼batch*spatial就表明了均值計算的範圍,從調用過程中的參數輸入可以知道spatial=l.out_h*l.out_w即輸出數據一個通道的元素個數,所以現在可以明確,均值計算是針對輸出數據的每一個通道,方差計算也是如此。而batch是網絡訓練的批次數量,即每次訓練使用batch張圖片,所以batch*spatial表明,均值計算針對的是batch個圖像在該層輸出數據的每一個通道計算一個均值。

我舉個例子:

該卷基層有batch個輸出,輸出都是m*n*c的三維數據,那麼均值有c個,即每個通道計算一個均值:

for i in range(c):
    sum = 0
    for i in range(batch):
        sum += 對第i個batch輸出數據的第c個通道求和
    第i個通道對應的均值爲sum/(batch*m*n)

上述僞代碼可以更清晰的表明該過程,如果對卷積過程中批次的組織方式和實現過程有疑惑,可以參考darknet的卷積層源代碼進行理解。

後續的循環求和過程就很好理解了,darknet中數據的組織方式是一維的數組,而循環求和過程就是尋找到元素的索引位置,求和而已。

均值計算過程已經非常明確了,即針對所有batch的某一個輸出數據通道求平均,那麼方差計算也是如此,代碼如下:

//variance_cpu(l.output, l.mean, l.batch, l.out_c, l.out_h*l.out_w, l.variance);
void variance_cpu(float *x, float *mean, int batch, int filters, int spatial, float *variance)
{
    float scale = 1./(batch * spatial - 1);
    int i,j,k;
    for(i = 0; i < filters; ++i){
        variance[i] = 0;
        for(j = 0; j < batch; ++j){
            for(k = 0; k < spatial; ++k){
                int index = j*filters*spatial + i*spatial + k;
                variance[i] += pow((x[index] - mean[i]), 2);
            }
        }
        variance[i] *= scale;
    }
}

從循環控制的參數可以看出,針對的是所有batch輸出的某一通道求方差,與均值計算是一致的,從z-score的計算方法來說,也必需是一致的。

在搞清楚均值和方差的計算後,回到forward_batchnormal_layer中:

void forward_batchnorm_layer(layer l, network net)
{
    if(l.type == BATCHNORM) copy_cpu(l.outputs*l.batch, net.input, 1, l.output, 1);
    copy_cpu(l.outputs*l.batch, l.output, 1, l.x, 1);
    if(net.train){//訓練過程
        mean_cpu(l.output, l.batch, l.out_c, l.out_h*l.out_w, l.mean);//計算均值
        variance_cpu(l.output, l.mean, l.batch, l.out_c, l.out_h*l.out_w, l.variance);//計算方差

        scal_cpu(l.out_c, .99, l.rolling_mean, 1);
        axpy_cpu(l.out_c, .01, l.mean, 1, l.rolling_mean, 1);
        scal_cpu(l.out_c, .99, l.rolling_variance, 1);
        axpy_cpu(l.out_c, .01, l.variance, 1, l.rolling_variance, 1);

        normalize_cpu(l.output, l.mean, l.variance, l.batch, l.out_c, l.out_h*l.out_w);   
        copy_cpu(l.outputs*l.batch, l.output, 1, l.x_norm, 1);
    } else {
        normalize_cpu(l.output, l.rolling_mean, l.rolling_variance, l.batch, l.out_c, l.out_h*l.out_w);
    }
    scale_bias(l.output, l.scales, l.batch, l.out_c, l.out_h*l.out_w);
    add_bias(l.output, l.biases, l.batch, l.out_c, l.out_h*l.out_w);
}

可以看到在訓練過程中計算完均值和方差後,還計算了一個rolling_mean,rolling_variance但是在做歸一化是並沒有使用這兩個值,而檢測過程的歸一化卻使用的是rolling_mean,rolling_variance。

這兩個值稱爲滾動平均和滾動方差,在講解這兩個數值的作用之前,我再次重申一遍,batch normalization是爲了卷積後的數據分佈一致,而我們使用了z-score的方法使用數據本身的均值和方差對數據進行標準化,這個均值是整體數據集的均值,這個方差也是整體數據集的方差,但是訓練過程中我們計算的是一個batch的均值和方差,由於訓練是以batch爲單位更新參數的,所以使用一個batch的均值和方差是合理的,而在檢測過程中所使用的就應當是整體數據的均值和方差。

雖然在訓練過程中可以記錄下所有的均值和方差,最後再求一個整體的平均,但是這樣做佔用太多的存儲空間,時間效率也非常低下,所以採用了一種滾動求平均的方式。

\bar{x_{m}}爲m個數據的均值,\bar{x_{m-1}}爲前m-1個數據的均值,則:

                                                                                     \bar{x_{m}}=(1-\frac{1}{m})\bar{x_{m-1}}+\frac{1}{m}x_{m}

這就是滾動平均,每一次只用保存一個均值,計算一次平均。這種方法有一個很大的問題,m是從1開始遞增的,隨着m的增大1/m的值越來越小,那麼\frac{1}{m}x_{m}的值也越來越小,也就是說後續數據對整體均值的影響越來越小,當m特別大時,均值幾乎不變。這導致整體均值實際上只受到前期數據的影響。

這個問題是很嚴重的,所以darknet在實現時,並不是使用1-1/m和1/m,而是直接使用兩個確定的數值0.99和0.01

scal_cpu(l.out_c, .99, l.rolling_mean, 1);
axpy_cpu(l.out_c, .01, l.mean, 1, l.rolling_mean, 1);
scal_cpu(l.out_c, .99, l.rolling_variance, 1);
axpy_cpu(l.out_c, .01, l.variance, 1, l.rolling_variance, 1);

這解決了後期數據對均值的影響問題,但是這樣計算出的還是數據集的均值?

他計算出的當然不再是數據集的均值,但是回到batch normalliza的意義上來,我們的目的是將卷積後的輸出數據歸一化到同一個數據分佈中,我們這樣去求均值,使得歸一化後的數據不再是理想情況下的均值爲0方差爲1的分佈,但是用這樣的均值進行歸一化數據的分佈依然都是一樣的,只不過不是均值爲0方差爲1的分佈,而我們的目標就是希望他們分佈一樣,至於是什麼樣的分佈還重要嗎?當然是不重要的。

在訓練過程結束後rolling_mean和rolling_variance會作爲訓練參數存入權重文件,供檢測時使用。

當明確求取均值和方差的目標後,歸一化就很清晰了,實現函數爲src/blas.c 中的normalize_cpu函數,代碼如下:

//normalize_cpu(l.output, l.mean, l.variance, l.batch, l.out_c, l.out_h*l.out_w);
void normalize_cpu(float *x, float *mean, float *variance, int batch, int filters, int spatial)
{
    int b, f, i;
    for(b = 0; b < batch; ++b){
        for(f = 0; f < filters; ++f){
            for(i = 0; i < spatial; ++i){
                int index = b*filters*spatial + f*spatial + i;
                x[index] = (x[index] - mean[f])/(sqrt(variance[f]) + .000001f);
            }
        }
    }
}

他完全時根據計算公式計算的,上文說batch normalization和z-score標準化是一樣的,其實在實現中有改動。

由於卷積層後跟的是非線性激活函數,而通過歸一化改變了數據的分佈,可能使得原本工作在非線性激活區的數據跑到線性激活區了,爲了解決該問題,darknet框架在z-score標準化的計算公式中加入了一個縮放因子\beta和偏移量b

                                                                                                        v_{i}^{'}=\beta \frac{v_i-\bar{A}}{\sigma _{A}}+b

這兩個都是超參數,即這兩個參數是通過訓練得到的。而darknet爲了保證上式計算時,分母永不爲0,添加了一個極小數0.000001,所以最終的計算式爲:

                                                                                                        v_{i}^{'}=\beta \frac{v_i-\bar{A}}{\sigma _{A}+0.000001}+b

代碼完全時根據該計算式編寫的。

這就是整個darknet中batch normalization的實現過程,而其他CNN網絡的實現也會是大同小異。

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