batch normalization用於卷積層後的歸一化。卷積神經網絡每一層的參數更新後會導致數據分佈的變化,使得網絡學習更加困難,損失變化也更加振盪。通過歸一化後,將每一層的輸出數據歸一化在均值爲0方差爲1的高斯分佈中。
批次歸一化的方法於z-score數據標準化的方法是一致的,計算方法如下:
設數據集,數據集的均值爲,數據集的標準差爲
對A中的所有數據進行z-score標準化,計算過程如下:
對於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的均值和方差是合理的,而在檢測過程中所使用的就應當是整體數據的均值和方差。
雖然在訓練過程中可以記錄下所有的均值和方差,最後再求一個整體的平均,但是這樣做佔用太多的存儲空間,時間效率也非常低下,所以採用了一種滾動求平均的方式。
設爲m個數據的均值,爲前m-1個數據的均值,則:
這就是滾動平均,每一次只用保存一個均值,計算一次平均。這種方法有一個很大的問題,m是從1開始遞增的,隨着m的增大1/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標準化的計算公式中加入了一個縮放因子和偏移量b
這兩個都是超參數,即這兩個參數是通過訓練得到的。而darknet爲了保證上式計算時,分母永不爲0,添加了一個極小數0.000001,所以最終的計算式爲:
代碼完全時根據該計算式編寫的。
這就是整個darknet中batch normalization的實現過程,而其他CNN網絡的實現也會是大同小異。