transfromer终极理解
先见森林
在学习transfromer之前,一定要先搞懂transfromer的整体架构,不然会迷失在他的各种“子层”结构中。。。
论文采用 LayerNorm(x + Sublayer(x)) 的顺序,而不是其他变体,如;
-
原始Transformer论文(2017)确实使用了Post-LN(即残差连接后规范化),但后续研究和实践发现Pre-LN(规范化在前(
LayerNorm(x)
))更稳定高效,因此成为当前主流实现方式。在 Transformer 中更稳定,已成为主流选择。
下面的结构是原论文Post-LN结构:
输入部分 → 编码器 → 解码器 → 输出部分
详细结构:
1. 输入部分
编码器输入处理
- 源文本嵌入层
- 位置编码器
解码器输入处理
- 目标文本嵌入层
- 位置编码器
2. 编码器部分
- 由N个编码器层堆叠
- 每个编码器层包含:
* 多头注意力子层
* 前馈全连接子层
* 残差连接 + 层归一化
3. 解码器部分
- 由N个解码器层堆叠
- 每个解码器层包含:
* 掩码多头注意力子层
* 交叉注意力子层
* 前馈全连接子层
* 残差连接 + 层归一化
4. 输出部分
- 线性层
- Softmax层
Pre-LN架构图:
架构图源文件为:transfromer架构.rp
超参数调整:在实际应用中,需要根据任务的特点和数据规模来调整编码器的超参数。例如,对于长文本任务,可以适当增加编码器层数(num_layers)以捕捉更丰富的上下文信息,但同时要注意计算资源的消耗和过拟合问题。增加头数(num_heads)可以让模型学习到更多不同维度的特征,但也会增加计算量。调整前馈全连接层的中间层维度(d_ff)可以影响模型的非线性表达能力,一般建议根据模型维度(d_model)进行合理设置(如 d_ff = 4 * d_model)。
应用场景:Transformer 编码器在许多 NLP 任务中都有广泛应用。在文本分类任务中,编码器可以将输入文本转换为固定长度的向量表示,然后通过一个全连接层进行分类预测。在问答系统中,编码器可以对问题和上下文进行编码,为后续的答案提取或生成提供基础。在预训练模型(如 BERT、GPT 等)中,编码器是核心组件之一,通过大规模无监督数据的预训练,学习到通用的语言表示,然后可以在各种下游任务中进行微调。
最新研究方向:近年来,针对 Transformer 编码器在处理长序列时的效率和内存问题,有许多研究工作。一些新的架构(如 Longformer、Performer 等)提出了基于稀疏注意力或线性注意力的方法,以降低计算复杂度,使编码器能够处理更长的序列(如数千甚至数万个 token)。此外,还有研究探索如何将卷积神经网络(CNN)的局部特征提取能力与 Transformer 编码器的全局建模能力相结合,以提升模型在某些特定任务(如图像字幕生成、语音识别等)中的性能。
第一章:Transformer 背景介绍
1.1 Transformer 的诞生
1.2 Transformer 的优势
经典优势
1.3 技术演进
-
发展路线:
Transformer (2017) → BERT (2018) → GPT-2/XLNet (2019) → T5 (2020) → ChatGPT (2022) → Gemini/GPT-5 (2024)
-
关键突破:
-
位置编码:绝对位置 → 相对位置(RoPE)
-
注意力优化:计算复杂度从O(n²)降到O(n)(FlashAttention)
-
架构改进:Post-LN → Pre-LN(训练更稳定)
-
第二章:输入部分实现
2.1 文本嵌入层(Embedding)
2.2 位置编码器(Positional Encoding)
代码解析
# 步骤3:生成频率因子(用于控制位置编码的周期)
div_term = torch.exp(
torch.arange(0, d_model, 2).float() * (-math.log(10000.0) / d_model
)
步骤1: torch.arange(0, d_model, 2)
-
创建一个从0开始到
d_model
(不包括),步长为2的等差数列 -
例如当
d_model=512
时:
[0, 2, 4, 6, ..., 508, 510] # 共256个元素
步骤2: (-math.log(10000.0) / d_model)
div_term = torch.exp(#`torch.exp` 函数用于计算输入张量中每个元素的指数函数(exponential function)。具体来说,它计算每个元素的自然指数(e^x)
torch.arange(0, d_model, 2).float() * (-math.log(10000.0) / d_model)#math.log(x) 默认计算底数为 e 的对数(而非以 10 为底的对数)。
)
-
这实际是:
-ln(10000)/d_model
步骤3: 张量乘法
2i* (-ln(10000)/d_model)
#[0, 2, 4, 6, ..., 508, 510] # 共d_model/2个元素
步骤4: torch.exp(...)
-
对上述结果应用指数函数:
`torch.exp` 函数用于计算输入张量中每个元素的指数函数(exponential function)。具体来说,它计算每个元素的自然指数(e^x)
上面的第3个式子化简结果就是:,也就是div_term值=
(2i的意思就是偶数的意思)
-
:维度索引(i∈[0,(dmodel/2)−1])。
-
dmodel:编码向量的总维度。
div_term在i=0时取最大值=1,在2i=dmodel时取最小值10000-1即0.00001。
# 初始化全0矩阵;
pe = torch.zeros(max_len, d_model)#param max_len: 最大句子长度(需覆盖训练数据中最长的句子);aram d_model: 词嵌入维度(需与Embedding层的d_model一致)
# 步骤4:用三角函数生成位置编码(偶数位用sin,奇数位用cos)
pe[:, 0::2] = torch.sin(position * div_term) # 0::2表示从0开始,步长为2(偶数索引)
pe[:, 1::2] = torch.cos(position * div_term) # 1::2表示从1开始,步长为2(奇数索引)
- 假设
d_model=8#词嵌入维度
max_len=10#句子最大长度
点击查看数据整体变化过程
初始的位置编码矩阵pe = torch.zeros(max_len, d_model) # 初始化全0矩阵
tensor([[0., 0., 0., 0., 0., 0., 0., 0.],
[0., 0., 0., 0., 0., 0., 0., 0.],
[0., 0., 0., 0., 0., 0., 0., 0.],
[0., 0., 0., 0., 0., 0., 0., 0.],
[0., 0., 0., 0., 0., 0., 0., 0.],
[0., 0., 0., 0., 0., 0., 0., 0.],
[0., 0., 0., 0., 0., 0., 0., 0.],
[0., 0., 0., 0., 0., 0., 0., 0.],
[0., 0., 0., 0., 0., 0., 0., 0.],
[0., 0., 0., 0., 0., 0., 0., 0.]])
生成位置索引position = torch.arange(0, max_len, dtype=torch.float).unsqueeze(1)
tensor([[0.],
[1.],
[2.],
[3.],
[4.],
[5.],
[6.],
[7.],
[8.],
[9.]])
生成频率因子div_term= torch.exp(torch.arange(0, d_model, 2).float() * (-math.log(10000.0) / d_model))
tensor([1, 0.1, 0.01, 0.001])
position * div_term:
tensor([
[0.0000, 0.0000, 0.0000, 0.0000],
[1.0000, 0.1000, 0.0100, 0.0010],
[2.0000, 0.2000, 0.0200, 0.0020],
[3.0000, 0.3000, 0.0300, 0.0030],
[4.0000, 0.4000, 0.0400, 0.0040],
[5.0000, 0.5000, 0.0500, 0.0050],
[6.0000, 0.6000, 0.0600, 0.0060],
[7.0000, 0.7000, 0.0700, 0.0070],
[8.0000, 0.8000, 0.0800, 0.0080],
[9.0000, 0.9000, 0.0900, 0.0090]
])
4.三角函数应用
pe[:, 0::2] = torch.sin(position * div_term) # 0::2表示从0开始,步长为2(偶数索引)
pe[:, 1::2] = torch.cos(position * div_term) # 1::2表示从1开始,步长为2(奇数索引)
点击查看用三角函数生成位置编码的结果
非科学计数法:
tensor([
[ 0.0000, 1.0000, 0.0000, 1.0000, 0.0000, 1.0000, 0.0000, 1.0000],
[ 0.8415, 0.5403, 0.0998, 0.9950, 0.0100, 1.0000, 0.0010, 1.0000],
[ 0.9093, -0.4162, 0.1987, 0.9801, 0.0200, 0.9998, 0.0020, 1.0000],
[ 0.1411, -0.9900, 0.2955, 0.9553, 0.0300, 0.9996, 0.0030, 1.0000],
[-0.7568, -0.6536, 0.3894, 0.9211, 0.0400, 0.9992, 0.0040, 1.0000],
[-0.9589, 0.2837, 0.4794, 0.8776, 0.0500, 0.9988, 0.0050, 1.0000],
[-0.2794, 0.9602, 0.5646, 0.8253, 0.0600, 0.9982, 0.0060, 1.0000],
[ 0.6570, 0.7539, 0.6442, 0.7648, 0.0699, 0.9976, 0.0070, 1.0000],
[ 0.9894, -0.1455, 0.7174, 0.6967, 0.0800, 0.9968, 0.0080, 1.0000],
[ 0.4121, -0.9111, 0.7833, 0.6216, 0.0899, 0.9960, 0.0090, 1.0000]
])
科学计数法:
tensor([
[ 0.0000e+00, 1.0000e+00, 0.0000e+00, 1.0000e+00, 0.0000e+00,1.0000e+00, 0.0000e+00, 1.0000e+00],
[ 8.4147e-01, 5.4030e-01, 9.9833e-02, 9.9500e-01, 9.9998e-03,9.9995e-01, 1.0000e-03, 1.0000e+00],
[ 9.0930e-01, -4.1615e-01, 1.9867e-01, 9.8007e-01, 1.9999e-02,9.9980e-01, 2.0000e-03, 1.0000e+00],
[ 1.4112e-01, -9.8999e-01, 2.9552e-01, 9.5534e-01, 2.9995e-02,9.9955e-01, 3.0000e-03, 1.0000e+00],
[-7.5680e-01, -6.5364e-01, 3.8942e-01, 9.2106e-01, 3.9989e-02,9.9920e-01, 4.0000e-03, 9.9999e-01],
[-9.5892e-01, 2.8366e-01, 4.7943e-01, 8.7758e-01, 4.9979e-02,9.9875e-01, 5.0000e-03, 9.9999e-01],
[-2.7942e-01, 9.6017e-01, 5.6464e-01, 8.2534e-01, 5.9964e-02,9.9820e-01, 6.0000e-03, 9.9998e-01],
[ 6.5699e-01, 7.5390e-01, 6.4422e-01, 7.6484e-01, 6.9943e-02,9.9755e-01, 6.9999e-03, 9.9998e-01],
[ 9.8936e-01, -1.4550e-01, 7.1736e-01, 6.9671e-01, 7.9915e-02,9.9680e-01, 7.9999e-03, 9.9997e-01],
[ 4.1212e-01, -9.1113e-01, 7.8333e-01, 6.2161e-01, 8.9879e-02,9.9595e-01, 8.9999e-03, 9.9996e-01]
])
位置 | 维度0 (sin) | 维度1 (cos) | 维度2 (sin) | 维度3 (cos) |
---|---|---|---|---|
0 | sin(0×1) = 0 | cos(0×1) = 1 | sin(0×0.01) = 0 | cos(0×0.001) = 1 |
1 | sin(1×1) ≈ 0.8415 | cos(1×0.1) ≈ 0.9950 | sin(1×0.01) ≈ 0.0100 | cos(1×0.001) ≈ 1.0000 |
2 | sin(2×1) ≈ 0.9093 | cos(2×0.1) ≈ 0.9801 | sin(2×0.01) ≈ 0.0200 | cos(2×0.001) ≈ 0.9999 |
3 | sin(3×1) ≈ 0.1411 | cos(3×0.1) ≈ 0.9553 | sin(3×0.01) ≈ 0.0300 | cos(3×0.001) ≈ 0.9996 |
4 | sin(4×1) ≈ -0.7568 | cos(4×0.1) ≈ 0.9211 | sin(4×0.01) ≈ 0.0400 | cos(4×0.001) ≈ 0.9992 |
由上表可以看出,在低纬度,一句话中不同位置的“字”,位置编码相差较大(明细),维度越往后,“某个字在一句话内”的位置变动,对位置编码的影响越小,也就是频率因子div_term导致位置编码函数的“周期变大”了。
维度索引 (k) | 实际维度 (i=k/2) | div_term = 10000^{-k/d_model} | 波长 λ = 2π/div_term |
---|---|---|---|
0 | i=0 | 10000^{⁻⁰/₈} = 1 | 2π/1 ≈ 6.28 |
2 | i=1 | 10000^{⁻²/₈} = 0.1 | 2π/0.1 ≈ 62.8 |
4 | i=2 | 10000^{⁻⁴/₈} = 0.01 | 2π/0.01 ≈ 628.3 |
6 | i=3 | 10000^{⁻⁶/₈} = 0.001 | 2π/0.001 ≈ 6283.2 |
在更大的周期下,捕捉的可能就是“不同句子:句子在段落中的位置”、“不同段落:段落在文章中的位置”、“不同文章:文章在整本书中的位置”...
取值范围在 [-1, 1] 之间,避免数值过大影响梯度;
可扩展到超过 max_len 的句子(对于未见过的长句子,直接计算对应位置的 sin/cos 即可)。
二、为什么同时用 sin 和 cos?解决 “单函数的歧义性”
- 正弦/余弦互补:成对的sin/cos提供位置信息
正弦:
余弦:
2.3 旋转位置编码(RoPE)(2024主流方案:革命性升级)
import torch
import torch.nn as nn
import math
class RotaryPositionalEncoding(nn.Module):
"""
2024主流方案:旋转位置编码(RoPE)
解决传统位置编码的远距离衰减问题
"""
def __init__(self, d_model):
super().__init__()
# 初始化旋转频率参数
# 计算公式:theta_i = 1/(10000^(2i/d_model))
theta = 1.0 / (10000 ** (torch.arange(0, d_model, 2).float() / d_model))#tensor([1.0000, 0.1000, 0.0100, 0.0010])
# 注册为不参与梯度更新的缓冲区张量
self.register_buffer('theta', theta)
def forward(self, x):
"""
输入:形状为[batch_size, seq_len, d_model]的张量
输出:包含旋转位置信息的增强张量
处理流程:
1. 获取序列长度和位置索引
2. 计算旋转角度
3. 分割输入为奇偶两部分
4. 应用旋转矩阵变换
"""
seq_len = x.size(1)
# 创建位置索引:[0, 1, 2, ..., seq_len-1]
positions = torch.arange(seq_len, device=x.device).unsqueeze(0).unsqueeze(-1)
# 计算旋转角度:位置索引 * 旋转频率
angles = positions * self.theta
# 计算正弦和余弦值
cos_vals = torch.cos(angles)
sin_vals = torch.sin(angles)
# 将输入张量分割为两部分
x1, x2 = x.chunk(2, dim=-1)
# 应用旋转操作
return torch.cat([
x1 * cos_vals - x2 * sin_vals, # 奇数组分
x2 * cos_vals + x1 * sin_vals # 偶数组分
], dim=-1)
调用示例:
d_model = 512
drop_out = 0.2
maxlength=60
pos=RotaryPositionalEncoding(d_model)
pos_result = pos(emb_result)#emb_result是文本嵌入层的输出结果
print('pos_result:\n',pos_result)
点击查看pos_result打印结果
pos_result:
tensor([[[-22.7073, 47.7814, -34.8996, ..., 27.4981, 29.5390, -37.6457],
[ 4.4975, 26.1692, 5.2723, ..., 30.2044, -17.5261, 4.4119],
[ 30.7179, -46.2061, -8.8106, ..., 30.9138, 48.0335, 24.1294],
[ 3.7339, -4.0399, 25.4750, ..., 1.4083, 0.2464, 5.5368]],
[[-25.0991, -20.2715, -23.3490, ..., -16.5882, 5.9151, 10.8141],
[ 24.0511, 22.3200, -32.3607, ..., 9.0523, -69.3613, -4.6919],
[ -7.7432, 8.7808, 11.6668, ..., -8.2613, -44.4877, 36.0045],
[-22.1680, 8.6165, 2.4500, ..., 8.2094, 2.4702, -39.4504]]],
grad_fn=<CatBackward0>)
点击查看数据整体变化过程
d_model=8
seq_len=10
# 初始化旋转频率参数
theta = 1.0 / (10000 ** (torch.arange(0, d_model, 2).float() / d_model))
tensor([1.0000, 0.1000, 0.0100, 0.0010])
# 创建位置索引:[0, 1, 2, ..., seq_len-1]
torch.arange(seq_len, device=x.device):
tensor([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
torch.arange(seq_len, device=x.device).unsqueeze(0)
tensor([[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]])
positions = torch.arange(seq_len, device=x.device).unsqueeze(0).unsqueeze(-1)
tensor([[[0],
[1],
[2],
[3],
[4],
[5],
[6],
[7],
[8],
[9]]])
# 计算旋转角度:位置索引 * 旋转频率
angles = positions * theta
tensor([[
[0.0000e+00, 0.0000e+00, 0.0000e+00, 0.0000e+00],
[1.0000e+00, 1.0000e-01, 1.0000e-02, 1.0000e-03],
[2.0000e+00, 2.0000e-01, 2.0000e-02, 2.0000e-03],
[3.0000e+00, 3.0000e-01, 3.0000e-02, 3.0000e-03],
[4.0000e+00, 4.0000e-01, 4.0000e-02, 4.0000e-03],
[5.0000e+00, 5.0000e-01, 5.0000e-02, 5.0000e-03],
[6.0000e+00, 6.0000e-01, 6.0000e-02, 6.0000e-03],
[7.0000e+00, 7.0000e-01, 7.0000e-02, 7.0000e-03],
[8.0000e+00, 8.0000e-01, 8.0000e-02, 8.0000e-03],
[9.0000e+00, 9.0000e-01, 9.0000e-02, 9.0000e-03]
]])
# 计算正弦和余弦值
cos_vals = torch.cos(angles)
tensor([[
[ 1.0000, 1.0000, 1.0000, 1.0000],
[ 0.5403, 0.9950, 0.9999, 1.0000],
[-0.4161, 0.9801, 0.9998, 1.0000],
[-0.9900, 0.9553, 0.9996, 1.0000],
[-0.6536, 0.9211, 0.9992, 1.0000],
[ 0.2837, 0.8776, 0.9988, 1.0000],
[ 0.9602, 0.8253, 0.9982, 1.0000],
[ 0.7539, 0.7648, 0.9976, 1.0000],
[-0.1455, 0.6967, 0.9968, 1.0000],
[-0.9111, 0.6216, 0.9960, 1.0000]
]])
# 将输入张量分割为奇偶两部分
x1, x2 = x.chunk(2, dim=-1)
x:
tensor([[
[-0.9999, 2.3156, -1.4695, 0.4755, -0.6917, 1.5989, 1.1258,-1.6053],
[ 1.6772, -0.6332, -1.0321, 0.8752, 0.3437, 0.4056, 0.7239,-0.1152],
[ 0.3354, 0.0683, -1.7851, -0.0522, 0.1394, -0.9185, -0.4194,-0.0896],
[ 1.3673, 1.8694, 0.3584, -0.6479, 1.5024, -0.6312, 0.3728, 0.7276],
[-0.0589, -0.0741, -0.3615, 0.7701, 0.2082, -0.5631, -0.7430, 1.1125],
[-0.1710, -1.0832, -0.2468, 0.1303, -0.5629, -1.2353, -0.5304,-0.7129],
[ 0.1327, 1.6995, -0.3278, 0.4369, -1.6799, -0.5550, -0.9677, 0.0654],
[-0.1957, -1.6168, -0.6508, 0.4742, -0.1579, -0.9069, -0.7761, 0.9913],
[ 0.3600, 0.5687, -0.5375, -0.2589, -0.1088, 0.9356, 0.9630,-0.5976],
[ 0.5196, 1.3758, -0.9453, -0.7429, 1.4650, -1.2425, 1.2043,-0.2617]]])
x1:
tensor([[[-0.9999, 2.3156, -1.4695, 0.4755],
[ 1.6772, -0.6332, -1.0321, 0.8752],
[ 0.3354, 0.0683, -1.7851, -0.0522],
[ 1.3673, 1.8694, 0.3584, -0.6479],
[-0.0589, -0.0741, -0.3615, 0.7701],
[-0.1710, -1.0832, -0.2468, 0.1303],
[ 0.1327, 1.6995, -0.3278, 0.4369],
[-0.1957, -1.6168, -0.6508, 0.4742],
[ 0.3600, 0.5687, -0.5375, -0.2589],
[ 0.5196, 1.3758, -0.9453, -0.7429]]])
x2:
tensor([[[-0.6917, 1.5989, 1.1258, -1.6053],
[ 0.3437, 0.4056, 0.7239, -0.1152],
[ 0.1394, -0.9185, -0.4194, -0.0896],
[ 1.5024, -0.6312, 0.3728, 0.7276],
[ 0.2082, -0.5631, -0.7430, 1.1125],
[-0.5629, -1.2353, -0.5304, -0.7129],
[-1.6799, -0.5550, -0.9677, 0.0654],
[-0.1579, -0.9069, -0.7761, 0.9913],
[-0.1088, 0.9356, 0.9630, -0.5976],
[ 1.4650, -1.2425, 1.2043, -0.2617]]])
# 应用旋转操作
return torch.cat([
x1 * cos_vals - x2 * sin_vals, # 奇数组分
x2 * cos_vals + x1 * sin_vals # 偶数组分
], dim=-1)
x1 * cos_vals - x2 * sin_vals:
tensor([[[-1.7142, 0.1674, -1.2152, 0.2609],
[-0.0853, -0.4854, -1.8653, -0.4615],
[-0.4890, -1.1125, 0.3871, 0.6703],
[ 1.4957, -0.9261, 1.4876, 2.5978],
[ 0.7999, -0.5478, 0.2526, -0.1888],
[ 0.2050, -1.4112, -0.7035, -0.1867],
[-0.7773, 0.8368, 0.0151, 0.2949],
[ 1.2017, -0.0907, -0.1569, 0.6140],
[-1.9494, -0.0552, -0.7687, 0.2508],
[ 3.0350, -0.0765, -0.6146, 1.2512]]])
位置编码可视化对比
点击查看代码
import torch
import torch.nn as nn
import math
# 传统位置编码器
class PositionalEncoding(nn.Module):
def __init__(self, d_model, dropout=0.1, max_len=5000):
"""
初始化位置编码器
:param d_model: 词嵌入维度(需与Embedding层的d_model一致)
:param dropout: dropout比率(防止过拟合,通常设为0.1)
:param max_len: 最大句子长度(需覆盖训练数据中最长的句子)
"""
super().__init__()
self.dropout = nn.Dropout(p=dropout) # 实例化dropout层
# 步骤1:创建位置编码矩阵pe(形状:[max_len, d_model])
pe = torch.zeros(max_len, d_model) # 初始化全0矩阵
# 步骤2:生成位置索引(形状:[max_len, 1])
# 例如max_len=5时,position = [[0], [1], [2], [3], [4]]
position = torch.arange(0, max_len, dtype=torch.float).unsqueeze(1)
# 步骤3:生成频率因子(用于控制位置编码的周期)
# div_term = tensor([1, 0.1, 0.01, 0.001,...])
# 公式:div_term = exp(2i * -log(10000) / d_model),其中i为偶数索引
div_term = torch.exp(#`torch.exp` 函数用于计算输入张量中每个元素的指数函数(exponential function)。具体来说,它计算每个元素的自然指数(e^x)
torch.arange(0, d_model, 2).float() * (-math.log(10000.0) / d_model)#math.log(x) 默认计算底数为 e 的对数(而非以 10 为底的对数)。
)
# 步骤4:用三角函数生成位置编码(偶数位用sin,奇数位用cos)
pe[:, 0::2] = torch.sin(position * div_term) # 0::2表示从0开始,步长为2(偶数索引)
pe[:, 1::2] = torch.cos(position * div_term) # 1::2表示从1开始,步长为2(奇数索引)
# 步骤5:扩展维度,适配批次输入(形状:[1, max_len, d_model])
# 因为输入是[batch_size, seq_len, d_model],需要在batch维度扩展
pe = pe.unsqueeze(0)
# 步骤6:将pe注册为"非参数缓冲区"(不参与训练,但会随模型保存)
self.register_buffer('pe', pe)
def forward(self, x):
"""
前向传播:将位置编码添加到词嵌入向量
:param x: 嵌入层的输出,形状为[batch_size, seq_len, d_model]
:return: 加入位置信息后的向量,形状不变
"""
# 步骤1:添加位置编码(仅取与输入句子长度匹配的部分)
# x.size(1)是当前句子的长度,pe[:, :x.size(1)]表示截取前seq_len个位置的编码
x = x + self.pe[:, :x.size(1)] # 广播机制:[1, seq_len, d_model]与[batch_size, seq_len, d_model]相加
# 步骤2:应用dropout,防止过拟合
return self.dropout(x)
class RotaryPositionalEncoding(nn.Module):
"""
2024主流方案:旋转位置编码(RoPE)
解决传统位置编码的远距离衰减问题
"""
def __init__(self, d_model):
super().__init__()
# 初始化旋转频率参数
# 计算公式:theta_i = 1/(10000^(2i/d_model))
theta = 1.0 / (10000 ** (torch.arange(0, d_model, 2).float() / d_model))#tensor([1.0000, 0.1000, 0.0100, 0.0010])
# 注册为不参与梯度更新的缓冲区张量
self.register_buffer('theta', theta)
def forward(self, x):
"""
输入:形状为[batch_size, seq_len, d_model]的张量
输出:包含旋转位置信息的增强张量
处理流程:
1. 获取序列长度和位置索引
2. 计算旋转角度
3. 分割输入为奇偶两部分
4. 应用旋转矩阵变换
"""
seq_len = x.size(1)
# 创建位置索引:[0, 1, 2, ..., seq_len-1]
positions = torch.arange(seq_len, device=x.device).unsqueeze(0).unsqueeze(-1)
# 计算旋转角度:位置索引 * 旋转频率
angles = positions * self.theta
# 计算正弦和余弦值
cos_vals = torch.cos(angles)
sin_vals = torch.sin(angles)
# 将输入张量分割为两部分
x1, x2 = x.chunk(2, dim=-1)
# 应用旋转操作
return torch.cat([
x1 * cos_vals - x2 * sin_vals, # 奇数组分
x2 * cos_vals + x1 * sin_vals # 偶数组分
], dim=-1)
import matplotlib.pyplot as plt
import numpy as np
plt.rcParams['font.sans-serif'] = ['SimHei'] # 使用黑体
plt.rcParams['axes.unicode_minus'] = False # 解决负号显示问题
plt.rcParams['font.size'] = 20
def visualize_positional_encodings():
"""对比传统位置编码与RoPE的效果"""
plt.figure(figsize=(15, 6))
# 子图1:传统正弦编码
plt.subplot(1, 2, 1)
# 注:传统PositionalEncoding实现参考原始教程
pe = PositionalEncoding(20, 0) # 维度20,dropout=0
y = pe(torch.zeros(1, 100, 20)) # 模拟输入
# 绘制第4-7维特征在不同位置的变化
plt.plot(np.arange(100), y[0, :, 4:8].data.numpy())
plt.title("传统正弦位置编码")
plt.xlabel("位置索引")
plt.ylabel("编码值")
# 子图2:RoPE旋转编码
plt.subplot(1, 2, 2)
rope = RotaryPositionalEncoding(20)
# 生成随机输入(模拟词向量)
y_rope = rope(torch.randn(1, 100, 20))
plt.plot(np.arange(100), y_rope[0, :, 4:8].detach().numpy())
plt.title("旋转位置编码(RoPE)")
plt.xlabel("位置索引")
plt.tight_layout()
plt.savefig("positional_encoding_comparison.png", dpi=120)
plt.show()
visualize_positional_encodings()
性能对比:
指标 | 传统Transformer (2017) | RoPE改进版 (2024) |
---|---|---|
有效上下文长度 | 512-1024 tokens | 4000+ tokens |
长文档任务准确率 | 下降30-50% (>2k tokens) | 保持90%+ |
内存消耗 | O(n²) 显著增长 | O(n) 可控增长 |
关键突破点:
-
长度扩展性:RoPE的旋转性质使得模型无需重新训练即可处理比训练时更长的序列(如从2k扩展到8k)
-
典型应用:
-
法律合同分析(平均长度3k-5k tokens)
-
学术论文处理(10k+ tokens)
-
代码仓库理解(跨文件上下文)
-
技术演进图示
固定位置编码时代(2017-2021)
│
├── 绝对位置编码(BERT式)
│ └── 最大512长度硬限制
│
└── 相对位置编码(Transformer-XL)
└── 片段递归机制复杂
↓
旋转编码革命(2022-2024)
│
├── RoPE(Llama/Mistral采用)
│ └── 4k-32k长度支持
│
└── NTK扩展RoPE(2023)
└── 无损扩展到100k+
这种演进使得现代大模型处理长文档的能力接近人类水平(人类阅读长文时也能保持前后一致性)。
2.4 输入部分总结
工业级配置推荐
模型规模 |
嵌入维度 |
前馈网络维度 |
---|---|---|
base |
768 |
3072 |
large |
1024 |
4096 |
xl |
2048 |
8192 |
编码器
- 生成过程:在推理阶段,解码器的生成过程是一个迭代的过程。首先,输入一个起始标记(如<SOS>),然后通过解码器生成第一个词的概率分布,选择概率最高的词作为输出。接着,将这个输出词与起始标记组成新的输入序列,再次输入解码器生成下一个词的概率分布,如此循环,直到生成结束标记(如<EOS>)。这种自回归的生成方式使得 Transformer 解码器能够灵活地生成各种长度的序列。
- 与编码器的协同:解码器与编码器紧密协作。编码器将输入序列编码为上下文向量,解码器通过编码器 - 解码器注意力机制利用这些上下文向量来指导目标生成。
编码器的输入
1. 训练阶段 (Teacher Forcing)
在训练时,我们已知完整的目标序列(例如翻译后的目标句子)。为了高效地并行训练,我们会使用一种叫 “Teacher Forcing” 的策略。
-
解码器的输入:
-
目标序列(要生成的句子)整体右移一位,并在开头加上一个
[SOS]
(Start of Sentence) 起始符。-
例如,要生成“I am a student”,输入会是
[SOS] I am a
。
-
-
这个右移后的序列进行词嵌入(Embedding) 和位置编码(Positional Encoding)。
-
-
解码器的输出:
-
我们期望解码器能输出下一个词元。对于输入
[SOS]
,它应该预测“I”;对于输入“I”,它应该预测“am”,以此类推。 -
最终的输出是“I am a student
[EOS]
” (End of Sentence)。
-
-
编码器输出的作用:
-
解码器的“编码器-解码器注意力层”(又称交叉注意力层)中,解码器会使用它自身的输出作为
Query
,去查询编码器输出的Key-Value
对,从而知道应该关注源语句的哪些部分来生成下一个词。
-
训练阶段小结:
解码器接收处理后的目标序列作为其直接输入,同时接收编码器对源语句的处理结果作为额外的上下文参考。两者缺一不可。
2. 推理/预测阶段 (Inference)
在推理时(比如用训练好的模型进行翻译),我们是没有目标序列的,需要模型一个字一个字地生成。
-
第一步:
-
输入是起始符
[SOS]
(经过嵌入和位置编码)。 -
编码器处理完源语句,将结果(
Key/Value
)传给解码器。 -
解码器基于
[SOS]
和编码器的信息,输出第一个词(例如“I”)。
-
-
第二步:
-
输入不再是完整的目标序列了,而是到目前为止生成的所有词元:
[SOS], I
(经过嵌入和位置编码)。 -
编码器的输出(
Key/Value
)再次传给解码器。 -
解码器基于
[SOS], I
和编码器信息,输出下一个词(“am”)。
-
-
后续步骤:
-
复这个过程,将之前生成的所有结果(
[SOS], I, am, a ...
)作为输入,一步步自回归地生成,直到输出[EOS]
结束符为止。
-
推理阶段小结:
解码器的输入是它自己在前一步生成的结果(逐步累积),同时每一步都接收编码器对源语句的处理结果作为上下文参考。
总结与对比
可以把它想象成:解码器是一个写作者,它自己写的内容(目标序列嵌入)是它正在阅读和续写的稿纸,而编码器提供的资料(编码器输出)是它放在手边用于参考的百科全书。它需要同时参考这两样东西才能写出正确的句子。
第三章:掩码张量与注意力机制全家桶(含 2021-2025 技术演进)
一、掩码张量(Mask):注意力的 "交通规则"
1.1 两种核心掩码类型
(1)填充掩码(Padding Mask)
(2)后续掩码(Subsequent Mask)
1.2 掩码张量的实现(PyTorch 最新版)
1.3 掩码的应用场景(2021-2025 实践补充)
二、注意力机制:让模型学会 "聚焦"
2.1 缩放点积注意力(Scaled Dot-Product Attention)
实现代码(含 FlashAttention 优化)
2.2 注意力机制的变种(2021-2025 进展)
三、多头注意力机制(Multi-Head Attention)
3.1 多头注意力的工作流程
实现代码
3.2 多头注意力的优势与最新实践
多头注意力(工业级实现,多查询注意力)
class MultiHeadAttention(nn.Module):
"""2024优化版多头注意力"""
def __init__(self, d_model, num_heads, dropout=0.1, use_mqa=False):
super().__init__()
assert d_model % num_heads == 0, "d_model必须能被num_head整除"
self.d_model = d_model
self.num_heads = num_heads
self.head_dim = d_model // num_heads
self.use_mqa = use_mqa # 多查询注意力选项
# 投影层
self.Wq = nn.Linear(d_model, d_model)
if use_mqa:
# MQA:共享键值头
self.Wk = nn.Linear(d_model, self.head_dim)
self.Wv = nn.Linear(d_model, self.head_dim)
else:
self.Wk = nn.Linear(d_model, d_model)
self.Wv = nn.Linear(d_model, d_model)
self.Wo = nn.Linear(d_model, d_model)
self.dropout = nn.Dropout(dropout)
def split_heads(self, x):
"""分割头维度"""
batch_size = x.size(0)
return x.view(batch_size, -1, self.num_heads, self.head_dim).transpose(1, 2)
def forward(self, Q, K, V, mask=None):
batch_size = Q.size(0)
# 线性投影
Q = self.Wq(Q)
K = self.Wk(K)
V = self.Wv(V)
# 多查询注意力(MQA)优化
if self.use_mqa:
# 查询:多头
Q = self.split_heads(Q)
# 键值:单头(广播到多头)
K = K.unsqueeze(1).expand(-1, self.num_heads, -1, -1)
V = V.unsqueeze(1).expand(-1, self.num_heads, -1, -1)
else:
# 标准多头处理
Q = self.split_heads(Q)
K = self.split_heads(K)
V = self.split_heads(V)
# 注意力计算
attn_output = scaled_dot_product_attention(
Q, K, V, mask, self.dropout.p
)
# 合并头部
attn_output = attn_output.transpose(1, 2).contiguous()
attn_output = attn_output.view(batch_size, -1, self.d_model)
return self.Wo(attn_output)
注意力可视化工具
def visualize_attention(attn_weights, tokens, filename="attention_heatmap.png"):
"""注意力热力图可视化工具(2024升级版)"""
plt.figure(figsize=(12, 10))
# 平均所有头部的注意力权重
avg_attn = attn_weights.mean(dim=1)[0].detach().cpu().numpy()
# 创建热力图
ax = sns.heatmap(
avg_attn,
annot=False,
cmap="viridis",
xticklabels=tokens,
yticklabels=tokens,
cbar_kws={'label': 'Attention Weight'}
)
# 优化标签显示
plt.xticks(rotation=45, ha='right', fontsize=10)
plt.yticks(rotation=0, fontsize=10)
plt.xlabel("Key Position")
plt.ylabel("Query Position")
plt.title("Attention Heatmap (2024)", fontsize=14)
plt.tight_layout()
plt.savefig(filename, dpi=300, bbox_inches='tight')
plt.close()
# 2024新增:交互式HTML输出
plotly_fig = ff.create_annotated_heatmap(
z=avg_attn,
x=tokens,
y=tokens,
colorscale='Viridis'
)
plotly_fig.write_html(f"{filename.split('.')[0]}.html")
四、实战示例:注意力机制的完整应用
五、总结与技术演进展望
第四章:前馈全连接层、规范化层与子层连接结构
一、前馈全连接层(Position-wise Feed-Forward Networks)
二、规范化层(Layer Normalization)
三、子层连接结构(Sublayer Connection)
四、总结与技术演进
第五章:Transformer 编码器与解码器的实现
一、编码器层(Encoder Layer)
编码器层是 Transformer 编码器的基本组成单元,它通过多层堆叠,逐步对输入序列进行特征提取和抽象。每个编码器层包含两个主要子模块:多头自注意力(Multi-Head Self-Attention)和前馈全连接层(Position-wise Feed-Forward Networks),并且在每个子模块后都应用了子层连接结构(结合残差连接和层规范化)来稳定训练过程。
1.1 结构与原理
以一层编码器为例,其处理流程如下:
- 输入嵌入与位置编码:输入的 token 序列首先经过词嵌入层,将每个 token 映射为固定维度的向量(如 512 维),然后加上位置编码,使模型能够感知序列中的位置信息。这部分在前面输入处理章节已详细介绍。
- 多头自注意力:将嵌入与位置编码后的输入作为多头自注意力的输入,此时 Q=K=V 均为输入序列本身。通过多头自注意力机制,每个 token 能够动态地关注序列中的其他所有 token,从而捕捉到全局语义信息。不同的头可以关注到不同子空间的依赖关系,增强模型的表达能力。
- 子层连接 1:多头自注意力的输出先进行层规范化(LayerNorm),以防止训练过程中的梯度消失,保持信息流动的稳定,并使激活值分布统一,加快收敛速度。再通过子层计算(如多头注意力或前馈网络),最后执行残差连接(与输入相加)。
- 前馈全连接层:子层连接 1 的输出进入前馈全连接层,这是一个两层的全连接网络(MLP),对每个位置上的 token 单独进行处理。虽然是点对点操作,但它为模型提供了非线性特征转换能力,进一步提升模型的表达和抽象能力。
- 子层连接 2:前馈全连接层的输出同样经过层规范化和残差连接,得到最终的编码器层输出。这个输出将作为下一层编码器的输入。
1.2 实现代码(含 2021 - 2025 优化)
在实际实现中,我们可以使用 PyTorch 框架来构建编码器层。以下是一个完整的编码器层实现,包括对一些最新优化的支持(如使用 FlashAttention 加速多头自注意力计算):
说明:
下面代码中引用的这些模块(SublayerConnection
, MultiHeadedAttention
, PositionwiseFeedForward
)不是 PyTorch 自带的,它们是 Transformer 模型的自定义组件,通常需要您自己实现(例如上面我们的实现方式)或从第三方库中导入。
-
学习目的:建议手动实现这些模块,深入理解 Transformer 细节。
-
生产环境:直接使用 HuggingFace 的
TransformerEncoderLayer
,避免重复造轮子。import torch.nn as nn # 子层连接:LayerNorm + 残差 # import SublayerConnection # # 多头注意力 # import MultiHeadedAttention # # 前馈网络 # import PositionwiseFeedForward class EncoderLayer(nn.Module): def __init__(self, d_model, num_heads, d_ff=2048, dropout=0.1, use_flash=False): super().__init__() # 多头自注意力层 self.self_attn = MultiHeadedAttention(num_heads, d_model, use_flash=use_flash) # 前馈全连接层 self.ffn = PositionwiseFeedForward(d_model, d_ff, dropout) # 两个子层连接结构 self.sublayer1 = SublayerConnection(d_model, dropout) self.sublayer2 = SublayerConnection(d_model, dropout) def forward(self, x, mask=None): # 子层1:LayerNorm +多头自注意力 + 残差连接 x = self.sublayer1(x, lambda x: self.self_attn(x, x, x, mask)) # 子层2:LayerNorm + 前馈全连接层 + 残差连接 x = self.sublayer2(x, self.ffn) return x
1.3 关键细节解析
- 多层堆叠:通常,Transformer 编码器由 6 层或更多层编码器层堆叠而成(如 BERT-base 使用 12 层,BERT-large 使用 24 层)。通过多层堆叠,模型能够逐层提炼更高级的抽象特征,捕捉序列中更复杂的语义和结构信息。
- 与其他组件的协同:编码器层依赖于前面介绍的多个组件,如多头自注意力机制中的缩放点积注意力、前馈全连接层中的 GELU 激活函数以及子层连接结构中的LayerNorm和 残差连接。这些组件相互配合,共同提升编码器层的性能。
- 优化与效率:在 2021 - 2025 期间,为了提高编码器层在处理长序列时的效率,出现了一些优化技术。例如,使用 FlashAttention 可以显著减少多头自注意力计算中的内存访问次数,提升计算速度,尤其在处理长序列(如超过 1000 个 token)时效果明显。此外,一些研究还探索了如何动态调整前馈全连接层的中间层维度(如根据输入序列的复杂度动态设置 d_ff),以在保持模型性能的同时降低计算成本。
点击查看调用示例
# 实例化参数(文档示例)
size = 512 # 特征维度(与d_model一致)
head = 8 # 注意力头数
d_model = 512 # 模型维度
d_ff = 64 # 前馈层中间维度(文档示例,实际常用2048)
dropout = 0.2 # dropout比率
# 实例化子层组件
ff = PositionwiseFeedForward(d_model, d_ff, dropout) # 前馈层
mask = torch.zeros(8, 4, 4) # 掩码张量(示例形状)
# 实例化编码器层并计算
el = EncoderLayer(size, head, d_ff, dropout)
el_result = el(pe_result, mask) # pe_result为带位置编码的输入
print("编码器层输出形状:", el_result.shape) # torch.Size([2, 4, 512])
二、编码器(Transformer Encoder)
编码器由多个编码器层堆叠而成,它的主要作用是将输入序列转换为一个包含丰富上下文信息的向量表示,为后续的解码器或下游任务(如文本分类、问答系统等)提供高质量的特征输入。
2.1 结构与原理
Transformer 编码器的整体结构如下:
- 输入处理:输入的 token 序列首先经过输入嵌入层(将 token 映射为向量)和位置编码层(添加位置信息),得到初始的输入表示。
- 多层编码器层:将输入表示依次通过多个编码器层进行处理。每一层编码器都对输入进行一次特征提取和抽象,通过多头自注意力捕捉全局依赖关系,前馈全连接层增强非线性表达能力,子层连接结构稳定训练过程。随着层数的增加,输出的特征表示逐渐包含更高级、更抽象的语义信息。
- 最终输出:经过所有编码器层处理后,得到的输出是一个形状为(batch_size, seq_len, d_model)的张量。其中,batch_size是批次大小,seq_len是序列长度,d_model是模型维度(如 512)。这个输出张量中的每个元素都表示输入序列中对应位置 token 的 “上下文增强” 向量表示,它融合了整个输入序列的信息。
2.2 实现代码
基于前面实现的编码器层,我们可以轻松构建 Transformer 编码器:
说明:
下面代码中引用的这些模块( InputEmbedding
和 EncoderLayer
)都是需要自己实现的类。
# # 词嵌入
# import Embedding
# # 位置编码
# import EncoderLayer
class TransformerEncoder(nn.Module):
def __init__(self, vocab_size, d_model, num_heads, num_layers, d_ff=2048, dropout=0.1, use_flash=False):
super().__init__()
# 输入嵌入与位置编码层
self.embedding = Embedding(vocab_size, d_model)
# 多个编码器层
self.layers = nn.ModuleList([
EncoderLayer(d_model, num_heads, d_ff, dropout, use_flash) for _ in range(num_layers)
])
def forward(self, x, mask=None):
# 输入嵌入与位置编码
x = self.embedding(x)
# 依次通过多个编码器层
for layer in self.layers:
x = layer(x, mask)
return x
2.3 关键细节解析
- 超参数调整:在实际应用中,需要根据任务的特点和数据规模来调整编码器的超参数。例如,对于长文本任务,可以适当增加编码器层数(num_layers)以捕捉更丰富的上下文信息,但同时要注意计算资源的消耗和过拟合问题。增加头数(num_heads)可以让模型学习到更多不同维度的特征,但也会增加计算量。调整前馈全连接层的中间层维度(d_ff)可以影响模型的非线性表达能力,一般建议根据模型维度(d_model)进行合理设置(如 d_ff = 4 * d_model)。
- 应用场景:Transformer 编码器在许多 NLP 任务中都有广泛应用。在文本分类任务中,编码器可以将输入文本转换为固定长度的向量表示,然后通过一个全连接层进行分类预测。在问答系统中,编码器可以对问题和上下文进行编码,为后续的答案提取或生成提供基础。在预训练模型(如 BERT、GPT 等)中,编码器是核心组件之一,通过大规模无监督数据的预训练,学习到通用的语言表示,然后可以在各种下游任务中进行微调。
- 最新研究方向:近年来,针对 Transformer 编码器在处理长序列时的效率和内存问题,有许多研究工作。一些新的架构(如 Longformer、Performer 等)提出了基于稀疏注意力或线性注意力的方法,以降低计算复杂度,使编码器能够处理更长的序列(如数千甚至数万个 token)。此外,还有研究探索如何将卷积神经网络(CNN)的局部特征提取能力与 Transformer 编码器的全局建模能力相结合,以提升模型在某些特定任务(如图像字幕生成、语音识别等)中的性能。
点击查看测试代码
# 1. 生成模拟的整数词索引输入(正确)
batch_size = 2
seq_len = 4
vocab_size = 10000 # 假设词表大小为10000
input_ids = torch.randint(0, vocab_size, (batch_size, seq_len)) # 生成随机词索引
# 2. 实例化编码器
encoder = TransformerEncoder(
vocab_size=vocab_size,
d_model=512,
num_heads=8,
num_layers=6,
d_ff=2048,
dropout=0.1
)
# 3. 运行编码器(传入整数型词索引)
mask = None # 无掩码
encoder_output = encoder(input_ids, mask)
print("编码器输出形状",encoder_output.shape) # 应输出: torch.Size([2, 4, 512])
三、解码器层(Decoder Layer)
解码器层是 Transformer 解码器的核心组成部分,与编码器层类似,它也包含多个子模块,但在功能和结构上有一些关键区别。解码器层主要负责根据编码器的输出以及之前生成的部分输出,逐步生成完整的目标序列。
3.1 结构与原理
每个解码器层包含三个主要子模块:
- 多头自注意力(Masked Multi-Head Self-Attention):与编码器中的多头自注意力不同,解码器中的自注意力需要使用掩码(mask)来确保在生成当前位置的输出时,模型只能关注到之前已经生成的位置,而不能看到未来的信息。这是为了保证生成过程的因果性,符合人类语言生成的逻辑。例如,在生成句子时,模型在生成第 3 个词时,只能使用第 1 和第 2 个词的信息,不能使用第 4、5 个词等未来信息。
- 编码器 - 解码器注意力(Encoder - Decoder Attention):这是解码器特有的一个子模块,它允许解码器关注编码器的输出。在计算时,查询(Query)来自解码器的上一层输出,键(Key)和值(Value)来自编码器的最终输出。通过这种方式,解码器可以利用编码器提取的输入序列的上下文信息来指导目标序列的生成。
- 前馈全连接层(Position-wise Feed-Forward Networks):与编码器中的前馈全连接层相同,对每个位置的输入进行非线性变换,提升模型的表达能力。
在每个子模块后,同样使用子层连接结构(层规范化和残差连接)来稳定训练过程。
3.2 实现代码
以下是使用 PyTorch 实现的解码器层代码:
说明:
下面代码中引用的这些模块(SublayerConnection
, MultiHeadedAttention
, PositionwiseFeedForward
)不是 PyTorch 自带的,它们是 Transformer 模型的自定义组件,通常需要您自己实现(例如上面我们的实现方式)或从第三方库中导入。
-
学习目的:建议手动实现这些模块,深入理解 Transformer 细节。
-
生产环境:直接使用 HuggingFace 的
TransformerDecoderLayer
,避免重复造轮子。
# 子层连接:LayerNorm + 残差
# import SublayerConnection
# # 多头注意力
# import MultiHeadedAttention
# # 前馈网络
# import PositionwiseFeedForward
class DecoderLayer(nn.Module):
def __init__(self, d_model, num_heads, d_ff=2048, dropout=0.1, use_flash=False):
super().__init__()
# 掩码多头自注意力层(Q=K=V)
self.self_attn = MultiHeadedAttention(num_heads, d_model, use_flash=use_flash)
# 编码器 - 解码器注意力层(Q!=K=V)
self.enc_dec_attn = MultiHeadedAttention(num_heads, d_model, use_flash=use_flash)
# 前馈全连接层
self.ffn = PositionwiseFeedForward(d_model, d_ff, dropout)
# 三个子层连接结构
self.sublayer1 = SublayerConnection(d_model, dropout)
self.sublayer2 = SublayerConnection(d_model, dropout)
self.sublayer3 = SublayerConnection(d_model, dropout)
def forward(self, x, memory, src_mask, tgt_mask):
"""forward函数中的参数有4个,分别是来自上一层的输入X,来自编码器层的语义存储变量mermory,以及源数据掩码张量和目标数据掩码张量"""
# 子层1:LayerNorm + 掩码多头自注意力 + 残差连接
# 将x传入第一个子层结构,第一个子层结构的输入分别是x和se1f-attn函数,因为是自注意力机制,所以Q,K,V都是x;
# tgt_mask是目标数据掩码张量,这时要对目标数据进行遮掩,因为此时模型可能还没有生成任何目标数据,比如在解码器准备生成第一个字符或词汇时,我们其实已经传入了第一个字符以便计算损失,但是我们不希望在生成第一个字符时模型能利用这个信息,因此我们会将其遮掩;同样生成第二个字符或词汇时,模型只能使用第一个字符或词汇信息,第二个字符以及之后的信息都不允许被模型使用。
x = self.sublayer1(x, lambda x: self.self_attn(x, x, x, tgt_mask))
# 子层2:编码器 - LayerNorm + 解码器注意力 + 残差连接
#这个子层中使用常规的注意力机制,q是输入x;
# k,v是编码层输出memory;
# source_mask,但是进行源数据遮掩的原因并非是抑制信息泄漏,而是遮蔽掉对结果没有意义的字符而产生的注意力值,以此提升模型效果和训练速度。
x = self.sublayer2(x, lambda x: self.enc_dec_attn(x, memory, memory, src_mask))
# 子层3:前馈全连接层 + 残差连接 + LayerNorm
x = self.sublayer3(x, self.ffn)
return x
3.3 关键细节解析
- 掩码的应用:在掩码多头自注意力中,掩码的使用至关重要。掩码通常是一个上三角矩阵,在生成当前位置的输出时,将未来位置的注意力权重设为 0,从而防止模型 “偷看” 未来信息。例如,对于一个长度为 5 的序列,掩码矩阵在生成第 3 个位置的输出时,会将第 4 和第 5 个位置对应的注意力权重设为 0。
- 与编码器的交互:编码器 - 解码器注意力是解码器与编码器进行信息交互的关键。通过这种机制,解码器可以在生成目标序列时,充分利用编码器提取的输入序列的全局上下文信息。在实际应用中,例如在机器翻译任务中,编码器将源语言句子编码为上下文向量,解码器通过编码器 - 解码器注意力关注这些上下文向量,从而生成目标语言句子。
- 训练与推理的差异:在训练时,解码器通常会一次性接收完整的目标序列(教师强制),并计算生成结果与真实目标之间的损失。而在推理时,解码器需要逐个生成目标序列的元素,每次生成一个元素后,将其作为下一次生成的输入,直到生成结束标记(如<EOS>)。这种训练和推理方式的差异需要在实现中进行特殊处理,例如在推理时需要动态更新掩码以确保生成的因果性。
- 最新技术进展:在 2021 - 2025 期间,针对解码器层的优化也有不少研究。一些工作提出了改进的注意力机制,如动态调整注意力头的权重,使模型能够更自适应地关注不同的信息。还有研究探索在解码器中使用强化学习的方法,以提高生成序列的质量和多样性,尤其在文本生成任务中取得了一定的效果。
点击查看测试代码
# 实例化参数
head = 8 # 注意力头数
d_model = 512 # 模型维度
d_ff = 64 # 前馈层中间维度(文档示例,实际常用2048)
dropout = 0.2 # dropout比率
x = pe_result # 目标序列嵌入(与输入部分格式一致)
memory = el_result # 编码器输出
source_mask = target_mask = mask # 掩码(实际场景中通常不同)
# 实例化解码器层并计算
dl = DecoderLayer(d_model, head, d_ff, dropout)#d_model, num_heads, d_ff=2048, dropout=0.1, use_flash=False
dl_result = dl(x, memory, source_mask, target_mask)
print("解码器层输出形状:", dl_result.shape) # torch.Size([2, 4, 512])
四、解码器(Transformer Decoder)
Transformer 解码器由多个解码器层堆叠而成,它的主要任务是根据编码器的输出和已生成的部分目标序列,逐步生成完整的目标序列。在许多应用中,如机器翻译、文本摘要、语言生成等,解码器都起着关键作用。
4.1 结构与原理
Transformer 解码器的整体结构如下:
- 输入处理:与编码器类似,解码器的输入也需要进行嵌入和位置编码。但解码器的输入是已生成的部分目标序列(在训练时为真实目标序列的前 n 个元素,在推理时为上一步生成的元素)。
- 多层解码器层:将输入表示依次通过多个解码器层进行处理。每个解码器层通过掩码多头自注意力捕捉已生成序列的内部依赖关系,通过编码器 - 解码器注意力利用编码器的输出信息,前馈全连接层增强非线性表达能力,子层连接结构稳定训练过程。随着层数的增加,输出的特征表示逐渐包含更适合生成目标序列的信息。
- 输出层:经过所有解码器层处理后,输出层通常是一个线性层,将解码器的输出映射到目标词汇表的大小,然后通过 softmax 函数得到每个词在目标词汇表中的概率分布,从而预测下一个词。
4.2 实现代码
基于前面实现的解码器层,我们可以构建 Transformer 解码器:
# import Embedding
#
# import DecoderLayer
class TransformerDecoder(nn.Module):
def __init__(self, vocab_size, d_model, num_heads, num_layers, d_ff=2048, dropout=0.1, use_flash=False):
super().__init__()
# 输入嵌入与位置编码层
self.embedding = Embedding(vocab_size, d_model)
# 多个解码器层
self.layers = nn.ModuleList([
DecoderLayer(d_model, num_heads, d_ff, dropout, use_flash) for _ in range(num_layers)
])
# 输出线性层
self.output_layer = nn.Linear(d_model, vocab_size)
def forward(self, x, memory, src_mask, tgt_mask):
# 输入嵌入与位置编码
x = self.embedding(x)
# 依次通过多个解码器层
for layer in self.layers:
x = layer(x, memory, src_mask, tgt_mask)
# 输出层
output = self.output_layer(x)
return output
4.3 关键细节解析
- 生成过程:在推理阶段,解码器的生成过程是一个迭代的过程。首先,输入一个起始标记(如<SOS>),然后通过解码器生成第一个词的概率分布,选择概率最高的词作为输出。接着,将这个输出词与起始标记组成新的输入序列,再次输入解码器生成下一个词的概率分布,如此循环,直到生成结束标记(如<EOS>)。这种自回归的生成方式使得 Transformer 解码器能够灵活地生成各种长度的序列。
- 与编码器的协同:解码器与编码器紧密协作。编码器将输入序列编码为上下文向量,解码器通过编码器 - 解码器注意力机制利用这些上下文向量来指导目标生成。
点击查看测试代码
# 生成模拟的整数词索引输入(正确方式)
batch_size = 2
seq_len = 4
vocab_size = 512 # 词表大小
input_ids = torch.randint(0, vocab_size, (batch_size, seq_len)) # 形状:[2, 4]
# 实例化解码器
decoder = TransformerDecoder(
vocab_size=vocab_size,
d_model=512,
num_heads=8,
num_layers=6,
d_ff=2048,
dropout=0.1
)
# 模拟编码器输出(memory)
memory = torch.randn(batch_size, seq_len, 512) # 形状:[2, 4, 512]
# 运行解码器(传入整数型词索引)
src_mask = tgt_mask = None # 无掩码
output = decoder(input_ids, memory, src_mask, tgt_mask)
print("解码器输出形状:",output.shape) # 应输出: torch.Size([2, 4, 512])
第六章:输出部分实现(Generator)
输出部分包含:
- 线性层
- 通过对上一步的线性变化得到指定维度的输出,也就是转换维度的作用
- softmax层
- 使最后一维的向量中的数字缩放到0-1的概率值域内,并满足他们的和为1
第七章:核心组件联动总结
补充
教师强制模式
一个绝佳的类比:教小孩做填空题
想象一下,你正在教一个小孩做完形填空(Cloze Test),题目是:
“我今天心情很 ____,因为考试得了满分。”
正确答案是:“好”
你的教学方法(对应Transformer的训练策略,Teacher Forcing):
- 1.
你把完整的句子给小孩看:你把“我今天心情很好,因为考试得了满分”这句话给他看,但你把“好”字遮住,变成了“我今天心情很 ____,因为考试得了满分”。
- 2.
你问他:“看,这里应该填什么?”
- 3.
小孩看了看上下文(他可以看到“心情很”和“因为考试得了满分”),他预测了一个词,比如他说“棒”。
- 4.
你立刻纠正他:“不对,你看,这里的标准答案是‘好’。所以你刚才的预测(‘棒’)和答案(‘好’)有差距,这是个错误。你要从这个错误中学习,下次看到类似上下文,应该猜‘好’。”
第二天,你又用同一题考他(第二次训练迭代):
- 1.
你还是给他看完整的带遮罩的句子。
- 2.
他又猜了一次,这次他记住了上次的错误,猜的是“好”。
- 3.
你表扬他:“猜对了!这次预测和答案完全一致,没有误差。”
通过无数次这样的练习,小孩终于学会了“心情很”后面接“好”是最常见的搭配,他掌握了这个模式。
对应到Transformer的训练过程
现在我们把类比中的概念映射到模型上:
类比中的概念 |
Transformer中的对应 |
---|---|
完整的句子 |
完整的目标序列(例如翻译结果 “I am a student”) |
被遮住的词 |
输入给解码器的、右移一位的目标序列( |
小孩看到的上下文 |
解码器的输入 + 编码器提供的源语句信息(例如中文“我是一个学生”) |
小孩的猜测 |
解码器的输出(生成结果)(一个概率分布,预测下一个词是什么) |
标准答案 |
原始的目标序列(未右移)( |
你比较猜测和答案 |
计算损失函数(Loss Function)(比较解码器的输出概率分布和真实词元的分布) |
你纠正小孩 |
反向传播(Backpropagation) 和 优化(Optimization),调整模型数百万的参数,让下次预测得更准 |
回答您的核心疑问:
- •
“既然已经完整接收到了目标序列,那为什么还会有‘生成结果’?”
- •
模型接收完整序列是为了获取上下文,但我们在时间步
t
会故意隐藏t
时刻之后的词。解码器的任务是利用t
时刻之前的所有词(已知上下文)和编码器信息,来预测t
时刻的词。 - •
它不是“复述”整个句子,而是逐个词元(Token)地进行预测。在每一步,它都只知道历史,不知道未来。
- •
- •
“为什么还会有损失?”
- •
损失函数计算的正是模型每一步的预测(如预测“棒”)与该步的真实目标(即答案“好”)之间的差异。
- •
这个差异(损失)至关重要!它是模型学习的“信号”或“纠正”。通过反向传播这个误差,模型的所有参数(权重和偏置)都会被微调,使得下一次在同样上下文中,它输出“好”的概率会比输出“棒”的概率更高。
- •
总结
所以,提供完整的目标序列不是为了让它抄答案,而是为了给它提供用于学习预测的“标准上下文”。
这种方法的巨大优势在于并行化:模型可以一次性处理整个序列,并行地计算出所有时间步的预测和损失,从而极大地加速训练过程。如果像推理阶段那样一步步生成,训练速度会慢得无法接受。