Task03 Encoder
整体文章的行文结构是自顶向下地解释Encoder,从总体Encoder结构到具体每一个子模块,再到子模块里面的组成,解释其机理。
1. 编码器
1.1 Encoder的工作流程
Encoder模块,它由于6个相同的子模块串联起来。前面子模块的输出就是后面子模块的输入。
-
输入阶段
第一个Encoder的子模块的整个输入为:序列的所有单词的WordEmbedding表示+位置编码表示,这样的一个组合向量。 -
核心处理阶段
- 多头自注意力层处理,其实有点像CNN里面的多个卷积核的意思,目的是为了让不同的注意力层捕获不同的特征信息,发现不同的规律。里面的QKV都是来自同一个输入本身。
- 前馈层处理,由全连接网络等构成,对上一层输出的特征做一次非线性变换,然后输入到下一个Encoder。
-
残差与归一化阶段
前面多头自注意力层和前馈层里,它们都附加了残差连接这样的操作(一种并行通路,可以参考残差网络原理,作用是缓解梯度消失或者爆炸问题),也就是经过这些层的输出和输入是合并起来相加作为最后的输出。
1.2 Encoder组成部分
2.多头自注意力
Transformer中,多头注意力机制有多个并行的注意力模块去关注输入序列的空间关系,每一个模块都有自己的QKV,那么最后所有模块的输出会通过线性层做拼接。
都是针对QKV来解释各自含义,感觉太泛了,还是我来自己写写吧。
- 一般视频和书上解释QKV,是这么说的,Q就是要自己要查询感兴趣的方向,K代表了不同的方向,V代表了不同方向的具体内容,QK结合相当于匹配出Q想要关注的方向,然后拿着这个去V里提取这个方向对应的内容。
- 对于我来说,之前最难理解的是Q是怎么知道自己要查询什么。其实Q并不知道自己要查询什么,其实Q是源自于序列中每个词所代表的方向。例如其中一个词是‘苹果’,它既有科技含义也有水果含义,但整个输入里面的每个词所代表的方向都是科技的方向,经过QK匹配出,得出来的向量基本是科技方向的权重比较大,中间什么点积缩放先不说,然后去V里面把该方向的信息提取出来,整个过程的计算核心还是线性变换,相当于把’苹果’这个词的方向从原来的摇摆不定往科技方向拉一把,意思就是把对‘苹果’的理解往科技方向上靠拢,这样得出来的理解更贴近上下文内容。
- 以上是个比较粗糙的理解,还有个 W Q W_Q WQ权重没说,它是可以训练,训练得好的话是可以在模型推理时根据任务需求精准地对输入的每个词进行查询,帮助模型更好地捕捉语义关系。
2.1 缩放点积注意力
整个公式就是
A
t
t
e
n
t
i
o
n
(
Q
,
K
,
V
)
=
s
o
f
t
m
a
x
(
Q
K
T
d
k
)
V
Attention(Q,K,V) = softmax(\frac{QK^T}{\sqrt{}d_k})V
Attention(Q,K,V)=softmax(dkQKT)V
里面softmax出来的结果就是对各个方向进行加权求和,相关程度高的方向将会更高分。
在多头注意力中,
d
k
d_k
dk是由于词嵌入维度
d
m
o
d
e
l
d_{model}
dmodel与注意力个数h有关系,总的来说在多头注意力里,
d
k
d_k
dk=
d
m
o
d
e
l
d_{model}
dmodel/h
- 缩放因子的作用:防止内积过大,使得softmax后落入饱和区间,梯度为0。
2.2 多头注意力机制
多头(Multi-Head) 的方式是将多个 head 的输出 z,进行拼接(concat)后,通过线性变换得到最后的输出 z。
这里我又不得不出现了,task里面的公式和参数描述相当乱,无法有个完整参考可以串联思考。以下我给出我对Transformer中多头注意力超参数的理解。
:---------------------美丽的分割线-----------------------:
- Transformer中里面的多头注意力,有两个重要超参数,一个是头的数量h,另外一个是模型的输出维度 d m o d e l d_{model} dmodel。一般基础模型用的是h=8个头,而模型维度 d m o d e l d_{model} dmodel=512.
- 对于每个头来说,它们里面的QKV的向量维度为 d m o d e l h \frac{d_{model}}{h} hdmodel,这么看来,随着头越多,每个头处理的向量的维度变得更小,从而使多头注意力并行计算多个不同的注意力模式。
输入向量维度是 d m o d e l d_{model} dmodel=512,使用8个头来处理,也即每个头的输出维度为64,把8个头的输出再拼接回来还是512.
有实验表明,头之间的差距随着所在层数变大而减小,因此这种差异性是否是模型追求的还不一定。 至于头数 h 的设置,并不是越大越好,到了某一个数就没效果了。
2.3 自注意力机制 self attention
一般来说,自注意力中的QKV,都是来自同一个输入序列,允许模型在处理序列数据时候,同时考虑序列中所有元素之间的关系。
看看三种self attention:
- 编码器的自注意力(Encoder self-attention):
- 在 Encoder 部分进行
- 每个位置的词都能通过自注意力机制与序列中所有其他词建立联系。
- 每个词的表示会考虑到序列中其他所有词的上下文,因此 Encoder Self-Attention 是 双向的,它能够同时使用序列中 前后 的信息
- 带掩码的解码器里的自注意力(Masked Decoder Self-Attention)
- 在Decoder端进行
- 目的是让每个词在生成时可以关注到它前面已经生成的词
- Masking 的作用在于计算注意力时,会将当前位置之后的词的注意力权重屏蔽(通常是将权重设置为负无穷大)。这样,模型在生成第 𝑖个词时,只能利用前 𝑖−1个词的信息,而不能看到后续的词。这个过程确保了自回归生成,即每次生成一个词时,模型只能依赖于已经生成的部分,不能使用未来的词。
- 编码器-解码器组成的注意力(Encoder-Decoder Attention)
- Decoder 的输入通常是已生成的部分序列(或目标序列的部分),但是Encoder-Decoder Attention 的查询(Query)来自 Decoder 的输入,键(Key)和值(Value)来自 Encoder 的输出(即 Encoder 最终的表示)
- 可以看到,Decoder 可以从 Encoder 中提取上下文信息,使得 Decoder 能够根据整个输入序列来生成输出,不再局限于当前生成的词。
2.3.1 自注意力的特点
- 实践上:存在一些任务,RNN能够轻松应对,而Transformer则未能有效解决。例如,在复制字符串的任务中,或者当推理过程中遇到的序列长度超出训练时的最大长度时(由于遇到了未曾见过的位置嵌入),Transformer的表现不如RNN。
- 在处理NLP领域中的推理和决策等计算密集型问题时,存在固有的局限性。即无法独立完成某些复杂的计算任务。
2.3.2 里面的技巧
- 残差连接:成一个短路,允许梯度直接流过网络层,有助于缓解深层网络中的梯度消失问题。
- 层归一化:
- 减少了特征之间的尺度差异,这有助于避免某些特征在学习过程中占据主导地位,从而提高了模型的泛化能力和稳定性。
- 减少内部协变量偏移(Internal Covariate Shift)。这种偏移是指神经网络在训练过程中,由于参数更新导致的每层输入分布的变化。通过归一化,可以使得每一层的输入分布更加稳定,从而加速训练过程。
2.3.3 整个流程
-
Add & Norm 步骤:
- 计算子层输出:SubLayer(x)
- 残差连接:Res = x + SubLayer(x)
- 层归一化:out = LayNorm(Res)
-
回顾整个流程
- 输入的 x 序列经过 “Multi-Head Self-Attention”
- 之后再经过一个“Add & Norm”层
- 再进入“feed-forward network”(后面简称FFN)
- 在FFN之后又经过一个norm再输入下一个encoder layer
注意:几乎每个子层都会经过一个归一化操作,然后再将其加在原来的输入上,这个过程被称为残余连接(Residual Connection)
2.4 前馈全连接网络
- FFN实际上是一个线性变换层。
- FFN层是一个顺序结构:包括一个全连接层(FC) + ReLU 激活层 + 第二个全连接层(FC),通过在两个 FC 中间添加非线性变换,增加模型的表达能力,使模型能够捕捉到复杂的特征和模式。
- 第一层维度为2048,第二层为512。从参数来看是先升维,后降维,这是为了扩充中间层的表示能力,从而抵抗 ReLU 带来的模型表达能力的下降。
2.5 多头注意力 vs 多头自注意力
1.区别在哪里?查询、键、值的来源不同
- Multi-Head Attention : 查询(Query)、键(Key)和值(Value)可以来自不同的输入源。例如 Decoder部分,查询(Query)来自解码器当前的输入,而键(Key)和值(Value)通常来自编码器的输出。这种机制使得模型能够将解码器当前的信息与编码器已经处理好的信息进行关联,从而更好地生成输出序列。
- Multi-Head Self-Attention:查询(Query)、键(Key)和值(Value)都来自同一个输入序列。这意味着模型关注的是输入序列自身不同位置之间的关系。
2.功能重点有所差异
- Multi - Head Attention
主要用于融合不同来源的信息。比如在机器翻译的任务中,它用于将元文本经过 Encoder编码后的信息(作为 K 和 V )与解码器当前生成的部分目标语言句子(作为 Q )相结合,帮助解码器在生成目标语言句子时更好地参考源语言句子的语义和结构,从而生成更准确的翻译。- Multi-Head Self-Attention
更侧重于挖掘输入序列自身的内在结构和关系。在文本生成任务中,它可以帮助模型理解当前正在生成的文本自身的语义连贯和语法结构。例如,在续写一个故事时,通过 Multi-Head Self-Attention 可以让模型把握已经生成的部分文本的主题、情节发展等内部关系,以便更好地续写。
3. 交叉注意力
- 简述:
交叉注意力是 Encoder-Decoder Attention 的核心,指的是在 Decoder 中,生成目标序列的每个位置的表示都依赖于 Encoder 的输出,也就是说,Decoder 中的每个词的表示会与 Encoder 中所有词的表示进行交互。
- 特点:
- 作用范围: 交叉注意力计算时,Decoder 中的每个元素(例如目标序列中的某个词)会和来自 Encoder 的整个输出进行交互,Encoder 输出是源语言的表示,Decoder 输出是目标语言的表示。
- 信息流动:跨模块依赖,通过 Decoder 查询(Q)和 Encoder 提供的键(K)和值(V)进行交互。
- 目的: 让 Decoder 获取 Encoder 处理后的上下文信息。即,Decoder 会通过交叉注意力获取源语言信息,来帮助生成目标语言。
4. 交叉注意力 与 自注意力 主要的区别
交叉注意力代码展示
import torch
from torch import nn
import torch.nn.functional as F
import math
# 定义CrossAttention类
class CrossAttention(nn.Module):
def __init__(
self, dim, num_heads=8, qkv_bias=False, qk_scale=None, attn_drop=0.,
proj_drop=0., window_size=None, attn_head_dim=None):
super().__init__()
self.num_heads = num_heads
head_dim = dim // num_heads
if attn_head_dim is not None:
head_dim = attn_head_dim
all_head_dim = head_dim * self.num_heads
self.scale = qk_scale or head_dim ** -0.5
self.q = nn.Linear(dim, all_head_dim, bias=False)
self.k = nn.Linear(dim, all_head_dim, bias=False)
self.v = nn.Linear(dim, all_head_dim, bias=False)
if qkv_bias:
self.q_bias = nn.Parameter(torch.zeros(all_head_dim))
self.v_bias = nn.Parameter(torch.zeros(all_head_dim))
else:
self.q_bias = None
self.k_bias = None
self.v_bias = None
self.attn_drop = nn.Dropout(attn_drop)
self.proj = nn.Linear(all_head_dim, dim)
self.proj_drop = nn.Dropout(proj_drop)
def forward(self, x, bool_masked_pos=None, k=None, v=None):
B, N, C = x.shape
N_k = k.shape[1]
N_v = v.shape[1]
q_bias, k_bias, v_bias = None, None, None
if self.q_bias is not None:
q_bias = self.q_bias
k_bias = torch.zeros_like(self.v_bias, requires_grad=False)
v_bias = self.v_bias
q = F.linear(input=x, weight=self.q.weight, bias=q_bias)
q = q.reshape(B, N, 1, self.num_heads, -1).permute(2, 0, 3, 1, 4).squeeze(0) # (B, N_head, N_q, dim)
k = F.linear(input=k, weight=self.k.weight, bias=k_bias)
k = k.reshape(B, N_k, 1, self.num_heads, -1).permute(2, 0, 3, 1, 4).squeeze(0)
v = F.linear(input=v, weight=self.v.weight, bias=v_bias)
v = v.reshape(B, N_v, 1, self.num_heads, -1).permute(2, 0, 3, 1, 4).squeeze(0)
q = q * self.scale
attn = (q @ k.transpose(-2, -1)) # (B, N_head, N_q, N_k)
attn = attn.softmax(dim=-1)
attn = self.attn_drop(attn)
x = (attn @ v).transpose(1, 2).reshape(B, N, -1)
x = self.proj(x)
x = self.proj_drop(x)
return x
# 设置相关的维度参数和输入张量示例
batch_size = 2 # 批次大小
dim = 64 # 特征维度
num_heads = 4 # 头的数量
seq_len_query = 10 # 查询序列长度
seq_len_key_value = 8 # 键值对序列长度
# 随机生成输入张量,模拟查询、键、值
query = torch.rand(batch_size, seq_len_query, dim)
key = torch.rand(batch_size, seq_len_key_value, dim)
value = torch.rand(batch_size, seq_len_key_value, dim)
# 实例化CrossAttention模块
cross_attention_module = CrossAttention(dim=dim, num_heads=num_heads)
# 进行前向传播计算
output = cross_attention_module(query, k=key, v=value)
print("输出结果的形状:", output.shape)