本篇博客对Transformer进行了回顾,并动手实践了一下,后续会继续完善遇到的小问题!
需要关注的
需要理解Attention、Self-Attention、Mult-head Attention三部分内容
理论部分参考:李宏毅机器学习2021
博客参考:
其他参考:
LLMs 相关知识及面试题
【研1基本功 (真的很简单)注意力机制】手写多头注意力机制
Attention
- 什么是注意力机制?
它的核心思想是让模型在处理序列数据时,能够聚焦于输入数据中与当前任务最相关的部分。 - 如何做注意力?
- 需要注意的内容
- 为什么softmax前要缩放?为什么是除以维度的根号?
可以参考:self-attention为什么要除以根号d_k
a. 首先要除以一个数,防止输入softmax的值过大,导致偏导数趋近于0
b. 选择根号d_k是因为可以使 得q*k的结果满足期望为0,方差为1的分布,类似于归一化。
- 为什么softmax前要缩放?为什么是除以维度的根号?
Self-Attention
Self-Attention 的关键点:Q、K、V是同一个东西,或者三者来源于同一个X,三者同源。
通过X找到X里面的关键点,从而更关注X的关键信息,忽略X的不重要信息。
- Attention和Self-Attention的区别
- 如何做自注意力
- 自注意力的意义
- Self-Attention vs LSTM&RNN
Mult-head Attention
- 什么是多头?
- 为什么多头?有什么作用
- 并行处理能力:多头注意力允许模型并行地处理多个注意力头,这可以显著提高计算效率。
- 捕捉不同信息:每个注意力头可以学习到序列的不同方面或特征,比如一个头可能关注语法结构,而另一个头可能关注语义信息。这样,模型可以更全面地理解输入数据。
- 增强表达能力:通过结合多个注意力头的输出,模型可以构建一个更加丰富和复杂的表示,这有助于捕捉更细微的语言特征。
- 减少参数冗余:尽管多头注意力增加了模型的复杂度,但它通过共享底层参数(如查询、键和值的参数)来减少整体参数数量,从而避免参数冗余。
- 提高泛化能力:多头注意力机制可以帮助模型更好地泛化到未见过的数据上,因为它可以从不同的角度理解输入,而不是依赖单一的、可能过于简化的表示。
- 灵活性和可扩展性:多头注意力机制非常灵活,可以根据任务的需要调整注意力头的数量,以适应不同的复杂性和数据特性。
import math
import torch
print(torch.randn(1,1,1)) # [batch, time ,dimension]
class multi_head_attention(torch.nn.Module):
def __init__(self, d_model, num_heads)->None:
super(multi_head_attention, self).__init__()
self.num_heads = num_heads
self.d_model = d_model
self.w_q = torch.nn.Linear(d_model, d_model)
self.w_k = torch.nn.Linear(d_model, d_model)
self.w_v = torch.nn.Linear(d_model, d_model)
self.w_combine = torch.nn.Linear(d_model, d_model)
self.softmax = torch.nn.Softmax(dim=-1)
def forward(self, query, key, value):
batch_size, time, dim = query.shape
n_d = self.d_model // self.num_heads
q, k, v = self.w_q(query), self.w_k(key), self.w_v(value)
q = q.view(batch_size, time, self.num_heads, n_d).permute(0, 2, 1, 3)
k = k.view(batch_size, time, self.num_heads, n_d).permute(0, 2, 1, 3)
v = v.view(batch_size, time, self.num_heads, n_d).permute(0, 2, 1, 3)
score = q @ k.transpose(2,3) / math.sqrt(n_d)
mask = torch.tril(torch.ones(time,time,dtype=torch.bool)) # 生成下三角矩阵(对角线下方是1),padding_mask的处理方式不同
score = score.masked_fill(mask==0,float('-inf'))
score = self.softmax(score) @ v # @是矩阵乘法运算,同torch.matmul()
score = score.permute(0, 2, 1, 3).contiguous(). view(batch_size, time, self.d_model)
output = self.w_combine(score)
return output
attention = multi_head_attention(d_model=512, num_heads=8)
X = torch.randn(128,64,512)
output = attention(X,X,X)
print(output,output.size())
位置编码:Positional Encoding
- 为什么需要位置编码
- 怎么做位置编码
- 位置编码的生成
mask细节
在 Transformer 模型中,mask
用于控制注意力机制的计算,确保模型能够处理不同类型的序列数据。
两个 mask
的实现:make_pad_mask
和 make_casual_mask
。它们分别用于处理不同的任务。
1. make_pad_mask
import torch
# 示例数据
"""
src = torch.tensor([[1, 2, 0, 0], [3, 4, 5, 0]]) 表示的是一个批次(batch)的两个句子,每个句子有 4 个词(或位置),每个位置的值表示词的索引。
具体来说:
src 的形状是 (2, 4),表示有 2 个句子(或者说 2 个样本),每个句子有 4 个位置。
1, 2, 0, 0 和 3, 4, 5, 0 分别是两个句子的词索引序列。
在这个上下文中:
1, 2, 3, 4, 5 是词的索引。
0 是填充符,用来补齐不同长度的句子。
"""
src = torch.tensor([[1, 2, 0, 0],
[3, 4, 5, 0]])
pad_idx = 0
def make_pad_mask(q, k, pad_idx_q, pad_idx_k):
len_q, len_k = q.size(1), k.size(1)
q_mask = q.ne(pad_idx_q).unsqueeze(1).unsqueeze(3)
q_mask = q_mask.repeat(1, 1, 1, len_k)
k_mask = k.ne(pad_idx_k).unsqueeze(1).unsqueeze(2)
k_mask = k_mask.repeat(1, 1, len_q, 1)
mask = q_mask & k_mask
return mask
# 生成填充掩码
src_mask = make_pad_mask(src, src, pad_idx, pad_idx)
print(src_mask)
# 输出
tensor([[[[ True, True, False, False],
[ True, True, False, False],
[False, False, False, False],
[False, False, False, False]]],
[[[ True, True, True, False],
[ True, True, True, False],
[ True, True, True, False],
[False, False, False, False]]]])
make_pad_mask
函数用于生成填充(padding)掩码。这个掩码确保模型在计算注意力时忽略填充的部分,避免对这些填充部分进行无用的计算。
实现细节:
q.ne(pad_idx_q).unsqueeze(1).unsqueeze(3)
:首先,将查询张量q
中的填充位置转换为布尔值(填充的位置为False
,其他为True
),然后扩展维度以便后续的广播操作。q.repeat(1, 1, 1, len_k)
:重复q
的掩码,以匹配k
的维度。k.ne(pad_idx_k).unsqueeze(1).unsqueeze(2)
:将键张量k
中的填充位置转换为布尔值,并扩展维度以便后续的广播操作。k.repeat(1, 1, len_q, 1)
:重复k
的掩码,以匹配q
的维度。q & k
:对查询和键的掩码进行按位与操作,得到最终的填充掩码。
用途:确保模型在计算注意力时,不关注填充的部分,只关注有效的序列数据。
使用位置:填充掩码通常应用于Encoder 和 Decoder 中的自注意力(Self-Attention)机制,以及 Decoder 中的Encoder-Decoder Attention 机制。
填充掩码的应用:
2. make_casual_mask
import torch
# 示例数据
trg = torch.tensor([[6, 7, 8], [9, 10, 11]])
def make_casual_mask(q, k):
len_q, len_k = q.size(1), k.size(1)
mask = torch.tril(torch.ones(len_q, len_k, dtype=torch.bool)).to(q.device)
return mask
# 生成自回归掩码
trg_mask = make_casual_mask(trg, trg)
print(trg_mask)
# 输出
tensor([[ True, False, False],
[ True, True, False],
[ True, True, True]])
trg_mask[i][j] 为 True 时,位置 i 的元素可以关注到位置 j 的元素
make_casual_mask
函数用于生成自回归(causal)掩码。这个掩码确保模型在生成序列时不会看到未来的信息,从而保持生成的自回归特性。
实现细节:
torch.tril(torch.ones(len_q, len_k)).type(torch.BoolTensor).to(self.device)
:创建一个下三角矩阵,确保当前位置只关注当前位置及之前的位置,未来的位置被掩盖。这是通过生成一个全为1
的矩阵,然后使用tril
函数将矩阵转换为下三角矩阵。type(torch.BoolTensor).to(self.device)
:将矩阵转换为布尔类型并移动到指定设备(通常是 GPU)。
用途: 确保模型在生成序列时,不会利用当前时间步之后的词,这对于训练自回归模型非常重要。
使用位置:自回归掩码主要应用于 Decoder 的自注意力(Self-Attention)机制中。
总结
make_pad_mask
用于处理填充位置,确保模型在计算注意力时忽略填充部分。make_casual_mask
用于自回归掩码,确保模型在生成序列时只关注当前位置及之前的位置。
这两个掩码在 Transformer 模型中发挥了不同的作用,分别处理填充和自回归的问题。
训练Transformer的流程(以文本翻译为例)
在训练 Transformer 模型时,每一步处理的数据维度可能会有所不同。以下是每一步及其对应的一般数据维度的概述:
-
原始文本:
- 维度:N/A(文本数据)
-
分词:
- 输入:句子列表(例如,[“Hello world”])
- 输出:单词列表(例如,[“Hello”, “world”])
- 维度:N/A(列表数据)
-
索引化:
- 输入:单词列表
- 输出:单词索引列表(例如,[5, 10])
- 维度:[句子数量, 序列长度]
-
填充或截断:
- 输入:索引列表
- 输出:填充或截断后的索引矩阵(例如,[[5, 10, 0, …, 0]])
- 维度:[batch_size, seq_len]
-
词嵌入:
- 输入:索引矩阵
- 输出:词嵌入矩阵(每个索引转换为一个嵌入向量)
- 维度:[batch_size, seq_len, embedding_dim]
-
位置编码:
- 输入:词嵌入矩阵
- 输出:位置编码矩阵(与词嵌入矩阵相加)
- 维度:[batch_size, seq_len, embedding_dim]
-
编码器处理:
- 输入:位置编码矩阵
- 输出:编码器输出矩阵(经过多头自注意力和前馈网络处理)
- 维度:[batch_size, seq_len, model_dim]
-
解码器处理(对于生成任务,如翻译):
- 输入:编码器输出矩阵
- 输出:解码器输出序列(逐个生成的单词的嵌入表示)
- 维度:[batch_size, seq_len, model_dim]
-
损失函数:
- 输入:解码器输出序列与目标序列
- 输出:损失值(例如,交叉熵损失)
- 维度:N/A(标量值)
-
反向传播:
- 根据损失函数计算梯度,并更新模型参数。
-
优化:
- 使用优化算法调整模型参数。
注意,embedding_dim
是词嵌入的维度,model_dim
是模型内部表示的维度,通常与 embedding_dim
相同或略大。batch_size
是一次处理的样本数量,seq_len
是序列的最大长度,实际长度可能因句子而异,但通过填充或截断操作统一为 seq_len
。
在实际应用中,这些维度可能会根据具体的模型配置和任务需求进行调整。例如,对于翻译任务,目标序列的处理方式与源序列类似,但目标序列的解码器输出将用于计算损失函数。