别再死记硬背Transformer结构了!用PyTorch手搓一个,从代码反推原理才真懂
从零实现Transformer:用PyTorch代码逆向理解注意力机制
很多开发者都有过这样的困惑:明明看了无数篇讲解Transformer的文章,公式推导也能看懂,但真要自己动手实现时却无从下手。这种"一看就懂,一写就懵"的困境,根源在于我们习惯了被动接受知识,而缺乏从代码层面逆向拆解原理的实践。本文将带你用PyTorch从零实现一个简化版Transformer,在编写forward函数、调试张量形状的过程中,让Q/K/V矩阵、多头注意力、残差连接等抽象概念变得触手可及。
1. 环境准备与基础架构
1.1 初始化项目环境
在开始编码前,我们需要配置好开发环境。建议使用Python 3.8+和PyTorch 1.12+版本:
conda create -n transformer python=3.8
conda activate transformer
pip install torch torchvision torchaudio
pip install numpy matplotlib ipykernel
创建一个基础的Transformer类骨架:
import torch
import torch.nn as nn
class Transformer(nn.Module):
def __init__(self, d_model=512, n_head=8, num_layers=6):
super().__init__()
self.d_model = d_model
self.n_head = n_head
self.num_layers = num_layers
def forward(self, x):
return x
1.2 理解输入输出维度
Transformer处理的是三维张量:[batch_size, seq_len, d_model]。举个例子,当处理一批32个句子,每个句子50个词,词向量维度512时,输入形状就是[32, 50, 512]。
# 示例输入
batch_size = 32
seq_len = 50
d_model = 512
x = torch.randn(batch_size, seq_len, d_model)
print(x.shape) # torch.Size([32, 50, 512])
2. 实现核心注意力机制
2.1 缩放点积注意力
这是Transformer最核心的运算,公式为: $$ \text{Attention}(Q,K,V) = \text{softmax}(\frac{QK^T}{\sqrt{d_k}})V $$
PyTorch实现:
class ScaledDotProductAttention(nn.Module):
def __init__(self, dropout=0.1):
super().__init__()
self.dropout = nn.Dropout(dropout)
def forward(self, q, k, v, mask=None):
d_k = q.size(-1) # 获取特征维度
scores = torch.matmul(q, k.transpose(-2, -1)) / (d_k ** 0.5)
if mask is not None:
scores = scores.masked_fill(mask == 0, -1e9)
attn = torch.softmax(scores, dim=-1)
attn = self.dropout(attn)
output = torch.matmul(attn, v)
return output, attn
调试技巧:在开发过程中,可以使用以下代码检查注意力矩阵:
q = torch.randn(2, 5, 64) # [batch, seq_len, d_model]
k = torch.randn(2, 5, 64)
v = torch.randn(2, 5, 64)
attn = ScaledDotProductAttention()
output, attn_weights = attn(q, k, v)
print(f"注意力权重形状: {attn_weights.shape}") # 应为[2,5,5]
2.2 多头注意力机制
多头注意力将输入拆分为多个子空间,让模型在不同表示子空间中学习信息:
class MultiHeadAttention(nn.Module):
def __init__(self, d_model=512, n_head=8, dropout=0.1):
super().__init__()
assert d_model % n_head == 0
self.d_k = d_model // n_head
self.n_head = n_head
self.w_q = nn.Linear(d_model, d_model)
self.w_k = nn.Linear(d_model, d_model)
self.w_v = nn.Linear(d_model, d_model)
self.attention = ScaledDotProductAttention(dropout)
self.fc = nn.Linear(d_model, d_model)
def forward(self, q, k, v, mask=None):
batch_size = q.size(0)
# 线性变换
q = self.w_q(q).view(batch_size, -1, self.n_head, self.d_k)
k = self.w_k(k).view(batch_size, -1, self.n_head, self.d_k)
v = self.w_v(v).view(batch_size, -1, self.n_head, self.d_k)
# 转置以计算注意力
q, k, v = q.transpose(1, 2), k.transpose(1, 2), v.transpose(1, 2)
# 计算注意力
scores, attn = self.attention(q, k, v, mask)
# 拼接多头结果
concat = scores.transpose(1, 2).contiguous() \
.view(batch_size, -1, self.n_head * self.d_k)
output = self.fc(concat)
return output, attn
常见问题排查:
- 如果遇到维度不匹配错误,检查view和transpose操作的顺序
- 确保最终输出的维度与输入一致([batch, seq, d_model])
- 使用torch.einsum可以更清晰地表达矩阵运算
3. 构建Transformer完整层
3.1 位置编码实现
由于Transformer没有递归结构,需要显式添加位置信息:
class PositionalEncoding(nn.Module):
def __init__(self, d_model, max_len=5000):
super().__init__()
pe = torch.zeros(max_len, d_model)
position = torch.arange(0, max_len).unsqueeze(1)
div_term = torch.exp(torch.arange(0, d_model, 2) *
-(math.log(10000.0) / d_model))
pe[:, 0::2] = torch.sin(position * div_term)
pe[:, 1::2] = torch.cos(position * div_term)
pe = pe.unsqueeze(0)
self.register_buffer('pe', pe)
def forward(self, x):
return x + self.pe[:, :x.size(1)]
3.2 前馈网络
Transformer中的前馈网络是两个线性变换加ReLU激活:
class FeedForward(nn.Module):
def __init__(self, d_model, d_ff=2048, dropout=0.1):
super().__init__()
self.linear1 = nn.Linear(d_model, d_ff)
self.dropout = nn.Dropout(dropout)
self.linear2 = nn.Linear(d_ff, d_model)
def forward(self, x):
x = self.dropout(torch.relu(self.linear1(x)))
x = self.linear2(x)
return x
3.3 编码器层整合
将多头注意力、前馈网络和残差连接组合起来:
class EncoderLayer(nn.Module):
def __init__(self, d_model, n_head, d_ff, dropout=0.1):
super().__init__()
self.self_attn = MultiHeadAttention(d_model, n_head, dropout)
self.ffn = FeedForward(d_model, d_ff, dropout)
self.norm1 = nn.LayerNorm(d_model)
self.norm2 = nn.LayerNorm(d_model)
self.dropout1 = nn.Dropout(dropout)
self.dropout2 = nn.Dropout(dropout)
def forward(self, x, mask=None):
# 自注意力子层
residual = x
x, attn = self.self_attn(x, x, x, mask)
x = self.norm1(residual + self.dropout1(x))
# 前馈子层
residual = x
x = self.ffn(x)
x = self.norm2(residual + self.dropout2(x))
return x, attn
4. 训练与调试技巧
4.1 初始化策略
Transformer对参数初始化敏感,推荐使用Xavier初始化:
def initialize_weights(m):
if isinstance(m, nn.Linear):
nn.init.xavier_uniform_(m.weight)
if m.bias is not None:
nn.init.constant_(m.bias, 0)
elif isinstance(m, nn.LayerNorm):
nn.init.constant_(m.weight, 1)
nn.init.constant_(m.bias, 0)
model.apply(initialize_weights)
4.2 学习率调度
使用带warmup的学习率调度器:
class WarmupScheduler:
def __init__(self, optimizer, d_model, warmup_steps=4000):
self.optimizer = optimizer
self.d_model = d_model
self.warmup_steps = warmup_steps
self.current_step = 0
def step(self):
self.current_step += 1
lr = (self.d_model ** -0.5) * \
min(self.current_step ** -0.5,
self.current_step * self.warmup_steps ** -1.5)
for param_group in self.optimizer.param_groups:
param_group['lr'] = lr
4.3 梯度裁剪
防止梯度爆炸:
torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)
5. 可视化与理解
5.1 注意力权重可视化
理解模型关注的重点:
import matplotlib.pyplot as plt
def plot_attention(attention, sentence):
fig = plt.figure(figsize=(10,10))
plt.imshow(attention, cmap='hot', interpolation='nearest')
plt.xticks(range(len(sentence)), sentence, rotation=90)
plt.yticks(range(len(sentence)), sentence)
plt.colorbar()
plt.show()
5.2 张量形状检查
在forward方法中添加调试语句:
print(f"输入形状: {x.shape}")
x = self.self_attn(x, x, x)
print(f"注意力后形状: {x.shape}")
x = self.ffn(x)
print(f"前馈后形状: {x.shape}")
6. 性能优化技巧
6.1 内存优化
使用梯度检查点减少内存占用:
from torch.utils.checkpoint import checkpoint
def custom_forward(x):
# 定义前向计算
return x
x = checkpoint(custom_forward, x)
6.2 混合精度训练
加速训练过程:
scaler = torch.cuda.amp.GradScaler()
with torch.cuda.amp.autocast():
outputs = model(inputs)
loss = criterion(outputs, targets)
scaler.scale(loss).backward()
scaler.step(optimizer)
scaler.update()
7. 扩展功能实现
7.1 掩码处理
实现序列padding和未来位置掩码:
def create_padding_mask(seq, pad_idx):
return (seq != pad_idx).unsqueeze(1).unsqueeze(2) # [batch, 1, 1, seq_len]
def create_lookahead_mask(size):
mask = torch.triu(torch.ones(size, size), diagonal=1)
return mask == 1 # [seq_len, seq_len]
7.2 位置感知前馈网络
增强位置信息处理能力:
class PositionwiseFeedForward(nn.Module):
def __init__(self, d_model, d_ff, dropout=0.1):
super().__init__()
self.conv1 = nn.Conv1d(d_model, d_ff, kernel_size=1)
self.conv2 = nn.Conv1d(d_ff, d_model, kernel_size=1)
self.dropout = nn.Dropout(dropout)
def forward(self, x):
x = x.transpose(1, 2) # [batch, d_model, seq_len]
x = self.dropout(torch.relu(self.conv1(x)))
x = self.conv2(x).transpose(1, 2)
return x
在实现过程中,最让我印象深刻的是调试多头注意力机制时的维度转换问题。最初没有正确理解transpose和view操作的配合,导致注意力权重计算出现偏差。通过逐步打印每个步骤的张量形状,最终理清了维度变换的逻辑链条。这种从代码实践中获得的理解,远比单纯阅读论文公式要深刻得多。
更多推荐


所有评论(0)