赞
踩
目录
2.2.3 BN 可以没有 Scale & Shift 过程吗?
通常,Normalization 意为 规范化、标准化、归一化。
在 机器学习 中,一个重要的假设便是 独立同分布 (independent and identically distributed, i.i.d.)。独立同分布并非所有机器学习模型的必然要求 (如 Naive Bayes 建立在特征彼此独立的基础上,而 LR 和 NN 则在非独立的特征数据上依然可以训练出很好的模型),但独立同分布的数据的确能够简化机器学习模型训练并提升机器学习模型的预测能力。因此,在将数据馈入机器学习模型前,白化 (Whitening) 作为一个重要的数据预处理步骤,旨在去除特征之间的相关性 (独立),并使所有特征具有相同的均值和方差 (同分布),其中最典型的方法便是 PCA。
通常,对 输入数据 使用以下两种 Normalization 来优化输入分布/损失空间 (便于优化/加快收敛):
1. Max-Min Normailzation
2. Z-score Normalization (Standardization)
而在 深度学习 中,Normalization 根据对 初始输入/中间特征 的操作对象/维度的不同,大致可分为两类:
1. 对 神经网络层输出值 Normalization,例如 Batch Norm、Layer Norm、Instance Norm、Group Norm。
2. 对 神经元连接边权重 Normalization,例如 Weight Norm。
本文将重点归纳 深度学习 中的 Normalization。
Batch Normalization (简称 BN) 出自 2015 年的论文《Batch Normalization: Accelerating Deep Network Training by Reducing Internal Covariate Shift》,由此可知,使用 (Batch) Normalization 旨在处理 Internal Covariate Shift 问题。
Internal Covariate Shift (简称 ICS) 指 神经网络在更新参数后各层输入的分布会发生变化,从而给训练带来困难的现象。换言之,若网络在传播过程中使数据分布稍微变化,负面影响便会不断累积至深层,以至于最终产生梯度弥散 (深层输入分布变化几乎消失 或 梯度爆炸 (深层输入分布变化非常剧烈) 的问题。更直白地,尽管一开始标准化原始输入,但是再经历过一个网络层后它的分布就发生了改变,那么下一层又需要重新学习另外一种分布,这就意味着每一层其实都是在学习不同的分布。因此,作者将这种由于网络参数发生变化而引起分布发生改变的现象称为网络的 Internal Covariate Shift (ICS) 问题
关于 ICS 的含义,@魏秀参 做了一个很好的解释:
ICS 所带来的问题主要有以下三点:
- 在训练时,每层网络输出的分布都在变化,使深层网络需不断适应浅层分布变化,从而降低了网络收敛速度;
- 在训练时,模型容易陷入激活函数的饱和区 (如 Sigmoid 和 Tanh),使梯度变小,从而降低了参数更新速度,甚至使学习过早停止。
- 在训练时,每层更新都会影响到其它层,因此每层的参数更新策略需尽可能谨慎。
起初,大多人采用 较小的学习率 和 特定的权重初始化 来缓解和应对。然而,参数初始化的方式非常依赖于激活函数,且难以给出通用的解决方案。为此,Google 在 2015 年给出了 Batch Normalization 这一解决方法。
以视觉图像场景为例,设当前有一个 batch 的 feature maps
计算 BN 时,将沿维度
- # 官方版 BN 和手工版 BN 对比 - 验证上述说明的正确性
- import torch
- from torch import nn
-
- # 1. 随机构造一个 batch 的 feature maps, shape = (N, C, H, W)
- # 数值乘 10000 扩大, 使细微的差别更明显
- x = torch.rand(10, 3, 5, 5) * 10000
-
- # 2. 官方版 BN
- # 设 track_running_stats=False, 求当前 batch 真实平均值和标准差, 而非更新全局平均值和标准差
- # 设 affine=False, 只做归一化, 不乘 gamma 加 beta (通过训练才能确定)
- # num_features 为 feature maps 的 channel 数
- # 设 eps=0, 让官方代码和手工版结果尽量接近
- bn = nn.BatchNorm2d(num_features=3, eps=0, affine=False, track_running_stats=False)
- # 计算 BN
- official_bn = bn(x)
-
- # 3. 手工版 BN
- # 把 channel 维度单独提出, 而把其它需要求均值和标准差的维度融合到一起
- x_c = x.permute(1, 0, 2, 3).view(3, -1)
- # 计算标准差
- # 设 unbiased=False, 求方差时不做无偏估计 (除 N-1 而非 N), 和原论文一致
- # 无偏估计可能仅是数学形式上好看而实际应用中差别不大
- std = x_c.std(dim=1, unbiased=False).view(1, 3, 1, 1)
- # 计算均值
- mu = x_c.mean(dim=1).view(1, 3, 1, 1)
- # 计算 BN
- manmade_bn = (x - mu) / std
-
- # 4. 官方版 BN 和手工版 BN 之差
- diff = (official_bn - manmade_bn).sum()
- # 差在 10-5 数量级, 证明二者基本一致
- print(f'diff={diff}')
事实上,BN 还包含另外两个可学习参数:放缩参数 (Scale Parameter)
设经 Standardization 的 feature maps 为 (满足均值为 0 方差约为 1 的标准正态分布) (
则经 Scale & Shift 得到的新分布 —— 最终 BN 结果为 (均值为
从而,BN 整体过程可表示为 (dimension i 即 channel i):
可见,输入的一个 batch 的 feature maps
注意,在推理期间,均值和方差是固定的,即不再是可学习、会变化的参数。
一方面,BN 仅适用于通道数固定的场景,如 CNN。而对于 RNN、LSTM 和 Transformer 等序列长度不一致网络而言,BN 并不适用,通常改用 LN。
另一方面,BN 在大、中 batch size 中具有良好的性能,并对多个视觉任务 (如,NAS) 具有良好的泛化性。然而,在ImageNet 实验中,BN 在小 batch size 时难以通过少量样本来估计训练数据整体的均值和方差,因而性能显著下降了 10%。而以通道分组为核心思想的 GN 应运而生。
使用 BN 的好处在于:
- 可 使用更大的学习率,使训练过程更稳定,加快模型训练的收敛速度 (因为分布被归一化了)。
- 可 令 bias=0,因为 BN 的 Standardization 过程会移除直流分量/常量偏置,故不再需要 bias。
- 对 权重初始化不再敏感,通常 权重采样自零均值某方差的高斯分布,以往对高斯分布的方差设置十分重要。有了 BN 后,对与同一个输出节点相连的权重进行放缩,其标准差
σ 也会放缩同样倍数,最后相除抵消影响。- 对 权重的尺度不再敏感,理由同上,尺度统一由
γ 参数控制,在训练中决定。- 深层网络可使用 Sigmoid 和 Tanh 了,理由同上,BN 使数据 跳出激活函数饱和区,抑制了梯度消失/弥散问题。
- BN 具有 某种正则作用,无需太依赖 Dropout 来缓解过拟合。
一方面,1 个 kernel 生成 1 个 feature map (有 1 对
BN 由两个过程构成:Standardization 和 Scale & Shift。其中,Standardization 作为机器学习常用的数据预处理技术,在浅层模型中只需对数据进行 Standardization 即可。然而,BN 可否只有 Standardization 过程呢?可以,但网络的表达能力会下降。
直觉上,在浅层模型中只需令模型适应数据分布即可,故 Standardization 足矣。然而,对于深度神经网络,每层的输入分布和权重需相互协调。但浅层神经元可能很努力地在学习,但不论其如何变化,其输出的结果在交给深层神经元之前,将被 Standardization 过程 “粗暴” 地重新调整到并非最佳选择的固定范围 (把数据分布强行限制在 Zero Mean Unit Variance)。所以,加入
除了充分利用浅层神经元的学习能力,Scale & Shift 还可以使获得模型非线性的表达能力。Sigmoid 等激活函数通过区分饱和区和非饱和区,使得神经网络的数据变换具有非线性表达能力。而 Standardization 过程很容易将几乎所有数据分布映射到激活函数的非饱和区 (线性区),仅具有线性表达能力 / 降低了神经网络的 (非线性) 表达能力。而 Scale & Shift 则可将数据从线性区变换到非线性区,恢复模型的非线性表达能力。从而,可学习参数
此外,表达能力更强,在实践中性能就会更好吗?不一定,就像参数越多不见得性能越好一样。在 caffenet-benchmark-batchnorm 中,作者的实验表面没有 Scale & Shift 过程性能可能更好些。
在原论文中,BN 被建议插入在 ReLU 激活层前面,从而构成 Conv+BN+ReLU 的组合,如下所示:
原因在于 ReLU 的输出非负,不能近似为高斯分布。但是,在 caffenet-benchmark-batchnorm 中,作者基于 caffenet 在ImageNet2012 上做的对比实验表明,放在前后的差异似乎不大,甚至放在 ReLU 后还好一些。BN 放 ReLU 后相当于直接对每层的输入进行归一化,如下图所示,这与浅层模型的 Standardization 是一致的。
因此 BN 放在什么位置并无定论,还需要实验结果说明。
BN 层的有效性目共睹,但具体原因还需进一步研究,以下存在若干角度的解释:
1) BN 层让损失函数更平滑。《How Does Batch Normalization Help Optimization》一文通过分析训练过程中每步梯度方向上步长变化引起的损失变化范围、梯度幅值的变化范围、光滑度的变化,认为添加 BN 层后,损失函数的 Landscape (Loss Surface) 变得更平滑。相比高低不平上下起伏的 Loss Surface,平滑 Loss Surface 的梯度预测性更好,可选取较大的步长。如下图所示:
2) BN 更有利于梯度下降。《An empirical analysis of the optimization of deep network loss surfaces》一文绘制了 VGG 和NIN 网络在有无 BN 层的情况下的 Loss Surface 差异,包含初始点位置及不同优化算法最终收敛到的 Local Minima 位置,如下图所示。没有 BN 层的,其 Loss Surface 存在 Plateau (梯度近乎为 0 的平坦、难收敛、难优化区域),有 BN 层的则少 Plateau 多 Valley,因此更容易下降。
3) BN 有利于降低分布调整难度。一个直觉上的理解是,没有 BN 层时,网络无法直接控制每层输入的分布,其分布由前面层的权重共同决定,或者说分布的均值和方差 “隐藏” 在前面层的每个权重中。因此,网络若想调整其输入的分布,需通过复杂的反向传播过程来调整前面层的权重。而 BN 层则相当于将分布的均值和方差从权重中剥离了出来,只需调整
此外,《How Does Batch Normalization Help Optimization 》一文对比了标准 VGG 以及加了 BN 层的 VGG 每层分布随训练过程的变化,发现两者并无明显差异,认为 BN 并没有改善 Internal Covariate Shift。
- def batchnorm_forward(x, gamma, beta, bn_param):
- """
- Input:
- - x: (N, D) 维输入数据
- - gamma: (D,) 维尺度变化参数
- - beta: (D,) 维尺度变化参数
- - bn_param: Dictionary with the following keys:
- - mode: 'train' 或者 'test'
- - eps: 一般取 1e-8 ~ 1e-4
- - momentum: 计算均值、方差的更新参数
- - running_mean: (D,) 动态变化 array 存储训练集的均值
- - running_var:(D,) 动态变化 array 存储训练集的方差
- Returns a tuple of:
- - out: 输出 y_i(N,D)维
- - cache: 存储反向传播所需数据
- """
- mode = bn_param['mode']
- eps = bn_param.get('eps', 1e-5)
- momentum = bn_param.get('momentum', 0.9)
-
- N, D = x.shape
- # 动态变量,存储训练集的均值方差
- running_mean = bn_param.get('running_mean', np.zeros(D, dtype=x.dtype))
- running_var = bn_param.get('running_var', np.zeros(D, dtype=x.dtype))
-
- out, cache = None, None
- # TRAIN 对每个batch操作
- if mode == 'train':
- sample_mean = np.mean(x, axis=0)
- sample_var = np.var(x, axis=0)
- x_hat = (x - sample_mean) / np.sqrt(sample_var + eps)
- out = gamma * x_hat + beta
- cache = (x, gamma, beta, x_hat, sample_mean, sample_var, eps)
- # 滑动平均(影子变量)这种Trick的引入,目的是为了控制变量更新的速度,
- # 防止变量的突然变化对变量的整体影响,这能提高模型的鲁棒性。
- running_mean = momentum * running_mean + (1 - momentum) * sample_mean
- running_var = momentum * running_var + (1 - momentum) * sample_var
- # TEST:要用整个训练集的均值、方差
- elif mode == 'test':
- x_hat = (x - running_mean) / np.sqrt(running_var + eps)
- out = gamma * x_hat + beta
- else:
- raise ValueError('Invalid forward batchnorm mode "%s"' % mode)
-
- bn_param['running_mean'] = running_mean
- bn_param['running_var'] = running_var
-
- return out, cache
- def batchnorm_backward(dout, cache):
- """
- Inputs:
- - dout: 上一层的梯度,维度(N, D),即 dL/dy
- - cache: 所需的中间变量,来自于前向传播
- Returns a tuple of:
- - dx: (N, D)维的 dL/dx
- - dgamma: (D,)维的dL/dgamma
- - dbeta: (D,)维的dL/dbeta
- """
- x, gamma, beta, x_hat, sample_mean, sample_var, eps = cache
- N = x.shape[0]
-
- dgamma = np.sum(dout * x_hat, axis = 0)
- dbeta = np.sum(dout, axis = 0)
-
- dx_hat = dout * gamma
- dsigma = -0.5 * np.sum(dx_hat * (x - sample_mean), axis=0) * np.power(sample_var + eps, -1.5)
- dmu = -np.sum(dx_hat / np.sqrt(sample_var + eps), axis=0) - 2 * dsigma*np.sum(x-sample_mean, axis=0)/ N
- dx = dx_hat /np.sqrt(sample_var + eps) + 2.0 * dsigma * (x - sample_mean) / N + dmu / N
-
- return dx, dgamma, dbeta
BN 的缺点是:
- 需要较大的 batch size 才能合理估计训练数据整体的均值和方差
- 对内存需求高
- 且很难应用在数据长度不同的 RNN 模型上
而 Layer Normalization (LN) 的一个优势便是无需批训练,其在单个样本内部就能归一化。
同样,设有一个 batch 的 feature maps
计算 LN 时,将沿维度
类似于 BN,LN 也包含另外两个可学习参数:放缩参数 (Scale Parameter)
- import torch
- from torch import nn
-
- x = torch.rand(10, 3, 5, 5) * 10000
-
- # 官方版 LN
- # normalization_shape 相当于告诉程序这本书有多少页, 每页多少行多少列
- # 设 eps=0 排除干扰
- # elementwise_affine=False 不作映射
- # 这里的映射和 BN 以及下文的 IN 有区别, 它是 elementwise 的 affine,
- # 即 gamma 和 beta 不是 channel 维的向量, 而是维度等于 normalized_shape 的矩阵
- ln = nn.LayerNorm(normalized_shape=[3, 5, 5], eps=0, elementwise_affine=False)
- official_ln = ln(x)
-
- # 手工版 LN
- x1 = x.view(10, -1)
- mu = x1.mean(dim=1).view(10, 1, 1, 1)
- std = x1.std(dim=1, unbiased=False).view(10, 1, 1, 1)
- manmade_ln = (x - mu) / std
-
- # 差别和官方版本数量级在 1e-5
- diff = (manmade_ln - official_ln).sum()
- print(f'diff={diff}')
对于 LN 与 BN 而言,BN 取的是不同样本的同一个特征,而 LN 取的是同一个样本的不同特征。在 LN 和 BN 都能使用的场景中,BN 的效果一般优于 LN,原因是 基于不同数据,由同一特征得到的归一化特征更不容易损失信息。通常,LN 适用于 RNN、LSTM 和 Transformer 等不定长序列模型 (动态网络)。以 RNN 的角度为例,LN 的均值和方差计算仅取决于当前时间步的层输入,而不取决于当前 batch 的所有输入,因此可用于任意长度的序列 (batch size 不固定),由 LN 得到的模型更稳定且起到正则化的作用。而当将 LN 添加到 CNN 之后,实验结果发现破坏了卷积学习到的特征,模型无法收敛,所以在 CNN 之后使用非 LN 的 Normalization 是一个更好的选择
此外,LN 无需保存 batch 的均值和方差,节省了额外的存储空间。LN 还只有一组在所有时间步中共享的放缩和平移参数
- def Layernorm(x, gamma, beta):
- '''
- x_shape:[B, C, H, W]
- '''
- results = 0.
- eps = 1e-5
-
- x_mean = np.mean(x, axis=(1, 2, 3), keepdims=True) # 对 C, H, W 维求均值
- x_var = np.var(x, axis=(1, 2, 3), keepdims=True) # 对 C, H, W 维求方差
- x_normalized = (x - x_mean) / np.sqrt(x_var + eps) # Standardization
- results = gamma * x_normalized + beta # Scale $ Shift
-
- return results
Instance Normalization (IN) 最初用于图像风格迁移。作者发现,在生成模型中,feature maps 的各 channel 的均值和方差会影响到最终生成图像的风格,因此可以 先把图像在 channel 维度归一化,然后再用目标风格图片对应 channel 的均值和标准差“去归一化”,以期获得目标图片的风格。IN 也在单个样本内进行,不依赖 batch size。
同样,设当前有一个 batch 的 feature maps
计算 IN 时,将沿维度
- import torch
- from torch import nn
-
- x = torch.rand(10, 3, 5, 5) * 10000
-
- # 官方版 IN
- # track_running_stats=False,求当前 batch 真实平均值和标准差,
- # 而不是更新全局平均值和标准差
- # affine=False, 只做归一化,不乘以 gamma 加 beta(通过训练才能确定)
- # num_features 为 feature map 的 channel 数目
- # eps 设为 0,让官方代码和我们自己的代码结果尽量接近
- In = nn.InstanceNorm2d(num_features=3, eps=0, affine=False, track_running_stats=False)
- official_in = In(x)
-
- # 手工版 IN
- x1 = x.view(30, -1)
- mu = x1.mean(dim=1).view(10, 3, 1, 1)
- std = x1.std(dim=1, unbiased=False).view(10, 3, 1, 1)
- manmade_in = (x - mu) / std
-
- # 误差量级在 1e-5
- diff = (manmade_in-official_in).sum()
- print(f'diff={diff}')
有别于 BN 在判别任务上的优势,IN 在 GAN,Style Transfer 和 Domain Adaptation 等生成任务上的效果优于 BN。因为 BN 同时对多个样本 (batch 内) 统计均值和方差,而这多个样本的 Domain 很可能不同,相当于把不同 Domain 的数据分布进行了归一化。 而 IN 仅对单样本单通道内进行归一化,避免了不同 Doman 之间的相互影响。
- def Instancenorm(x, gamma, beta):
- '''
- x_shape:[B, C, H, W]
- '''
- results = 0.
- eps = 1e-5
-
- x_mean = np.mean(x, axis=(2, 3), keepdims=True) # 对 H, W 维求均值
- x_var = np.var(x, axis=(2, 3), keepdims=True0) # 对 H, W 维求方差
- x_normalized = (x - x_mean) / np.sqrt(x_var + eps) # Standardization
- results = gamma * x_normalized + beta # Scale & Shift
-
- return results
Group Normalization (GN) 适用于 图像分割等显存占用较大 的任务。对于这类任务,batch size 的大小设置很可能只能是个位数 (如 1、2、4 等),再大就爆显存了。而当 batchsize 取小数字时,BN 的表现很差,因为无法通过几个样本来近似数据集整体的均值和标准差 (代表性较差)。而作为 LN 和 IN 的折中,GN 也是独立于 batch 的。
同样,设当前有一个 batch 的 feature maps
计算 GN 时,将沿维度
- import torch
- from torch import nn
-
- x = torch.rand(10, 20, 5, 5) * 10000
-
- # 官方版 GN
- # 分成 4 个 group (G=4)
- # 其余设定和之前相同
- gn = nn.GroupNorm(num_groups=4, num_channels=20, eps=0, affine=False)
- official_gn = gn(x)
-
- # 手工版 GN
- # 把同一 group 的元素融合到一起
- x1 = x.view(10, 4, -1)
- mu = x1.mean(dim=-1).reshape(10, 4, -1)
- std = x1.std(dim=-1).reshape(10, 4, -1)
- x1_norm = (x1 - mu) / std
- manmade_gn = x1_norm.reshape(10, 20, 5, 5)
-
- # 比较
- diff = (manmade_gn-official_gn).sum()
- print(f'diff={diff}') # 误差在 1e-4 级
在 batch size 较大时,GN 效果略低于BN,但当 batch size 较小时,明显优于 BN。
由于 GN 在 channel 维度上分组,因此要求 channel 数
GN 常应用于目标检测,语义分割等 要求图像分辨率尽可能大的任务,由于内存限制,更大分辨率意为着只能取更小的batch size,此时可以选择 GN 这种不依赖于 batch size 的 Normalization 方法。
- def GroupNorm(x, gamma, beta, G=16):
- '''
- x_shape:[B, C, H, W]
- gamma, beta, scale, offset : [1, c, 1, 1]
- G: num of groups for GN
- '''
- results = 0.
- eps = 1e-5
- x = np.reshape(x, (x.shape[0], G, x.shape[1]/16, x.shape[2], x.shape[3])) # shape = (N, G, C/G, H, W)
-
- x_mean = np.mean(x, axis=(2, 3, 4), keepdims=True) # 对 C/G, H, W 维求均值
- x_var = np.var(x, axis=(2, 3, 4), keepdims=True0) # 对 C/G, H, W 维求方差
- x_normalized = (x - x_mean) / np.sqrt(x_var + eps) # Standardization
- results = gamma * x_normalized + beta # Scale & Shift
-
- return results
Weight Normalization (WN) 出自《Weight Normalization: A Simple Reparameterization to Accelerate Training of Deep Neural Networks》一文。
在神经网络中权重向量的重参数化,将这些权重向量的长度与其方向解耦。通过以这种方式重新设置权重,改善了优化问题的条件,并加快了随机梯度下降的收敛速度。WN 受到 BN 的启发,但并未在小 batch 中引入实例间的任何依赖关系 (不依赖 batch size)。这意味 WN 还可以成功地应用于递归模型 (如 LSTM) 和对噪声敏感的应用 (例如深度强化学习或生成模型),而 BN 则不太适合此类方法。此外, WN 的计算开销较低,从而允许在相同的时间内采取更多的优化步骤。
可见,诸如 BN、LN、IN、GN 等都对 feature maps 进行 Normalization,而 WN 则对 weights 进行 Normalization。
WN 将权重向量
WN 也是和样本量无关的,所以可以应用在较小 batch size 以及 RNN 等动态网络中;另外 BN 使用的基于 mini batch的归一化统计量代替全局统计量,相当于在梯度计算中引入了噪声。而 WN 则没有这个问题,所以在生成模型与强化学习等噪声敏感的环境中 WN 的效果也要优于 BN。
WN 的缺陷在于,WN 不像 BN 有归一化特征尺度的作用,因此 WN 的初始化需慎重。为此,作者提出了对向量 和标量 的初始化方法。
- import torch.nn as nn
- import torch.nn.functional as F
-
- # 以 2 层 FCs 为例
- class Model(nn.Module):
- def __init__(self, input_dim, output_dim, hidden_size):
- super(Model, self).__init__()
- # weight_norm
- self.dense1 = nn.utils.weight_norm(nn.Linear(input_dim, hidden_size))
- self.dense2 = nn.utils.weight_norm(nn.Linear(hidden_size, output_dim))
-
- def forward(self, x):
- x = self.dense1(x)
- x = F.leaky_relu(x)
- x = self.dense2(x)
- return x
参考资料:
如何区分并记住常见的几种 Normalization 算法_北国觅梦-CSDN博客
【深度学习】Batch Normalization(BN)超详细解析
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。