Seq2Seq,即序列到序列,是一种经典 NLP 任务。具体而言,是指模型输入的是一个自然语言序列 x 1 , x 2 , x 3 , . . . x_1, x_2, x_3,... x1,x2,x3,...,然后输出一个序列(长度可能不等)。事实上,Seq2Seq 是 NLP 最经典的任务,几乎所有的 NLP 任务都可以视为 Seq2Seq 任务。但这一看似直观的任务背后,隐藏着怎样的技术实现?为什么 Encoder-Decoder 结构会成为 Seq2Seq 的标配设计?本文将拆解这一经典框架,探讨它是如何通过'编码-解码'的协作,让机器学会'理解'并'生成'自然语言的。
Encoder 和 Decoder

一、把大象装进冰箱:核心思想
想象一下,你要请一位不精通中文的朋友帮你取一本中文书,书名是《时间简史》。你该怎么做?
- 你(编码器 - Encoder) :你不会逐字翻译"时间简史"的发音。相反,你会先理解这本书的核心信息------"一本关于宇宙和时间的科普书,作者是霍金,封面是黑色的,有个漩涡"。你把这些复杂的信息编码成一个简洁、浓缩的"思想摘要"。
- 思想摘要(上下文向量 - Context Vector):这就是你脑中的那个核心概念。
- 你的朋友(解码器 - Decoder) :他接收到你这个"思想摘要"后,开始解码。他根据这个摘要,在他的知识库里搜索,然后用他自己的语言(比如肢体动作和简单的英文单词)来一步步确认:"Ah, physics book? Hawking? Black cover?"。最终,他找到了正确的书。
这个过程完美地诠释了Encoder-Decoder架构的精髓:
- Encoder (编码器) :负责"阅读"和"理解"输入序列(比如一句中文),并将其压缩成一个固定长度的、蕴含了全部语义的数学向量------上下文向量 (Context Vector, C)。这个向量就像是我们前面提到的"思想摘要"。
- Decoder (解码器):负责接收这个"思想摘要" (Context Vector),然后根据它,一步步地生成目标序列(比如一句英文)。
这个"一压一解"的框架,可以处理一个可变长度的输入序列,并生成一个可变长度的Output序列,因此在许多领域大放异彩:
- 机器翻译:输入中文,输出英文。
- 文本摘要:输入长篇文章,输出简短摘要。
- 对话系统(聊天机器人):输入一个问题,输出一个回答。
- 图像描述:输入一张图片(用CNN编码),输出一句话描述。
二、深入内部:Encoder和Decoder的实现

