Batch Normalization

未经过(批量)归一化的多层神经网络会出现什么问题

Internal Covariate Shift

Internal Covariate Shift,简称ICS,中文名为内部协变量偏移。

我们先来看一组神经网络:

神经网络

其中,第一层为输入层,最后一层为输出层,其余为隐藏层:

神经网络

隐藏层1接收的是输入层的数据,隐藏层2接收的是隐藏层1的数据,隐藏层3接收的是隐藏层2的数据,输出层接收的是隐藏层3的数据。

也就是说,除了输入层外,每一层神经网络是从上一层神经网络那里接收数据(循环神经网络除外),那么,除了输入层,对每一层神经网络而言,上一层神经网络都可以算作是输入层

那么,对于一个\(L\)层的神经网络,我们在分析第\(l\)层神经网络的时候,可以将\(l-1\)层看做是输入层,整个神经网络可以等价为\(L-l+1\)层的神经网络

比如对于上面这个神经网络,分析隐藏层2,我们可以将其等价为:

分析隐藏层2的等价神经网络

在我们平时训练神经网络的时候,一般训练集都是不变的,对应着在每次的epoch,输入层的数据都是不变的,这样我们能根据训练集训练出一个拟合度不错的神经网络。

分析隐藏层2及后面的神经网络层时,我们自然希望隐藏层1在每次epoch的数据也是不变的,这样才能使隐藏层2到输出层的神经网络部分能够取得不错的训练效果。

但是,我们知道,对于隐藏层1而言,其输出的数据和其权重、偏差、激活函数等息息相关,在每次iteration的反向传播中,隐藏层1的各个参数都会进行梯度下降从而更新参数,参数更新后,隐藏层1输出的数据很有可能不再相同。

而神经网络学习的本质还是数据,如果数据分布总是发生变动,学习效果自然不好。

对于隐藏层2而言,相当于每次epoch的训练集都不同,那么隐藏层2及后面的神经网络层的学习效率自然就非常低下。

因此,当隐藏层1收敛后,隐藏层2的学习速度才能快起来。

以上的现象就称为Internal Covariate Shift:在每一次的迭代更新后,上一层网络的输出数据在经过这一层网络的计算后,数据的分布发生了变化,为下一层网络的学习带来了困难。

Covariate Shift

Covariate Shift,协变量偏移。

Covariate Shift与Internal Covariate Shift的概念具有相似性,但它们也有区别:Internal Covariate Shift发生在神经网络内部,Covariate Shift则主要发生在输入数据上:训练数据和测试数据的分布差异性,给网络的泛化性和训练速度带来了影响。

Covariate Shift
Covariate Shift

也就是说,我们训练的数据和测试的数据的分布区域可能不一样,因此,用训练数据拟合的曲线很可能对于测试数据欠拟合。

比如,如果我们做一个猫的分类器,用来判断一张图片上的动物是不是猫,但我们的训练样本只有黑猫的图片,而测试的样本是彩色的猫,那么训练集和测试机的数据分布就有偏差,训练出来的神经网络模型很可能就认为彩色的猫不是猫,只有黑色的猫才是猫。

为了避免这个问题,我们需要对输入的数据进行归一化,使得分类器不会因为颜色这个变量分布差异对模型造成的影响。

由此看来,让数据分布均匀是很有必要的

补充:后来人们发现Batch Normalization对Covariate Shift没什么作用

批量归一化(Batch Normalization)

什么是Batch Normalization

Batch Normalization,也称Batch Norm,批量归一化,在每个网络层都对处理后的数据(线性部分或者非线性部分)进行归一化,使传递给下一层神经网络的数据都经过了归一化的处理。

Batch Normalization的方法

基本思路和步骤

全连接层

