Transformer学习总结

一、Self-Attention详解
Attention
(
Q
,
K
,
V
)
=
softmax
(
Q
K
T
d
k
)
V
\text{Attention}(Q, K, V) = \text{softmax}\left(\frac{QK^T}{\sqrt{d_k}}\right) V
Attention(Q,K,V)=softmax(dkQKT)V
Q(Query)、K(Key)、V(Value)、
d
k
d_k
dk为Key向量的维度
1、如何计算Q、K、V
如图中所示,Q、K、V通过输入对应的嵌入向量与对应的
W
Q
、
W
K
、
W
V
W^Q、W^K、W^V
WQ、WK、WV相乘得到,
W
Q
、
W
K
、
W
V
W^Q、W^K、W^V
WQ、WK、WV是模型训练过程中学习到的权重矩阵,通过初试化、前向传播、计算损失、反向传播等方式进行更新,以达到最优的结果。
2、 Q K T QK^T QKT相乘的原理是什么
在自注意力机制中,我们计算
Q
K
T
QK^T
QKT 来衡量查询向量和键向量之间的相关性或相似性。
$Q$和 $K$通常表示多个查询和键向量组成的矩阵。设 $Q$是一个形状为
n
×
d
k
n \times d_k
n×dk 的矩阵,
K
K
K是一个形状为
m
×
d
k
m \times d_k
m×dk的矩阵,其中
n
n
n 和
m
m
m 分别为查询和键的数量,
d
k
d_k
dk 为它们的维度。
Q
K
T
QK^T
QKT的结果是一个形状为
n
×
m
n \times m
n×m的矩阵,其中每个元素
(
i
,
j
)
(i, j)
(i,j)表示第
i
i
i个查询向量
Q
i
Q_i
Qi与第
j
j
j个键向量
K
j
K_j
Kj 的内积。这些值反映了每个查询向量对每个键向量的注意力权重。
3、为什么要除以 d k \sqrt{d_k} dk,以及为什么要开根号
计算
Q
K
T
QK^T
QKT时值可能会变得非常的大,在进一步计算中(如
s
o
f
t
m
a
x
softmax
softmax 操作)出现数值不稳定的问题,例如梯度爆炸或消失。
采用
d
k
\sqrt{d_k}
dk而不是其他数值是因为这样的缩放与下列情况有直接关系:高维向量的长度在期望上与维度的平方根成比例,如果输入是均匀随机向量,这样可以更公平地处理不同维度的向量。
详细过程如下:
我们考虑一个高维向量,它的每一个元素是从均值为0,标准差为1的均匀或者标准正态分布中采样得到的随机变量。这个向量的维度为
d
k
d_k
dk。我们要计算这个向量的长度,即它的欧几里得范数:
∥
x
∥
2
=
x
1
2
+
x
2
2
+
⋯
+
x
n
2
\| \mathbf{x} \|_2 = \sqrt{x_1^2 + x_2^2 + \cdots + x_n^2}
∥x∥2=x12+x22+⋯+xn2
由于每个
x
i
x_i
xi是一个独立随机变量,并且服从均值为0和方差为1的分布,得到的平方
x
i
2
x_i^2
xi2服从方差为1的卡方分布。从统计学中我们知道,求和的均值将是这些变量的期望之和,因此期望是
d
k
⋅
E
(
x
i
2
)
d_k⋅E(x_i^2)
dk⋅E(xi2)。由于每个
x
i
2
x_i^2
xi2的期望为1,算出来的期望是
d
k
d_k
dk。
因此,向量的期望的平方根(即均方根值)就是
d
k
\sqrt{d_k}
dk。
4、计算过程
将得到的
S
o
f
t
m
a
x
Softmax
Softmax分数分别与每个Value向量相乘。这种办法的原理是,相乘后的值越大,就将更多的注意力放在他们身上。对于分数低的地方,这些位置的的词相关性不大,便可以忽略掉这些未知的词。
二、多头注意力机制(Multi-head Attention)
原理:通过
h
h
h个不同的线性变换对Q、K、V进行映射,然后将得到的h个不同的Attention拼接起来,再通过一个线性变换就可以得到Multi-head Attention。
本质:将同样的Q、K、V映射到原来的高维空间的不同子空间中进行Attention计算,最后再合并不同子空间的Attention信息。这样降低了每个head的Attention时没个向量的维度,在某种意义上防止了过拟合;由于Attention的不同的子空间有不同的分布,Multi-head Attention实际上是寻找了序列之间不同角度的关联关系,最后通过拼接这一步骤,将不同子空间捕获的关联关系再综合起来。
Scaled Dot-Product Attention(缩放点积注意力):表示子注意力机制的计算过程
# [2, 4, 100]/[2, 6, 100], 5
def transpose_qkv(X, num_heads):
"""为了多注意力头的并行计算而变换形状"""
# 输入X的形状:(batch_size,查询或者“键-值”对的个数,num_hiddens)
# 输出X的形状:(batch_size,查询或者“键-值”对的个数,num_heads,num_hiddens/num_heads)
# reshape(-1):首先把张量中的所有元素平铺,然后在变形成指定的形状
X = X.reshape(X.shape[0], X.shape[1], num_heads, -1) # 2*4*100/2*4*5 = 20, -1就代表20
# print(X.size())
# 输出X的形状:(batch_size,num_heads,查询或者“键-值”对的个数, num_hiddens/num_heads)
X = X.permute(0, 2, 1, 3) # 更改矩阵形状 torch.Size([2, 5, 4, 20])
# print(X.size())
# 最终输出的形状:(batch_size*num_heads,查询或者“键-值”对的个数, num_hiddens/num_heads)
output = X.reshape(-1, X.shape[2], X.shape[3]) # 2*5*4*20/4*20 = 10, -1就代表10
print("transpose_qkv:", output.size())
return output
# [10, 4, 20], 5
def transpose_output(X, num_heads):
"""逆转transpose_qkv函数的操作"""
# print(X.size())
X = X.reshape(-1, num_heads, X.shape[1], X.shape[2]) # [2, 5, 4, 20]
X = X.permute(0, 2, 1, 3) # [2, 4, 5, 20]
output = X.reshape(X.shape[0], X.shape[1], -1) # [2, 4, 100]
print("transpose_output:", output.size())
return output
class MultiHeadAttention(nn.Module):
"""多头注意力"""
# 100, 100, 100, 100, 5, 0.5
def __init__(self, key_size, query_size, value_size, num_hiddens,
num_heads, dropout, bias=False, **kwargs):
super(MultiHeadAttention, self).__init__(**kwargs)
self.num_heads = num_heads
self.DP_Attention = d2l.DotProductAttention(dropout) # 缩放点积注意力,舍弃50%的神经元参数
# 输入样本的大小、输出样本的大小、偏置设置为False
self.W_q = nn.Linear(query_size, num_hiddens, bias=bias) # [100, 100]
self.W_k = nn.Linear(key_size, num_hiddens, bias=bias) # [100, 100]
self.W_v = nn.Linear(value_size, num_hiddens, bias=bias) # [100, 100]
self.W_o = nn.Linear(num_hiddens, num_hiddens, bias=bias) # [100, 100]
# [2, 4, 100], [2, 6, 100], [2, 6, 100], torch.tensor([3, 2])
def forward(self, queries, keys, values, valid_lens):
# queries,keys,values的形状:(batch_size,查询或者“键-值”对的个数,num_hiddens)
# valid_lens 的形状:(batch_size,)或(batch_size,查询的个数)
# 经过变换后,输出的 queries,keys,values 的形状:
# (batch_size*num_heads,查询或者“键-值”对的个数,num_hiddens/num_heads)
queries = transpose_qkv(self.W_q(queries), self.num_heads)
keys = transpose_qkv(self.W_k(keys), self.num_heads)
values = transpose_qkv(self.W_v(values), self.num_heads)
# print(queries)
if valid_lens is not None:
# 在轴0,将第一项(标量或者矢量)复制num_heads次,
# 然后如此复制第二项,然后诸如此类。
valid_lens = torch.repeat_interleave(valid_lens, repeats=self.num_heads, dim=0)
# print(valid_lens) # tensor([3, 3, 3, 3, 3, 2, 2, 2, 2, 2])
print("valid_lens:", valid_lens.size())
# output的形状:(batch_size*num_heads,查询的个数,num_hiddens/num_heads)
# torch.Size([10, 4, 20])、torch.Size([10, 6, 20])、torch.Size([10, 6, 20])、torch.Size([10])
output = self.DP_Attention(queries, keys, values, valid_lens)
# output_concat的形状:(batch_size,查询的个数,num_hiddens)
output_concat = transpose_output(output, self.num_heads)
return self.W_o(output_concat)
num_hiddens, num_heads = 100, 5
attention = MultiHeadAttention(num_hiddens, num_hiddens, num_hiddens,
num_hiddens, num_heads, 0.5)
attention.eval()
batch_size, num_queries = 2, 4
num_kvpairs, valid_lens = 6, torch.tensor([3, 2])
X = torch.ones((batch_size, num_queries, num_hiddens)) # [2, 4, 100]
Y = torch.ones((batch_size, num_kvpairs, num_hiddens)) # [2, 6, 100]
print("result:", attention(X, Y, Y, valid_lens).shape) # torch.Size([2, 4, 100])
三、位置前馈网络(Position-wise Feed-Forward Networks)
-
位置不变性:位置前馈网络是对每个位置单独应用的相同的全连接网络。这意味着它在序列中的每个位置上独立地处理输入特征,不依赖于其他位置的特征。这种设计保留了位置的不变性。
-
网络结构:位置前馈网络由两层全连接层组成,通常包含非线性激活函数。给定输入向量 ( \boldsymbol{x} ),网络实现的公式是:
FFN ( x ) = ReLU ( x W 1 + b 1 ) W 2 + b 2 \text{FFN}(\boldsymbol{x}) = \text{ReLU}( \boldsymbol{x} \mathbf{W}_1 + \mathbf{b}_1) \mathbf{W}_2 + \mathbf{b}_2 FFN(x)=ReLU(xW1+b1)W2+b2
其中, W 1 \mathbf{W}_1 W1 和 W 2 \mathbf{W}_2 W2 是线性变换的权重矩阵, b 1 \mathbf{b}_1 b1和 b 2 \mathbf{b}_2 b2 是偏置向量, ReLU \text{ReLU} ReLU 是一种非线性激活函数。
-
维度变化:通常第一个全连接层会将输入向量的维度从较低维度增加到较高维度(例如乘以一个放大因子),然后第二层将维度缩减回原始大小。这样做的目标是提高模型的表达能力。
-
独立运算:由于位置前馈网络对每个位置应用相同的参数,它非常高效并且容易并行化。这种并行性的设计非常适合在 GPU 上进行加速计算。
-
结合层归一化:在 Transformer 中,位置前馈网络通常结合层归一化(Layer Normalization)使用,以确保模型的稳定性和训练效率。
作用:位置前馈网络在语言模型中起到提升特征表达能力的作用,帮助模型更好地捕获输入序列的复杂模式和结构,同时保证计算效率。
import math
import pandas as pd
import torch
from matplotlib import pyplot as plt
from torch import nn
from d2l import torch as d2l
class PositionWiseFFN(nn.Module):
"""基于位置的前馈网络"""
def __init__(self, ffn_num_input, ffn_num_hiddens, ffn_num_outputs,
**kwargs):
super(PositionWiseFFN, self).__init__(**kwargs)
self.dense1 = nn.Linear(ffn_num_input, ffn_num_hiddens)
self.relu = nn.ReLU()
self.dense2 = nn.Linear(ffn_num_hiddens, ffn_num_outputs)
def forward(self, X):
return self.dense2(self.relu(self.dense1(X)))
# demo
# [2, 3, 4] * [4, 4] * [4, 8] = [2, 3, 8]
ffn = PositionWiseFFN(4, 4, 8)
ffn.eval()
print("基于位置的前馈网络:")
print(ffn(torch.ones((2, 3, 4)))[0])
ln = nn.LayerNorm(2) # 层规范化
bn = nn.BatchNorm1d(2) # 批标准化
X = torch.tensor([[1, 2], [2, 3]], dtype=torch.float32)
# 在训练模式下计算 X 的均值和方差
print('layer norm:', ln(X), '\nbatch norm:', bn(X))
四、残差连接与层归一化
残差连接(Residual Connection)和层归一化(Layer Normalization)是深度学习及 Transformer 模型中的关键技术,有助于模型的稳定性和训练效率。以下是对这两者的详细讲解:
1、残差连接
- 概念与目的:
残差连接是一种连接方式,用于缓解梯度消失或梯度爆炸问题,尤其是在深层神经网络中。
其主要思想是通过引入身份映射(identity mapping),使得网络相对更容易优化。具体来说,它允许梯度直接通过跳跃层(skip-layer)进行反向传播,从而减轻了梯度消失的问题。 - 优点:
残差连接的优点包括改善梯度流、提升网络表现以及加快收敛速度。
2、层归一化
-
概念与目的:
层归一化是一种归一化技术,它对单个数据样本的特征进行标准化,而非对整个批次进行处理。
层归一化旨在减少内部协变量偏移,提高训练稳定性和加速模型训练效果。 -
公式:
对于输入向量 x i \boldsymbol{x}_i xi的每个元素 x i x_i xi,层归一化的计算公式为:
x ^ i = x i − μ σ \hat{x}_i = \frac{x_i - \mu}{\sigma} x^i=σxi−μ
其中, μ \mu μ 是输入向量的均值, σ \sigma σ 是输入向量的标准差。在归一化后,还会应用可训练的偏置 β \boldsymbol{\beta} β 和缩放参数 γ \boldsymbol{\gamma} γ:
LN ( x ) = γ ⋅ x ^ + β \text{LN}(\boldsymbol{x}) = \boldsymbol{\gamma} \cdot \hat{\boldsymbol{x}} + \boldsymbol{\beta} LN(x)=γ⋅x^+β -
优点:
优点包括减小训练的不稳定性,提高收敛速度,并且不依赖批次大小。
在 Transformer 模型中,残差连接和层归一化通常结合使用,以确保每一层处理后结果的稳定性和模型的高效训练。通过确保模型的稳定性和可训练性,这两者共同提升了网络的整体性能。
class AddNorm(nn.Module):
"""残差连接后进行层规范化"""
def __init__(self, normalized_shape, dropout, **kwargs):
super(AddNorm, self).__init__(**kwargs)
self.dropout = nn.Dropout(dropout)
self.ln = nn.LayerNorm(normalized_shape)
def forward(self, X, Y):
return self.ln(self.dropout(Y) + X)
# demo
add_norm = AddNorm([3, 4], 0.5)
add_norm.eval()
print("残差连接后进行层规范化:")
print(add_norm(torch.ones((2, 3, 4)), torch.ones((2, 3, 4))).shape)
五、位置编码(Positional Encoding)
位置编码( P o s i t i o n a l E n c o d i n g Positional Encoding PositionalEncoding)是 Transformer 模型中的重要机制之一,用来处理输入序列的位置信息。由于自注意力机制不配备序列的绝对位置意识,这导致必须使用额外的信息来让模型理解序列元素的顺序。位置编码填补了这个空缺,它能够把输入中的位置信息注入到模型中。
1. 位置编码的必要性
输入序列的无序性:自注意力机制处理序列的方式不依赖于元素的顺序,它仅关注元素之间的相似性和关系。因此,在输入的序列中需要一种方法来注入位置信息,以便模型能够区别开原始输入的位置。
传统 RNN 的劣势:传统的循环神经网络(RNN)由于其内在结构能够隐式捕获序列顺序信息,但它们难以并行化。而 Transformer 模型完全基于注意力机制,提升了并行计算能力,但需要外部编码策略来引入位置信息。
2. 位置编码的实现
正弦和余弦函数:用来生成固定的位置编码,是最普遍的实现方法。给定位置 (pos) 和维度 (i),位置编码的公式是:
P E ( p o s , 2 i ) = sin ( p o s 1000 0 2 i d model ) PE_{(pos, 2i)} = \sin\left(\frac{pos}{10000^{\frac{2i}{d_{\text{model}}}}}\right) PE(pos,2i)=sin(10000dmodel2ipos)
P E ( p o s , 2 i + 1 ) = cos ( p o s 1000 0 2 i d model ) PE_{(pos, 2i+1)} = \cos\left(\frac{pos}{10000^{\frac{2i}{d_{\text{model}}}}}\right) PE(pos,2i+1)=cos(10000dmodel2ipos)
这里, d model d_{\text{model}} dmodel 是模型的输入向量的维度。例如,在一个序列中,偶数位置使用正弦函数,奇数位置使用余弦函数。
- 特点:
- 正弦和余弦函数提供了一种周期性的位置信息编码方式,它允许模型可以通过这些变换的信息量来推断序列位置。
- 这种方法不增加额外的可学习参数,并且提供了一种稳定的编码方式。
六、Mask(掩码)
Mask 表示掩码,它对某些值进行掩盖,使其在参数更新时不产生效果。Transformer 模型里面涉及两种 mask,分别是 Padding Mask 和 Sequence Mask。其中,Padding Mask 在所有的 scaled dot-product attention 里面都需要用到,而 Sequence Mask 只有在 Decoder 的 Self-Attention 里面用到。
1、Padding Mask
什么是 Padding mask 呢?因为每个批次输入序列的长度是不一样的,所以我们要对输入序列进行对齐。具体来说,就是在较短的序列后面填充 0(但是如果输入的序列太长,则是截断,把多余的直接舍弃)。因为这些填充的位置,其实是没有什么意义的,所以我们的 Attention 机制不应该把注意力放在这些位置上,所以我们需要进行一些处理。
具体的做法:把这些位置的值加上一个非常大的负数(负无穷),这样的话,经过 Softmax 后,这些位置的概率就会接近 0。
2、Sequence Mask
Sequence Mask 是为了使得 Decoder 不能看见未来的信息。也就是对于一个序列,在 t t t 时刻,我们的解码输出应该只能依赖于 t t t 时刻之前的输出,而不能依赖 t t t 之后的输出。因为我们需要想一个办法,把 t t t 之后的信息给隐藏起来。
具体的做法:产生一个上三角矩阵,上三角的值全为 0。把这个矩阵作用在每个序列上,就可以达到我们的目的。