目录
- 一、为什么是Transformer?
- 二、什么是Transformer?
- 1、整体框架
- 2、Embedding
- 2.1、字向量:Word embedding
- 2.2、位置编码:Positional Encoding
- 3、Encoder
- 3.1、自注意力机制:Self-Attention
- 3.2、多头自注意力层:Multi-Head Self Attention
- 3.3、连接与归一化:Add & Norm
- 3.4、前向反馈网络层:Feed-Forward network
- 总览Encoder
- 4、Decoder
- 4.1、掩码多头注意力层:Mask-Multi-Head-Attention
- 4.2、编码-解码 多头注意力层:Encoder-Decoder Multi-head Attention
- 4.3、输出层:Output
- 总览Decoder
- 三、十万个为什么?
- 1、Transformer为什么需要进行Multi-head Attention?
- 2、self-attention为什么要使用Q、K、V?
- 参考文献:
Transformer是谷歌在2017年发表的论文Attention Is All You Need中提出的一种seq2seq模型,首先是在自然语言处理方面应用,现在已经取得了更大范围的应用与拓展,如今已经成为非常火热的模型。
本篇博文将带着问题,边阅读文章和源码,阐述Transfomer的特点、框架、原理和相关理解。
传统的CNN、RNN(或者LSTM,GRU)计算是顺序的、迭代的、串行的,即只能从左向右依次计算或者从右向左依次计算,而这会导致两个问题:
而Transformer使用了位置嵌入 (Positional Encoding) 来理解语言的顺序,使用自注意力机制(Self Attention Mechanism)和全连接层进行计算,所有字都是同时训练,具有更好的并行性,不仅大大提高了计算效率,从长远来看更符合GPU的逻辑。
首先Transformer的整体主要分为Encoder和Decoder两大部分。输入的序列首先变成计算机便于处理的Embedding,然后Embedding传入Encoder进行编码,映射成隐藏层特征,经过Encoder后再结合上一次的output输入到Decoder中,最后用softmax计算序列下一个单词的概率。
Embedding的作用是将输入(“我是一个学生”)初步整理成计算机能识别的特征信息。
Transformer将输入的序列首先转换为Word Embedding,由于 Transformer 模型没有循环神经网络的迭代操作,其还需要编码位置信息,即Positional Encoding。
可能你曾经学过one-hot编码,one-hot编码无法表示词与词之间的关系。Word Embedding解决了这个问题,随着训练,他能够使得意思相近的单词表达结果越来越近。
Word Embedding首先设计一个可学习的权重矩阵W,通过词向量与权重矩阵进行相乘,不断训练得到新的表示结果。如下例:
爱” 和 “喜欢” 这两个词经过 one-hot 后分别表示为 10000 和 00001,权重矩阵如下:
W 00 W_{00} W00 W 01 W_{01} W01 W 02 W_{02} W02
W 10 W_{10} W10 W 11 W_{11} W11 W 12 W_{12} W12
W 20 W_{20} W20 W 21 W_{21} W21 W 22 W_{22} W22
W 30 W_{30} W30 W 31 W_{31} W31 W 32 W_{32} W32
W 40 W_{40} W40 W 41 W_{41} W41 W 42 W_{42} W42
相乘后,“爱”被编码成[ W 00 W_{00} W00 W 01 W_{01} W01 W 02 W_{02} W02],"喜欢“被编码成[ W 40 W_{40} W40 W 41 W_{41} W41 W 42 W_{42} W42],而这两个词的语法和意思比较相近,在学习过程中,权重矩阵的参数不断更新,从而使得[ W 00 W_{00} W00 W 01 W_{01} W01 W 02 W_{02} W02]和[ W 40 W_{40} W40 W 41 W_{41} W41 W 42 W_{42} W42]的值越来越近。这就是Word embedding的工作流程。
在pytorch中,我们使用torch自带的embedding功能,权重矩阵先进行随机初始化(当然也可以选择 Pre-trained 的结果),但设为 Trainable。这样在 training 过程中不断地对 Embeddings 进行改进
class Embeddings(nn.Module):
def __init__(self, d_model, vocab):
super(Embeddings, self).__init__()
self.lut = nn.Embedding(vocab, d_model) #vocab表示词汇表的数量,d_model表示embedding的维度,即词向量的维度
self.d_model = d_model #表示embedding的维度
def forward(self, x):
return self.lut(x) * math.sqrt(self.d_model)
Transformer 摈弃了 RNN 的结构,因此需要一个东西来标记各个字之间的时序 or 位置关系,而这个东西,就是位置嵌入。
以往我们根据单词之间的间隔比例算距离,如果设置整个句子长度为1,如:Attention is all you need ,其中is和you之间的距离为0.5。而:To follow along you will first need to install PyTorch较长文本中子里的0.5距离则会隔很多单词,这显然不合适。
所以总结一下理想的位置编码应该满足:
作者为此设计了一种Positional Encoding,首先,它不是一个数字,而是一个包含句子中特定位置信息的d维向量。其次,这种嵌入方式没有集成到模型中,相反,这个向量是用来给句子中的每个字提供位置信息的,换句话说,我们通过注入每个字位置信息的方式,增强了模型的输入(其实说白了就是将位置嵌入和字嵌入相加,然后作为输入)
其计算位置的公式为:
P E ( p o s , 2 i ) = sin ( p o s / 1000 0 2 i / d model ) \begin{aligned} P E_{(p o s, 2 i)} &=\sin \left(p o s / 10000^{2 i / d_{\text {model }}}\right) \end{aligned} PE(pos,2i)=sin(pos/100002i/dmodel )
P E ( pos , 2 i + 1 ) = cos ( p o s / 1000 0 2 i / d model ) \begin{aligned}P E_{(\text {pos }, 2 i+1)} &=\cos \left(p o s / 10000^{2 i / d_{\text {model }}}\right) \end{aligned} PE(pos ,2i+1)=cos(pos/100002i/dmodel )
符号解释:其中,pos代表是词在句子中的位置,d是词向量的维度(通常为512),2i代表d中的偶数维度,同理,i代表奇数维度。
公式解释:由公式可知,每一维 i 都对应不同周期的正余弦曲线: i=0 时是周期为 2 π 2 \pi 2π 的 sin 函数, i=1 时是周期为 2 π 2\pi 2π的cos 函数…位置嵌入在 embedding_dimension维度上产生一种包含位置信息的纹理,对于不同的两个位置 pos1 和 pos2 ,若它们在某一维i上有相同的编码值,则说明这两个位置的差值等于该维所在曲线的周期,即 ∣ pos 1 − pos 2 ∣ = T i |\operatorname{pos} 1-\operatorname{pos} 2|=T_{i} ∣pos1−pos2∣=Ti 。而对于另一个维度 j ( j ≠ i ) j(j \neq i) j(j=i) ,由于 T j ≠ T i T_{j} \neq T_{i} Tj=Ti , 因此 pos 1 \operatorname{pos} 1 pos1 和 p o s 2 pos 2 pos2 在这个维度 j 上的编码值就不会相等,对于其它任意 k ∈ { 0 , 1 , 2 , … , d − 1 } ; k ≠ i \in\{0,1,2, \ldots, d-1\} ; k \neq i ∈{0,1,2,…,d−1};k=i 也是如此。
综上可知,这种编码方式保证了不同位置在所有维度上不会被编码到完全一样的值,从而使每 个位置都获得独一无二的编码。
在pytorch中,使用自带的Positional Embedding模块进行设计
在# Positional Encoding
class PositionalEncoding(nn.Module):
"实现PE功能"
def __init__(self, d_model, dropout, max_len=5000): ##vocab表示词汇表的数量,d_model表示embedding的维度,即词向量的维度
super(PositionalEncoding, self).__init__()
self.dropout = nn.Dropout(p=dropout)
pe = torch.zeros(max_len, d_model) # max_len=5000表示句子最多有5000个词
position = torch.arange(0., max_len).unsqueeze(1)
div_term = torch.exp(torch.arange(0., d_model, 2) *
-(math.log(10000.0) / d_model)) # 那个分数
pe[:, 0::2] = torch.sin(position * div_term) # 偶数列
pe[:, 1::2] = torch.cos(position * div_term) # 奇数列
pe = pe.unsqueeze(0) # [1, max_len, d_model]
self.register_buffer('pe', pe) # 将pe注册到模型的buffers()属性中,这代表该变量对应的是一个持久态,不会有梯度传播给它,但是能被模型的state_dict记录下来。
def forward(self, x):
x = x + Variable(self.pe[:, :x.size(1)], requires_grad=False) #输入模型的整个Embedding是Word Embedding与Positional Encoding直接相加之后的结果。
return self.dropout(x)
Encoder的作用是将刚刚初步整理的Embedding,在Encoder中通过注意力机制等进行进一步的编码。
Encoder部分是由个层相同小Encoder Layer串联而成。小Encoder Layer可以简化为两个部分:(1)多头自注意力层:Multi-Head Self Attention (2) 前向反馈网络层:Feed-Forward network。前者用于捕捉特征之间的关系,后者是进一步编码学习
之前通过word embedding和positional encoding我们得到了字(单词)编码的向量表示: x t x_t xt。
接着,我们现在设计三个变换:key = linear_k(x)、query = linear_q(x)、value = linear_v(x) 将编码后的 x t x_t xt进行变换生成三个矩阵:查询矩阵 W Q W^Q WQ(Query)、键矩阵 W K W^K WK(Key)、值矩阵 W V W^V WV(Value)
于是所有的字向量: x t x_t xt又衍生出三个新的向量: Q Q Q、 K K K、 V V V,将其代入公式: Attention ( Q , K , V ) = softmax ( Q K T d k ) V \operatorname{Attention}(Q, K, V)=\operatorname{softmax}\left(\frac{Q K^{T}}{\sqrt{d_{k}}}\right) V Attention(Q,K,V)=softmax(dkQKT)V,其中的 d k \sqrt{d_{k}} dk是为了防止Q和K的点积值过大,避免在经过softmax后梯度太小,形象表示为:
至此我们得到了单个自注意力的输出,当然可以把上面的向量计算变成矩阵的形式(即将多个字向量摞成矩阵形式,矩阵第t行为第t个词的向量),从而一次计算出所有时刻的输出。
Multi-Head Self Attention 实际上是由h个Self Attention 层并行组成(原文为8),就是说线性变换的矩阵从一组( W Q W^Q WQ、 W K W^K WK、 W V W^V WV)变成多组( W 0 Q W_0^Q W0Q、 W 0 K W_0^K W0K、 W 0 V W_0^V W0V)、( W 1 Q W_1^Q W1Q、 W 1 K W_1^K W1K、 W 1 V W_1^V W1V)、( W 2 Q W_2^Q W2Q、 W 2 K W_2^K W2K、 W 2 V W_2^V W2V)…了,对于输入矩阵X,每一组Q、K、V都可以得到一个输出矩阵Z,最后将他们拼接起来就好了。
pytorch实现:
class MultiHeadedAttention(nn.Module):
def __init__(self, h, d_model, dropout=0.1):
"Take in model size and number of heads."
super(MultiHeadedAttention, self).__init__()
assert d_model % h == 0
# 每个head中向量的维度,通常是512/8=64
self.d_k = d_model // h
# head的数量,通常为8
self.h = h
# 设置4个变换,其中3个用于生成Q、K、V clones是一个方法,代表将结构相同的层实例化n次
self.linears = clones(nn.Linear(d_model, d_model), 4)
self.attn = None
self.dropout = nn.Dropout(p=dropout)
def forward(self, query, key, value, mask=None):
"""
实现MultiHeadedAttention。
输入的q,k,v是形状 [batch, L, d_model]。
输出的x 的形状同上。
"""
if mask is not None:
# Same mask applied to all h heads.
mask = mask.unsqueeze(1)
nbatches = query.size(0)
# 1) 矩阵点乘生成Q、K、V
# 这一步qkv变化:[batch, L, d_model] ->[batch, h, L, d_model/h]
query, key, value = \
[l(x).view(nbatches, -1, self.h, self.d_k).transpose(1, 2)
for l, x in zip(self.linears, (query, key, value))]
# 2) 计算注意力attn 并加权到V,得到Z,维度与Q、K、V类似。
# qkv :[batch, h, L, d_model/h] -->x:[b, h, L, d_model/h], attn[b, h, L, L]
x, self.attn = attention(query, key, value, mask=mask, dropout=self.dropout)
# 3) 维度复原
# 上一步的结果合并在一起还原成原始输入序列的形状
x = x.transpose(1, 2).contiguous().view(nbatches, -1, self.h * self.d_k)
# 最后将Z输入最后一个全连接层
return self.linears[-1](x)
连接Add
Add操作和原理很简单,就是把上一步得到的Self-attention(Q、K、V)(如果是多头自注意力,就是对应拼接后的矩阵)与经过了Positional Encoding的embedding连接。
X e m b e d d i n g + Self-Attention ( Q , K , V ) X_{e m b e d d i n g}+\operatorname{Self-Attention}(Q, K, V) Xembedding+Self-Attention(Q,K,V)
归一化
采用的是Layer Normalization,Layer Normalization的作用是把神经网络中隐藏层归一为标准正态分布,以起到加快训练速度,加速收敛。
1.首先以列为单位计算均值和方差:
μ j = 1 m ∑ i = 1 m x i j \mu_{j}=\frac{1}{m} \sum_{i=1}^{m} x_{i j} μj=m1∑i=1mxij
σ j 2 = 1 m ∑ i = 1 m ( x i j − μ j ) 2 \sigma_{j}^{2}=\frac{1}{m} \sum_{i=1}^{m}\left(x_{i j}-\mu_{j}\right)^{2} σj2=m1∑i=1m(xij−μj)2
2.然后用每列的每个元素减去这列的均值,再除以这列的标准差,最后得到这列的标准差。
Layer Norm ( x ) = x i j − μ j σ j 2 + ϵ \operatorname{Layer} \operatorname{Norm}(x)=\frac{x_{i j}-\mu_{j}}{\sqrt{\sigma_{j}^{2}+\epsilon}} LayerNorm(x)=σj2+ϵxij−μj
Add&Norm的pytorch实现如下:
class LayerNorm(nn.Module):
"""构造一个layernorm模块"""
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):
"Norm"
mean = x.mean(-1, keepdim=True)
std = x.std(-1, keepdim=True)
return self.a_2 * (x - mean) / (std + self.eps) + self.b_2
class SublayerConnection(nn.Module):
"""Add+Norm"""
def __init__(self, size, dropout):
super(SublayerConnection, self).__init__()
self.norm = LayerNorm(size)
self.dropout = nn.Dropout(dropout)
def forward(self, x, sublayer):
"add norm"
return x + self.dropout(sublayer(self.norm(x)))
这部分实质上是两个全连接层映射,第一层是一个线性激活函数,第二层是激活函数是ReLU。
X h i d d e n = L i n e a r ( R e L U ( L i n e a r ( X h i d d e n ) ) ) X_{hidden}=Linear(ReLU(Linear(X_{hidden}))) Xhidden=Linear(ReLU(Linear(Xhidden)))
实现很简单:
# Position-wise Feed-Forward Networks
class PositionwiseFeedForward(nn.Module):
"实现FFN函数"
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(F.relu(self.w_1(x))))
通过上面的步骤,我们大致已经基本了解了Encoder的全部原理和主要构成,那么在此回顾整理一下:
根据图上的,将步骤总结如下:
因为输入(“我是一个学生”)在Encoder中进行了编码,这里我们具体讨论Decoder的操作,也就是如何得到输出(“I am a student”)的过程。
Encoder与Decoder的关系可以用下图描述(以机器翻译为例):
Decoder的大框架如下:
可以看到Decoder主要由Masked Multi-Head Self-Attention、Multi-Head Encoder-Decoder Attention、FeedForward Network组成,和 Encoder 一样,上面三个部分的每一个部分,都有一个残差连接,后接一个 Layer Normalization。
传统 Seq2Seq 中 Decoder 使用的是 RNN 模型,因此在训练过程中输入t时刻的词,模型无论如何也看不到未来时刻的词,因为循环神经网络是时间驱动的,但是Transformer 抛弃了RNN改为Self-Attention,会发生一个问题:整个ground truth都暴露在Decoder中,这显然不对。
所以这一层目的是忽略某些位置,不计算与其相关的注意力权重。为的是防止为了模型看到要预测的数据,防止泄露。
我们要对 Scaled Scores 进行 Mask,举个例子现在的ground truth 为 " I am fine",当我们输入 “I” 时,模型目前仅知道包括 “I” 在内之前所有字的信息,即 “” 和 “I” 的信息,不应该让其知道 “I” 之后词的信息。道理很简单,我们做预测的时候是按照顺序一个字一个字的预测,怎么能这个字都没预测完,就已经知道后面字的信息了呢?Mask 非常简单,首先生成一个下三角全 0,上三角全为负无穷的矩阵,然后将其与 Scaled Scores 相加即可:
之后为了数据更方便处理,做一次softmax,就能将负无穷变换为0,得到各个字之间的权重。而Multi无非就是并行的对上述步骤多做几次,不再赘述。
这一部分的pytorch实现为:
def subsequent_mask(size):
"""
mask后续的位置,返回[size, size]尺寸下三角Tensor
对角线及其左下角全是1,右上角全是0
"""
attn_shape = (1, size, size)
subsequent_mask = np.triu(np.ones(attn_shape), k=1).astype('uint8')
return torch.from_numpy(subsequent_mask) == 0
这一部分就是之前讲过的多头自注意力层,只不过输入不同,这部分的K,V为之前Encoder的输出,Q为Decoder中Masked Multi-head Attention 的输出。
这一部分的pytorch实现为:
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):
"将decoder的三个Sublayer串联起来"
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)
当decoder层全部执行完毕后,怎么把得到的向量映射为我们需要的词呢,只需要在结尾再添加一个全连接层和softmax层,假如我们的词典是1w个词,那最终softmax会输入1w个词的概率,概率值最大的对应的词就是我们最终的结果。
对应上图,回顾整个Decoder的全部流程如下
Time Step 1
初始输入: 起始符 + Positional Encoding(位置编码)
中间输入:(我是一个学生)Encoder Embedding(Encoder编码后的文本特征)
最终输出:产生预测“I”
Time Step 2
初始输入:起始符 + “I”+ Positonal Encoding
中间输入:(我是一个学生)Encoder Embedding
最终输出:产生预测“I am”
…
…
Time Step n
初始输入:起始符 + “I”+ “am”+ Positonal Encoding
中间输入:(我是一个学生)Encoder Embedding
最终输出:产生预测“I am a student”
下面从各类网站、博客、论文结合个人理解摘录和整理了部分重要问题(持续更新),看完后会对Transformer有个更好的理解。
self-attention,是一种通过自身和自身相关联的attention机制,从而得到一个更好的 representation 来表达自身,引入Self Attention后会更容易捕获句子中长距离的相互依赖的特征,Multi-head Attention是多层次的self-attention,多头的注意力有助于网络捕捉到更丰富的特征/信息(类比 CNN 中同时使用多个卷积核)。
在self-attention中,Q=K=V,序列中的每个单词(token)和该序列中其余单词(token)进行attention计算。self-attention的特点在于无视词(token)之间的距离直接计算依赖关系,从而能够学习到序列的内部结构,实现起来也比较简单
Query,Key,Value的概念取自于信息检索系统,Q表示的就是与我这个单词相匹配的单词的属性,K就表示我这个单词的本身的属性,V表示的是我这个单词的包含的信息本身。
实验发现self-attention使用Q、K、V,这样三个参数独立,模型的表达能力和灵活性很好。
觉得本文不错请点赞或收藏,虽然不会给作者带来经济收益,但是可以让本文获得更多曝光机会。
有其他疑问推荐在评论区留言,你提出的问题将对其他人也提供帮助。
Transformer 详解:https://wmathor.com/index.php/archives/1438/
【NLP】Transformer:
http://mantchs.com/2019/09/26/NLP/Transformer/
Transformer 中的 Positional Encoding:https://wmathor.com/index.php/archives/1453/
Transformer 修炼之道:https://www.jianshu.com/p/e6b5b463cf7b