趋近智
Transformer编码器包含多个组件,其中包括用于捕捉语境关系的多头自注意力 (self-attention)、用于非线性转换的位置维度前馈网络、用于梯度流动的残差连接以及用于稳定激活的层归一化 (normalization)。这些组件被组合成一个功能性的EncoderBlock。构建EncoderBlock呈现了这些子层在一个编码器层中如何协同工作,从而形成堆叠以创建完整编码器的可重复单元。
我们的目标是实现一个标准的Transformer编码器块模块,通常使用像PyTorch或TensorFlow这样的框架。本例中,我们将使用PyTorch语法。
回顾单个编码器块内部的数据流:
Dropout通常应用于自注意力和前馈子层之后,在残差连接和归一化之前。
标准Transformer编码器块内部的数据流(后归一化变体)。虚线表示残差连接。
让我们为EncoderBlock定义一个PyTorch nn.Module。我们假设你已经有了MultiHeadAttention(如第3章所述)和PositionwiseFeedForward(本章前面已讨论)的实现。
import torch
import torch.nn as nn
# 假设 MultiHeadAttention 和 PositionwiseFeedForward 类已在其他地方定义
# class MultiHeadAttention(nn.Module): ...
# class PositionwiseFeedForward(nn.Module): ...
class EncoderBlock(nn.Module):
"""
实现一个Transformer编码器块。
此块遵循“Attention Is All You Need”中描述的结构:
输入 -> 自注意力 -> 加法 & 归一化 -> 前馈网络 -> 加法 & 归一化 -> 输出
使用后置层归一化(先加法,后归一化)。
"""
def __init__(self, d_model: int, num_heads: int, d_ff: int, dropout_prob: float = 0.1):
"""
参数:
d_model: 输入和输出嵌入的维度(模型维度)。
num_heads: 注意力头的数量。
d_ff: 前馈网络的内部维度。
dropout_prob: 在注意力和FFN后应用的dropout概率。
"""
super().__init__()
if d_model % num_heads != 0:
raise ValueError(f"'d_model' ({d_model}) 必须能被 'num_heads' ({num_heads}) 整除")
self.self_attn = MultiHeadAttention(d_model, num_heads)
self.feed_forward = PositionwiseFeedForward(d_model, d_ff)
self.norm1 = nn.LayerNorm(d_model)
self.norm2 = nn.LayerNorm(d_model)
self.dropout = nn.Dropout(dropout_prob)
def forward(self, x: torch.Tensor, mask: torch.Tensor = None) -> torch.Tensor:
"""
将输入通过编码器块。
参数:
x: 输入张量,形状为 (batch_size, seq_len, d_model)。
mask: 自注意力层的可选掩码。通常用于填充。
形状应能广播到 (batch_size, num_heads, seq_len, seq_len)。
返回:
输出张量,形状为 (batch_size, seq_len, d_model)。
"""
# 1. 多头自注意力 + 残差连接 + 层归一化
# 计算注意力输出。对于自注意力,Q=K=V=x。
attn_output = self.self_attn(x, x, x, mask)
# 对注意力输出应用dropout,然后添加残差连接(输入 x),
# 最后应用层归一化。
x = self.norm1(x + self.dropout(attn_output))
# 存储第一个子层(注意力 + 加法&归一化)的输出
# 这将是第二个残差连接的输入。
sublayer1_output = x
# 2. 前馈网络 + 残差连接 + 层归一化
# 计算前馈网络输出
ff_output = self.feed_forward(sublayer1_output)
# 对FFN输出应用dropout,然后添加残差连接
# (使用第一个子层的输出),并应用层归一化。
x = self.norm2(sublayer1_output + self.dropout(ff_output))
return x
__init__):我们实例化了必要的子模块:MultiHeadAttention、PositionwiseFeedForward、两个LayerNorm层和一个Dropout层。LayerNorm层对特征维度(d_model)进行归一化 (normalization)。为了多头注意力 (multi-head attention)机制 (attention mechanism),代码进行了一个检查,确保d_model能够被num_heads整除。forward):
x作为查询、键和值。输出attn_output使用dropout进行正则化 (regularization)。x添加到(经过dropout修改的)注意力输出。这个和随后通过第一个层归一化(self.norm1)。结果更新变量x。x)的输出通过前馈网络(self.feed_forward)。ff_output)使用dropout进行正则化。self.norm1的输出,在优化后的代码中临时存储为sublayer1_output)添加到(经过dropout修改的)前馈输出。self.norm2)。以下是如何创建和使用EncoderBlock,包括子模块的占位符定义,以使示例可运行:
# 示例参数
batch_size = 4
seq_len = 50
d_model = 512 # 模型维度
num_heads = 8 # 注意力头数量
d_ff = 2048 # 前馈网络内部维度
dropout_prob = 0.1
# 创建虚拟输入张量 (batch_size, seq_len, d_model)
dummy_input = torch.rand(batch_size, seq_len, d_model)
# --- 假设 MultiHeadAttention 和 PositionwiseFeedForward 已定义 ---
# 这部分只是为了让示例能够独立运行。
# 请替换为你在前面章节/部分中的实际实现。
class MultiHeadAttention(nn.Module):
# 占位符实现
def __init__(self, d_model, num_heads):
super().__init__()
self.d_model = d_model
self.num_heads = num_heads
if d_model % num_heads != 0:
raise ValueError("d_model 必须能被 num_heads 整除")
self.head_dim = d_model // num_heads
# 通常,这些是 Q, K, V 的投影
self.fc_q = nn.Linear(d_model, d_model)
self.fc_k = nn.Linear(d_model, d_model)
self.fc_v = nn.Linear(d_model, d_model)
self.fc_out = nn.Linear(d_model, d_model) # 最终输出投影
def forward(self, query, key, value, mask=None):
# 简化占位符:投影输入,应用线性输出层
# 在实际实现中,这会执行多头缩放点积注意力,并拼接结果。
batch_size = query.shape[0]
# 仅模拟最终输出投影以保持形状一致性
# 为简化起见,此处忽略实际注意力计算
projected_q = self.fc_q(query) # 示例投影
return self.fc_out(projected_q) # 返回形状 (batch_size, seq_len, d_model)
class PositionwiseFeedForward(nn.Module):
# 标准实现
def __init__(self, d_model, d_ff, dropout=0.1):
super().__init__()
self.linear1 = nn.Linear(d_model, d_ff)
self.relu = nn.ReLU()
self.dropout = nn.Dropout(dropout) # Dropout 通常也应用于此处
self.linear2 = nn.Linear(d_ff, d_model)
def forward(self, x):
# (batch, seq_len, d_model) -> (batch, seq_len, d_ff) -> (batch, seq_len, d_model)
return self.linear2(self.dropout(self.relu(self.linear1(x))))
# ------------------------------------------------------------------------
# 使用(占位符)模块实例化编码器块
encoder_block = EncoderBlock(d_model, num_heads, d_ff, dropout_prob)
# 将输入通过该块
output = encoder_block(dummy_input)
print(f"输入形状: {dummy_input.shape}")
print(f"输出形状: {output.shape}")
# 验证形状
# 预期输出:
# 输入形状: torch.Size([4, 50, 512])
# 输出形状: torch.Size([4, 50, 512])
输出张量保持形状(batch_size, seq_len, d_model),这非常重要。这使得一个EncoderBlock的输出可以直接作为输入传入堆叠中的下一个EncoderBlock,从而能够构建深度编码器模型。
d_model、num_heads、d_ff和dropout_prob的选择显著影响模型的容量、计算成本和泛化能力。原始的Transformer使用了d_model=512、num_heads=8、d_ff=2048和dropout_prob=0.1。更大的模型通常使用更大的值。forward方法的结构。我们将在第6章讨论前置归一化与后置归一化的权衡。这个实践示例提供了一个编码器块的具体实现,结合了前面讨论的理论组件。通过理解如何构建这个基础单元,你将能够构建整个编码器堆栈,并理解Transformer架构内发生的数据转换。
这部分内容有帮助吗?
© 2026 ApX Machine LearningAI伦理与透明度•