深入理解Transformer
Transformer模型详解及学习笔记
本文将详细介绍Transformer模型的各个组成部分,包括多头注意力机制、位置前馈网络、残差连接、层归一化等,并结合给定的Python代码进行详细解析。
0.结构解析

输入:
我们输入序列后,token映射为高维向量,即embedding,每个向量的方向,大小对应了他们的语义.
position_encoding即位置编码,为了解决独热向量在长序列中产生高维向量出现冗余的情况,以及同一个词不同意思的情况,引入了位置编码,通过正余弦计算每一个词在文本中的位置,并加到文本向量中
编码器:
结构:由多头自注意力机制,残差归一化层,位置前馈神经网络组成
流程:1、输入经过向量化和加了位置编码的输入向量,2、在多头自注意力机制前,复制三份,分别对应Q, K ,V因为是自回归模型,在编码器时可以学习到每一个词的信息。然后query在多头注意力中,与所有的Key进行相似度计算,乘以所有的Value做加权和,得到输出后,进行LN且加上输入向量防止梯度消失。3、进入PositionFFN中再学习一次特征后得到编码器输出
解码器:
结构:包含了带掩码的多头自注意力机制,残差归一化层,交叉多头注意力机制,位置前馈神经网络
流程:前一层解码器的输出(目标序列)做embedding后输入到当前解码器中,在带掩码的多头自注意力机制前,复制三份,为什么要Mask因为在生成过程中是不可以对目标序列后的词做相似性计算,否则将毫无意义。然后通过MMA后,将编码器输出复制两份作为Key和Value,然后将MMA的输出作为Query,这样就可以捕捉到编码器输入序列的特征学习到上下文信息,编码器-解码器注意力机制允许解码器关注源序列中的不同位置,捕捉源序列和目标序列之间的关系。剩下的部分就是和编码器一样的输出流程不多赘述
这就是Transformer!!!!!
1. 张量变换函数
在多头注意力机制中,输入的查询矩阵Q 键矩阵K 和 值矩阵V 通常具有相同的特征维度。然而,每个注意力头可能需要处理的特征维度和可能与输入维度不同。通过张量变换,可以将输入张量的维度调整为适合每个注意力头的维度。
代码解析
def transpose_qkv(X, num_heads):
"""为了多注意力头的并行计算而变换形状"""
# 将 X 转换为 (batch_size, num_steps, num_heads, num_hiddens/num_heads)
X = X.reshape(X.shape[0], X.shape[1], num_heads, -1)
# 调整维度顺序为 (batch_size, num_heads, num_steps, num_hiddens/num_heads)
X = X.permute(0, 2, 1, 3)
# 合并前两个维度,以便后续操作
return X.reshape(-1, X.shape[2], X.shape[3])
def transpose_output(X, num_heads):
"""逆转 transpose_qkv 函数的操作"""
# 将 X 转换回 (batch_size, num_heads, num_steps, num_hiddens/num_heads)
X = X.reshape(-1, num_heads, X.shape[1], X.shape[2])
# 调整维度顺序为 (batch_size, num_steps, num_heads, num_hiddens/num_heads)
X = X.permute(0, 2, 1, 3)
# 合并后两个维度,恢复原始形状
return X.reshape(X.shape[0], X.shape[1], -1)
张量运算细节
transpose_qkv函数- 输入张量
X的形状为(batch_size, num_steps, num_hiddens)。 - 使用
reshape方法将X转换为(batch_size, num_steps, num_heads, num_hiddens // num_heads)。 - 使用
permute方法调整维度顺序为(batch_size, num_heads, num_steps, num_hiddens // num_heads)。 - 最后使用
reshape方法将张量形状变为(batch_size * num_heads, num_steps, num_hiddens // num_heads),以便于并行计算。
- 输入张量
transpose_output函数- 输入张量
X的形状为(batch_size * num_heads, num_steps, num_hiddens // num_heads)。 - 使用
reshape方法将X转换为(batch_size, num_heads, num_steps, num_hiddens // num_heads)。 - 使用
permute方法调整维度顺序为(batch_size, num_steps, num_heads, num_hiddens // num_heads)。 - 最后使用
reshape方法将张量形状恢复为(batch_size, num_steps, num_hiddens)。
- 输入张量
2. 多头注意力层
多头注意力机制的核心思想是通过多个不同的注意力头来捕获输入序列的不同特征。每个注意力头都有自己的权重矩阵,这些矩阵是在训练过程中学习得到的。
数学公式
多头注意力机制可以表示为:
其中,每个注意力头 head定义为:
注意力机制的具体计算如下:
从上述数学公式不难看出,多头注意力每一个头的权重矩阵都是不同的,之所以使用多头是因为多组不同的权重矩阵可以捕捉序列不同的模式信息,这样可以使模型对序列上下问的学习更加充分更具灵活性,每一个头得到输出矩阵后,再把他们全部连接起来乘以输出权重得到多头输出
为什么要除以根号下d_k:因为softmax函数的在值越大的时候越平缓,处于梯度,数值稳定性的考虑,选择在点乘后除以根号下特征维度,**使得梯度不会消失
其中:
- Q是查询矩阵,形状为[batch_size,seq_len,feature_len],其中 N是批量大小,L是序列长度,D是特征维度。
- K是键矩阵,形状为 [batch_size,seq_len,feature_len]。
- V是值矩阵,形状为 [batch_size,seq_len,feature_len]。
- d_k是键的维度。
- W_i^Q, W_i^K, W_i^V是线性变换矩阵。
- W^O是输出线性变换矩阵。
代码解析
class MultiHeadAttention(nn.Module):
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.attention = d2l.DotProductAttention(dropout) # 点积注意力机制
# 线性变换层,分别对应 Q, K, V 的线性变换
self.W_q = nn.Linear(query_size, num_hiddens, bias=bias)
self.W_k = nn.Linear(key_size, num_hiddens, bias=bias)
self.W_v = nn.Linear(value_size, num_hiddens, bias=bias)
# 输出线性变换层
self.W_o = nn.Linear(num_hiddens, num_hiddens, bias=bias)
def forward(self, queries, keys, values, valid_lens):
# 对查询、键、值进行线性变换,并调整维度以适应多头注意力
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)
# 如果有有效长度限制,重复有效长度以匹配多头注意力的维度
if valid_lens is not None:
valid_lens = torch.repeat_interleave(valid_lens, repeats=self.num_heads, dim=0)
# 执行多头注意力机制
output = self.attention(queries, keys, values, valid_lens)
# 恢复原始维度
output_concat = transpose_output(output, self.num_heads)
return self.W_o(output_concat) # 最终输出线性变换
详细解析
- 初始化方法 (
__init__)key_size,query_size,value_size分别是键、查询、值的特征维度。num_hiddens是隐藏层的维度。num_heads是注意力头的数量。dropout是用于注意力机制的dropout概率。bias表示是否使用偏置项。self.attention是点积注意力机制的实例。self.W_q,self.W_k,self.W_v是分别用于查询、键、值的线性变换层。self.W_o是最终输出的线性变换层。
- 前向传播方法 (
forward)- 对查询、键、值进行线性变换。
- 使用
transpose_qkv函数调整维度,以便多头注意力机制的并行计算。 - 如果有有效长度限制,重复有效长度以匹配多头注意力的维度。
- 执行多头注意力机制。
- 使用
transpose_output函数恢复原始维度。 - 进行最终的线性变换并返回结果。
3. 位置前馈网络(Position-wise Feed-Forward Network)
数学公式
位置前馈网络是一个简单的全连接神经网络,应用于序列的每个位置。位置前馈神经网络的主要特点是它在序列的每个位置上独立地应用相同的全连接神经网络。这意味着对于输入序列中的每个元素,都会经过相同的变换,但每个元素的变换是独立进行的。位置前馈神经网络的关键在于,尽管输入序列中的每个位置都经过相同的变换,但这些变换是独立进行的。也就是说,每个位置的变换不会影响其他位置的变换结果。这使得位置前馈神经网络可以并行计算,提高了计算效率。:
其中:
是输入张量,形状为[batch_size,seq_len,feature_len]。
和
是第一层线性变换的权重和偏置。
和
是第二层线性变换的权重和偏置。
代码解析
class PositionWiseFFN(nn.Module):
def __init__(self, ffn_input, ffn_hidden, ffn_output):
super(PositionWiseFFN, self).__init__()
# 第一层线性变换,增加非线性
self.dense1 = nn.Linear(ffn_input, ffn_hidden)
self.relu = nn.ReLU() # 激活函数
# 第二层线性变换,输出维度与输入维度相同
self.dense2 = nn.Linear(ffn_hidden, ffn_output)
def forward(self, X):
# 前向传播,先通过第一层线性变换,再激活,最后第二层线性变换
return self.dense2(self.relu(self.dense1(X)))
详细解析
- 初始化方法 (
__init__)ffn_input是输入的特征维度。ffn_hidden是隐藏层的特征维度。ffn_output是输出的特征维度。self.dense1是第一层线性变换层。self.relu是ReLU激活函数。self.dense2是第二层线性变换层。
- 前向传播方法 (
forward)- 输入张量
X先通过第一层线性变换。 - 使用ReLU激活函数增加非线性。
- 再通过第二层线性变换。
- 返回最终的输出张量。
- 输入张量
为什么使用positionFNN:
- 增加非线性能力:通过非线性激活函数,增强模型的表达能力。
- 特征变换和增强:通过多层变换,提取和增强输入特征。
- 并行计算:在每个位置上独立应用变换,提高计算效率。
- 局部特征处理:对每个位置的特征进行局部处理,捕捉局部模式。
- 结构简洁性和通用性:结构简单,易于实现和优化,适用于多种任务。
- 补充注意力机制:与多头注意力机制结合,处理好全局和局部特征。
4. 残差连接与层归一化层
残差连接
数学公式
残差连接是一种简单的技巧,通过将输入直接添加到输出中来避免梯度消失问题。具体公式如下:
归一化
已知我们向量的形状为[batch_size,seq_len,feature_len],可以理解为一个长为batch_size,宽为seq_len,高为feature_len的向量

BN层归一化可以理解为垂直于feature_len,平行于seq_len来切割平面,对平面的数据进行归一化,又可以理解为特征归一化,为什么在transformer中不用BN是因为这样的切割方式忽略了序列长度,不适合序列处理
如果是LN的话,可以理解为垂直batch_size,平行于feature_Len进行切割,这样每一切割完后每一层,就对应一个序列,这样就适合处理序列数据
层归一化(Layer Normalization)
层归一化是对每个样本的特征进行归一化,而不是像批归一化那样对整个批次的数据进行归一化。具体公式如下:
其中:
- X_norm是输入张量。
是输入张量在特征维度上的均值。
是输入张量在特征维度上的标准差。
是一个小常数,用于防止除零错误。
批归一化(Batch Normalization)
批归一化是对每个批次的数据进行归一化,公式如下:
其中:
是输入张量。
是当前批次数据的均值。
是当前批次数据的标准差。
是一个小常数,用于防止除零错误。
为什么在Transformer中使用层归一化?
- 稳定性:层归一化在小批量或单个样本的情况下也能保持稳定,而批归一化在小批量情况下可能会导致梯度不稳定。
- 并行性:层归一化可以在每个样本上独立进行,不需要跨样本的信息交换,更适合并行计算。
- 长序列处理:Transformer模型通常处理长序列,层归一化可以帮助模型更好地处理长依赖关系。
代码解析
class AddNorm(nn.Module):
def __init__(self, norm_shape, dropout):
super(AddNorm, self).__init__()
self.norm = nn.LayerNorm(norm_shape) # 层归一化
self.dropout = nn.Dropout(dropout) # Dropout 层
def forward(self, X, Y):
# 残差连接:将输入 X 与经过变换后的 Y 相加,然后进行层归一化
return self.norm(self.dropout(Y) + X)
详细解析
- 初始化方法 (
__init__)norm_shape是层归一化的形状。dropout是Dropout层的概率。self.norm是层归一化层。self.dropout是Dropout层。
- 前向传播方法 (
forward)- 输入张量
X与经过变换后的张量Y相加。 - 使用Dropout层进行随机失活。
- 使用层归一化层进行归一化。
- 返回最终的输出张量。
- 输入张量
5. 编码器块
代码解析
class EncoderBlock(nn.Module):
def __init__(self, key_size, query_size, value_size, num_hiddens,
norm_shape, ffn_num_input, ffn_num_hiddens, num_heads,
dropout, use_bias=False, **kwargs):
super(EncoderBlock, self).__init__(**kwargs)
# 多头注意力层
self.attention = MultiHeadAttention(
key_size, query_size, value_size, num_hiddens, num_heads, dropout,
use_bias)
# 残差连接与层归一化层
self.AddNorm1 = AddNorm(norm_shape, dropout)
# 位置前馈网络层
self.PositionFFN = PositionWiseFFN(ffn_num_input, ffn_num_hiddens, ffn_num_input)
# 另一个残差连接与层归一化层
self.AddNorm2 = AddNorm(norm_shape, dropout)
def forward(self, X, valid_lens):
# 先通过多头注意力层,再进行残差连接与层归一化
Y = self.AddNorm1(X, self.attention(X, X, X, valid_lens))
# 再通过位置前馈网络层,再次进行残差连接与层归一化
return self.AddNorm2(Y, self.PositionFFN(Y))
详细解析
- 初始化方法 (
__init__)key_size,query_size,value_size分别是键、查询、值的特征维度。num_hiddens是隐藏层的维度。norm_shape是层归一化的形状。ffn_num_input是位置前馈网络的输入维度。ffn_num_hiddens是位置前馈网络的隐藏层维度。num_heads是注意力头的数量。dropout是Dropout层的概率。use_bias表示是否使用偏置项。self.attention是多头注意力层的实例。self.AddNorm1和self.AddNorm2是残差连接与层归一化层的实例。self.PositionFFN是位置前馈网络层的实例。
- 前向传播方法 (
forward)- 输入张量
X先通过多头注意力层。 - 使用
AddNorm1进行残差连接与层归一化。 - 再通过位置前馈网络层。
- 使用
AddNorm2进行残差连接与层归一化。 - 返回最终的输出张量。
- 输入张量
6. Transformer编码器
代码解析
class TransformerEncoder(d2l.Encoder):
"""Transformer编码器"""
def __init__(self, vocab_size, key_size, query_size, value_size,
num_hiddens, norm_shape, ffn_num_input, ffn_num_hiddens,
num_heads, num_layers, dropout, use_bias=False, **kwargs):
super(TransformerEncoder, self).__init__(**kwargs)
self.num_hiddens = num_hiddens
self.embedding = nn.Embedding(vocab_size, num_hiddens)
self.pos_encoding = d2l.PositionalEncoding(num_hiddens, dropout)
self.blks = nn.Sequential()
for i in range(num_layers):
self.blks.add_module("block"+str(i),
EncoderBlock(key_size, query_size, value_size, num_hiddens,
norm_shape, ffn_num_input, ffn_num_hiddens,
num_heads, dropout, use_bias))
def forward(self, X, valid_lens, *args):
# 因为位置编码值在-1和1之间,
# 因此嵌入值乘以嵌入维度的平方根进行缩放,
# 然后再与位置编码相加。
X = self.pos_encoding(self.embedding(X) * math.sqrt(self.num_hiddens))
self.attention_weights = [None] * len(self.blks)
for i, blk in enumerate(self.blks):
X = blk(X, valid_lens)
self.attention_weights[
i] = blk.attention.attention.attention_weights
return X
详细解析
- 初始化方法 (
__init__)vocab_size是词汇表的大小。key_size,query_size,value_size分别是键、查询、值的特征维度。num_hiddens是隐藏层的维度。norm_shape是层归一化的形状。ffn_num_input是位置前馈网络的输入维度。ffn_num_hiddens是位置前馈网络的隐藏层维度。num_heads是注意力头的数量。num_layers是编码器块的数量。dropout是Dropout层的概率。use_bias表示是否使用偏置项。self.embedding是词嵌入层。self.pos_encoding是位置编码层。self.blks是包含多个编码器块的Sequential容器。
- 前向传播方法 (
forward)- 输入张量
X先通过词嵌入层。 - 嵌入值乘以嵌入维度的平方根进行缩放。
- 加上位置编码。
- 遍历所有编码器块,依次通过每个块。
- 记录每个块的注意力权重。
- 返回最终的输出张量。
- 输入张量
7. 解码器块
代码解析
class DecoderBlock(d2l.Decoder):
def __init__(self, key_size, query_size, value_size, num_hiddens,
norm_shape, ffn_num_input, ffn_num_hiddens, num_heads,
dropout, i, **kwargs):
super(DecoderBlock, self).__init__()
# 第一个自注意力层
self.attention1 = MultiHeadAttention(key_size, query_size, value_size, num_hiddens, num_heads, dropout)
# 第二个“编码器-解码器”注意力层
self.attention2 = MultiHeadAttention(key_size, query_size, value_size, num_hiddens, num_heads, dropout)
# 位置前馈网络层
self.Position_ffn = PositionWiseFFN(ffn_num_input, ffn_num_hiddens, ffn_num_input)
# 残差连接与层归一化层
self.AddNorm1 = AddNorm(norm_shape, dropout)
self.AddNorm2 = AddNorm(norm_shape, dropout)
self.AddNorm3 = AddNorm(norm_shape, dropout)
self.i = i # 当前解码器块的索引
def forward(self, X, state):
enc_outputs, enc_valid_lens = state[0], state[1]
batch_size, num_steps, _ = X.shape
# dec_valid_lens的开头:(batch_size,num_steps),
# 其中每一行是[1,2,...,num_steps]
dec_valid_lens = torch.arange(
1, num_steps + 1, device=X.device).repeat(batch_size, 1)
# 自注意力
X2 = self.attention1(X, X, X, dec_valid_lens)
Y = self.AddNorm1(X, X2)
# 编码器-解码器注意力。
# enc_outputs的开头:(batch_size,num_steps,num_hiddens)
Y2 = self.attention2(Y, enc_outputs, enc_outputs, enc_valid_lens)
Z = self.AddNorm2(Y, Y2)
return self.AddNorm3(Z, self.Position_ffn(Z)), state
详细解析
- 初始化方法 (
__init__)key_size,query_size,value_size分别是键、查询、值的特征维度。num_hiddens是隐藏层的维度。norm_shape是层归一化的形状。ffn_num_input是位置前馈网络的输入维度。ffn_num_hiddens是位置前馈网络的隐藏层维度。num_heads是注意力头的数量。dropout是Dropout层的概率。i是当前解码器块的索引。self.attention1是第一个自注意力层。self.attention2是第二个“编码器-解码器”注意力层。self.Position_ffn是位置前馈网络层。self.AddNorm1,self.AddNorm2,self.AddNorm3是残差连接与层归一化层的实例。
- 前向传播方法 (
forward)- 获取编码器的输出
enc_outputs和有效长度enc_valid_lens。 - 计算解码器的有效长度
dec_valid_lens。 - 输入张量
X先通过第一个自注意力层。 - 使用
AddNorm1进行残差连接与层归一化。 - 再通过第二个“编码器-解码器”注意力层。
- 使用
AddNorm2进行残差连接与层归一化。 - 再通过位置前馈网络层。
- 使用
AddNorm3进行残差连接与层归一化。 - 返回最终的输出张量和状态。
- 获取编码器的输出
8. Transformer解码器
代码解析
class TransformerDecoder(d2l.AttentionDecoder):
def __init__(self, vocab_size, key_size, query_size, value_size,
num_hiddens, norm_shape, ffn_num_input, ffn_num_hiddens,
num_heads, num_layers, dropout, **kwargs):
super(TransformerDecoder, self).__init__(**kwargs)
self.num_hiddens = num_hiddens
self.num_layers = num_layers
self.embedding = nn.Embedding(vocab_size, num_hiddens)
self.pos_encoding = d2l.PositionalEncoding(num_hiddens, dropout)
self.blks = nn.Sequential()
for i in range(num_layers):
self.blks.add_module("block"+str(i),
DecoderBlock(key_size, query_size, value_size, num_hiddens,
norm_shape, ffn_num_input, ffn_num_hiddens,
num_heads, dropout, i))
self.dense = nn.Linear(num_hiddens, vocab_size)
def init_state(self, enc_outputs, enc_valid_lens, *args):
self.seqX = None
return [enc_outputs, enc_valid_lens]
def forward(self, X, state):
if not self.training:
self.seqX = X if self.seqX is None else torch.cat((self.seqX, X), dim=1)
X = self.seqX
X = self.pos_encoding(self.embedding(X) * math.sqrt(self.num_hiddens))
self._attention_weights = [[None] * len(self.blks) for _ in range (2)]
for i, blk in enumerate(self.blks):
X, state = blk(X, state)
# 解码器自注意力权重
self._attention_weights[0][
i] = blk.attention1.attention.attention_weights
# “编码器-解码器”自注意力权重
self._attention_weights[1][
i] = blk.attention2.attention.attention_weights
if not self.training:
return self.dense(X)[:, -1:, :], state
return self.dense(X), state
@property
def attention_weights(self):
return self._attention_weights
详细解析
- 初始化方法 (
__init__)vocab_size是词汇表的大小。key_size,query_size,value_size分别是键、查询、值的特征维度。num_hiddens是隐藏层的维度。norm_shape是层归一化的形状。ffn_num_input是位置前馈网络的输入维度。ffn_num_hiddens是位置前馈网络的隐藏层维度。num_heads是注意力头的数量。num_layers是解码器块的数量。dropout是Dropout层的概率。self.embedding是词嵌入层。self.pos_encoding是位置编码层。self.blks是包含多个解码器块的Sequential容器。self.dense是最终的线性变换层。
- 初始化状态方法 (
init_state)- 初始化解码器的状态,包括编码器的输出
enc_outputs和有效长度enc_valid_lens。 - 初始化
seqX为None。
- 初始化解码器的状态,包括编码器的输出
- 前向传播方法 (
forward)- 如果不在训练模式下,更新
seqX并使用最新的X。 - 输入张量
X先通过词嵌入层。 - 嵌入值乘以嵌入维度的平方根进行缩放。
- 加上位置编码。
- 初始化注意力权重列表。
- 遍历所有解码器块,依次通过每个块。
- 记录每个块的注意力权重。
- 如果不在训练模式下,只返回最后一个时间步的输出。
- 返回最终的输出张量和状态。
- 如果不在训练模式下,更新
- 属性方法 (
attention_weights)- 返回记录的注意力权重。
总结
通过详细解析每个模块的代码实现和数学公式,我们可以更深入地理解Transformer模型的工作原理。Transformer模型通过以下关键技术显著提升了自然语言处理任务的性能
- 多头注意力机制:通过多个注意力头并行计算,增强了模型的表达能力。
- 位置前馈网络:通过简单的全连接神经网络引入非线性,增强模型的建模能力。
- 残差连接:通过将输入直接添加到输出中,避免了梯度消失问题,提高了模型的训练效率。
- 层归一化:在每个样本上独立进行归一化,提高了模型的稳定性和并行性。
更多推荐

所有评论(0)