一、基础模块:前馈神经网络 (FFN)
前馈神经网络是最基本的神经网络结构之一,由若干层神经元组成,信息单向流动。每一个 Encoder Layer 都包含一个上文讲的注意力机制和一个前馈神经网络。在Encoder-Decoder架构中,FFN常用于对每个位置的隐藏状态进行非线性变换。
公式:
一个两层的前馈神经网络可以表示为:
FFN ( x ) = W 2 ⋅ σ ( W 1 ⋅ x + b 1 ) + b 2 \text{FFN}(x) = W_2 \cdot \sigma(W_1 \cdot x + b_1) + b_2 FFN(x)=W2⋅σ(W1⋅x+b1)+b2
其中:
- x x x 是输入向量。
- W _ 1 W\_1 W_1 和 b _ 1 b\_1 b_1 是第一层线性变换的权重矩阵和偏置向量。
- s i g m a \\sigma sigma 是激活函数(例如 ReLU)。
- W _ 2 W\_2 W_2 和 b _ 2 b\_2 b_2 是第二层线性变换的权重矩阵和偏置向量。
PyTorch代码:
以下是一个简单的两层前馈神经网络的PyTorch实现:
python
import torch
import torch.nn as nn
class FeedForwardNetwork(nn.Module):
def __init__(self, input_dim, hidden_dim, output_dim, dropout=0.1):
super(FeedForwardNetwork, self).__init__()
self.linear1 = nn.Linear(input_dim, hidden_dim)
self.relu = nn.ReLU()
self.dropout = nn.Dropout(dropout)
self.linear2 = nn.Linear(hidden_dim, output_dim)
def forward(self, x):
x = self.linear1(x)
x = self.relu(x)
x = self.linear2(x)
x = self.dropout(x)
return x
# 示例
input_dim = 512
hidden_dim = 2048
output_dim = 512
ffn = FeedForwardNetwork(input_dim, hidden_dim, output_dim)
input_tensor = torch.randn(32, 50, input_dim) # (batch_size, seq_len, input_dim)
output_tensor = ffn(input_tensor)
print("FFN Output Shape:", output_tensor.shape)
在Encoder和Decoder的每个块中,FFN通常应用于注意力机制的输出,以进一步提取特征。
二、加速训练:层归一化 (Layer Normalization)
归一化核心是为了让不同层输入的取值范围或者分布能够比较一致。由于深度神经网络中每一层的输入都是上一层的输出,因此多层传递下,对网络中较高的层,之前的所有神经层的参数变化会导致其输入的分布发生较大的改变。也就是说,随着神经网络参数的更新,各层的输出分布是不相同的,且差异会随着网络深度的增大而增大。但是,需要预测的条件分布始终是相同的,从而也就造成了预测的误差。
层归一化是一种归一化技术,它在每个样本的特征维度 上计算均值和标准差,并对该样本的所有特征进行归一化。这有助于缓解内部协变量偏移问题,加速模型收敛,并提高训练的稳定性。
公式:
给定一个输入向量 x = ( x _ 1 , x _ 2 , . . . , x _ D ) x = (x\_1, x\_2, ..., x\_D) x=(x_1,x_2,...,x_D),层归一化的计算过程如下:
-
计算均值 μ \mu μ:
μ = 1 D ∑ x i \mu = \frac{1}{D} \sum{x_i} μ=D1∑xi -
计算标准差 σ \sigma σ:
σ = 1 D ∑ ( x i − μ ) 2 \sigma = \sqrt{\frac{1}{D} \sum{(x_i-\mu)^2}} σ=D1∑(xi−μ)2 -
归一化:
x ^ _ i = x _ i − μ σ 2 + ϵ \hat{x}\_i = \frac{x\_i - \mu}{\sqrt{\sigma^2+\epsilon}} x^_i=σ2+ϵ x_i−μ
ϵ \epsilon ϵ是一个很小的正数,防止除数为0 -
缩放和平移(可选,模型学习参数 γ \gamma γ 和 β \beta β):
y _ i = γ x ^ _ i + β y\_i = \gamma \hat{x}\_i + \beta y_i=γx^_i+β
PyTorch代码:
python
class LayerNorm(nn.Module):
def __init__(self, features_dim, epsilon=1e-6):
super(LayerNorm, self).__init__()
self.gamma = nn.Parameter(torch.ones(features_dim))
self.beta = nn.Parameter(torch.zeros(features_dim))
self.epsilon = epsilon
def forward(self, x):
mean = x.mean(-1, keepdim=True)
std = x.std(-1, keepdim=True)
normalized_x = (x - mean) / (std + self.epsilon)
return self.gamma * normalized_x + self.beta
# 示例
features_dim = 512
layer_norm = LayerNorm(features_dim)
input_tensor = torch.randn(32, 50, features_dim)
output_tensor = layer_norm(input_tensor)
print("LayerNorm Output Shape:", output_tensor.shape)
在Encoder-Decoder架构中,LayerNorm通常应用于每个子层的输入和输出(例如,在自注意力之后、FFN之后),并结合残差连接。
三、构建更深的网络:残差连接 (Residual Connection)
随着网络层数的加深,梯度消失或梯度爆炸以及网络退化问题会变得更加严重。残差连接通过将浅层的输入直接添加到深层的输出来缓解这些问题。
公式:
一个包含残差连接的模块可以表示为:
Output = SubLayer ( x ) + x \text{Output} = \text{SubLayer}(x) + x Output=SubLayer(x)+x
其中:
-
x x x 是子层的输入。
-
SubLayer ( x ) \text{SubLayer}(x) SubLayer(x) 是子层(例如,自注意力或FFN)的输出。
-
Output \text{Output} Output 是残差连接后的输出。
注意力计算
h = x + self.attention.forward(self.attention_norm(x))
经过前馈神经网络
out = h + self.feed_forward.forward(self.fnn_norm(h))
在Transformer等现代Encoder-Decoder架构中,每个Encoder和Decoder层都包含多个子层(如自注意力和FFN),并且每个子层都使用了残差连接和层归一化。
在上文代码中,self.attention_norm 和 self.fnn_norm 都是 LayerNorm 层,self.attn 是注意力层,而 self.feed_forward 是前馈神经网络。
四、上下文的理解者:Encoder

