批归一化(Batch Normalization)
批归一化方法方法(Batch Normalization,BatchNorm)是由Ioffe和Szegedy于2015年提出的,已被广泛应用在深度学习中,其目的是对神经网络中间层的输出进行标准化处理,使得中间层的输出更加稳定。
通常我们会对神经网络的数据进行标准化处理,处理后的样本数据集满足均值为0,方差为1的统计分布,这是因为当输入数据的分布比较固定时,有利于算法的稳定和收敛。对于深度神经网络来说,由于参数是不断更新的,即使输入数据已经做过标准化处理,但是对于比较靠后的那些层,其接收到的输入仍然是剧烈变化的,通常会导致数值不稳定,模型很难收敛。BatchNorm能够使神经网络中间层的输出变得更加稳定,并有如下三个优点:
-
使学习快速进行(能够使用较大的学习率)
-
降低模型对初始值的敏感性
-
从一定程度上抑制过拟合
BatchNorm主要思路是在训练时按mini-batch为单位,对神经元的数值进行归一化,使数据的分布满足均值为0,方差为1。具体计算过程如下:
1. 计算mini-batch内样本的均值
uB <- 1/m * Sigma ( x(i)) (i=1->m)
其中x(i)表示mini-batch中的第i个样本。
例如输入mini-batch包含3个样本,每个样本有2个特征,分别是:
x(1)=(1,2), x(2)=(3,6), x(3)=(5,10)
对每个特征分别计算mini-batch内样本的均值:
μB0=(1+3+5)/3=3, μB1=(2+6+10)/3=6
则样本均值是:
μB=(μB0,μB1)=(3,6)
2. 计算mini-batch内样本的方差
σB2←1/m*∑(i=1,m) (x(i)−μB)^2
上面的计算公式先计算一个批次内样本的均值μB和方差σB2,然后再对输入数据做归一化,将其调整成均值为0,方差为1的分布。
对于上述给定的输入数据x(1),x(2),x(3),可以计算出每个特征对应的方差:
σB0 ^2=1/3⋅((1−3)^2+(3−3)^2+(5−3)^2)=8/3
σB1 ^2=1/3⋅((2−6)^2+(6−6)^2+(10−6)^2)=32/3
则样本方差是:
σB ^2=(σB0 ^2,σB1 ^2)=(8/3,32/3)
3. 计算标准化之后的输出
x^(i)← (x(i)−μB) / sqrt(σB^2+ϵ)
其中ϵ是一个微小值(例如1e−7),其主要作用是为了防止分母为0。
对于上述给定的输入数据x(1),x(2),x(3),可以计算出标准化之后的输出:
x^(1)=((1−3)/sqrt(8/3), (2−6)/sqrt(32/3))=(−sqrt(3/2), −swrt(3/2))
x^(2)=(3−3)/sqrt(8/3), ( 6−6)/sqrt(32/3))=(0, 0)
x^(3)=(5−3)/sqrt(8/3), (10−6)/sqrt(32/3))=(sqrt(3/2), sqrt(3/2))
- 读者可以自行验证由x^(1),x^(2),x^(3)构成的mini-batch,是否满足均值为0,方差为1的分布。
如果强行限制输出层的分布是标准化的,可能会导致某些特征模式的丢失,所以在标准化之后,BatchNorm会紧接着对数据做缩放和平移。
yi←γ x^i+β
其中γ和β是可学习的参数,可以赋初始值γ=1,β=0,在训练过程中不断学习调整。
上面列出的是BatchNorm方法的计算逻辑,下面针对两种类型的输入数据格式分别进行举例。飞桨支持输入数据的维度大小为2、3、4、5四种情况,这里给出的是维度大小为2和4的示例。
- 示例一: 当输入数据形状是[N,K]时,一般对应全连接层的输出,示例代码如下所示。
这种情况下会分别对K的每一个分量计算N个样本的均值和方差,数据和参数对应如下:
- 输入 x, [N, K]
- 输出 y, [N, K]
- 均值 μB,[K, ]
- 方差 σB^2, [K, ]
- 缩放参数γ, [K, ]
- 平移参数β, [K, ]
“BatchNorm里面不是还要对标准化之后的结果做仿射变换吗,怎么使用Numpy计算的结果与BatchNorm算子一致?
” 这是因为BatchNorm算子里面自动设置初始值γ=1,β=0,这时候仿射变换相当于是恒等变换。在训练过程中这两个参数会不断的学习,这时仿射变换就会起作用。
1 # 输入数据形状是[N, C, H, W]时的batchnorm示例 2 import numpy as np 3 4 import paddle 5 import paddle.fluid as fluid 6 from paddle.fluid.dygraph.nn import BatchNorm 7 8 # 设置随机数种子,这样可以保证每次运行结果一致 9 np.random.seed(100) 10 # 创建数据 11 data = np.random.rand(2,3,3,3).astype('float32') 12 # 使用BatchNorm计算归一化的输出 13 with fluid.dygraph.guard(): 14 # 输入数据维度[N, C, H, W],num_channels等于C 15 bn = BatchNorm('bn', num_channels=3) 16 x = fluid.dygraph.to_variable(data) 17 y = bn(x) 18 print('input of BatchNorm Layer: {}'.format(x.numpy())) 19 print('output of BatchNorm Layer: {}'.format(y.numpy())) 20 21 # 取出data中第0通道的数据, 22 # 使用numpy计算均值、方差及归一化的输出 23 a = data[:, 0, :, :] 24 a_mean = a.mean() 25 a_std = a.std() 26 b = (a - a_mean) / a_std 27 print('channel 0 of input data: {}'.format(a)) 28 print('std {}, mean {}, output: {}'.format(a_mean, a_std, b)) 29 30 # 提示:这里通过numpy计算出来的输出 31 # 与BatchNorm算子的结果略有差别, 32 # 因为在BatchNorm算子为了保证数值的稳定性, 33 # 在分母里面加上了一个比较小的浮点数epsilon=1e-05
输出结果:
input of BatchNorm Layer: [[[[0.54340494 0.2783694 0.4245176 ] [0.84477615 0.00471886 0.12156912] [0.67074907 0.82585275 0.13670659]] [[0.5750933 0.89132196 0.20920213] [0.18532822 0.10837689 0.21969749] [0.9786238 0.8116832 0.17194101]] [[0.81622475 0.27407375 0.4317042 ] [0.9400298 0.81764936 0.33611196] [0.17541045 0.37283206 0.00568851]]] [[[0.25242636 0.7956625 0.01525497] [0.5988434 0.6038045 0.10514768] [0.38194343 0.03647606 0.89041156]] [[0.98092085 0.05994199 0.89054596] [0.5769015 0.7424797 0.63018394] [0.5818422 0.02043913 0.21002658]] [[0.5446849 0.76911515 0.25069523] [0.2858957 0.8523951 0.9750065 ] [0.8848533 0.35950786 0.59885895]]]] output of BatchNorm Layer: [[[[ 0.4126078 -0.46198368 0.02029109] [ 1.4071034 -1.3650038 -0.97940934] [ 0.832831 1.344658 -0.9294571 ]] [[ 0.2520175 1.2038351 -0.84927964] [-0.9211378 -1.1527538 -0.8176896 ] [ 1.4666051 0.96413004 -0.961432 ]] [[ 0.9541142 -0.9075856 -0.36629617] [ 1.37925 0.9590063 -0.6945517 ] [-1.2463869 -0.5684581 -1.8291974 ]]] [[[-0.5475932 1.2450331 -1.3302356 ] [ 0.5955492 0.6119205 -1.0335984 ] [-0.12019944 -1.2602081 1.5576957 ]] [[ 1.473519 -1.2985382 1.2014993 ] [ 0.25745988 0.7558342 0.41783488] [ 0.27233088 -1.4174379 -0.8467981 ]] [[ 0.02166975 0.79234385 -0.98786545] [-0.86699003 1.0783203 1.4993572 ] [ 1.1897788 -0.6142123 0.20769882]]]] channel 0 of input data: [[[0.54340494 0.2783694 0.4245176 ] [0.84477615 0.00471886 0.12156912] [0.67074907 0.82585275 0.13670659]] [[0.25242636 0.7956625 0.01525497] [0.5988434 0.6038045 0.10514768] [0.38194343 0.03647606 0.89041156]]] std 0.418368607759, mean 0.303022772074, output: [[[ 0.41263014 -0.46200886 0.02029219] [ 1.4071798 -1.3650781 -0.9794626 ] [ 0.8328762 1.3447311 -0.92950773]] [[-0.54762304 1.2451009 -1.3303081 ] [ 0.5955816 0.61195374 -1.0336547 ] [-0.12020606 -1.2602768 1.5577804 ]]]
预测时使用BatchNorm
上面介绍了在训练过程中使用BatchNorm对一批样本进行归一化的方法,但如果使用同样的方法对需要预测的一批样本进行归一化,则预测结果会出现不确定性。
例如样本A、样本B作为一批样本计算均值和方差,与样本A、样本C和样本D作为一批样本计算均值和方差,得到的结果一般来说是不同的。那么样本A的预测结果就会变得不确定,这对预测过程来说是不合理的。解决方法是在训练过程中将大量样本的均值和方差保存下来,预测时直接使用保存好的值而不再重新计算。实际上,在BatchNorm的具体实现中,训练时会计算均值和方差的移动平均值。在飞桨中,默认是采用如下方式计算:
saved_uB <- saved_uB * 0.9 + uB * (1-0.9)
saved_thedaB^2 <- savedthedaB^2 * 0.9 + thedaB^2 * (1-0.9)
在训练过程的最开始将saved_uB,saved_thedaB^2 设置为0,每次输入一批新的样本,计算出uB,thedaB^2,然后通过上面的公式更新saved_uB,saved_thedaB^2,在训练的过程中不断的更新它们的值,并作为BatchNorm层的参数保存下来。预测的时候将会加载参数saved_uB,saved_thedaB^2,用他们来代替