由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网络的实现也会是大同小异。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

猫猫虫(——)

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值