LayerNorm层
作用
BatchNorm计算的是一批样本,样本间同一个通道进行正则化
LayerNorm计算的是一个样本内,一个样本内进行正则化。
实现代码
class LayerNorm(nn.Module):
def __init__(self,features,eps=1e-6):
super(LayerNorm,self).__init__()
self.a_2 = nn.Parameter(torch.ones(features))#可学习的参数
self.b_2 = nn.Parameter(torch.zeros(features))#可以学习的参数
self.eps =eps
def forward(self,x):
#BatchNorm计算的是一批样本,相同位置之间的均值和方差。
#LayerNorm计算的是一个样本内,所有单词的均值和方差。
mean = x.mean(-1,keepdim=True) #计算最后一维度的均值 3x100
std = x.std(-1,keepdim=True) #计算最后一个维度的方差
return self.a_2 * (x-mean) / (std+self.eps) + self.b_2 #计算每个样本x内正则化
位置编码
作用
弥补自注意力机制的短板,区分不同位置字符的绝对位置。
自注意力机制没有位置信息,比如“我爱你”和“你爱我”,每个词组合后注意力得分是相同的,所以需要在每个字符的嵌入上加上位置编码。
公式
pos为token的位置,dim为token嵌入后的维度,i为单个token的嵌入后的维度中第i个向量。
结构
每个token嵌入后的维度为dim,每个token就对应一个dim维的位置编码。
代码实现
class PositionalEncoding(nn.Module): #整理特征,不改变特征维度
def __init__(self,max_len,dim,dropout):
#max_len:位置编码的总个数,位置编码与输入的特征无关,可以预先生成指定个数,与特征相加的时候可以切特取部分。
#dim:每个词的维度
super(PositionalEncoding,self).__init__()
self.dropout = nn.Dropout(p=dropout)
pe = torch.zeros(max_len,dim) #生成max_len x dim 维度的矩阵
position = torch.arange(0,max_len).unsqueeze(1) #
div_term = torch.exp( torch.arange(0,dim,2) * -(math.log(10000.0)/dim)).unsqueeze(0)
pe[:,0::2]=torch.sin(position*div_term)
pe[:,1::2]=torch.cos(position*div_term)
pe = pe.unsqueeze(0)
self.register_buffer('pe',pe) #保证pe在训练的时候不会更新。
def foward(self,x):
out = x + self.pe[:,:x.size(1)] #将与x对应位置的位置编码加起来。
out = self.dropout(out)
return out
可以知道位置编码和特征x无关,且分子和分母公共的部分,可以用切片传播方式实现。
可以将相除改成相乘。
其中 -ln10000 / dim这是一个固定的数值。
pe用torch.zeros生成 ( max_len , dim ) 维度的0占位符。
position用torch.arrange生成0到max_len,形状为 ( maxlen,) 数组,表示生成0到max_len个字符的位置编码,为[0,1,2,3,...,max_len],再改变形状为 ( maxlen , 1 )。
div_ter先生成 [0,2,...,dim],形状为(dim/2,)占位符,一共dim/2个,因为2i和2i+1位置是相同,只不过是一个用sin和cos。然后再乘以一个固定的数值 -ln10000 / dim,再取exp,得到如下:
e^{ [0,2,...,dim] * -ln10000/dim },形状为(1,dim/2)
position的形状为 ( maxlen,1 )
position 和 div_term相乘后就是( maxlen,dim/2 )
取 sin( position * div_term) 赋值给pe第二维度的偶数下标。
取cos(position * div_term) 赋值给pe第二维度的奇数下标。
最后将pe第0维度扩充,为了对应句子的批次。