【避免了梯度消失和梯度爆炸、加速网络的收敛、优化网络结构、防止内部协变量偏移问题】
在NLP中,你可以这么认为:
N:batch维度,每一个块就是一个样本(句子)
C:特征维度(每个句子被映射到的维度)
H,W:空间维度(你可以理解为一个块就是一个单词)
在CV中,你可以这么认为:
N:batch维度,每一个块就是一个图像
C:每一个块就是一个通道
H,W:整体看成宽和高
- 相同点:均为减均值除标准差,然后再进行缩放和平移;都是基于 feature map 做归一化
- 不同点:均值和方差求取方式不同,LN/IN/GN 这三个解决的主要问题是 BN 的效果依赖于 batch size,当 batch size 比较小时,性能退化严重。可以看到,IN,LN 和 GN 都与 batch size 无关
BN | 在N、H、W上求均值和方差 | 计算 所有样本一个通道 | 检测、分类 |
LN | 在C、H、W上求均值和方差 | 计算 单个样本的所有通道 | RNN,transformer |
IN | 在H、W上求均值和方差 | 计算 单个样本在单个通道 | GAN, style transfer 和 domain adaptation |
GN | 在C'、H、W上求均值和方差 | 计算 每组(单个样本 g 个通道上) | 大分辨率较小 batch size |
一. 本文的内容包括:
1. Batch Normalization,其论文:https://arxiv.org/pdf/1502.03167.pdf
2. Layer Normalizaiton,其论文:https://arxiv.org/pdf/1607.06450v1.pdf
3. Instance Normalization,其论文:https://arxiv.org/pdf/1607.08022.pdf
4. Group Normalization,其论文:https://arxiv.org/pdf/1803.08494.pdf
5. Switchable Normalization,其论文:https://arxiv.org/pdf/1806.10779.pdf
二. 介绍
在介绍各个算法之前,我们先引进一个问题:为什么要做归一化处理?
神经网络学习过程的本质就是为了学习数据分布,如果我们没有做归一化处理,那么每一批次训练数据的分布不一样,从大的方向上看,神经网络则需要在这多个分布中找到平衡点,从小的方向上看,由于每层网络输入数据分布在不断变化,这也会导致每层网络在找平衡点,显然,神经网络就很难收敛了。当然,如果我们只是对输入的数据进行归一化处理(比如将输入的图像除以255,将其归到0到1之间),只能保证输入层数据分布是一样的,并不能保证每层网络输入数据分布是一样的,所以也需要在神经网络的中间层加入归一化处理。【简单来说,不能只做输入数据归一化,还要对NN的每一层做归一化,从而保证每层的数据分布大致是一致的】
BN、LN、IN和GN这四个归一化的计算流程几乎是一样的,可以分为四步:
- 计算出均值
- 计算出方差
- 归一化处理到均值为0,方差为1
- 变化重构,恢复出这一层网络所要学到的分布
a、内部协变量偏移问题(Internal Covariate Shift)
- 我们知道,在统计机器学习中算法中,一个常见的问题是协变量偏移(Covariate Shift),协变量可以看作是 输入变量。一般的深度神经网络都要求输入变量在训练数据和测试数据上的分布是相似的,这是通过训练数据获得的模型能够在测试集获得好的效果的一个基本保障。
- 传统的深度神经网络在训练时,随着参数的不算更新,中间每一层输入的数据分布往往会和参数更新之前有较大的差异,导致网络要去不断的适应新的数据分布,进而使得训练变得异常困难,我们只能使用一个很小的学习速率和精调的初始化参数来解决这个问题。而且这个中间层的深度越大时,这种现象就越明显。由于是对层间数据的分析,也即是内部(internal),因此这种现象叫做内部协变量偏移(internal Covariate Shift)。
b、解决方法
为了解决这个问题,在2015年首次提出了批量标准化(Batch Normalization,BN)的想法。该想法是:不仅仅对输入层做标准化处理,还要对 每一中间层的输入(激活函数前) 做标准化处理,使得输出服从均值为 0,方差为 1 的正态分布,从而避免内部协变量偏移的问题。之所以称之为 批 标准化:是因为在训练期间,我们仅通过计算 当前层一小批数据的均值和方差来标准化每一层的输入。
BN 的具体流程如下图所示:
- 首先,它将隐藏层的输出结果(如,第一层:Wh1 *x ,状态值)在 batch 上进行标准化;
- 然后,经过缩放(scale)和平移(shift)处理,
- 最后经过 RELU 激活函数得到h1 送入下一层(就像我们在数据预处理中将 x 进行标准化后送入神经网络的第一层一样)。
- γ 和 β 的解释: γ 和 β 都是可学习参数,分别用作对标准化后的值进行缩放(scale)和平移(shift),提高网络的表达能力(不加此参数的话,中间层的输出将被限制在标准正态分布下)。当参数分别初始化为 σB 和 μB 时,标准化的变量被还原为原来的值。随着网络的训练,网络会学到最合适的 γ 和 β ,最终中间层的输出将服从均值为 β,方差为 γ^2 的正态分布
- ϵ 的解释: 为了避免方差为 0 而加入的微小正数(eg:0.001)
的解释: 减去第一层 batch 个数据的均值 μ B 后, 偏置项 b 的作用会被抵消掉,所以在使用 BN 的层中没必要加入偏置项 b (红色删除线),其平移的功能将由 β 来代替
三、各种归一化理解 + 代码
它们的实现方式都是这样的(除了WN)
输出= 输入-均值 / 根号(标准差+)
1、BN
在N、H、W上求均值和方差
BN的office实现API:BatchNorm1d — PyTorch 2.2 documentation
pytorch官方实现:
import torch
# N: batch_size
batch_size = 2
# L: NLP句子中的单词数量
time_steps =3
# C: 每个单词被表示为一个N维的向量
embedding_dim = 4
inputx = torch.randn(batch_size, time_steps, embedding_dim) # N * L * C
## 1、实现 BN 并验证API
batch_norm_op = torch.nn.BatchNorm1d(embedding_dim, affine=False)
bn_y = batch_norm_op(inputx.transpose(-1,-2)).transpose(-1,-2)
print(bn_y)
手动实现:
所有样本一个通道(所有样本的所有单词的一个维度)
所以应该是对batch维度+time-step维度求均值和方差
## 2、手动实现BN
# BN是对所有样本N所有单词的一个通道进行归一化,所以在计算均值和方差时,需要在dim=0,1上进行计算,这样默认dim2就是1维度
# inputx.mean(dim=(0,1)是求C维度上的均值,keepdim=True保持与输入的维度不变
bn_mean = inputx.mean(dim=(0,1), keepdim=True)
# 这里要使用有偏估计,所以unbiased=False
bn_var = inputx.var(dim=(0,1), unbiased = False, keepdim=True)
bn_y = (inputx - bn_mean) / torch.sqrt(bn_var + 1e-5)
print(bn_y)
2、LN
LN与其他的有些不同,在vit,NLP transformer中,最常用的就是LN,它们并没有channel这个概念,所以对于vit是使用 T = H * W / patch_size ** 2 来充当NLP中的seq_len
领域 | 输入 |
NLP | (N, Seq_len, D) |
CV(transformer) | (N, T, D) |
CV(卷积) | (N, C, H, W) |
单个样本的所有通道上
在C、H、W上求均值和方差
nn.LayerNorm() API使用方法(可以用于CV中的transformer、CNN、NLP)
LayerNorm — PyTorch 2.2 documentation
1)NLP示例:
import torch
import torch.nn as nn
from timm.models.vision_transformer import PatchEmbed
# NLP Example
batch, sentence_length, embedding_dim = 16, 5, 256
embedding = torch.randn(batch, sentence_length, embedding_dim)
layer_norm = nn.LayerNorm(embedding_dim)
# Activate module
layer_norm(embedding)
2)CV 示例(transformer):
import torch
import torch.nn as nn
from timm.models.vision_transformer import PatchEmbed
# Image Example
N, C, H, W = 16, 3, 32, 32
D = 256
input = torch.randn(N, C, H, W)
patchEmbed = PatchEmbed(img_size=H, patch_size=4, in_chans=C, embed_dim=D) # (16,64,256)
patch_input = patchEmbed(input)
layer_norm = nn.LayerNorm(D)
output = layer_norm(patch_input)
print(output.size())
2)CV 示例(卷积):
import torch
import torch.nn as nn
N, C, H, W = 16, 3, 32, 32
input = torch.randn(N, C, H, W)
layer_norm = nn.LayerNorm([C, H, W])
output = layer_norm(input)
print(output.size())
3、IN
在H、W上求均值和方差
单个样本在单个通道(一个样本的一个维度)
所以应该是在所有单词上求均值和方差
inputx = torch.randn(batch_size, time_steps, embedding_dim) # N * L * C
## 1、实现 IN 并验证API
instance_norm_op = torch.nn.InstanceNorm1d(embedding_dim, affine=False)
in_y = instance_norm_op(inputx.transpose(-1,-2)).transpose(-1,-2)
print(in_y)
## 2、手动实现IN
# 对所有的单词进行归一化,所以在计算均值和方差时,需要在dim=2上进行计算,这样默认dim0、dim1就是1维
in_mean = inputx.mean(dim=1, keepdim=True)
in_var = inputx.var(dim=(1), unbiased = False, keepdim=True)
in_y = (inputx - in_mean) / torch.sqrt(in_var + 1e-5)
print(in_y)
InstanceNorm1d() API使用方法(适用于CV中的transformer)
InstanceNorm1d — PyTorch 2.4 documentation
对于cv 中的transformer进行IN操作
由于transformer使图像变为了:C就是D,H和W变为了T。所以可以对transformer的输出也进行IN
import torch
import torch.nn as nn
from timm.models.vision_transformer import PatchEmbed
N, C, H, W = 16, 3, 256, 256
D = 512
input = torch.randn(N, C, H, W)
patchEmbed = PatchEmbed(img_size=H, patch_size=4, in_chans=C, embed_dim=D)
patch_input = patchEmbed(input) # (N, T, D)
patch_input = patch_input.permute(0, 2, 1) # (N, D, T)
layer_norm = nn.InstanceNorm1d(D)
output = layer_norm(patch_input)
print(output.size())
InstanceNorm2d() API使用方法(适用于CV中的CNN)
InstanceNorm2d — PyTorch 2.4 documentation
示例代码:
import torch
from torch import nn
# 初始化IatchNorm2d()参数为通道数/Dim
m = nn.InstanceNorm2d(512)
input = torch.randn(16, 3, 256, 256)
# 使用(N, C, H, W) 或者 (C, H, W)进行计算
output = m(input)
print(output.size())
4、GN
在C'、H、W上求均值和方差
每组(单个样本 g 个通道上)
每一group所有单词所有维维度上求均值和方差
GroupNorm — PyTorch 2.4 documentation
## 1、实现GN
num_groups = 2
group_norm_op = torch.nn.GroupNorm(num_groups=num_groups, num_channels=embedding_dim)
gn_y = group_norm_op(inputx.transpose(-1,-2)).transpose(-1,-2)
print(gn_y)
先split为N个组,就相当于将原来的块分成了N个块。然后对每个group的所有单词所有维度求均值和方差
## 2、手动实现GN
group_inputx = torch.split(inputx, embedding_dim//num_groups, dim=-1)
results = []
for g_inputx in group_inputx:
gn_mean = g_inputx.mean(dim=(1,2), keepdim=True)
gn_var = g_inputx.var(dim=(1,2), unbiased = False, keepdim=True)
gn_y = (g_inputx - gn_mean) / torch.sqrt(gn_var + 1e-5)
results.append(gn_y)
gn_y = torch.cat(results, dim=-1)
print(gn_y)
四、讨论
1、为什么在NLP中不使用BN而是用LN
BN的劣势以及为什么不使用BN
BN的前提是对一个批次的某个维度进行归一化。如果对于“小红、小绿、小紫、...”来说,第一维度是身高,第二维度是体重...你这样对某个维度进行BN是没有问题的,但是对于NLP来说,并不会这样的情况。
这是因为你要注意3个问题:
0)第一个维度是不对应的,例如“我爱你”、“这是一只狗”,我和这是无法对应的
1)BN对于小的batch情况下效果不行,是因为你只用3个同学的均值和方差来表示班级的所有同学的均值和方差是不对的
2)NLP中句子的长度是变化的,所以对于下面的例子,你使用长度为20的句子的6-20的单词的均值和方差来表示长度为5的句子的均值和方差是不对的,就回到了第一个问题上。具体解答看:
5.Batch Normal详解_哔哩哔哩_bilibili
LN的优势为什么使用LN?
LN是对一个句子的所有单词来求均值和方差
这就很正确了,你直接对整个句子来求,是没有问题的
45、五种归一化的原理与PyTorch逐行手写实现讲解(BatchNorm/LayerNorm/InsNorm/GroupNorm/WeightNorm)_哔哩哔哩_bilibili