开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第5天,点击查看活动详情
关于transformer中的attention部分中的矩阵运算,在上一篇文章 Transformer原理解读篇<一> - 掘金 (juejin.cn) 浅浅解析了一番。
本文就Transformer剩下的部分做个了解。
参考资料
Transformer【原文】 :Attention is all you need
李宏毅 【seq2seq】 :seq2seq.pdf
李宏毅 【Youtube】 Self-Attention :YouToBe-Transformer
知乎图解 【Transformer】 :知乎图解Transformer
哈佛团队注解版 Transformer 【Code】 github1s.com/harvardnlp/…
背景
这里再补充一下Transformer出现的背景。
The goal of reducing sequential computation also forms the foundation of the Extended Neural GPU, ByteNet and ConvS2S, all of which use convolutional neural networks as basic building block, computing hidden representations in parallel for all input and output positions.
减少顺序计算的目标也构成了扩展神经GPU、ByteNet和ConvS2S的基础,所有这些都使用卷积神经网络作为基本构建块,并行计算所有输入和输出位置的隐藏表示。
In these models, the number of operations required to relate signals from two arbitrary input or output positions grows in the distance between positions, linearly for ConvS2S and logarithmically for ByteNet. This makes it more difficult to learn dependencies between distant positions.
在这些模型中,将来自两个任意输入或输出位置的信号关联起来所需的操作数量随着位置之间的距离而增长,ConvS2S为线性增长,ByteNet为对数增长。这使得学习远程位置之间的依赖关系变得更加困难。
In the Transformer this is reduced to a constant number of operations, albeit at the cost of reduced effective resolution due to averaging attention-weighted positions, an effect we counteract with Multi-Head Attention.
在 Transformer 中,这个操作数已经减少到指定的数量,尽管代价是由于注意力加权位置的平均而降低了有效分辨率,我们用多头注意力抵消了这种影响
End-to-end memory networks are based on a recurrent attention mechanism instead of sequencealigned recurrence and have been shown to perform well on simple-language question answering and language modeling tasks.
端到端记忆网络基于循环注意力机制,而不是序列对齐的循环,并已被证明在简单语言问答和语言建模任务中表现良好。
To the best of our knowledge, however, the Transformer is the first transduction model relying entirely on self-attention to compute representations of its input and output without using sequence aligned RNNs or convolution.
Transformer 是第一个完全依靠自我关注来计算其输入和输出表示的转导模型,而无需使用序列对齐的RNN或卷积。
仍然回到Transformer架构总图:
Most competitive neural sequence transduction models have an encoder-decoder structure (cite). Here, the encoder maps an input sequence of symbol representations to a sequence of continuous representations . Given , the decoder then generates an output sequence of symbols one element at a time. At each step the model is auto-regressive (cite), consuming the previously generated symbols as additional input when generating the next.
大多数竞争性神经序列转导模型都具有编码器-解码器结构。编码器: 将输入序列映射到连续的序列。 基于给定的,编码器会生成一个输出序列。一个元素一个元素的生成。在每个步骤中,模型都是自回归的,在生成下一个输出符号时需要消耗先前生成的符号作为附加输入。
标准的Encoder-Decoder架构代码如下:
class EncoderDecoder(nn.Module):
"""
A standard Encoder-Decoder architecture. Base for this and many
other models.
"""
def __init__(self, encoder, decoder, src_embed, tgt_embed, generator):
super(EncoderDecoder, self).__init__()
self.encoder = encoder
self.decoder = decoder
self.src_embed = src_embed
self.tgt_embed = tgt_embed
self.generator = generator
def forward(self, src, tgt, src_mask, tgt_mask):
"Take in and process masked src and target sequences."
return self.decode(self.encode(src, src_mask), src_mask, tgt, tgt_mask)
def encode(self, src, src_mask):
return self.encoder(self.src_embed(src), src_mask)
def decode(self, memory, src_mask, tgt, tgt_mask):
return self.decoder(self.tgt_embed(tgt), memory, src_mask, tgt_mask)
Transformer也遵循这种Encoder-Decoder整体架构。
The Transformer follows this overall architecture using stacked self-attention and point-wise, fully connected layers for both the encoder and decoder。
Transformer 遵循这种整体架构,编码器和解码器均使用堆叠的self-attention和point-wise、全连接层。
这里的point-wise,我猜是逐点相乘的意思,即前文讲解的点积运算。如果有更科学的解释,请留言嗷🫡
Encoder
Add & Norm
Encoder中的Add & Norm
表示的是:a (Input)+ b (Output) = b' ,再将b'进行LayerNorm。
关于残差连接,最熟悉的网络结构莫过于残差网络 ResNet了吧。
长下面这样:
在forword函数中一般:
def forward(self, x):
identity = x
out = self.layer1(x)
...
out += identity # 残差连接
out = self.relu(out)
Transformer中的Add操作也是如此,在使用Multi-Head Attention之前先保存输入值,然后经过多头之后在与保存值相加,在使用LayerNorm进行层归一化。
这里拓展一下LayerNorm和BatchNorm的区别:
BN & LN
参考链接:
1.zhuanlan.zhihu.com/p/441573901 Batch Norm详解之原理及为什么神经网络需要它
2.zhuanlan.zhihu.com/p/521535855 超细节的 BatchNorm/BN/LayerNorm/LN 知识点
3.blog.csdn.net/qq_37541097… 🎯强推!Batch Normalization详解以及pytorch实验
?定义
BatchNorm: 对batch个样本的每一个特征维度进行归一化
BN的目的是使我们的一批
feature map
满足均值为0,方差为1的分布规律。
LayerNorm: 对于每一个样本的的所有特征进行归一化,(可以理解为将二维矩阵进行转置之后进行BatchNorm,在转置回来)
LN 一般搭配RNN来使用 (transformer中用的归一化也是LN)
Mean
均值:
Std Dev
标准差:
Normalize
Batch Normalize
( 缩放参数, 平移参数)
在正向传播中统计得到
在反向传播过程中训练得到, 默认值 1, 默认值为 0
是为了防止分母为0的情况出现
?区别
batch 和 layer归一化的区别:
batchNorm是对一个batch中的所有的样本的同一个维度进行归一化;LayerNorm是对一个样本中所有特征维度进行归一化。
说简单点,其实深度学习里的正则化方法就是"通过把一部分不重要的复杂信息损失掉,以此来降低拟合难度以及过拟合的风险,从而加速了模型的收敛"。Normalization目的就是让分布稳定下来(降低各维度数据的方差)。
?BN和LN 有哪些差异
1)两者做Norm的维度不一样,BN是在Batch维,而LN一般是在最后一维
2)BN需要在训练过程中,滑动平均累积每个神经元的均值和方差,并保存在模型文件中用于推理过程,而LN不需要
3)因为Norm维度的差异,使得它们适用的领域也有差异,BN更多用于CV领域,LN更多用于NLP领域
?为什么Transformer/BERT使用LN,而不使用BN
- layer normalization 有助于得到一个球体空间中符合0均值1方差高斯分布的 embedding, batch normalization不具备这个功能。
- layer normalization可以对transformer学习过程中由于多词条embedding累加可能带来的“尺度”问题施加约束,相当于对表达每个词一词多义的空间施加了约束,有效降低模型方差。batch normalization也不具备这个功能。
LN代码实现
class LayerNorm(nn.Module):
"Construct a layernorm module (See citation for details)."
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):
mean = x.mean(-1, keepdim=True)
std = x.std(-1, keepdim=True)
return self.a_2 * (x - mean) / (std + self.eps) + self.b_2
BN代码实现
直接使用torch的内置函数:
import torch.nn as nn
import torch
bn = nn.BatchNorm2d(2, eps=1e-5)
# 随机生成一个batch为2,channel为2,height=width=2的特征向量
# [batch, channel, height, width]
feature1 = torch.randn(2, 2, 2, 2)
output = bn(feature1)
print(output)
自定义实现:
import numpy as np
import torch.nn as nn
import torch
def bn_process(feature, mean, var):
feature_shape = feature.shape
for i in range(feature_shape[1]):
# [batch, channel, height, width]
feature_t = feature[:, i, :, :]
mean_t = feature_t.mean()
# 总体标准差
std_t1 = feature_t.std()
# 样本标准差
std_t2 = feature_t.std(ddof=1)
# bn process
# 这里记得加上eps和pytorch保持一致
feature[:, i, :, :] = (feature[:, i, :, :] - mean_t) / np.sqrt(std_t1 ** 2 + 1e-5)
# update calculating mean and var
mean[i] = mean[i] * 0.9 + mean_t * 0.1
var[i] = var[i] * 0.9 + (std_t2 ** 2) * 0.1
print(feature)
# 随机生成一个batch为2,channel为2,height=width=2的特征向量
# [batch, channel, height, width]
feature1 = torch.randn(2, 2, 2, 2)
# 初始化统计均值和方差
calculate_mean = [0.0, 0.0]
calculate_var = [1.0, 1.0]
# print(feature1.numpy())
# 注意要使用copy()深拷贝
bn_process(feature1.numpy().copy(), calculate_mean, calculate_var)
其他归一化方法
除了LN和BN归一化,还有IN(Instance Normalization)、 GN(Group Normalization)
举个例子:
计算机视觉(CV)领域的数据xxx一般是4维形式,如果把类比为一摞书,这摞书总共有 N 本,每本有 C 页,每页有 H 行,每行 W 个字符。
计算均值时:
- BN 相当于把这些书按页码一一对应地加起来(例如:第1本书第36页,加第2本书第36页…),再除以每个页码下的字符总数:N×H×W,因此可以把 BN 看成求“平均书”的操作(注意这个“平均书”每页只有一个字)
- LN 相当于把每一本书的所有字加起来,再除以这本书的字符总数:C×H×W,即求整本书的“平均字”
- IN 相当于把一页书中所有字加起来,再除以该页的总字数:H×W,即求每页书的“平均字”
-
GN 相当于把一本 C 页的书平均分成 G 份,每份成为有 C/G 页的小册子,对这个 C/G 页的小册子,求每个小册子的“平均字”
-
Switchable Normalization 即:将 BN、LN、IN 结合,赋予权重,让网络自己去学习归一化层应该使用什么方法
相关论文:
Batch Normalization,其论文:arxiv.org/pdf/1502.03…
Layer Normalizaiton,其论文:arxiv.org/pdf/1607.06…
Instance Normalization,其论文:arxiv.org/pdf/1607.08…
Group Normalization,其论文:arxiv.org/pdf/1803.08…
Switchable Normalization,其论文:arxiv.org/pdf/1806.10…
使用BN需要注意的问题
使用BatchNorm的时候不需要使用bias(偏置
经过Add & Norm 之后 FFN就是Encoder最后一个未解之谜了。
FFN - FeedForward Network
Feed Forward 层比较简单,是一个两层的全连接层,第一层的激活函数为 Relu,第二层不使用激活函数,对应的公式如下:
X是输入,Feed Forward 最终得到的输出矩阵的维度与X一致。
在具体代码实现的时候使用Linear
层即可达到目的。
class PositionwiseFeedForward(nn.Module):
"Implements FFN equation."
# d_ff=2048 d_model=512
def __init__(self, d_model, d_ff, dropout=0.1):
super(PositionwiseFeedForward, self).__init__()
self.w_1 = nn.Linear(d_model, d_ff)
self.w_2 = nn.Linear(d_ff, d_model)
self.dropout = nn.Dropout(dropout)
def forward(self, x):
return self.w_2(self.dropout(self.w_1(x).relu()))
整个Encoder就是由多个Encoder Block堆叠而成。
以下代码仅供参考:
class EncoderLayer(nn.Module):
"Encoder is made up of self-attn and feed forward (defined below)"
def __init__(self, size, self_attn, feed_forward, dropout):
super(EncoderLayer, self).__init__()
self.self_attn = self_attn
self.feed_forward = feed_forward
self.sublayer = clones(SublayerConnection(size, dropout), 2)
self.size = size
def forward(self, x, mask):
"Follow Figure 1 (left) for connections."
x = self.sublayer[0](x, lambda x: self.self_attn(x, x, x, mask))
return self.sublayer[1](x, self.feed_forward)
# 残差连接
class SublayerConnection(nn.Module):
"""
A residual connection followed by a layer norm.
Note for code simplicity the norm is first as opposed to last.
"""
def __init__(self, size, dropout):
super(SublayerConnection, self).__init__()
self.norm = LayerNorm(size)
self.dropout = nn.Dropout(dropout)
def forward(self, x, sublayer):
"Apply residual connection to any sublayer with the same size."
return x + self.dropout(sublayer(self.norm(x)))
# LN归一化
class LayerNorm(nn.Module):
"Construct a layernorm module (See citation for details)."
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):
mean = x.mean(-1, keepdim=True)
std = x.std(-1, keepdim=True)
return self.a_2 * (x - mean) / (std + self.eps) + self.b_2
Decoder
介绍完Encoder,Decoder就很好理解了。
不同点:
- Decoder包含两个 Multi-Head Attention 层。
- 第一个 Multi-Head Attention 层采用了 Masked 操作。
- 第二个 Multi-Head Attention 层的K, V矩阵使用 Encoder 的编码信息矩阵C进行计算,而Q使用上一个 Decoder block 的输出计算。这样做的好处是在 Decoder 的时候,每一位单词都可以利用到 Encoder 所有单词的信息 (这些信息无需 Mask)。
- 最后输出有一个 Softmax 层计算下一个翻译单词的概率。
Decoder中的Attention是Masked Attention,是想说明在解码阶段,最后的输出是一个一个产生的。即先翻译完第 i 个单词,才可以翻译第 i+1 个单词,所以需要掩码来去掉后面词的影响。而在Encoder阶段,是提前知道全局的信息,所以没有mask。 具体例子可查看【Transformer模型详解(图解最完整版) - 知乎 (zhihu.com)】
Cross Attention
训练时:第i个decoder的输入 = encoder输出 + ground truth embeding
预测时:第i个decoder的输入 = encoder输出 + 第(i-1)个decoder输出
训练时因为知道ground truth embedding,相当于知道正确答案,网络可以一次训练完成。
预测时,首先输入start,输出预测的第一个单词 然后start和新单词组成新的query,再输入decoder来预测下一个单词,循环往复直至end
# 生成带掩码块的N层decoder block
class Decoder(nn.Module):
"Generic N layer decoder with masking."
def __init__(self, layer, N):
super(Decoder, self).__init__()
self.layers = clones(layer, N)
self.norm = LayerNorm(layer.size)
def forward(self, x, memory, src_mask, tgt_mask):
for layer in self.layers:
x = layer(x, memory, src_mask, tgt_mask)
return self.norm(x)
class DecoderLayer(nn.Module):
"Decoder is made of self-attn, src-attn, and feed forward (defined below)"
def __init__(self, size, self_attn, src_attn, feed_forward, dropout):
super(DecoderLayer, self).__init__()
self.size = size
self.self_attn = self_attn
self.src_attn = src_attn
self.feed_forward = feed_forward
self.sublayer = clones(SublayerConnection(size, dropout), 3)
def forward(self, x, memory, src_mask, tgt_mask):
"Follow Figure 1 (right) for connections."
m = memory
x = self.sublayer[0](x, lambda x: self.self_attn(x, x, x, tgt_mask))
x = self.sublayer[1](x, lambda x: self.src_attn(x, m, m, src_mask))
return self.sublayer[2](x, self.feed_forward)
掩码具体实现可以参考代码
def subsequent_mask(size):
"Mask out subsequent positions."
attn_shape = (1, size, size)
subsequent_mask = torch.triu(torch.ones(attn_shape), diagonal=1).type(
torch.uint8
)
return subsequent_mask == 0
"""
参数 diagonal 控制要考虑的对角线。如果 diagonal = 0,则保留主对角线上和上方的所有元素。正值排除主对角线和对角线上方的部分元素。
>>> a = torch.randn(4, 4)
>>> a
tensor([[ 0.5144, 0.5091, -0.3698, 0.3694],
[-1.1344, -0.2793, 1.6651, -1.3632],
[-0.3397, -0.1468, -0.0300, -1.1186],
[-2.1449, 1.3087, -0.1409, 2.4678]])
>>> torch.triu(a, diagonal=1)
tensor([[ 0.0000, 0.5091, -0.3698, 0.3694],
[ 0.0000, 0.0000, 1.6651, -1.3632],
[ 0.0000, 0.0000, 0.0000, -1.1186],
[ 0.0000, 0.0000, 0.0000, 0.0000]])
"""
复杂度分析
计算查询矩阵Q,键矩阵K,值矩阵V
计算注意力矩阵A
归一化注意力矩阵A
加权求和计算输出H
自注意力的运算复杂度
根据上述分析可知,自注意力机制的整体运算复杂度为:
其中 是输入序列长度。一般地,选择向量的特征维度, 进而等价于
在自然语言处理等任务中一般有
全连接层的计算复杂度
通常认为Transformer的计算量主要来源于自注意力层,然而全连接层的计算量也不可忽略。全连接层一般为两层,第一层的特征维度由 , 第二层的特征维度为 。 所以全连接层的计算复杂度为:
番外:改进复杂度
关于如何改进复杂度,可以移步至复杂度开篇的链接,这里为截取图片。
综上,这篇文章还存在许多不成熟的地方,尽请指出!引用的文章均在文章中给了链接,如有侵权请联系删除~