Transformer 架构 - 位置编码层代码实现
1 课程概览
本课实现位置编码层代码。继承 nn.Module,初始化参数 d_model、dropout、max_len。创建位置矩阵 PE(max_len × d_model),通过 sin/cos 公式填充。位置编码层与词嵌入层同属输入部分,写在同一文件中。
2 核心概念与定义
- PositionalEncoding:位置编码层,继承 nn.Module。
- d_model:词向量维度(如 512)。
- dropout:随机失活概率(防止过拟合)。
- max_len:最大句子长度(如 60)。
- PE:位置编码矩阵,形状为 (max_len, d_model)。
- position:位置列向量,形状为 (max_len, 1)。
- div_term:变化矩阵,公式中的 $10000^{2i/d_{model}}$。
3 模型与算法详解
位置编码层结构
PositionalEncoding(nn.Module)
├── __init__(d_model, dropout, max_len)
│ ├── dropout层
│ └── PE矩阵 (max_len × d_model)
└── forward(x)
└── x + PE[:x.size(1)] → dropout
初始化步骤
- 初始化参数:d_model、dropout、max_len
- 初始化父类:
super().__init__() - 定义 dropout 层:防止过拟合
- 创建 PE 矩阵:
torch.zeros(max_len, d_model) - 定义位置列向量:
torch.arange(0, max_len).unsqueeze(1) - 定义变化矩阵:
torch.exp(torch.arange(0, d_model, 2) * (-math.log(10000.0) / d_model)) - 填充 PE 矩阵:
- 偶数维度:
PE[:, 0::2] = torch.sin(position * div_term) - 奇数维度:
PE[:, 1::2] = torch.cos(position * div_term)
- 偶数维度:
- 注册缓冲区:
register_buffer('pe', pe.unsqueeze(0))
前向传播
def forward(self, x):
# x + 位置编码
x = x + self.pe[:, :x.size(1)]
# dropout
return self.dropout(x)
4 数学原理与推导
位置编码公式
$$\text{PE}(pos, 2i) = \sin\left(\frac{pos}{10000^{2i/d_{model}}}\right)$$
$$\text{PE}(pos, 2i+1) = \cos\left(\frac{pos}{10000^{2i/d_{model}}}\right)$$
变化矩阵推导
$$\text{div_term} = \exp\left(\frac{-\ln(10000)}{d_{model}} \cdot 2i\right) = \frac{1}{10000^{2i/d_{model}}}$$
最终公式
$$\text{PE}[:, 0::2] = \sin(pos \cdot \text{div_term})$$ $$\text{PE}[:, 1::2] = \cos(pos \cdot \text{div_term})$$
5 代码示例
import torch
import torch.nn as nn
import math
class PositionalEncoding(nn.Module):
"""位置编码层"""
def __init__(self, d_model, dropout=0.1, max_len=60):
"""
初始化位置编码层
:param d_model: 词向量维度,例如 512
:param dropout: 随机失活概率
:param max_len: 最大句子长度
"""
super().__init__()
# 1. 定义 dropout 层(防止过拟合)
self.dropout = nn.Dropout(p=dropout)
# 2. 创建位置编码矩阵 PE (max_len, d_model)
pe = torch.zeros(max_len, d_model)
# 3. 定义位置列向量 (max_len, 1)
position = torch.arange(0, max_len).unsqueeze(1)
# 4. 定义变化矩阵 (d_model/2,)
# 公式中的 10000^(2i/d_model)
div_term = torch.exp(torch.arange(0, d_model, 2).float() *
(-math.log(10000.0) / d_model))
# 5. 填充 PE 矩阵
# 偶数维度:sin
pe[:, 0::2] = torch.sin(position * div_term)
# 奇数维度:cos
pe[:, 1::2] = torch.cos(position * div_term)
# 6. 注册缓冲区(不参与梯度更新)
self.register_buffer('pe', pe.unsqueeze(0))
def forward(self, x):
"""
前向传播
:param x: 词向量 (batch, seq_len, d_model)
:return: 加入位置编码的词向量
"""
# x + 位置编码
x = x + self.pe[:, :x.size(1)]
# dropout
return self.dropout(x)
# 测试代码
if __name__ == "__main__":
# 参数设置
d_model = 512 # 词向量维度
dropout = 0.1 # 随机失活概率
max_len = 60 # 最大句子长度
# 创建位置编码层
pe = PositionalEncoding(d_model, dropout, max_len)
# 测试输入 (batch=2, seq_len=5, d_model=512)
x = torch.randn(2, 5, d_model)
# 前向传播
output = pe(x)
print(f"输入形状: {x.shape}")
print(f"输出形状: {output.shape}")
print(f"位置编码矩阵形状: {pe.pe.shape}")
# 查看位置编码
print(f"\n位置 0 的编码(前 8 个维度):")
print(pe.pe[0, 0, :8])
print(f"\n位置 1 的编码(前 8 个维度):")
print(pe.pe[0, 1, :8])
代码说明
| 代码 | 说明 |
|---|---|
nn.Dropout(p=dropout) | 随机失活层,防止过拟合 |
torch.zeros(max_len, d_model) | 创建位置编码矩阵 |
torch.arange(0, max_len).unsqueeze(1) | 位置列向量 |
torch.exp(...) | 变化矩阵 |
pe[:, 0::2] = torch.sin(...) | 偶数维度用 sin |
pe[:, 1::2] = torch.cos(...) | 奇数维度用 cos |
register_buffer('pe', ...) | 注册缓冲区,不参与梯度更新 |
self.pe[:, :x.size(1)] | 取前 seq_len 个位置编码 |
6 重难点与易错提醒
- ❗重点:位置编码层继承 nn.Module。
- ❗重点:PE 矩阵形状为 (max_len, d_model)。
- ❗重点:偶数维度用 sin,奇数维度用 cos。
- ⚠️易错:
register_buffer注册的变量不参与梯度更新。 - ⚠️易错:
div_term的计算用torch.exp和-math.log(10000.0)。 - 💡深入理解:位置编码与词嵌入层同属输入部分,写在同一文件。
7 课堂问答精选
Q1:位置编码层的初始化参数有哪些?
A:①d_model:词向量维度(如 512);②dropout:随机失活概率(防止过拟合);③max_len:最大句子长度(如 60)。
Q2:PE 矩阵的形状是什么?
A:PE 矩阵的形状是 (max_len, d_model),例如 (60, 512)。表示 60 个位置,每个位置有 512 维的编码。
Q3:为什么用 register_buffer?
A:register_buffer 注册的变量不参与梯度更新。位置编码是固定的,不需要学习,所以用 register_buffer。
Q4:偶数维度和奇数维度分别用什么函数?
A:偶数维度(0::2)用 sin 函数,奇数维度(1::2)用 cos 函数。
8 本课小结
- 位置编码层继承 nn.Module。
- 初始化参数:d_model、dropout、max_len。
- PE 矩阵形状:(max_len, d_model)。
- 偶数维度用 sin,奇数维度用 cos。
register_buffer注册的变量不参与梯度更新。- 与词嵌入层同属输入部分,写在同一文件。
9 延伸思考
- 如何测试位置编码层的效果?
- 位置编码如何与词嵌入层结合?