【NLP自然语言处理学习笔记06 attention is all you need理论+pytorch版代码理解】
Transformers是一种用于自然语言处理(NLP)任务的神经网络架构。它由多个编码器-解码器层组成,其中每个层都包含一个自注意力机制。自注意力机制使得模型能够在输入序列的各个位置上进行关注和权重分配,以便更好地捕捉序列中的相关性。自注意力机制通过将输入序列中的每个元素与所有其他元素进行比较和加权,为每个元素分配一个注意力权重。这个过程可以帮助模型更好地理解序列中元素之间的依赖关系。
transform
前言
Transformers是一种用于自然语言处理(NLP)任务的神经网络架构。它由多个编码器-解码器层组成,其中每个层都包含一个自注意力机制。
自注意力机制使得模型能够在输入序列的各个位置上进行关注和权重分配,以便更好地捕捉序列中的相关性。
自注意力机制通过将输入序列中的每个元素与所有其他元素进行比较和加权,为每个元素分配一个注意力权重。这个过程可以帮助模型更好地理解序列中元素之间的依赖关系。
因此,可以说自注意力是transformer模型中的关键步骤之一,它使得transformer能够在处理序列数据时具备更强大的建模能力。

transformer
许多encoder(self-attentio➕feed forward)组成transform的encoder;
许多decoder(self-attention➕encoder-decoder attention➕feed forward)组成transformer的decoder;
feed forward: 做一个非线性的转换
PositionalEncoding 位置信息
加一个position embedding,加上一些时序信息