通常,我们将批量归一化(Batch Normalization)层置于全连接层中的线性变换(仿射变换)和激活函数之间。设全连接层的输入为\(\mathbf{x}\),权重参数和偏执参数分别为\(\mathbf{W}\)\(\mathbf{b}\),激活函数为\(\phi\),批量归一化的运算符为\(\mathrm{BN}\)。那么,使用批量归一化的全连接层的输出的计算详情如下: \[ \mathbf{h} = \phi(\mathrm{BN}(\mathbf{Wx}+\mathbf{b})) \] 对于全连接层,BN作用在特征维,即对一个样本的线性输出的向量的所有元素进行批量归一化。

对全连接层而言,如果使用Batch Normalization,则线性部分无需使用偏置\(b\)

在设置偏置的情况下: \[ \mathbf{y} = \gamma\Bigg(\frac{\mathbf{Wx}+\mathbf{b}-\mathbf{\mu}}{\sqrt{(\mathbf{\sigma}^2)+\epsilon}}\Bigg) +\beta \] 其中\(\mathbf{y}\)\(\mathrm{BN}(\mathbf{Wx}+\mathbf{b})\)

在不设置偏置的情况下: \[ \mathbf{y} = \gamma\Bigg(\frac{\mathbf{Wx}-\mathbf{\mu}'}{\sqrt{(\mathbf{\sigma}^2)+\epsilon}}\Bigg) +\beta \] 其中\(\mathbf{\mu}' = \mathbf{\mu}-\mathbf{b}\)\(\sigma\)并不会因为是否设置偏置而改变。

由此可以发现,不设置偏置的情况下得出来的\(\mathbf{y}\)和设置偏置得出来的\(\mathbf{y}\)相同,因此我们可以设置bias=False,从而减小计算量

卷积层

对于卷积层,我们可以在卷积层之后和非线性激活函数之前应用批量归一化。当卷积有多个输出通道时,我们需要对这些通道的每个输出执行批量归一化,也就是将每个像素当成一个样本,每个像素的所有通道就是像素的所有特征,对这些特征进行批量归一化。

卷积层可以看成特殊的全连接层,因此,如果使用Batch Normalization,也可以不设置偏置

做法

官方论文的写法为:

使用小批量梯度下降时,batch size为\(m\),对于小批量子集\(B\)\(B = \{x_{1...m}\}\)

Input: Values of x over a mini-batch: \(B=\{x_{1...m}\}\);

Output: \(\{y_i=BN_{\gamma,\beta}(x_i)\}\) \[ \begin{aligned} &\mu_B\leftarrow \frac{1}{m}\displaystyle\sum_{i=1}^m{x_i}\\\ &(\sigma_B)^2 \leftarrow \frac{1}{m}\displaystyle\sum_{i=1}^m{(x_i-\mu_B)^2}\\\ &\tilde x_i \leftarrow \frac{x_i-\mu_B}{\sqrt{(\sigma_B)^2+\epsilon}}\\\ &y_i\leftarrow\gamma\tilde x_i+\beta \end{aligned} \] 前三步很明显就是输入数据分布归一化为一个标准正态分布,而\(\epsilon\)是一个很小的值,作用是防止分母为0或者趋于0

而第四步,就是将归一化的数据重新设置方差与均值,修改后的方差为\(\gamma^2\),均值为\(\beta\),当然,我们也可以这么认为:\(\gamma\)是伸缩(Scale)系数,可以对数据扩大或缩小;\(\beta\)是一个偏移(Shift)系数,可以将数据进行偏移。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import torch
import torch.nn as nn

def batch_norm(X, gamma, beta, moving_mean, moving_var, eps, momentum):
# 通过is_grad_enabled来判断当前模式是训练模式还是预测模式
if not torch.is_grad_enabled():
# 如果是在预测模式下,直接使用传入的移动平均所得的均值和方差
X_hat = (X - moving_mean) / torch.sqrt(moving_var + eps)
else:
# 此处仅处理线性和卷积层的情况(线性输入为二维,卷积输入为四维)
assert len(X.shape) in (2, 4)
if len(X.shape) == 2:
# 使用全连接层的情况,计算特征维上的均值和方差
mean = X.mean(dim=0)
var = ((X - mean)**2).mean()

else:
# 使用二维卷积层的情况,计算通道维上(axis=1)的均值和方差。
# 这里我们需要保持X的形状以便后面可以做广播运算
mean = X.mean(dim=(0, 2, 3), keepdim=True)
var = ((X - mean)**2).mean(dim=(0, 2, 3), keepdim=True)

# 训练模式下,用当前的均值和方差做标准化
X_hat = (X - mean) / torch.sqrt(var + eps)
# 更新移动平均的均值和方差
moving_mean = momentum * moving_mean + (1.0 - momentum) * mean
moving_var = momentum * moving_var + (1.0 - momentum) * var
Y = gamma * X_hat + beta # 缩放和移位
return Y, moving_mean.data, moving_var.data

为什么要设置\(\gamma\)\(\beta\)

如果我们不设置\(\gamma\)\(\beta\),那么得到的数据就满足标准正太分布,但我们不知道标准正态分布是否真的能够让模型达到最优解,是否能够诠释好这些数据。

理解:

  • 加入\(\gamma\)\(\beta\),能够让模型的表达能力不会因为归一化而下降。因为机器学习实际上是对数据的学习,而均值和方差本身就是这些数据的一个特征,如果真实结果与均值和方差有关,而转化为标准正太分布后,均值和方差所携带的信息就相当于丢失掉了。加入\(\gamma\)\(\beta\),并让其进行学习,可以恢复这部分损失的信息。
  • 如果不加入\(\gamma\)\(\beta\),这一层的输出就会被粗暴地限制在某个固定的范围内,后续层输入都是这样一个分布的数据。有了伸缩和平移,且这两个参数需要学习的参数,那么这一层的输出就不再是被粗暴的归一化了

总的而言,或许数据的分布会有着一定的意义,而将数据归一化后变成标准正太分布则失去了可能存在的意义,因此,我们设置一个\(\gamma\)\(\beta\)来控制数据的分布,并让其进行学习、优化。

Batch Normalization的预测

在训练的时候,每个小批量都有对应的平均值和方差,因此我们可以在每次都使用每个小批量各自的均值和方差,但是,在预测的时候,我们通常都只有一个输入,只获得一个结果,也就是说,在预测的时候,往往只有一个“样本”,我们无法获得均值和方差。

为了解决这个问题,我们可以在训练的时候就求出一个均值\(\mu\)和方差\(\sigma^2\),这个均值和方差在我们预测的时候进行使用。

这里有几种方法:

方法一(整体平均)

直接使用所有训练数据的均值和方差,也就是使用整个训练集的均值和方差。

在我们获得最终模型后,此时在这个模型上运行整个训练集,我们便可以得到每个神经网络层上对应的方差和均值。

注意:一定要是模型收敛后,模型未收敛之前,因为上层的输出可能都会出现不同,用来预测这层得到数据的均值和方差,没有多大的意义。而收敛后,均值和方差不会再改变,这个时候计算得到的均值和方差可以用于当做预测时使用的均值和方差。

方法二(指数加权平均)

模型收敛后,如果训练集过大,我们很难一次性运行一整个训练集而得到各层的均值和方差,因此,我们只能通过小批量的方式分批次运行训练集。

每个小批量都可以得到一个均值和方差,我们可以对这些均值和方差使用指数加权平均,从而得到最终的均值和方差。

即,模型收敛后,对于每个小批量\(B\) \[ \mu^{[l]} = \beta\mu^{[l]} + (1-\beta)\mu_{B}^{[l]} \] 需要注意的是\(\beta\)的大小,如果总共没多少小批量,而\(\beta\)又比较大,可能导致偏差,如果想要避免偏差,可以使用偏差修正。

通常来讲,\(\beta\)越大,真正起到作用的小批量就越多,\(\beta\)如果比较小,可能前面隔了几个的小批量的均值和方差就很难起到什么作用了。

如果\(\beta\)比较大,偏差造成影响的数据就越多,这个数据的偏差量也越大,我们就越要使用偏差修正。

如果\(\beta\)比较小,偏差造成影响的数据就比较少。

我们默认这些小批量上的数据都是随机分布的,因此,当\(\beta\)比较小时,比如\(\beta=0.9\),仅最近十几次的数据会对预测的\(\mu\)\(\sigma\)形成影响,如果小批量的个数有几十上百个,我们很容易取到接近于真实值的平均值。

但如果是对一个并不是随机分布的数据用指数加权移动平均进行平均,如果\(\beta\)比较小,因为只能考虑最近的一些数据,那么得到的结果很可能和真实的结果相差比较大。

因此,对于一个并不是随机分布的数据,我们通常选用较大的\(\beta\),然后使用偏差修正,往往能得到相对精确的结果。

比如:

  • 随机分布的数据
  1. \(\beta\)比较小
1
2
3
4
5
6
7
8
9
10
import numpy as np
a = np.random.randint(1, 10000, 10000)
beta = 0.9
avg = np.sum(a)/np.size(a) # avg 为真实均值
v = 0 # v 为指数加权移动平均估计的均值
for i in a:
v = beta*v + (1-beta)*i
v = v / (1-beta**np.size(a)) # 偏差修正
print(f"真实均值:{avg}")
print(f"指数加权平均估计均值:{v}")

输出结果:

1
2
真实均值:4997.4601
指数加权平均估计均值:4950.54912791809
  1. \(\beta\)很大
1
2
3
4
5
6
7
8
9
10
import numpy as np
a = np.random.randint(1, 10000, 10000)
beta = 0.99999999
avg = np.sum(a)/np.size(a) # avg 为真实均值
v = 0 # v 为指数加权移动平均估计的均值
for i in a:
v = beta*v + (1-beta)*i
v = v / (1-beta**np.size(a)) # 偏差修正
print(f"真实均值:{avg}")
print(f"指数加权平均估计均值:{v}")

输出结果:

1
2
真实均值:5026.8771
指数加权平均估计均值:5026.876957423084
  1. \(\beta\)比较大
1
2
3
4
5
6
7
8
9
10
import numpy as np
a = np.random.randint(1, 10000, 10000)
beta = 0.9999
avg = np.sum(a)/np.size(a) # avg 为真实均值
v = 0 # v 为指数加权移动平均估计的均值
for i in a:
v = beta*v + (1-beta)*i
v = v / (1-beta**np.size(a)) # 偏差修正
print(f"真实均值:{avg}")
print(f"指数加权平均估计均值:{v}")

输出结果:

1
2
真实均值:5012.6614
指数加权平均估计均值:5020.120736529974
  • 不是随机分布的数据
  1. \(\beta\)比较小
1
2
3
4
5
6
7
8
9
10
import numpy as np
a = np.arange(1, 10000)
beta = 0.9
avg = np.sum(a)/np.size(a) # avg 为真实均值
v = 0 # v 为指数加权移动平均估计的均值
for i in a:
v = beta*v + (1-beta)*i
v = v / (1-beta**np.size(a)) # 偏差修正
print(f"真实均值:{avg}")
print(f"指数加权平均估计均值:{v}")

输出结果:

1
2
真实均值:5000.0
指数加权平均估计均值:9990.0
  1. \(\beta\)很大
1
2
3
4
5
6
7
8
9
10
import numpy as np
a = np.arange(1, 10000)
beta = 0.9999999
avg = np.sum(a)/np.size(a) # avg 为真实均值
v = 0 # v 为指数加权移动平均估计的均值
for i in a:
v = beta*v + (1-beta)*i
v = v / (1-beta**np.size(a)) # 偏差修正
print(f"真实均值:{avg}")
print(f"指数加权平均估计均值:{v}")

输出结果:

1
2
真实均值:5000.0
指数加权平均估计均值:5000.833166693852
  1. \(\beta\)比较大
1
2
3
4
5
6
7
8
9
10
import numpy as np
a = np.arange(1, 10000)
beta = 0.9999
avg = np.sum(a)/np.size(a) # avg 为真实均值
v = 0 # v 为指数加权移动平均估计的均值
for i in a:
v = beta*v + (1-beta)*i
v = v / (1-beta**np.size(a)) # 偏差修正
print(f"真实均值:{avg}")
print(f"指数加权平均估计均值:{v}")

输出结果:

1
2
真实均值:5000.0
指数加权平均估计均值:5819.64542299763

因此,在计算整体均值时,我们可以使用较大的\(\beta\)和偏差修正,这样的结果更加准确

当然,因为我们认为小批量样本为随机抽样,因此我们可以选用较小的\(\beta\),这样我们就能在训练过程中直接加上对整体\(\mu\)\(\sigma\)的估计,正是因为\(\beta\)较小,前面未收敛时的均值与方差所占权重很小很小,所以预测比较准确。这样我们就无需等到模型收敛后再运行一个epoch了。

方法三(无偏估计)

我们认为每个小批量是整体样本随机抽样而来的,因此,我们可以将一个小批量当成是对整体随机抽样,然后用一个小批量的均值和方差估计整体的均值和方差。

而随机抽样为了保证估计的无偏性,总体方差\(\sigma^2\)满足 \[ \sigma^2 = \frac{1}{m-1}\displaystyle\sum_{i=1}^{m}(Y_i-\bar Y)^2 \] 而这个小批量\(B\)本身的方差\(\sigma_B^2\)满足 \[ \sigma_B^2 = \frac{1}{m}\displaystyle\sum_{i=1}^{m}{(Y_i-\bar Y)^2} \]\[ \sigma^2 = \frac{m}{m-1}\sigma_B^2 \] 随机抽样的平均值与总体平均值则保持一致: \[ \mu = \mu_B \] 那么,我们可以选取最后一个epoch中的最后一个iteration,假设这个iteration的小批量子集为\(B\),则 \[ \begin{aligned} &(\sigma^{[l]})^2 = \frac{m}{m-1}(\sigma_B^{[l]})^2\\\ &\mu^{[l]} = \mu_B^{[l]} \end{aligned} \]

Batch Normalization的优缺点

优点

  • Batch Normalization 可以解决Internal Covariate Shift问题,加快学习速度;Batch Normalization 对Covariate Shift问题也有作用,可以缓解欠拟合的问题
  • Batch Normalization 将每层的输入重新分布,这样可以缓解梯度消失与梯度爆炸。
  • Batch Normalization 可以让每层的神经网络难以依赖上层神经网络输出数据的特性,使得每层的神经网络可以更加“独立”地学习,而归一化的数据可以当成含有了一部分“噪音”,可以起到轻微正则化的作用。
  • Batch Normalization 可以加快学习速度,归一化可以不同特征都满足相同分布,可以设置更大的学习率。
  • 可以让模型收敛更快

缺点

如果mini-batch的大小比较小,那么一个mini-batch很难代表整体的数据,计算出来的均值和方差与真实的均值和方差误差比较大,多个mini-batch得到的均值和方差往往不准确且不稳定。

有研究表明,对于ResNet类模型在ImageNet数据集上,当batch size从16下降到8时开始有非常明显的性能下降。

因此,Batch Normalization不适合以下几种场景:

  • batch size非常小
  • 循环神经网络(RNN),因为它是一个动态的网络结构,同一个mini batch中训练实例有长有短,导致每一个时间步长必须维持各自的统计量,这就使得BN不能正确发挥效果

Batch Normalization
https://blog.shinebook.net/2025/03/15/人工智能/理论基础/深度学习/Batch Normalization/
作者
X
发布于
2025年3月15日
许可协议