学习内容参考:
- 超详细图解Self-Attention
- Transformer - Attention is all you need
- 熬了一晚上,我从零实现了Transformer模型,把代码讲给你听
- 《 神经网络与深度学习》.邱锡鹏版
目录
2024/02/27
一. Attention mechanism
1. 注意力分类
- 聚焦式注意力(focus attention):有目的的。比如在嘈杂的环境里你也能关注到和你谈话的人的说话内容。
- 基于显著性的注意力(saliency-based attention):受外界刺激驱动。比如别人叫你的名字,会引起你的注意力。
2. 相关参数
- 查询向量: q \boldsymbol{q} q,和特定任务相关的表示;
- 注意力分布: α n \alpha_n αn,在给定 q \boldsymbol{q} q和 X \mathbf{X} X下,选定第 n n n个输入向量的概率。
- 打分函数:
s
(
x
n
,
q
)
s(\boldsymbol{x}_n, \boldsymbol{q})
s(xn,q),有多种计算方式选择,如
- 加性模型: s ( x , q ) = v ⊤ t a n h ( W x + U q ) s(\boldsymbol{x}, \boldsymbol{q})=\boldsymbol{v}^{\top}tanh(\mathbf{W}\boldsymbol{x}+\mathbf{U}\boldsymbol{q}) s(x,q)=v⊤tanh(Wx+Uq),
- 点积模型: s ( x , q ) = x ⊤ q s(\boldsymbol{x}, \boldsymbol{q})=\boldsymbol{x}^{\top}\boldsymbol{q} s(x,q)=x⊤q,
- 缩放点积模型: s ( x , q ) = x ⊤ q D s(\boldsymbol{x}, \boldsymbol{q})=\frac{\boldsymbol{x}^{\top}\boldsymbol{q}}{\sqrt{D}} s(x,q)=Dx⊤q,D是输入向量的维度。(为什么有缩放因子?为了归一化,D很大是,softmax(xq)的分布会集中在元素绝对值大的区域,乘上缩放因子使得分子xq的方差变为1,使得在训练过程中梯度值保持稳定。)
- 双线性模型:
s
(
x
,
q
)
=
x
⊤
W
q
s(\boldsymbol{x}, \boldsymbol{q})=\boldsymbol{x}^{\top}\mathbf{W}\boldsymbol{q}
s(x,q)=x⊤Wq。
其中,点积模型相对加性模型计算效率更高;缩放点积解决(D较大时,点积模型方差较大,导致softmax函数的梯度较小)问题;双线性模型可理解为分别对 x \boldsymbol{x} x和 q \boldsymbol{q} q做线性变换后计算点积,在计算相似度时引入了非对称性。
3. 计算步骤
输入向量 X = [ x 1 , … , x N ] ∈ R D × N \mathbf{X}=[\boldsymbol{x}_1,\dots,\boldsymbol{x}_N]\in\mathbb{R}^{D\times N} X=[x1,…,xN]∈RD×N :
- 在所有输入信息上计算注意力分布;
在给定 q \boldsymbol{q} q和 X \mathbf{X} X下,第 n n n个输入向量 x n \boldsymbol{x}_n xn受关注的程度:
α n = p ( x n ∣ X , q ) = s o f t m a x ( s ( x n , q ) ) . \alpha_n=p(\boldsymbol{x}_n|\mathbf{X}, \boldsymbol{q})=softmax(s(\boldsymbol{x}_n, \boldsymbol{q})). αn=p(xn∣X,q)=softmax(s(xn,q)). - 根据注意力分布来计算输入信息的加权平均。
软性注意力机制: a t t ( X , q ) = ∑ n = 1 N α n x n = E x n ∼ p ( x n ∣ X , q ) [ x n ] . 软性注意力机制: att(\mathbf{X,\boldsymbol{q}})=\sum^N_{n=1}\alpha_n\boldsymbol{x}_n=\mathbb{E}_{\boldsymbol{x}_n\sim p(\boldsymbol{x}_n|\mathbf{X,\boldsymbol{q}})}[\boldsymbol{x}_n]. 软性注意力机制:att(X,q)=n=1∑Nαnxn=Exn∼p(xn∣X,q)[xn].
4. 注意力机制变体
-
硬性注意力:选取最高概率的输入向量:
a t t ( X , q ) = x n ^ , 其中 n ^ = arg max n = 1 , … , N α n att(\mathbf{X,\boldsymbol{q}})=\boldsymbol{x}_{\hat{n}}, 其中\hat{n}=\argmax_{n=1,\dots,N} \alpha_n att(X,q)=xn^,其中n^=n=1,…,Nargmaxαn
损失函数和注意力分布之间不可导,无法使用反向传播进行训练,通常使用强化学习进行训练。 -
键值对注意力:key-计算注意力分布 α n \alpha_n αn;Value-计算聚合信息。输入 ( K , V ) = [ ( k 1 , v 1 ) , … , ( k N , v N ) ] (\mathbf{K},\mathbf{V})=[(\boldsymbol{k}_1,\boldsymbol{v}_1), \dots, (\boldsymbol{k}_N,\boldsymbol{v}_N)] (K,V)=[(k1,v1),…,(kN,vN)]:
a t t ( [ K , V ] ) = ∑ n = 1 N α n v n = ∑ n = 1 N s o f t m a x ( k n , q ) v n . att([\mathbf{K},\mathbf{V}])=\sum^N_{n=1}\alpha_n\boldsymbol{v}_n=\sum^N_{n=1}softmax(\boldsymbol{k}_n,\boldsymbol{q})\boldsymbol{v}_n. att([K,V])=n=1∑Nαnvn=n=1∑Nsoftmax(kn,q)vn. -
多头注意力:利用多个查询向量 Q = [ q 1 , … , q M ] \mathbf{Q}=[\boldsymbol{q}_1, \dots, \boldsymbol{q}_M] Q=[q1,…,qM]并行地从输入信息中选取多组信息,每个注意力头关注输入信息的不同部分:
a t t ( [ K , V ] , Q ) = a t t ( [ K , V ] , q 1 ) ⊕ ⋯ ⊕ a t t ( [ K , V ] , q 1 ) att([\mathbf{K},\mathbf{V}],\mathbf{Q})=att([\mathbf{K},\mathbf{V}],\boldsymbol{q}_1)\oplus\cdots\oplus att([\mathbf{K},\mathbf{V}],\boldsymbol{q}_1) att([K,V],Q)=att([K,V],q1)⊕⋯⊕att([K,V],q1)
其中 ⊕ \oplus ⊕表示向量拼接。 -
结构化注意力:输入信息 本身具有层次(Hierarchical)结构,比如文本有词、句子、段落、篇章等不同粒度的层次,这种情况下可以使用层次化的注意力来进行更好的信息选择。
-
指针网络:Seq2Seq模型,通过注意力分布“指出”相关信息的位置,输出序列索引。
二. Self-Attention model
- 建立输入输出序列之间长距离依赖关系:增加网络层数(通过一个深层网络来获取远距离信息交互)、利用全连接网络(无法处理长度可变输入序列)。
- 不同输入长度的连接权重大小也不同,考虑利用self-attention model “动态”生成不同连接的权重。
1. 采用查询-键-值模式(Query-Key-Value)
计算过程
- 将输入 X \mathbf{X} X线性映射到三个不同的空间获得查询向量 q n ∈ R D k \boldsymbol{q}_n\in \mathbb{R}^{D_k} qn∈RDk、键向量 k n ∈ R D k \boldsymbol{k}_n\in \mathbb{R}^{D_k} kn∈RDk、值向量 v n ∈ R D v \boldsymbol{v}_n\in \mathbb{R}^{D_v} vn∈RDv。
- 对于每个
q
n
\boldsymbol{q}_n
qn,利用注意力机制求得输出向量
h
n
∈
R
D
v
\boldsymbol{h}_n\in \mathbb{R}^{D_v}
hn∈RDv:
h
n
=
a
t
t
(
[
K
,
V
]
,
q
n
)
=
∑
j
=
1
N
α
n
,
j
v
j
=
∑
j
=
1
N
s
o
f
t
m
a
x
(
s
(
k
j
,
q
n
)
)
v
j
.
\boldsymbol{h}_n=att([\mathbf{K},\mathbf{V}],\boldsymbol{q}_n)=\sum^N_{j=1}\alpha_{n,j}\boldsymbol{v}_j=\sum^N_{j=1}softmax(s(\boldsymbol{k}_j,\boldsymbol{q}_n))\boldsymbol{v}_j.
hn=att([K,V],qn)=j=1∑Nαn,jvj=j=1∑Nsoftmax(s(kj,qn))vj.
α n , j = s o f t m a x ( s ( k j , q n ) ) \alpha_{n,j}=softmax(s(\boldsymbol{k}_j,\boldsymbol{q}_n)) αn,j=softmax(s(kj,qn))表示第 n n n个输出关注到第 j j j个输入的权重。 - 注意力打分函数采用缩放点积,输出向量为: H = V s o f t m a x ( K ⊤ Q D k ) ∈ R D v × N . \mathbf{H}=\mathbf{V}softmax(\frac{\mathbf{K}^{\top}\mathbf{Q}}{\sqrt{D_k}})\in\mathbb{R}^{D_v \times N}. H=Vsoftmax(DkK⊤Q)∈RDv×N.
2. 位置编码信息
只关注查询向量 q \boldsymbol{q} q和键向量 k \boldsymbol{k} k的相关性,忽略了位置信息。通常需要添加位置编码信息来修正。
import torch
import torch.nn
from math import sqrt
class Self_Attention(nn.module)
'''
input: batch_size * seq_len * input_dim
q: batch_size * input_dim * dim_k
k: batch_size * input_dim * dim_k
v: batch_size * input_dim * dim_v
output: batch_size * dim_v * input_dim
'''
def __init__(self, input_dim, dim_k, dim_v):
super(Self_Attention,self).__init__()
self.q = nn.Linear(input_dim, dim_k)
self.k = nn.Linear(input_dim, dim_k)
self.v = nn.Linear(input_dim, dim_v)
self._norm_fact = 1/sqrt(dim_k)
def forward(self, x)
Q = self.q(x) # Q: batch_size * seq_len * dim_k
K = self.k(x)
V = self.v(x)
atten = nn.Softmax(dim=-1)(torch.bmm(Q,K.permute(0,2,1))) * self._norm_fact
output = torch.bmm(atten,V)
return output
- nn.Softmax(dim=-1) ——Softmax激活函数;dim=-1:最后一个维度;按照指定维度将输入张量的每个元素转换为0-1之间的值。
- (torch.bmm(Q,K.permute(0,2,1)))——bmm(batch matrix multiplication)批量矩阵乘法操作;.permute(0,2,1):转置,将1和2维度进行交换。
- atten = nn.Softmax(dim=-1) (…)——将上述bmm结果作为输入,通过softmax激活函数进行处理,得到atten 。
class MultiHeadAttention(nn.module):
'''
方式一:先将X映射到dim_v空间,再dim_v/num_heads 拆解多头
'''
def __init__(self, input_dim, dim_k,dim_v,num_heads):
super(Self_Attention_Multi_Head).__init__()
assert dim_k % num_heads ==0
assert dim_v % num_heads ==0
self.q = nn.Linear(input_dim,dim_k)
self.k = nn.Linear(input_dim,dim_k)
self.v = nn.Linear(input_dim,dim_v)
self.num_heads = num_heads
self.dim_k = dim_k
self.dim_v = dim_v
self._norm_fact = 1/sqrt(dim_k)
def forward(self, x)
Q = self.q(x).reshape(-1, x.shape[0], x.shape[1], dim_k//self.num_heads )# //整除,dim_k//self.h 代表head的维度
K = self.k(x).reshape(-1, x.shape[0], x.shape[1], dim_k//self.num_heads )
V = self.v(x).reshape(-1, x.shape[0], x.shape[1], dim_v//self.num_heads )
print(x.shape)
print(Q.size())
atten = nn.Softmax(dim = -1)(torch.matmul(Q,K.permute(0,1,3,2)))
output = torch.matmul(atten,V).reshape(x.shape[0], x.shape[1],-1)
return output
multi_attention=MultiHeadAttention(embed_dim, num_heads)
attn_output, attn_output_weights = multi_attention(query, key, value)
三. Transformer model
优点:
- 完全依赖attention,解决了长距离以依赖问题;
- 可并行,减少计算资源损耗。
1. Embedding
1.1 context embedding(输入序列信息)
1.2 positional embedding(位置编码信息)
弥补了Attention机制无法捕捉sequence中token位置信息的缺点。
transformer的特性使得输入encoder的向量之间完全平等(不存在RNN的recurrent结构),token(NLP中指文本数据的基本单位,一个单词或者字词)的实际位置于位置信息编码唯一绑定。Positional Encoding的引入使得模型能够充分利用token在sequence中的位置信息。
2. Encoder(引入multi_head机制,可并行计算)
2.1 MultiHeadAttention
- 计算输入序列之间的相关性,帮助模型更好地学习上下文语义。
- 引入MultiHeadAttention,每个head关注输入序列的不同位置,增强了attention关注序列内部“单词”之间的表达能力。
2.2 FF(Feed forward network)
前馈神经网络引入非线性变换,增强模型拟合能力。
每一层经过attention后,进入FFN,包含2层linear transformation层,中间的激活函数是ReLu。
F
F
N
(
x
)
=
m
a
x
(
0
,
W
1
x
+
b
1
)
W
2
+
b
2
.
FFN(\boldsymbol{x})=max(0, \mathbf{W}_1 \boldsymbol{x}+\boldsymbol{b}_1)\mathbf{W}_2+ \boldsymbol{b}_2.
FFN(x)=max(0,W1x+b1)W2+b2.
2.3 Layer Normalization
保证数据特征分布的稳定性,将数据标准化到ReLU激活函数的作用区域,可以使得激活函数更好的发挥作用
- Normalization技术:
- Batch normalization: 在输入数据上进行归一化,在batch(批次)维度上计算均值和方差。
- Layer Normalization:在输出数据上进行归一化,feature(特征)维度上计算均值和方差。
2.4 Residual connection
残差网络,以使得网络只关注到当前差异的部分,使当模型中的层数较深时仍然能得到较好的训练效果。
3. Decoder(串行计算)
3.1 Masked MultiHeadAttention
mask 掩码,掩盖某些值使其在参数更新时不产生效果。padding mask+sequence mask。
- padding mask:所有缩放点积注意力中需要。不同批次序列长度不同,补短的截长的,把没意义的地方进行掩码(加上非常大的负数,经过softmax后,该位置概率接近为0)。
- sequence mask:只在decoder中自注意力模型中需要。防止信息泄露。做法:产生一个上三角矩阵,上三角的值全为0。把这个矩阵作用在每一个序列上,就可以达到目的。
3.2 FF
3.3 LayerNorm
3.4 Residual connection
5. Output
Linear_Softmax
解码组件最后会输出一个实数向量。我们如何把浮点数变成一个单词?
- Linear层可以把向量投射到比它大得多的对数几率(logits)向量里。比如从训练集中学习1w个不同的英文单词(输出词表),则对数几率向量为1w个单元格长度的向量,每个单元对应某一个单词的分数。
- Softmax层将分数变成概率。概率最高的单元格被选中,对应的单词作为该时间步的输出。
6. transformer code实现
import torch
import torch.nn as nn
import numpy as np
import math
class Config(object):
def __init__(self):
self.vocab_size = 6
self.d_model = 20
self.n_heads = 2
assert self.d_model % self.n_heads == 0
dim_k = d_model % n_heads
dim_v = d_model % n_heads
self.padding_size = 30
self.UNK = 5
self.PAD = 4
self.N = 6
self.p = 0.1
config = Config()
class Embedding(nn.Module):
def __init__(self,vocab_size):
super(Embedding, self).__init__()
# 一个普通的 embedding层,可通过设置padding_idx=config.PAD 来实现论文中的 padding_mask
self.embedding = nn.Embedding(vocab_size,config.d_model,padding_idx=config.PAD)
def forward(self,x):
# 根据每个句子的长度,进行padding,短补长截
for i in range(len(x)):
if len(x[i]) < config.padding_size:
x[i].extend([config.UNK] * (config.padding_size - len(x[i]))) # 注意 UNK是你词表中用来表示oov的token索引,这里进行了简化,直接假设为6
else:
x[i] = x[i][:config.padding_size]
x = self.embedding(torch.tensor(x)) # batch_size * seq_len * d_model
return x
class Positional_Encoding(nn.Module):
def __init__(self,d_model):
super(Positional_Encoding,self).__init__()
self.d_model = d_model
def forward(self,seq_len,embedding_dim):
positional_encoding = np.zeros((seq_len,embedding_dim))
for pos in range(positional_encoding.shape[0]):
for i in range(positional_encoding.shape[1]):
positional_encoding[pos][i] = math.sin(pos/(10000**(2*i/self.d_model))) if i % 2 == 0 else math.cos(pos/(10000**(2*i/self.d_model)))
return torch.from_numpy(positional_encoding)
class Mutihead_Attention(nn.Module):
def __init__(self,d_model,dim_k,dim_v,n_heads):
super(Mutihead_Attention, self).__init__()
self.dim_v = dim_v
self.dim_k = dim_k
self.n_heads = n_heads
self.q = nn.Linear(d_model,dim_k)
self.k = nn.Linear(d_model,dim_k)
self.v = nn.Linear(d_model,dim_v)
self.o = nn.Linear(dim_v,d_model)
self.norm_fact = 1 / math.sqrt(d_model)
def generate_mask(self,dim):
# 此处是 sequence mask ,防止 decoder窥视后面时间步的信息。
# padding mask 在数据输入模型之前完成。
matirx = np.ones((dim,dim))
mask = torch.Tensor(np.tril(matirx))
return mask==1 #函数返回一个布尔类型的张量,表示掩码矩阵中所有值是否等于 1。
#这样就得到了一个布尔类型的掩码,用于在解码器中限制注意力只关注当前位置之前的 token。
def forward(self,x,y,requires_mask=False):
assert self.dim_k % self.n_heads == 0 and self.dim_v % self.n_heads == 0
# size of x : [batch_size * seq_len * batch_size]
# 对 x 进行自注意力
Q = self.q(x).reshape(-1,x.shape[0],x.shape[1],self.dim_k // self.n_heads) # n_heads * batch_size * seq_len * dim_k
K = self.k(x).reshape(-1,x.shape[0],x.shape[1],self.dim_k // self.n_heads) # n_heads * batch_size * seq_len * dim_k
V = self.v(y).reshape(-1,y.shape[0],y.shape[1],self.dim_v // self.n_heads) # n_heads * batch_size * seq_len * dim_v
# print("Attention V shape : {}".format(V.shape))
attention_score = torch.matmul(Q,K.permute(0,1,3,2)) * self.norm_fact
if requires_mask:
mask = self.generate_mask(x.shape[1])
attention_score.masked_fill(mask,value=float("-inf")) # 注意这里的小Trick,不需要将Q,K,V 分别MASK,只MASKSoftmax之前的结果就好了
output = torch.matmul(attention_score,V).reshape(y.shape[0],y.shape[1],-1)
# print("Attention output shape : {}".format(output.shape))
output = self.o(output)
return output
class Feed_Forward(nn.Module):
def __init__(self,input_dim,hidden_dim=2048):
super(Feed_Forward, self).__init__()
self.L1 = nn.Linear(input_dim,hidden_dim)
self.L2 = nn.Linear(hidden_dim,input_dim)
def forward(self,x):
output = nn.ReLU()(self.L1(x))
output = self.L2(output)
return output
class Add_Norm(nn.Module):
def __init__(self):
self.dropout = nn.Dropout(config.p) # 随机失活,防止过拟合
super(Add_Norm, self).__init__()
def forward(self,x,sub_layer,**kwargs):
sub_output = sub_layer(x,**kwargs)
# print("{} output : {}".format(sub_layer,sub_output.size()))
x = self.dropout(x + sub_output) # 残差连接,直接学习输入与子层输出之间的差异
layer_norm = nn.LayerNorm(x.size()[1:])
out = layer_norm(x) #对每个样本的特征维度进行归一化 提高训练稳定性
return out
# 将encoder中所有模块 拼接作为Encoder
class Encoder(nn.Module):
def __init__(self):
super(Encoder, self).__init__()
self.positional_encoding = Positional_Encoding(config.d_model)
self.muti_atten = Mutihead_Attention(config.d_model,config.dim_k,config.dim_v,config.n_heads)
self.feed_forward = Feed_Forward(config.d_model)
self.add_norm = Add_Norm()
def forward(self,x): # batch_size * seq_len 并且 x 的类型不是tensor,是普通list
x += self.positional_encoding(x.shape[1],config.d_model)
# print("After positional_encoding: {}".format(x.size()))
output = self.add_norm(x,self.muti_atten,y=x)
output = self.add_norm(output,self.feed_forward)
return output
# 将decoder中所有模块 拼接作为Decoder
class Decoder(nn.Module):
def __init__(self):
super(Decoder, self).__init__()
self.positional_encoding = Positional_Encoding(config.d_model)
self.muti_atten = Mutihead_Attention(config.d_model,config.dim_k,config.dim_v,config.n_heads)
self.feed_forward = Feed_Forward(config.d_model)
self.add_norm = Add_Norm()
def forward(self,x,encoder_output): # batch_size * seq_len 并且 x 的类型不是tensor,是普通list
# print(x.size())
x += self.positional_encoding(x.shape[1],config.d_model)
# print(x.size())
# 第一个 sub_layer
output = self.add_norm(x,self.muti_atten,y=x,requires_mask=True)
# 第二个 sub_layer
output = self.add_norm(output,self.muti_atten,y=encoder_output,requires_mask=True)
# 第三个 sub_layer
output = self.add_norm(output,self.feed_forward)
return output
# 组装transformer
class Transformer_layer(nn.Module):
def __init__(self):
super(Transformer_layer, self).__init__()
self.encoder = Encoder()
self.decoder = Decoder()
def forward(self,x):
x_input,x_output = x
encoder_output = self.encoder(x_input)
decoder_output = self.decoder(x_output,encoder_output)
return (encoder_output,decoder_output)
class Transformer(nn.Module):
def __init__(self,N,vocab_size,output_dim):
super(Transformer, self).__init__()
self.embedding_input = Embedding(vocab_size=vocab_size)
self.embedding_output = Embedding(vocab_size=vocab_size)
self.output_dim = output_dim
self.linear = nn.Linear(config.d_model,output_dim)
self.softmax = nn.Softmax(dim=-1)
self.model = nn.Sequential(*[Transformer_layer() for _ in range(N)])
def forward(self,x):
x_input , x_output = x
x_input = self.embedding_input(x_input)
x_output = self.embedding_output(x_output)
_ , output = self.model((x_input,x_output))
output = self.linear(output)
output = self.softmax(output)
return output
总结问题
-
建立输入输出序列之间长距离依赖关系的方式?
增加网络层数(通过一个深层网络来获取远距离信息交互)、利用全连接网络(无法处理长度可变输入序列)。 -
Self-Attention model优点?
不同输入长度的连接权重大小也不同,考虑利用self-attention model “动态”生成不同连接的权重。 -
多头注意力的作用?
让模型关注不同方面的信息,捕捉更丰富的特征、信息。 -
相比RNN、LSTM的优势
可并行,解决长距离依赖问题(RNN t时刻输出依赖于t-1时刻,有序列依赖关系); -
与seq2seq相比
seq2seq:将encoder端所有信息压缩到一个固定长度向量(上下文向量c),作为decoder端第一个隐藏状态的输入,来预测decoder端第一个token的隐藏状态。在输入序列较长时,会损失encoder端的信息。且不利于特征信息的重点关注。
transformer:对上述缺点进行改进,利用多头注意力机制关注到多方面信息,FFN也增强模型表达能力。