在深度学习领域,序列到序列(Seq2Seq)模型曾经是处理机器翻译、文本摘要等任务的主流架构。这种基于RNN/LSTM的模型通过编码器-解码器结构,实现了变长序列到变长序列的转换。然而,随着任务复杂度的提升,传统Seq2Seq模型逐渐暴露出两个致命缺陷:信息瓶颈问题和长距离依赖问题。
信息瓶颈问题源于编码器需要将整个输入序列压缩成一个固定长度的上下文向量。这就好比要求一个人用一句话概括整本《战争与和平》,然后再根据这句概括重写全书。显然,这种粗暴的压缩方式会导致大量细节丢失,特别是当处理长文本时,序列开头的关键信息往往被稀释殆尽。
2017年,Google的研究团队在论文《Attention Is All You Need》中提出了Transformer架构,彻底改变了这一局面。Transformer完全摒弃了RNN的循环结构,转而依靠自注意力机制(Self-Attention)来捕捉序列内部的依赖关系。这一创新带来了两大优势:一是实现了高度并行化计算,大幅提升了训练效率;二是通过多头注意力机制,模型能够从不同子空间捕捉丰富的语法和语义关系。
注意力机制的核心思想是动态权重分配。在传统的Seq2Seq模型中,解码器每一步都只能看到同一个静态的上下文向量。而引入注意力机制后,解码器在生成每个词时,都能够"回头看"编码器的所有隐藏状态,并根据当前需求有选择性地聚焦于输入序列的不同部分。
具体来说,注意力计算分为三个关键步骤:
这种机制不仅解决了信息瓶颈问题,还带来了一个意外的好处——可解释性。通过可视化注意力权重矩阵,我们可以直观地看到模型在生成某个输出词时,主要关注了输入的哪些部分,这为模型决策提供了宝贵的洞见。
Transformer将注意力机制推向了一个新的高度,引入了自注意力(Self-Attention)和多头注意力(Multi-Head Attention)的概念。
自注意力与传统的交叉注意力不同,它的Q、K、V全部来自同一个序列。这使得模型能够捕捉序列内部的依赖关系,比如理解句子中代词与先行词的关系,或者识别长距离的语法结构。
多头注意力则进一步扩展了这一思想。通过并行运行多个独立的注意力头,模型能够从不同子空间学习多样化的关系模式。例如,一个头可能专注于捕捉语法结构,另一个头可能关注语义关联,第三个头可能识别指代关系。最后,这些不同视角的信息被拼接起来,经过线性变换后输出。
Transformer依然采用编码器-解码器架构,但每个部分都由多个相同的层堆叠而成(原始论文中使用6层)。编码器层的核心组件包括:
解码器层则更为复杂,包含三个子层:
每个子层后都应用了残差连接和层归一化,这种设计有效缓解了深层网络的梯度消失问题,使得模型能够稳定训练。
由于自注意力机制本身不具备位置感知能力,Transformer引入了位置编码来注入序列的顺序信息。原始论文使用固定的正弦/余弦函数生成位置编码,但现代实现更倾向于使用可学习的位置嵌入,或者更先进的旋转位置编码(RoPE)。
掩码机制在Transformer中扮演着两个重要角色:
在PyTorch中实现自注意力时,有几个关键优化点值得注意:
python复制class MultiHeadSelfAttention(nn.Module):
def __init__(self, hidden_size, num_heads):
super().__init__()
assert hidden_size % num_heads == 0
self.hidden_size = hidden_size
self.num_heads = num_heads
self.head_dim = hidden_size // num_heads
self.q_linear = nn.Linear(hidden_size, hidden_size)
self.k_linear = nn.Linear(hidden_size, hidden_size)
self.v_linear = nn.Linear(hidden_size, hidden_size)
self.wo = nn.Linear(hidden_size, hidden_size)
def forward(self, x):
batch_size, seq_len, _ = x.shape
q = self.q_linear(x)
k = self.k_linear(x)
v = self.v_linear(x)
# 拆分多头并转置以便并行计算
q = q.view(batch_size, seq_len, self.num_heads, self.head_dim).transpose(1, 2)
k = k.view(batch_size, seq_len, self.num_heads, self.head_dim).transpose(1, 2)
v = v.view(batch_size, seq_len, self.num_heads, self.head_dim).transpose(1, 2)
# 计算缩放点积注意力
scores = torch.matmul(q, k.transpose(-2, -1)) / math.sqrt(self.head_dim)
attention_weights = torch.softmax(scores, dim=-1)
context = torch.matmul(attention_weights, v)
# 合并多头结果
context = context.transpose(1, 2).contiguous().view(batch_size, seq_len, self.hidden_size)
return self.wo(context)
位置前馈网络虽然结构简单,但在实际实现时有几个注意事项:
python复制class PositionwiseFFN(nn.Module):
def __init__(self, d_model, d_ff, dropout=0.1):
super().__init__()
self.linear1 = nn.Linear(d_model, d_ff)
self.linear2 = nn.Linear(d_ff, d_model)
self.dropout = nn.Dropout(dropout)
self.activation = nn.GELU()
def forward(self, x):
return self.linear2(self.dropout(self.activation(self.linear1(x))))
一个完整的编码器层需要整合多头注意力和前馈网络,并添加残差连接和层归一化。现代实现通常采用Pre-LN结构,即将层归一化放在子层之前,这种结构训练更加稳定。
python复制class EncoderLayer(nn.Module):
def __init__(self, d_model, num_heads, d_ff, dropout=0.1):
super().__init__()
self.self_attn = MultiHeadSelfAttention(d_model, num_heads)
self.ffn = PositionwiseFFN(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):
# Pre-LN结构:先归一化再进入子层
attn_output = self.self_attn(self.norm1(x), mask)
x = x + self.dropout1(attn_output)
ffn_output = self.ffn(self.norm2(x))
x = x + self.dropout2(ffn_output)
return x
在实际训练Transformer模型时,以下几个技巧可以显著提升效果:
在推理阶段,特别是自回归生成任务中,KV缓存技术可以大幅提升效率:
python复制class TransformerDecoder(nn.Module):
def __init__(self, vocab_size, d_model, num_layers, num_heads, d_ff, max_seq_len, dropout=0.1):
super().__init__()
self.token_embedding = nn.Embedding(vocab_size, d_model)
self.pos_embedding = nn.Embedding(max_seq_len, d_model)
self.layers = nn.ModuleList([
DecoderLayer(d_model, num_heads, d_ff, dropout)
for _ in range(num_layers)
])
self.fc_out = nn.Linear(d_model, vocab_size)
def forward(self, x, encoder_output, src_mask=None, tgt_mask=None, past_key_values=None):
seq_len = x.shape[1]
pos = torch.arange(0, seq_len, dtype=torch.long, device=x.device).unsqueeze(0)
x = self.token_embedding(x) + self.pos_embedding(pos)
present_key_values = []
for i, layer in enumerate(self.layers):
x, layer_past = layer(
x, encoder_output, src_mask, tgt_mask,
past_key_values[i] if past_key_values is not None else None
)
present_key_values.append(layer_past)
logits = self.fc_out(x)
return logits, present_key_values
在实现和训练Transformer模型时,可能会遇到以下典型问题:
梯度消失/爆炸:
过拟合:
训练不稳定:
自原始Transformer提出以来,研究者们提出了多种改进架构,每种都有其独特的优势:
在实际应用中,选择哪种架构取决于具体任务需求。对于需要理解整个输入的任务(如文本分类),编码器架构更为合适;而对于生成任务(如对话系统),解码器架构是更好的选择。