Norm的作用
- 数据的Norm:数据的Norm主要是解决数据之间的量纲不一致问题,比如预测人体身材是否够好时,输入人体身高和重量之间往往存在着量纲的显著差距,因此我们希望通过标准化的操作将二者转换到相同区间中,避免其中某个特征对模型影响过大。如果某个特征的值明显大于另一个特征,这会导致模型输出的结果对该特征变化变的十分敏感。
BatchNorm
ICS 内部协变量偏移
在深度神经网络中,如果 L L L层网络的权重发生了更新,相应的 L L L层的输出分布发生了变化,相应的 L + 1 L+1 L+1层就需要在变化后的 L L L层输出学习一个新的权重,但是经过一轮迭代后, L L L层权重又发生了更新,这时候就需要 L L L层输出又会发生新的变化,但是这样会导致 L + 1 L+1 L+1层又需要去实验新的变化。就相当于每次都需要在新的数据分布学习一组新的权重。
问题一:容易陷入激活层的梯度饱和区,降低模型收敛速度
像sigmod和tanh在输入值比较大的时候,会很容易进入梯度接近0的区域(即饱和区域),随着网络层数的增多,容易产生梯度消失的问题。而前面提到的内部协变量偏移会导致网络中每一层输入的分布产生频繁的变化,这也就导致激活层的输入进入饱和区的概率变高。
**可以考虑使用Relu这类非饱和的激活函数**
问题二:需要降低学习速率,会降低模型收敛速度
因为网络中每一层的输入分布会频繁变化,因此后面的网络层需要更加频繁地变化去适应前面层网络的输出,因此这个时候往往需要使用较小地学习率来保持训练过程的稳定。
BatchNorm的思路
-
首先对每个batch的数据进行标准化,将一个batch中所有数据的每一个特征的分布都控制在0,1的标准分布中
-
但是如果只是单纯地将每个特征的分布控制在0-1分布类,可能会降低网络的学习能力,因为 L L L学习到东西可能就是将数据从一个难以处理的分布转化到一个便于处理分布,而这个时候将其强行变化到0-1分布上,可能就会导致 L L L层对模型不再起作用,导致模型能力的下降。为了解决这个问题,一般需要在Norm后再进行相应的线性变化(变化参数为 β \beta β γ \gamma γ,都是可学习参数)以保留原本 L L L层对数据的作用能力。
在执行层面,其是对batch中每个特征的分布进行控制,对于图像数据 B C H W BCHW BCHW,其有 C C C特征,因此需要计算得到 1 C 11 1C11 1C11的均值和方差,来控制不同样本在每一个特征上的分布,注意这里BatchNorm会保留每个样本之间的相对关系但是每个特征之间相对大小关系就不存在了(类似解决一个人体重和身高之间的不匹配),换句话BatchNorm更加关注不同样本之间的关系,对于样本内部的不同特征关系时忽略的。
为啥transformer中不使用BatchNorm
对于文本类的数据,其形状大小一般是
B
S
E
BSE
BSE,
S
S
S表示每个句子的词数目,
E
E
E表示每个词的的embedding维度。理论上讲
S
S
S应该对应着
H
W
HW
HW,然后
E
E
E对应着
C
C
C,这样就可以很好地将BatchNorm应用到文本类数据了。但是从网络上的信息来看,这里可以将句子里的每一个词都看成一个特征,这样
S
S
S应该对应着
C
C
C,有
S
S
S个词就代表着有
S
S
S个特征,而每个词向量的长度则视为
E
E
E则表示
H
W
HW
HW,即每个特征有
E
E
E个组成(类似于一个特征图由
H
W
HW
HW像素组成)。这里因为一个Batch中句子长度往往不一致,因此会导致
S
S
S长度不固定,因此这也为在NLP中引入BatchNorm带来了挑战。(这里可能是因为BatchNorm一开始应用在图像数据
B
C
H
W
BCHW
BCHW上,而文本数据是
B
S
E
BSE
BSE,所以一开始在代码实现就把
S
S
S当成
C
C
C来看了)
**此时可以考虑将每个句子填充至相同长度来解决 S S S长度不固定的问题,这样可以顺利得到形状为 1 S 1 1S1 1S1的均值和方差,但是这里我们可以看到这里相当于对个每个句子相同位置上的词向量进行标准化,这对于文本来说是没有意义的,文本更多是关注于一个句子不同词之间的相互关联,通过这样标准化反而会破坏一个句子内不同词向量之间的相对关系。同时通过填充来保证 S S S的长度,会导致一些位置靠后 S i S_i Si的计算并不准确,因为这些数据集能够达到这个长度数据量可能非常少,这样就会导致这些位置上的方差和特征并不准确。
BatchNorm很大程度上会受到Batchsize的影响,而LayerNorm则不存在着这样的问题,前者在分布式训练时可能会遇到一些机器只能使用较小的Batchsize,这样这些机器上得到BatchNorm就不再准确,此时就必须要进行Batch Sync,但是这样会增加对通信的需求。
BatchNorm优点
- BatchNorm解决内部协方差偏移的问题,使得每一层的输入更加稳定,这使得可以使用更大的学习率学习,能够加速模型的收敛
- BatchNorm起到了一定正则化的作用,通过将每个特征分布标准化后,可以避免模型对某一些特征过于敏感。
- BatchNorm可以避免在使用sigmoid等饱和激活函数时陷入饱和区域
- 后续研究说明ICS,内部协方差偏移并不会对模型的性能产生明显的负面影响,更多是能够平滑整个优化平面,便于模型收敛
BatchNorm计算
训练过程中BatchNorm
BatchNorm计算公式如下,注意这里
E
[
x
]
E[x]
E[x]和
V
a
r
(
x
)
Var(x)
Var(x)都是对
B
,
H
,
W
B,H,W
B,H,W进行求解的,所以相应的结果形状为
C
C
C维的向量。
γ
\gamma
γ和
β
\beta
β也是相应的
C
C
C维可学习的向量用于保留原本网络层的表达能力。
y
=
x
−
E
[
x
]
V
a
r
(
x
)
+
ϵ
∗
γ
+
β
y=\frac{x-E[x]}{\sqrt{Var(x)+\epsilon}} * \gamma + \beta
y=Var(x)+ϵx−E[x]∗γ+β
测试过程的BatchNorm
训练可以固定每个Batch的大小,然后求解相应的均值和方差来做BatchNorm,但是在测试时,Batch大小是不固定的,因为往往无法得到准确的均值和方差(反应真实数据分布的)。因此在测试时通常会使用训练阶段的均值和方差来作为推理阶段的均值和方差,也就是说推理阶段不会再去求输入数据的均值和方差了而是直接使用训练中得到均值和方差了。
在训练阶段每个Batch都会得到一个均值和方差,那推理阶段该训练该如何使用这些均值和方差了?一般有两种方法:
用训练集得到均值和方差做测试集均值和方差的无偏估计
具体来说,在训练过程中,我们会记录每个batch的均值和方差,然后利用这些方差和均值求取平均(无偏估计)来推断出测试集的均值和方差,这里
σ
b
a
t
c
h
2
\sigma^{2}_{batch}
σbatch2和
μ
b
a
t
c
h
\mu_{batch}
μbatch表示训练过程中记录的每个batch的均值的方差。这样做存在着两个缺点:1.需要记录每个batch的的均值和方差,这样会带来额外的开销;2.模型训练前期得到每个层的输出是不正确不稳定的,相应的均值和方差也可以与模型收敛时的均值和方差有比较大的差距,因此这些早期均值和方差可以会对估计出的均值和方差产生负面的影响。
μ
t
e
s
t
=
E
(
μ
b
a
t
c
h
)
\mu_{test}=E(\mu_{batch})
μtest=E(μbatch)
σ
t
e
s
t
2
=
B
B
−
1
E
(
σ
t
e
s
t
2
)
\sigma^{2}_{test}=\frac{B}{B-1}E(\sigma^2_{test})
σtest2=B−1BE(σtest2)
对于上述两个问题都可以通过移动加权平均来解决。首先我们不再记录每个batch的均值和方差,而是只记录到当前这个batch时,前面batch的均值和方差的均值以及经过的batch数,就可以计算出加入当前batch的后新的均值和方差,这样我们只需要记录额外记录一个均值的平均和方差的平均即可。然后进一步地,我们不在考虑所有batch的平均,而是给那些离当前batch近的batch的均值和方差以更大权重,这样在我们最终的结果中,模型训练后期得到batch的均值和方差会起到更大的作用,这样便可以估计出更加准确地方差和均值。
移动加权平均
μ
t
e
s
t
=
(
1
−
p
)
μ
t
e
s
t
+
p
μ
t
\mu_{test}=(1-p)\mu_{test}+p\mu_{t}
μtest=(1−p)μtest+pμt
σ
t
e
s
t
2
=
(
1
−
p
)
σ
t
e
s
t
2
+
p
σ
t
2
\sigma^{2}_{test}=(1-p)\sigma^{2}_{test}+p\sigma^2_{t}
σtest2=(1−p)σtest2+pσt2
除了上述好处外,一般来讲,在模型训练的中途,我们会塞入validation dataset,对模型训练的效果进行追踪。采用移动平均法,不需要等模型训练过程结束再去做无偏估计,我们直接用running mean和running variance就可以在validation上评估模型。(知乎猛猿)
特别注意这里的移动加权平均,计算方式和优化器的计算方式是反过来的,所有取值也要反过来,一般我在优化器可能是0.9或者0.99,但是在pytorch里一般BatchNorm默认取值是0.1
值得注意的在pytorch官方实现中BatchNorm中标准化的var和用于记录移动加权平均的方差var的计算方式是存在区别的,后者就torch.var()默认计算方式,前者为有偏估计。
At train time in the forward pass, the standard-deviation is calculated via the biased estimator, equivalent to torch.var(input, unbiased=False) 新版本变成了correction=0
. However, the value stored in the moving average of the standard-deviation is calculated via the unbiased estimator, equivalent to torch.var(input, unbiased=True)新版本变成了correction=1
.
代码实现
注意以下几点:
- running_var初始值是1,而不是0
- 训练过程中var是计算的有偏,即unbiased=False或者correction=0
- 统计过程中使用var是计算的无偏的,即unbiased=True或者correction=1
class BatchNorm(torch.nn.Module):
def __init__(self,channels,momtenum=0.1,eps=1e-5):
super().__init__()
#可学习参数
self.gamma=torch.ones((1,channels,1,1),requires_grad=True)
self.beta=torch.zeros((1,channels,1,1),requires_grad=True)
self.running_mean=torch.zeros((1,channels,1,1))
#特别注意这里的方差初始值为1
self.running_var=torch.ones((1,channels,1,1))
self.eps=eps
self.momtenum=momtenum
def forward(self,x):
#计算训练时的均值方差
mean=torch.mean(x,dim=(0,2,3),keepdim=True)
var=torch.var(x,dim=(0,2,3),correction=0,keepdim=True) #有偏
x_batchnorm=self.gamma*((x-mean)/torch.sqrt(var+self.eps))+self.beta
#统计移动加权平均用测试tu
self.running_mean=(1-self.momtenum)*self.running_mean+self.momtenum*mean
self.running_var=(1-self.momtenum) *self.running_var +self.momtenum*torch.var(x,dim=(0,2,3),keepdim=True,correction=1) #无偏
return x_batchnorm ,self.running_mean ,self.running_var
LayerNorm
正如我前面讨论的,由于在NLP任务中句子长度的不固定,因此并不适合使用BatchNorm,因此LayrNorm被提出。
LayerNorm的思路
与BatchNorm不一样的,LayerNorm不再在特征层面来进行标准化,而是直接对每条数据本身来进行标准化。
在执行层面:
-
对NLIP任务而言,理论上一条句子应该是一个数据,但是实际上计算时,其并不是在对每一个句子 S S S进行标准化,而是对每一个Token E E E来进行标准化,即对于 B S E BSE BSE的数据,计算出的均值方差大小为 B S 1 BS1 BS1
-
对于CV任务而言,同样理论上在CV中每条数据的大小应是 C H W CHW CHW,所以应该得到均值和方差大小是 B 111 B111 B111,但是实际上VIT等任务中,在使用LayrNorm都是对每个像素来计算的,即方差和均值大小是 B 1 H W B1HW B1HW,这应该是因为在使用LayerNorm都是讲图像序列化为 B ( H W ) C B(HW)C B(HW)C,所有类似于NLP中结果为 B ( H W ) 1 B(HW)1 B(HW)1
-
与BatchNorm一样,都需要增加可学习参数 γ \gamma γ和 β \beta β来保持相应模型原本的表达能力,后面的这些层基本都保持这样的设计,不过需要注意的是LayerNorm在做线性映射时是对每一个元素做的线性映射,如果像NLP这种对每个词做标准化,那么就按照一个词里元素数目来构造相应的映射参数, γ \gamma γ和 β \beta β的形状都是 11 E 11E 11E,那在CV中可能是需要对整张图片做标准化,即 H W C HWC HWC,那相应的相应的映射参数大小也为 H W C HWC HWC,因此这里layernorm的映射参数形状是对每个元素做,而BatchNorm和InstanceNorm都是只对通道维做,同一个通道的元素共享映射参数。
LayerNorm实现
由于LayerNorm是对单个数据进行标准化,因此不再受到Batch的影响,并且也不存在用训练集来估计测试集分布的问题,因此训练和测试是保持一致的,不再需要通过记录训练集的标准值和方差,标准化的计算过程也与之间BatchNorm基本一致,只在求取的通道维度有区别。不过由于在测试还需要计算每个样本的均值,因此在推理时layernorm的耗费要大于batchnorm
代码实现
class LayerNorm(torch.nn.Module):
def __init__(self,normal_shape,eps=1e-05):
super().__init__()
if isintance(normal_shape,int):
normal_shape=(normal_shape,)
#这里映射参数是针对每个被标准化的元素,而不像batchnorm和instancenorm一样针对channel
self.weight=torch.ones(normal_shape,requires_grad=True)
self.bias=torch.zeros(normal_shape,requires_grad=True)
self.eps=eps
self.d_len=len(normal_shape)
def forward(self,x):
#这里需要注意不同形状的均值和方差要求解的维度数是不一样的
x_mean=torch.mean(x,dim=[i-self.d_len for i in range(self.d_len)],keepdim=True)
x_var=torch.var(x,dim=[i-self.d_len for i in range(self.d_len)],keepdim=True)
x_normal=self.weight*((x-x_mean)/torch.sqrt(x_var+self.eps))+self.bias
return x_normal
InstanceNorm
InstanceNorm思路
InstanceNorm的初始思路主要是用来解决风格迁移中的问题。生成模型中,特征图中每个的channel的分布(方差和均值)会影响到最终生成图像风格,为了实现对图像风格的迁移,我们首先要消除掉原有图像的风格,因此我们会将原有特征图的每个channel归一化,这样每个channel都是0-1分布,原有的风格信息就消失了;然后我们计算出目标图像特征图的每个channel的均值和方差,然利用这个目标均值和方差来对归一化的原始特征图进行去归一化,这样原有特征图每个channel的分布就从源图像装换到目标图像的分布了,进而实现图像风格转换。
在执行层面:从前面的讨论可以看到,InstanceNorm需要对每条数据的每一个channel进行归一化,因此对于 B C H W BCHW BCHW,需要计算 B C 11 BC11 BC11的均值和方差,这样就可以对每一条数据 B B B和每一个通道 C C C进行归一化。不过这样只实现对通道的归一化,相当于只抹除原有的风格信息。因此还需要将其信息映射到目标风格。因此类似于BatchNorm和LayerNorm,其同样设置了两个可学习映射参数 γ \gamma γ和 β \beta β,通过这两个参数来将归一化后的通道特征映射目标特征分布。因此这里 γ \gamma γ和 β \beta β的形状大小为 1 C 11 1C11 1C11。这样模型就可以从目标图像数据学习到目标风格的均值和方差,来作为 γ \gamma γ和 β \beta β。
InstanceNorm代码实现
InstanceNorm因为在求取均值和方差时是对每一个数据的每一个通道进行求解均值和方差。因此和LayerNorm不受到Batch影响。InstanceNorm基本只用在CV任务中,因为毕竟是做图像风格迁移任务。
class InstanceNorm2d(torch.nn.Module):
def __init__(self,num_feature,eps=1e-05):
super().__init__()
self.weight=torch.ones((1,num_feature,1,1),requires_grad=True)
self.bias=torch.zeros((1,num_feature,1,1),requires_grad=True)
self.eps=eps
def forward(self,x):
#这里只对H,W求均值方差,要记录的B,C
x_mean=torch.mean(x,dim=(2,3),keepdim=True)
x_var=torch.var(x,dim=(2,3),keepdim=True,correction=0) #有偏求方差
x_normal=self.weight*((x-x_mean)/torch.sqrt(x_var+self.eps))+self.bias
return x_normal
需要注意的是:在pytorch的实现中还包括类似的动量设置,但是默认是不打开的。同时一般仿射变换参数这个也是默认不打开的,这一点还是有点出乎意料的。
InstanceNorm2d
and LayerNorm
are very similar, but have some subtle differences. InstanceNorm2d
is applied on each channel of channeled data like RGB images, but LayerNorm
is usually applied on entire sample and often in NLP tasks. Additionally, LayerNorm
applies elementwise affine transform, while InstanceNorm2d
usually don’t apply affine transform.
LayerNorm在很多层面都于InstanceNorm很接近,特别是在处理NLP任务时,对于 B S E BSE BSE的数据时,为了实现句子的风格迁移,需要将词数目 S S S视为通道数目(毕竟句子的风格是由词决定的),因此需要计算的均值和方差大小为 B S 1 BS1 BS1,相应的映射参数的形状为 1 S 1 1S1 1S1。LayerNorm需要对每个词向量进行标准化,因此最终需要计算的均值和方差也是 B S 1 BS1 BS1,但是相应的映射参数大小是 11 E 11E 11E。LayerNorm的映射时对每个标准化的元素而言的,而是InstanceNorm和BatchNorm则都是对通道来映射的。
GroupNorm
之前前面也说过BatchNorm在文本上面的劣势,但是BatchNorm在图像处理上也是存在不少的缺陷的,首先BatchNorm的效果非常收到BatchSize大小的影响,这对一些显存较小的显卡来说十分不友好,其次在测试时需要使用训练统计出的均值和方差来进行归一化,但测试集和训练集的分布差别比较大的时候,就会存在着gap,影响最终训练的效果。
GroupNorm
GroupNorm的核心思路也是绕开BatchNorm过度受限于Batch的限制,其基本只用在图像领域,可以视为InstanceNorm和LayerNorm的一个折中。LayerNorm在图中可以看成对一整个特征图进行归一化,而InstanceNorm则可以看成对特征图中每一个通道分别进行归一化;而GroupNorm则可以看成是二者的一个折中,具体来说GroupNorm将一张特征图沿通道维划分成若干组,然后分别每个组进行归一化。 这里可以理解成,在一幅特征图,我们将描述特征相近的通道划分到一组去,不相近的划分到不同组中,然后每个组的组内特征时接近的,组间特征差异是比较大,这个时候我们对每个组进行归一化就会合理很多,也可以认为组内特征分布式比较接近的,所以可以一组一组的归一化.
执行层面:
GroupNorm的实现和LayerNorm以及InstanceNorm直接其实没有较大区别,主要就是在求解均值和方差的维度上,因为GroupNorm是按组获得均值和方差,所以对于一个
B
C
H
W
BCHW
BCHW的输入而言,如果我们这里将分为
G
G
G组,则每组通道为
C
/
G
C/G
C/G,相应特征图可以视为
B
G
(
C
G
)
H
W
BG(\frac{C}{G})HW
BG(GC)HW,这个时候对每组进行归一的结果得到的均值和方差的大小为
B
G
BG
BG。
同样,GroupNorm也有一组可学习的映射参数 γ \gamma γ和 β \beta β用于调整分布,这里形状也是根据通道维来的,即 C C C,每一个通道有组映射参数,不过在我很好奇地为啥形状不是 G G G,因为既然归一化是按照组来,按组分配映射参数不更好吗?
GroupNorm的代码实现
class GroupNorm(torch.nn.Module):
def __init__(self,num_group,num_feature,eps=1e-05):
super().__init__()
assert num_feature//num_group,'num_feature 必须能够被 num_group整除'
self.weight=torch.ones((1,num_feature,1,1),requires_grad=True)
self.bias=torch.zeros((1,num_feature,1,1),requires_grad=True)
self.eps=eps
self.num_group=num_group
def forward(self,x):
shape=x.shape
x_=x.view(shape[0],self.num_group,shape[1]//self.num_group,shape[2],shape[3])
x_mean=torch.mean(x_,dim=(2,3,4),keepdim=True)
#不同版本torch的无偏估计的参数不一样(unbiased,correction)
x_var=torch.var(x_,dim=(2,3,4),unbiased=False,keepdim=True)
x_=(x_-x_mean)/torch.sqrt(x_var+self.eps).view(shape)
x_normal=self.weight*x_+self.bias
return x_normal
AdaIN
上面我们也说过InstanceNorm核心就是将特征从一个分布转换到另一个分布,但是InstanceNorm存在的一个缺陷就是InstanceNorm的目标分布
γ
\gamma
γ和
β
\beta
β在模型训练完成就固定下来了,只能进行固定风格的图像进行转换。那如何实现任意风格图像的转换,这就需要我们的
γ
\gamma
γ和
β
\beta
β能够根据我们想要映射的风格发生动态的变换,这样用户的要求(输入)的不同,最终生成图像的风格也就不同。一般在具体实现就是使用可学习的网络(简单的就是几层全连接层)从目标风格的图像或者其他约束中计算出相应的
γ
\gamma
γ和
β
\beta
β,这个时候映射参数就是网络的输入和输出了。
类似还有AdaLN和AdaGN,核心都是在于将特定的条件信息转换成
γ
\gamma
γ和
β
\beta
β来修改特征分布,实现对特征的控制。
引用
为什么Transformer要用LayerNorm? - 猛猿的回答 - 知乎
https://www.zhihu.com/question/487766088/answer/2309239401