Encoder的任务是将输入的序列(例如,一个句子中的单词序列)转换成一个固定长度的上下文向量(或一系列上下文向量),该向量应该捕获输入序列的关键信息。
在Transformer中,Encoder由多个相同的Encoder层堆叠而成。每个Encoder层通常包含以下子层:
- 多头自注意力 (Multi-Head Self-Attention):让模型能够关注输入序列中的不同位置,捕捉词语之间的依赖关系。
- 残差连接和层归一化 (Add & Norm):将自注意力的输出与输入相加,并进行层归一化。
- 前馈神经网络 (Feed-Forward Network):对每个位置的表示进行独立的非线性变换。
- 残差连接和层归一化 (Add & Norm) :将FFN的输出与输入相加,并进行层归一化。
简化版Encoder代码框架:
python
class EncoderLayer(nn.Module):
def __init__(self, dim, self_attn, feed_forward, dropout):
super(EncoderLayer, self).__init__()
self.self_attn = self_attn
self.feed_forward = feed_forward
self.norm1 = LayerNorm(dim)
self.norm2 = LayerNorm(dim)
self.dropout = nn.Dropout(dropout)
def forward(self, x, mask):
# Self-Attention
attn_output = self.self_attn(x, x, x, mask=mask) # Query, Key, Value
x = x + self.dropout(self.norm1(attn_output))
# Feed-Forward
ff_output = self.feed_forward(x)
x = x + self.dropout(self.norm2(ff_output))
return x
class Encoder(nn.Module):
def __init__(self, num_layers, dim, self_attn, feed_forward, dropout):
super(Encoder, self).__init__()
self.layers = nn.ModuleList([
EncoderLayer(dim, self_attn, feed_forward, dropout)
for _ in range(num_layers)
])
self.norm = LayerNorm(dim)
def forward(self, x, mask):
for layer in self.layers:
x = layer(x, mask)
return self.norm(x)
# 注意:这里的 self_attn 和 feed_forward 需要是实际的模块实例
Encoder的最终输出是一系列与输入序列长度相同的上下文向量,每个向量都编码了输入序列在对应位置周围的信息。
五、目标序列的生成器:Decoder

Decoder的任务是接收Encoder产生的上下文信息,并逐步生成目标输出序列。
与Encoder类似,Transformer中的Decoder也由多个相同的Decoder层堆叠而成。每个Decoder层通常包含以下子层:
- 带掩码的多头自注意力 (Masked Multi-Head Self-Attention):与Encoder的自注意力类似,但引入了掩码机制,防止Decoder在生成某个位置的词语时"看到"未来的词语。这保证了Decoder是自回归的。
- 残差连接和层归一化 (Add & Norm)。
- 多头注意力 (Multi-Head Attention) 对Encoder输出的注意力:Decoder的这一层接收Decoder前一层的输出和Encoder的输出,让Decoder能够关注输入序列的相关部分,类似于传统Encoder-Decoder模型中的注意力机制。
- 残差连接和层归一化 (Add & Norm)。
- 前馈神经网络 (Feed-Forward Network)。
- 残差连接和层归一化 (Add & Norm)。
简化版Decoder代码框架:
python
class DecoderLayer(nn.Module):
def __init__(self, dim, self_attn, cross_attn, feed_forward, dropout):
super(DecoderLayer, self).__init__()
self.self_attn = self_attn
self.cross_attn = cross_attn
self.feed_forward = feed_forward
self.norm1 = LayerNorm(dim)
self.norm2 = LayerNorm(dim)
self.norm3 = LayerNorm(dim)
self.dropout = nn.Dropout(dropout)
def forward(self, x, memory, src_mask, tgt_mask):
# Masked Self-Attention
attn1_output = self.self_attn(x, x, x, mask=tgt_mask)
x = x + self.dropout(self.norm1(attn1_output))
# Attention over Encoder output (memory)
attn2_output = self.cross_attn(x, memory, memory, mask=src_mask)
x = x + self.dropout(self.norm2(attn2_output))
# Feed-Forward
ff_output = self.feed_forward(x)
x = x + self.dropout(self.norm3(ff_output))
return x
class Decoder(nn.Module):
def __init__(self, num_layers, dim, self_attn, cross_attn, feed_forward, dropout):
super(Decoder, self).__init__()
self.layers = nn.ModuleList([
DecoderLayer(dim, self_attn, cross_attn, feed_forward, dropout)
for _ in range(num_layers)
])
self.norm = LayerNorm(dim)
def forward(self, x, memory, src_mask, tgt_mask):
for layer in self.layers:
x = layer(x, memory, src_mask, tgt_mask)
return self.norm(x)
# 注意:这里的 self_attn, cross_attn 和 feed_forward 需要是实际的模块实例
Decoder的输入是目标序列的embedding(通常会进行移位和掩码操作),以及Encoder的输出(通常称为memory
)。Decoder逐步生成目标序列,直到生成结束符。