ℹ️是词嵌入的维度,i越大,sin越平缓
position encoding:是一个固定计算出来的位置向量不需要训练(bert中需要)[ , ,embedding dimension]
class PositionalEncoding(nn.Module):
def __init__(self, d_hid, n_position=200):
super(PositionalEncoding, self).__init__()
# Not a parameter
self.register_buffer('pos_table', self._get_sinusoid_encoding_table(n_position, d_hid))
#论文3.5章节
#生成一个正弦位置编码表,命名为pos_table
def _get_sinusoid_encoding_table(self, n_position, d_hid):
''' Sinusoid position encoding table '''
def get_position_angle_vec(position):
return [position / np.power(10000, 2 * (hid_j // 2) / d_hid) for hid_j in range(d_hid)]
sinusoid_table = np.array([get_position_angle_vec(pos_i) for pos_i in range(n_position)])
sinusoid_table[:, 0::2] = np.sin(sinusoid_table[:, 0::2]) # dim 2i
sinusoid_table[:, 1::2] = np.cos(sinusoid_table[:, 1::2]) # dim 2i+1
return torch.FloatTensor(sinusoid_table).unsqueeze(0)
def forward(self, x):
return x + self.pos_table[:, :x.size(1)].clone().detach()
self- attention的过程
输入序列被分别映射为查询(Query)、键(Key)和值(Value)的向量表示,记作Q、K和V(Q、K、V不一定和原来矩阵同型)
自注意力机制的关键之处在于其能够同时考虑序列中的所有位置之间的相关性,而不仅仅局限于局部区域。这使得模型能够更好地捕捉长距离依赖关系,在处理自然语言处理等任务时有较好的表现。

对于Q、K、V的理解:
Q(相当于一个问卷)、K(找到关键内容)、V(关键内容上花时间)
Scaled Dot-Product Attention缩放点积注意力
实现下面操作:得到一个输出张量output和注意力矩阵attn

class ScaledDotProductAttention(nn.Module):
def __init__(self, temperature, attn_dropout=0.1):
super().__init__()
self.temperature = temperature # temperature是缩放因子,用于缩放点乘操作的结果
self.dropout = nn.Dropout(attn_dropout)
def forward(self, q, k, v, mask=None): # 表示是否进行遮蔽操作
# Q与K点乘,然后进行缩放(除以self.temperature),最后使用矩阵转置操作得到注意力矩阵
attn = torch.matmul(q / self.temperature, k.transpose(2, 3))
if mask is not None:
# 用-1e9填充注意力矩阵中对应位置的值,以实现遮蔽,填充一个超小的数,可以在soft Max时将这里的权重也变得很小
attn = attn.masked_fill(mask == 0, -1e9)
attn = self.dropout(F.softmax(attn, dim=-1)) # 避免过拟合
output = torch.matmul(attn, v) # 将归一化的注意力权重与值进行加权求和操作,得到权重矩阵
return output, attn
当查询和键是不同长度的⽮量时,可以使⽤可加性注意⼒评分函数。当它们的长度相同时,使⽤缩放的 “点-积”注意⼒评分函数的计算效率更⾼。
- 加性注意力(Additive Attention):使用两个独立的线性变换来计算注意力权重。
具体步骤如下:
(1)将查询(query)和键(key)分别通过两个线性变换映射到不同的特征空间。
(2)将得到的特征进行相加,并通过激活函数(如tanh或ReLU)进行非线性转换。
(3)将转换后的结果通过另一个线性变换得到注意力权重。
(4)使用注意力权重对值(value)进行加权求和,得到最终的输出。 - 点积注意力(Dot-Product Attention):使用查询(query)和键(key)的点积作为注意力权重的评分。
具体步骤如下:
(1)对查询和键进行点积操作。
(2)对点积结果进行缩放,一般是除以一个相对较大的数值(如根号下特征维度),以避免点积结果过大。
(3)使用softmax函数对缩放后的结果进行归一化,得到注意力权重。
(4)使用注意力权重对值(value)进行加权求和,得到最终的输出。
Multi-Head Attention
由于需要不同的信息,所以常常需要不同的W来生成信息,但是最后我们要的是一个向量,所以每个头得到的Z1,Z2 …进行加权平均得到最终的Z’;

class MultiHeadAttention(nn.Module):
def __init__(self, n_head, d_model, d_k, d_v, dropout=0.1):
super().__init__()
self.n_head = n_head
self.d_k = d_k
self.d_v = d_v
#生成W矩阵,用于点积
self.w_qs = nn.Linear(d_model, n_head * d_k, bias=False)
self.w_ks = nn.Linear(d_model, n_head * d_k, bias=False)
self.w_vs = nn.Linear(d_model, n_head * d_v, bias=False)
self.fc = nn.Linear(n_head * d_v, d_model, bias=False)
self.attention = ScaledDotProductAttention(temperature=d_k ** 0.5)
self.dropout = nn.Dropout(dropout)
self.layer_norm = nn.LayerNorm(d_model, eps=1e-6)
def forward(self, q, k, v, mask=None):
# 将x词向量复制三次,分别用于计算查询(query)、键(key)和值(value)
d_k, d_v, n_head = self.d_k, self.d_v, self.n_head
#对查询、键和值进行维度转置和拼接操作,使它们的形状符合缩放点积注意力的输入要求
sz_b, len_q, len_k, len_v = q.size(0), q.size(1), k.size(1), v.size(1)
residual = q
# 经过预注意力投影:b x lq x (n*dv)
# 将不同的头部分开:b x lq x n x dv
# 将W(batch_size, sequence_length, hidden_size)投影到形状为(batch_size, sequence_length, num_heads, head_size)的四维张量
q = self.w_qs(q).view(sz_b, len_q, n_head, d_k)
k = self.w_ks(k).view(sz_b, len_k, n_head, d_k)
v = self.w_vs(v).view(sz_b, len_v, n_head, d_v)
# Transpose for attention dot product: b x n x lq x dv
q, k, v = q.transpose(1, 2), k.transpose(1, 2), v.transpose(1, 2)
if mask is not None:
mask = mask.unsqueeze(1) # For head axis broadcasting.
# 调用缩放点积注意力模块进行计算,得到输出结果和注意力权重矩阵
q, attn = self.attention(q, k, v, mask=mask)
# 转置、调整形状,并且确保结果张量是连续存储的 b x lq x (n*dv)
# Transpose to move the head dimension back: b x lq x n x dv
q = q.transpose(1, 2).contiguous().view(sz_b, len_q, -1)
q = self.dropout(self.fc(q))
q += residual #进行参差连接
q = self.layer_norm(q) #对参差结果进行归一化
return q, attn
feed forward
FeedForward过程,可以实现维度的变换,这里用了一个两层前馈神经网络,得到一个与输入维度相同的输出
class PositionwiseFeedForward(nn.Module):
def __init__(self, d_in, d_hid, dropout=0.1):
super().__init__()
self.w_1 = nn.Linear(d_in, d_hid) # position-wise
self.w_2 = nn.Linear(d_hid, d_in) # position-wise
self.layer_norm = nn.LayerNorm(d_in, eps=1e-6)
self.dropout = nn.Dropout(dropout)
def forward(self, x):
residual = x #一会用于参差计算
#进行了一个两层的非线性变换
x = self.w_2(F.relu(self.w_1(x)))
x = self.dropout(x)
x += residual #进行参差连接
x = self.layer_norm(x) #对参差连接后的结果进行归一化
return x
encoder部分
input—>[batch_size,sequence length]
embeddings—>[batch_size,sequence length,embedding dimension]得到词嵌入
embeddings➕position encoding进行一个参差连接

每层encoder由一个self- attention和feed back组成:
class EncoderLayer(nn.Module):
def __init__(self, d_model, d_inner, n_head, d_k, d_v, dropout=0.1):
super(EncoderLayer, self).__init__()
self.slf_attn = MultiHeadAttention(n_head, d_model, d_k, d_v, dropout=dropout) # multi head attention的过程,得到注意力矩阵
self.pos_ffn = PositionwiseFeedForward(d_model, d_inner, dropout=dropout) #得到feed forward,可以得到feedback后的结果
def forward(self, enc_input, slf_attn_mask=None): #输出结果不用进行填充
enc_output, enc_slf_attn = self.slf_attn(enc_input, enc_input, enc_input, mask=slf_attn_mask) #进行一次self-attention
enc_output = self.pos_ffn(enc_output)#进行一次feed forward
return enc_output, enc_slf_attn
将输入序列经过词嵌入、位置编码、多层编码层迭代处理,并可以选择是否返回编码层的自注意力矩阵输出。最后返回编码的结果
class Encoder(nn.Module):
def __init__(
self, n_src_vocab, d_word_vec, n_layers, n_head, d_k, d_v,
d_model, d_inner, pad_idx, dropout=0.1, n_position=200, scale_emb=False):
super().__init__()
self.src_word_emb = nn.Embedding(n_src_vocab, d_word_vec, padding_idx=pad_idx) # 对原句进行词嵌入,并用0补齐输入大小
self.position_enc = PositionalEncoding(d_word_vec, n_position=n_position) # 拿到原句位置矩阵
self.dropout = nn.Dropout(p=dropout)
self.layer_stack = nn.ModuleList([
EncoderLayer(d_model, d_inner, n_head, d_k, d_v, dropout=dropout)
for _ in range(n_layers)]) #迭代了 6次encoder-layer 过程,将每次的结果的矩阵存放到layer_stack中
self.layer_norm = nn.LayerNorm(d_model, eps=1e-6)
self.scale_emb = scale_emb
self.d_model = d_model
def forward(self, src_seq, src_mask, return_attns=False):
enc_slf_attn_list = []
# -- Forward
enc_output = self.src_word_emb(src_seq) #得到原句的词嵌入
#如果 scale_emb 为 True,将对 enc_output 进行缩放操作。缩放的方式是将 enc_output 中的每个元素乘以 self.d_model 的平方根
#可以提升模型表达能力并减轻梯度消失或爆炸的问题
if self.scale_emb:
enc_output *= self.d_model ** 0.5
#对 enc_output 进行位置编码操作,并进行过拟合
enc_output = self.dropout(self.position_enc(enc_output))
enc_output = self.layer_norm(enc_output) #进行归一化
#得到注意力列表
for enc_layer in self.layer_stack:
enc_output, enc_slf_attn = enc_layer(enc_output, slf_attn_mask=src_mask) #取出每次的输出结果和注意力矩阵
# 将每个编码层的自注意力矩阵输出存储到 enc_slf_attn_list 中
enc_slf_attn_list += [enc_slf_attn] if return_attns else []
#用于控制是否返回编码层自注意力机制的输出列表 enc_slf_attn_list
if return_attns:
return enc_output, enc_slf_attn_list
return enc_output
decoder部分
DecoderLayer 类,它是由三个子层组成的:自注意力子层、编码器-解码器注意力子层、位置前馈神经网络子层
class DecoderLayer(nn.Module):
''' Compose with three layers '''
def __init__(self, d_model, d_inner, n_head, d_k, d_v, dropout=0.1):
super(DecoderLayer, self).__init__()
self.slf_attn = MultiHeadAttention(n_head, d_model, d_k, d_v, dropout=dropout)
self.enc_attn = MultiHeadAttention(n_head, d_model, d_k, d_v, dropout=dropout)
self.pos_ffn = PositionwiseFeedForward(d_model, d_inner, dropout=dropout)
def forward(
self, dec_input, enc_output,
slf_attn_mask=None, dec_enc_attn_mask=None):
dec_output, dec_slf_attn = self.slf_attn(dec_input, dec_input, dec_input, mask=slf_attn_mask) #自注意力子层
dec_output, dec_enc_attn = self.enc_attn(dec_output, enc_output, enc_output, mask=dec_enc_attn_mask) #编码器-解码器注意力子层
dec_output = self.pos_ffn(dec_output) #进行feed back
return dec_output, dec_slf_attn, dec_enc_attn
将编码器的输出转换为目标语言的序列
class Decoder(nn.Module):
def __init__(
self, n_trg_vocab, d_word_vec, n_layers, n_head, d_k, d_v,
d_model, d_inner, pad_idx, n_position=200, dropout=0.1, scale_emb=False):
super().__init__()
self.trg_word_emb = nn.Embedding(n_trg_vocab, d_word_vec, padding_idx=pad_idx)
self.position_enc = PositionalEncoding(d_word_vec, n_position=n_position) #用于创建位置编码
self.dropout = nn.Dropout(p=dropout)
self.layer_stack = nn.ModuleList([
DecoderLayer(d_model, d_inner, n_head, d_k, d_v, dropout=dropout)
for _ in range(n_layers)])
self.layer_norm = nn.LayerNorm(d_model, eps=1e-6)
self.scale_emb = scale_emb
self.d_model = d_model
def forward(self, trg_seq, trg_mask, enc_output, src_mask, return_attns=False):
dec_slf_attn_list, dec_enc_attn_list = [], []
# -- Forward
dec_output = self.trg_word_emb(trg_seq) # 得到目标输出y的词嵌入
if self.scale_emb:
dec_output *= self.d_model ** 0.5 # 进行缩放
dec_output = self.dropout(self.position_enc(dec_output)) # 得到目标y位置编码,并进行dropout
dec_output = self.layer_norm(dec_output)
# 得到解码器自注意力和编码器-解码器注意力
for dec_layer in self.layer_stack:
dec_output, dec_slf_attn, dec_enc_attn = dec_layer(
dec_output, enc_output, slf_attn_mask=trg_mask, dec_enc_attn_mask=src_mask)
dec_slf_attn_list += [dec_slf_attn] if return_attns else []
dec_enc_attn_list += [dec_enc_attn] if return_attns else []
# 用于控制是否返回注意力列表
if return_attns:
return dec_output, dec_slf_attn_list, dec_enc_attn_list
return dec_output,
思考🤔️
- feed forward和linear有区别
"Linear"层通常是指一个简单的线性变换,它将输入数据乘以一个权重矩阵,并加上一个可学习的偏差向量。这个线性变换没有引入非线性操作,仅仅只是对输入进行线性映射。在神经网络中,线性层常用于提取输入的线性组合特征。
"feed forward"层在transformer中是指由两个线性变换和激活函数组成的非线性变换层。它接收输入数据,通过一个线性变换(通常是一个全连接层),然后应用一个非线性激活函数(通常是ReLU),最后再通过另一个线性变换得到输出。"Feed forward"层引入了非线性操作,在模型中起到了增强特征表示能力的作用。
- encoder的S- A🆚decoder的S-A🆚decoder的E-D S-A
encoder中的self- attention是两个方向的;
decoder的self- attention只能包含前面的(虽然他里面包含了所两个方向的信息);
decoder的self- attention输入和输出均用的decoder的input和output;而enc-dec self-attention用的是en-output作为Q和de-output作为K和V,因为Q是查询,是已经生成的词,通过已经生成的词对未来生成的词的作用
- 为什么transform要进行填充?
(1)为了计算注意力权重,需要将序列中的每个位置都考虑进去。如果序列长度不同,就无法直接应用自注意力机制
(2) transform使用Multi-head self-attention(多头自注意力),所以要求在每个头中输入序列的长度相同,可以进行并行计算
- 关于掩码
在train阶段,是需要进行mask的;为了解决线性自回归的产生;因为train阶段知道目标语句;在测试阶段是不会知道的,so就不用啦
更多推荐




所有评论(0)