1 局部响应归一化(Local Response Normalization)[1]
1.1 概述
Local Response Normalization (LRN)是AlexNet中首次引入的归一化方法,但是在BatchNorm之后就很少使用这种方法了。
1.2 目的
对局部的值进行归一化操作,使其中比较大的值变得相对更大,增强了局部的对比度,在AlexNet中有1.2个百分比左右的提升
1.3 方法
LRN是一个non-trainable layer(非训练层),对局部区域的特征进行了平方归一化操作,在AlexNet中的公式如下:
\(b_{x,y}^i=a_{x,y}^i/(k+\alpha \sum_{j=max(0,i-n/2)}^{min(N-1,i+n/2)}(a_{x,y}^j)^2)^{\beta}\)
参数说明:需要手动设置的超参维\((k, \alpha, \beta, n)\)
- i 表示第 i 个通道
- x, y 表示要进行归一化的这个值的位置坐标
- k 的作用是防止发生除0的情况
- α 和 β 为常数
- n 表示邻域的范围,边界情况用0补齐
1.4 样例
LRN根据归一化方向不同有两种形式,分别为 Inter-Channel LRN 和 Intra-Channel LRN:
Inter-Channel LRN Example:
Inter-Channel LRN是AlexNet中使用的LRN形式,其中的邻域是定义在Channel维度上的
假设在以上的例子中我们的超参数设置为 (k,α,β,n)=(0,1,1,2),n=2表示当我们在对一个元素进行归一化的时候,只考虑到沿channel维度上(也就是一维的情况),这个元素的前一个元素和后一个元素,如果遇到边界情况,则用0补全。这意味着,我们在计算(i,x,y)这个点归一化后的值时,只需要考虑(i-1,x,y),(i,x,y)和(i+1,x,y)的值即可,如果超过边界则默认为0。
在以上例子中,不同颜色表示不同的通道,以第一个feature map中左上角的点为例,在归一化前它的值为1,归一化后为0.5,在计算这个值的时候我们要考虑到其前后的值,由于(i-1,x,y)是没有值的,也就是边界情况,所以我们默认为0,(i+1,x,y)为橙色feature map的左上角值=1。因此我们代入公式,可以得到归一化后的值为: \(b=1/(0+1*(0^2+1^2+1^2))^1=0.5\),以此类推。
Intra-Channel LRN Example:
我们采用同样的设置(k,α,β,n)=(0,1,1,2),在Intra-Channel LRN,由于是二维的情况,因此我们考虑的邻域范围为 (n+1)∗(n+1) ,当n=2的时候,在归一化时考虑的范围大小为一个3*3的size。可以代入公式计算得到上图中两个例子的值,分别为:
\(b=1/(0+1*(1^2+3^2+2^2+4^2)) \approx 0.03\) \(b=5/(0+1*(3^2+2^2+1^2+3^2+5^2+3^2+3^2+4^2+5^2)) \approx 0.05\)Pytorch 源码中实现了LRN的操作,可以看到与Alexnet论文中的方法略有不同,α所乘的项由所有数值的平方和变成了所有数值的均值,但无论是使用均值还是平方和,两者都起到了 lateral inhibition(横向抑制)的作用[2]。
def local_response_norm(input, size, alpha=1e-4, beta=0.75, k=1.):
# type: (Tensor, int, float, float, float) -> Tensor
r"""Applies local response normalization over an input signal composed of
several input planes, where channels occupy the second dimension.
Applies normalization across channels.
See :class:`~torch.nn.LocalResponseNorm` for details.
"""
dim = input.dim()
if dim < 3:
raise ValueError('Expected 3D or higher dimensionality \
input (got {} dimensions)'.format(dim))
div = input.mul(input).unsqueeze(1)
if dim == 3:
div = pad(div, (0, 0, size // 2, (size - 1) // 2))
div = avg_pool2d(div, (size, 1), stride=1).squeeze(1)
else:
sizes = input.size()
div = div.view(sizes[0], 1, sizes[1], sizes[2], -1)
div = pad(div, (0, 0, 0, 0, size // 2, (size - 1) // 2))
div = avg_pool3d(div, (size, 1, 1), stride=1).squeeze(1)
div = div.view(sizes)
div = div.mul(alpha).add(k).pow(beta)
return input / div
2 批归一化(Batch Normalization)[3,4]
Batch Normalization是2015年一篇论文中提出的数据归一化方法,往往用在深度神经网络中激活层之前。其作用可以加快模型训练时的收敛速度,使得模型训练过程更加稳定,避免梯度爆炸或者梯度消失。并且起到一定的正则化作用,几乎代替了Dropout。
2.1 BN核心公式
\(Input: B = \{ x_{1 \cdots m} \}; \gamma, \beta(parameters \quad to \quad be \quad learned) \\ Output: \{ y_i = BN_{\gamma, \beta} (x_i) \} \\ \mu_B \leftarrow \frac{1}{m} \sum_{i=1}^m x_i \\ \sigma_B^2 \leftarrow \frac{1}{m} \sum_{i=1}^m (x_i – \mu_B)^2 \\ \widetilde{x}_i \leftarrow \frac{x_i – \mu_B}{\sqrt{\sigma_B^2 + \epsilon}} \\ y_i \leftarrow \gamma \widetilde{x}_i + \beta \)上述公式解释:
- 输入为数值集合( B ),可训练参数 \(\gamma, \beta\)
- BN的具体操作为:先计算 B 的均值和方差,然后执行\(\widetilde{x}_i \leftarrow \frac{x_i – \mu_B}{\sqrt{\sigma_B^2 + \epsilon}}\),最后将 B 中每个元素乘以 γ 再加 β ,输出。γ,β 是可训练参数,参与整个网络
- 归一化的目的:将数据规整到统一区间,减少数据的发散程度,降低网络的学习难度。BN的精髓在于归一之后,使用 γ、β 作为还原参数,在一定程度上保留原数据的分布。
2.2 BN中均值、方差通过哪些维度计算得到?
神经网络中传递的张量数据,其维度通常记为[N, H, W, C],其中N是batch_size,H、W是行、列,C是通道数。那么上式中BN的输入集合 B 就是下图中蓝色的部分。
均值的计算,就是在一个批次内,将每个通道中的数字单独加起来,再除以 N×H×W 。举个例子:该批次内有10张图片,每张图片有三个通道RBG,每张图片的高、宽是H、W,那么均值就是计算10张图片R通道的像素数值总和除以10×H×W ,再计算B通道全部像素值总和除以10×H×W,最后计算G通道的像素值总和除以10×H×W。方差的计算类似。
可训练参数 、γ、β 的维度等于张量的通道数,在上述例子中,RBG三个通道分别需要一个 γ 和一个 β ,所以\(\overrightarrow{\gamma}, \overrightarrow{\beta}\)的维度等于3。
2.3 训练与推理时BN中的均值、方差分别是什么?
训练时,均值、方差分别是该批次内数据相应维度的均值与方差。
推理时,均值、方差是基于所有批次的期望计算所得。
2.4 在tensorflow中如何实现BN?
下述代码是基于tf.nn.batch_normalization封装好的BN函数:
import tensorflow as tf
from tensorflow.python.training import moving_averages
def batch_normalization(input, is_training, name="BN",
moving_decay=0.999, eps=1e-5):
input_shape = input.get_shape()
params_shape = input_shape[-1]
axis = list(range(len(input_shape) - 1))
with tf.variable_scope(name, reuse=tf.AUTO_REUSE) as scope:
beta = tf.get_variable('beta',
params_shape,
initializer=tf.zeros_initializer)
gamma = tf.get_variable('gamma',
params_shape,
initializer=tf.ones_initializer)
moving_mean = tf.get_variable('moving_mean',
params_shape,
initializer=tf.zeros_initializer,
trainable=False
)
moving_var = tf.get_variable('moving_var',
params_shape,
initializer=tf.ones_initializer,
trainable=False
)
def train():
# These ops will only be preformed when training.
mean, var = tf.nn.moments(input, axis)
update_moving_mean = moving_averages.assign_moving_average(moving_mean,
mean,
moving_decay)
update_moving_var = moving_averages.assign_moving_average(
moving_var, var, moving_decay)
return tf.identity(mean), tf.identity(var)
mean, var = tf.cond(tf.equal(is_training, True), train,
lambda: (moving_mean, moving_var))
return tf.nn.batch_normalization(input, mean, var, beta, gamma, eps)
在代码实现中有一个技巧,如果训练几百万个Batch,那么是不是要将其均值方差全部储存,最后再计算推理时所用的均值和方差?这样显然太过笨拙,占用内存随着训练次数不断上升。为了避免该问题,上述代码使用了滑动平均,储存固定个数Batch的均值和方差,不断迭代更新推理时需要的 E(x) 与 Var(x) 。
注意到代码中:
- beta、gamma在训练状态下,是可训练参数,在推理状态下,直接加载训练好的数值。
- moving_mean、moving_var在训练、推理中都是不可训练参数,只根据滑动平均计算公式更新数值,不会随着网络的训练而改变数值;在推理时,直接加载储存计算好的滑动平均之后的数值,作为推理时的均值和方差。
2.5 在网络中使用了BN,效果如何?
BN目前仍然是一个黑盒,论文中有一定的理论推导,但是普遍还是通过对比提升来验证BN的效果。原作者给出的效果如下图:
上图可见BN的两大收益:
- 收敛速率增加
- 可以达到更好的精度
在目标检测算法中,BN已经成为了标配。比如Yolov3引入了BN后,mAP提升了两个百分点。在更多实验中可以看到,BN同时起到了正则化作用,防止模型在训练集上过拟合,通常有BN的网络不再需要Dropout层。
来源
1.十指透兮的阳光. https://zhuanlan.zhihu.com/p/434773836. 知乎.
2.HAN Bing: 深入理解Batch Normalization
3.Algernon. 【基础算法】六问透彻理解BN(Batch Normalization). 知乎
4.论文链接:Batch Normalization: Accelerating Deep Network Training by Reducing Internal Covariate Shift