目录
前言
transformer笔记,用于学习或回顾transformer中的注意力机制,主要学习其中的公式与代码,浅显易懂。
自注意力机制
自注意力机制其实从属于注意力机制,那么注意力机制是什么呢?
注意力机制是一种用于捕捉输入序列不同位置之间相关性的技术 。其核心思想是通过为每个单词分配不同的权重 ,突出与当前任务关联性较强的信息。
通俗理解:依赖于给定任务,机器能够分清输入的重点内容更好完成任务。如翻译。
自注意力从属于注意力,根据其开头增加的一个自字 ,我们可以大概明白该机制更加依赖于输入的序列本身。
通俗理解就是能够结合上下文理解当前语境
例:
- 小明摔倒了,他感觉痛并大哭。
- 小明摔倒了,因为他感觉痛并且他年纪还小所以他大哭。
传统的神经网路如RNN按照单词的顺序分析,难以捕捉长句子中词语间关系 。可能在第一句能分析痛和哭的关系,而在第二个句子会受到年纪还小影响无法分析痛和哭。
通过自注意力机制,会计算痛与其他单词间的相关性,即使在第二个句子中痛和哭的距离较远,注意力机制依旧可以通过权重调整捕捉到痛和哭的关系。
注意力公式介绍
在注意力机制中,有这三个非常重要的向量
Q:查询是主观意识的特征向量,外生给定
K:键是物体的突出特征信息向量,外生给定
V:值则是代表物体本身的特征向量(输入一个句子机器无法直接理解,所以需要词向)
注意力机制涉及到了两个公式
- 相关性计算 F ( Q , K ) F(Q,K) F(Q,K):
- 点积: S i m i l a r i t y ( Q , K ) = Q ∗ K Similarity(Q, K) = Q * K Similarity(Q,K)=Q∗K,通过点积可以提取出语义信息的方向相关性,计算起来简单。
- 余弦相似度: S i m i l a r i t y ( Q , K ) = Q K ∣ Q ∣ ∣ K ∣ Similarity(Q, K) = \frac{Q K}{|Q| |K|} Similarity(Q,K)=∣Q∣∣K∣QK,与点积的不同在于除以了二者的模长,相当于两个方向向量的相关性提取,更加专注语义信息的方向相关性
- 类softmax:
先将注意力得分根据维度缩放 后再进行softmax归一化 ,维度缩放让输出更加平滑
S i m i = S i d i Sim_{i} = \frac{S_{i}}{\sqrt{d_{i}}} Simi=di Si
a i = S o f t m a x ( S i m i ) = e S i m i Σ j e S i m j a_{i} = Softmax(Sim_{i}) = \frac{e^{Sim_{i}}}{\Sigma_{j} e^{Sim_{j}}} ai=Softmax(Simi)=ΣjeSimjeSimi
自注意力公式介绍
从属于注意力的自注意力自然少不了QKV,但其QK与注意力机制的QK不同 。
从上图注意力机制计算流程,我们知道计算权重的计算过程X并没有参与 。
而对于自注意力,因为其专注于自,所以Q与K在计算的时候X也参与了进去。
Q = X W Q Q = X W_{Q} Q=XWQ( W Q W_{Q} WQ是权重矩阵,会对X进行线性变换,其参数会随着神经网络的优化而不断更新),K也有权重矩阵 W K W_{K} WK
下面这张图是一个自注意力机制图
刚接触可能会比较懵,我们加上矩阵大小变换来讲可能能帮助理解
公式过程中矩阵的大小变换(括号内为该矩阵大小)
PS:该图中简化计算,QK大小相同,但在实际运用如英译中,源语言序列长度(len_k)和目标语言序列长度(len_q)通常是不同的
-
X: (6, 3)
代表了1个句子由6个词组成,一共6个词向量 ,每个词向量有3个维度
-
W q , W k , W v W_{q}, W_{k}, W{v} Wq,Wk,Wv: (4, 6)
大小原因: Q = W q X Q = W_{q}X Q=WqX,图中 Q Q Q的大小为(4, 3),所以这里的 W q W_{q} Wq大小为(4, 6)
(矩阵乘法的基本知识 ( 4 , 6 ) ⋅ ( 6 , 3 ) = ( 4 , 3 ) (4, 6) \cdot (6, 3) = (4, 3) (4,6)⋅(6,3)=(4,3))
K与V的乘法与Q一致 -
Q , K , V Q,K,V Q,K,V:(4, 3)
-
a t t e n t i o n = S o f t m a x ( K T ⋅ Q D k ) attention = Softmax(\frac{K^{T} \cdot Q}{\sqrt{D_k}}) attention=Softmax(Dk KT⋅Q):(3, 3)
大小原因: K T ( 3 , 4 ) ⋅ Q ( 4 , 3 ) = ( 3 , 3 ) K^{T}(3, 4) \cdot Q(4, 3) = (3, 3) KT(3,4)⋅Q(4,3)=(3,3)
根据Q和K使用点乘计算相关性 ,接着除以词向量维度的根号进行特征缩放 ,最后进行softmax(与注意力机制的公式一样)
-
H = V ⋅ a t t e n t i o n H = V\cdot attention H=V⋅attention: (4, 3)
大小原因: V ( 4 , 3 ) ⋅ a t t e n t i o n ( 3 , 3 ) = H ( 4 , 3 ) V(4, 3) \cdot attention(3, 3) = H(4, 3) V(4,3)⋅attention(3,3)=H(4,3)
采用缩放点积作为注意力打分函数
多头自注意力机制
多头自注意力是对单头自注意力的扩展 ,通过并行计算多个自注意力机制 ,能够捕获不同子空间的信息,提高模型的表达能力和性能。其实就是计算多个H并merge在一起,最后需要进行一个线性变换哦转换成单头自注意力的H大小
掩码矩阵
因为自注意力机制本质上会涉及到这个序列的所有内容 ,而在进行输出的时候我们是不希望未来的词组数据泄露 给当前词组的,所以引入了掩码矩阵。该矩阵对目标序列中未来位置的注意力分数设置为负无穷,从而将这些分数的 softmax 结果变为零。其作用体现在 a t t e n t i o n attention attention的计算中
a t t e n t i o n = S o f t m a x ( K T ⋅ Q + M D k ) attention = Softmax(\frac{K^{T} \cdot Q + M}{\sqrt{D_k}}) attention=Softmax(Dk KT⋅Q+M)M为掩码矩阵
这个掩码矩阵如下面的公式所示
M i j = { 0 if i ≥ j − ∞ if i < j M_{ij} = \begin{cases} 0 & \text{if } i \geq j \\ -\infty & \text{if } i < j \end{cases} Mij={0−∞if i≥jif i<j
M = ( 0 − ∞ − ∞ − ∞ 0 0 − ∞ − ∞ 0 0 0 − ∞ 0 0 0 0 ) M = \begin{pmatrix} 0 & -\infty & -\infty & -\infty \\ 0 & 0 & -\infty & -\infty \\ 0 & 0 & 0 & -\infty \\ 0 & 0 & 0 & 0 \end{pmatrix} M= 0000−∞000−∞−∞00−∞−∞−∞0
个人理解:每一行都代表了一个词向量 ,所以当前( i i i)位置的词向量只能看到过去位置( i > = j i >= j i>=j)的词向量的 a t t e n t i o n attention attention ,而不能看到之后词向量 的attention,所以当 i < j i < j i<j时其attention应该为0
自注意力代码
分为
python
# 掩码矩阵
# 构造一个和注意力得分一样大小矩阵,说明哪个位置是PAD部分,之后在计算计算softmax之前会把这里置为无穷大;
# 一定需要注意的是这里得到的矩阵形状是batch_size x len_q x len_k,我们是对k中的pad符号进行标识,并没有对q中的做标识
# seq_q 和 seq_k 不一定一致,在交互注意力,q来自解码端,k来自编码端,seq是向量序列哦
def get_attn_pad_mask(seq_q, seq_k):
batch_size, len_q = seq_q.size()
batch_size, len_k = seq_k.size()
# eq(0)函数留下了值为0的坐标
pad_attn_mask = seq_k.data.eq(0).unsqueeze(1) # batch_size x 1 x len_k
return pad_attn_mask.expand(batch_size, len_q, len_k) # batch_size x len_q x len_k
# 计算多头注意力得分
class ScaledDotProductAttention(nn.Module):
def __init__(self):
super(ScaledDotProductAttention, self).__init__()
def forward(self, Q, K, V, attn_mask):
"""
QKV的大小应该都一样
:param Q: (batch_size, n_heads, len_q, d_k),代表了每一个多头下的所有词向量的中间矩阵
:param K: (batch_size, n_heads, len_k, d_k)
:param V: (batch_size, n_heads, len_v, d_k)
:param attn_mask: (batch_size, n_heads, len_q, len_k)
:return:
"""
## 输入进来的维度分别是 [batch_size x n_heads x len_q x d_k] K: [batch_size x n_heads x len_k x d_k] V: [batch_size x n_heads x len_k x d_v]
##首先经过matmul函数得到的scores形状是 : [batch_size x n_heads x len_q x len_k]
# matmul就是矩阵乘法,其中对于多维向量会先提取出batch(会广播到最大batch),反正要压缩到最后的一个二维矩阵进行相乘
scores = torch.matmul(Q, # (batch_size, n_head, len_q, d_k)
K.transpose(-1, -2) # (batch_size, n_head, d_k, len_k) 相当于k的转置,矩阵乘法
) / np.sqrt(d_k) # 除以根号d_k
## 下面这个就是用到了我们之前重点讲的attn_mask,把被mask的地方置为无限小,softmax之后基本就是0,对q的单词不起作用
# attn_mask是上三角矩阵,已经是False和True的了,True的位置会被填values,也就是1e-9
# score的大小为(batch_size, n_head, len_q, len_k)
# attn_mask大小为(batch_size, n_head, len_q, len_k)
scores.masked_fill_(attn_mask, -1e9) # 将上三角未来信息进行隐藏,越往下代表了这个词在句子中的位置越靠后
attn = nn.Softmax(dim=-1)(scores) # 进行激活(batch_size, n_head, len_q, len_k)
context = torch.matmul(attn, V) # 最后计算(batch_size, n_head, len_q, len_k) X (batch_size, n_head, len_v, d_k), len_k = len_v
# context = (batch_size, n_head, len_q, d_k)
# attn = (batch_size, n_head, len_q, len_k)
return context, attn
# 多头注意力机制
class MultiHeadAttention(nn.Module):
def __init__(self):
super(MultiHeadAttention, self).__init__()
## 我们会使用映射linear做一个映射得到参数矩阵W_q, W_k,W_v
# d_k 代表向量维度一般取64,固定值
# n_head是多头的数量
# 单个自注意力机制的组成是由:attention = softmax(Q * K转置 / sqrt(d_k))
self.W_Q = nn.Linear(d_model, d_k * n_heads) # 输入一个(loc, d_model)的词向量,将其转换为(loc, d_k * n_head)
self.W_K = nn.Linear(d_model, d_k * n_heads) # 同Q,K输出大小与Q一致
self.W_V = nn.Linear(d_model, d_v * n_heads)
self.linear = nn.Linear(n_heads * d_v, d_model) # 将注意力表格再次转换为原来规定的维度,权重矩阵的线性变换
self.layer_norm = nn.LayerNorm(d_model) # 进行层标准化,一行代码的事,输入层大小
def forward(self, Q, K, V, attn_mask):
"""
:param Q: X,这里的Q需要经过Wq线性变换才能变为目标Q
:param K: X,需要经过Wk线性变换
:param V: X,需要经过Wv线性变换
:param attn_mask: 是掩码矩阵,防止未来信息泄露,就是一个上三角矩阵!!!(batch_size, len_q, len_k)
:return: 下一个待处理的层
"""
## 这个多头分为这几个步骤,首先映射分头,然后计算atten_scores,然后计算atten_value;
## 输入进来的数据形状: Q: [batch_size x len_q x d_model], K: [batch_size x len_k x d_model], V: [batch_size x len_k x d_model]
residual, batch_size = Q, Q.size(0) # 将观测值提取出来,因为Q会经过下面的变换,Batch_size 没有问题
## 下面这个就是先映射,后用view分头;一定要注意的是q和k分头之后维度是一致,所以一看这里都是dk
# 以q_s距离,探讨大小变换
q_s = (self.W_Q(Q) # 当前Q(batch_size, len_q, d_k * n_heads)
.view(batch_size, -1, n_heads, d_k) # 将d_k和n_heads分开,每一个词向量都有一个n_heads行,d_k列的中间内容(batch_size, len_q, d_k, n_heads)
.transpose(1, 2)) # 将第二和第三维互换,(batch_size, n_heads, len_q, d_k)多头注意力的每一个头都有词向量len_q行,d_k列
# q_s: [batch_size x n_heads x len_q x d_k]
k_s = self.W_K(K).view(batch_size, -1, n_heads, d_k).transpose(1,
2) # k_s: [batch_size x n_heads x len_k x d_k]
v_s = self.W_V(V).view(batch_size, -1, n_heads, d_v).transpose(1,
2) # v_s: [batch_size x n_heads x len_k x d_v]
## [batch_size x n_heads x len_q x len_k],就是把pad信息重复了n个头上
attn_mask = (attn_mask.unsqueeze(1) # (batch_size, 1, len_q, len_k)代表了一个注意力机制下的的掩码矩阵,关注的是Q和K,而不是原始的词向量
.repeat(1, n_heads, 1, 1)) # repeat就是在某个维度上重复n次
# 输出为(batch_size, n_heads, lqn_q, len_k), 代表了每一个自注意力机制的掩码矩阵
## 得到的结果有两个:context: [batch_size x n_heads x len_q x d_v], attn: [batch_size x n_heads x len_q x len_k]
context, attn = ScaledDotProductAttention()(q_s, k_s, v_s, attn_mask)
context = (context.transpose(1, 2) # (batch_size, len_q, n_heads, d_v) 相当于每一个词都有对应的d_k,这俩是一样的
.contiguous() # 深拷贝
.view(batch_size, -1, n_heads * d_v)) # context(batch_size, len_q, n_head * d_v)降低维度,恢复成最初的V矩阵
# context: [batch_size x len_q x n_heads * d_v]
output = self.linear(context) # 乘上权重矩阵,返回初始的输入大小(batch_size, len_q, d_model), 其中len_q在一开始就判断过,其值大小与loc相同
# 残差链接:F(x) = H(x) - x,这里的F(x)是残差网络的值,x是真实值,H(x)是预测值,残差是预测值和观测值之间的差距,残差+预测值=真实值
# 下一层的输入output + residual = (batch_size, loc, d_model)
# attn = (batch_size, n_head, len_q, len_n)
return self.layer_norm(output + residual), attn
## 模型参数
d_model = 512 # Embedding Size,代表词嵌入的维度,每个词都会有512维
d_k = d_v = 64 # dimension of K(=Q), V
n_heads = 8 # 多头注意力机制的个数
总结
文章讨论了自注意力机制与多头自注意力机制,并使用掩码矩阵防止未来信息泄露。
注意pytorch代码中,最好标注出每一次输入输出的张量大小变换,更好理解其中的意义
PS:作为新手分享经验,如果有讲得不好的地方或者错误麻烦在评论区指出,我会努力修改学习的Orz