“循环神经网络(RNN)、长短期记忆网络(LSTM)和门控循环单元(GRU)按元素处理序列,但许多问题需要将一个长度的输入序列映射到可能不同长度的输出序列。例如,机器翻译(将法语句子翻译成英语)或文本摘要(将长文章压缩成几句话)。输入和输出的长度通常没有直接关系。标准RNN架构通常为每个输入产生一个输出,因此不直接适用于这些任务。”为解决此问题,序列到序列(seq2seq)框架应运而生,主要采用长短期记忆网络(LSTM)或门控循环单元(GRU)等循环架构。其核心思想是使用两个独立的循环神经网络:一个处理输入序列(编码器),另一个生成输出序列(解码器)。编码器-解码器架构seq2seq模型包含两个主要组成部分:编码器:这个循环神经网络逐个读取输入序列的令牌(例如,单词或子词)。它的目标不是在每一步都生成输出,而是将整个输入序列的信息压缩成一个固定大小的向量表示。这个向量常被称为“上下文向量”或“思维向量”,通常由编码器RNN的最终隐藏状态(以及长短期记忆网络的细胞状态)表示。解码器:这个循环神经网络将编码器生成的上下文向量作为其初始隐藏状态。然后,它逐个令牌地生成输出序列。在每一步$t$,解码器接收上下文向量、它自己的前一个隐藏状态$h_{t-1}$,以及前面生成的输出令牌$y_{t-1}$作为输入,以生成下一个输出令牌$y_t$并将其隐藏状态更新为$h_t$。生成过程通常以一个特殊的序列开始符<SOS>令牌开始,并持续到生成序列结束符<EOS>令牌或达到最大长度为止。digraph G { // --- 全局设置 --- // 更改布局为自上而下 rankdir=TB; // 建议目标宽度(根据需要调整“7”) size="7!"; ratio=auto; // 可选:提高位图导出的DPI // dpi=150; // --- 默认节点/边样式 --- // 增加字体大小和边距以提高可读性 node [shape=box, style=rounded, fontname="Arial", fontsize=14, margin="0.2,0.15"]; edge [fontname="Arial", fontsize=12]; // --- 编码器子图 --- subgraph cluster_encoder { label = "编码器"; fontsize=16; // 更大的标签 style=dashed; bgcolor="#e9ecef"; // 编码器内部节点的默认样式 node [fillcolor="#a5d8ff"]; EncInput [label="输入序列\n(X1, X2, ..., Xn)"]; EncoderRNN [label="RNN / LSTM / GRU"]; // 确保上下文向量形状独特 Context [label="上下文向量\n(最终隐藏状态)", shape=cylinder, fillcolor="#ffec99", height=0.7]; EncInput -> EncoderRNN; EncoderRNN -> Context [label=" 处理输入"]; } // --- 解码器子图 --- subgraph cluster_decoder { label = "解码器"; fontsize=16; // 更大的标签 style=dashed; bgcolor="#e9ecef"; // 解码器内部节点的默认样式 node [fillcolor="#96f2d7"]; DecoderRNN [label="RNN / LSTM / GRU"]; DecOutput [label="输出序列\n(Y1, Y2, ..., Ym)"]; // 确保开始令牌形状独特 StartToken [label="<SOS>", shape=cds, fillcolor="#ffc9c9", height=0.7]; // --- 连接 --- // 解码器RNN的初始输入 StartToken -> DecoderRNN [label=" 初始输入"]; // 编码器上下文馈入解码器初始状态(现在是垂直连接) Context -> DecoderRNN [label=" 初始隐藏状态"]; // 解码器生成输出 DecoderRNN -> DecOutput [label=" 生成输出"]; // 解码器自身的循环连接(constraint=false 有助于布局) DecoderRNN -> DecoderRNN [label=" 上一个输出令牌\n 上一个隐藏状态", constraint=false]; } }基于RNN的序列到序列模型的高层结构。编码器处理输入以生成上下文向量,该向量用于初始化解码器以生成输出序列。信息流与上下文向量编码器处理输入序列$X = (x_1, x_2, ..., x_n)$并输出一个上下文向量$c$。这个向量$c$旨在总结整个输入序列。 $$ c = \text{编码器}(x_1, x_2, ..., x_n) $$ 通常,对于长短期记忆网络, $c$ 将是最终的隐藏状态 $h_n$ 和细胞状态 $C_n$。解码器用这个上下文进行初始化(例如,$h_0^{\text{dec}} = h_n^{\text{enc}}$,$C_0^{\text{dec}} = C_n^{\text{enc}}$)。然后,它一次生成一个令牌,产生输出序列$Y = (y_1, y_2, ..., y_m)$。下一个令牌$y_t$的概率取决于上下文$c$、前一个令牌$y_{t-1}$以及解码器当前的隐藏状态$h_t^{\text{dec}}$: $$ P(y_t | y_1, ..., y_{t-1}, c) = \text{解码器}(y_{t-1}, h_{t-1}^{\text{dec}}, c) $$ 解码器的第一个输入通常是一个特殊的<SOS>令牌($y_0 = \text{<SOS>}$)。PyTorch实现概述我们来概述使用PyTorch nn.LSTM的简化编码器和解码器模块。import torch import torch.nn as nn class EncoderRNN(nn.Module): def __init__(self, input_size, hidden_size, num_layers=1): super(EncoderRNN, self).__init__() self.hidden_size = hidden_size self.num_layers = num_layers # 注意:假设 input_size 是嵌入维度 self.embedding = nn.Embedding(input_size, hidden_size) self.lstm = nn.LSTM( hidden_size, hidden_size, num_layers, batch_first=True ) def forward(self, input_seq): # input_seq 形状:(batch_size, seq_length) embedded = self.embedding(input_seq) # embedded 形状:(batch_size, seq_length, hidden_size) # 初始化隐藏状态(为简化起见未显示, # 默认为零) # hidden = self.init_hidden(batch_size) outputs, (hidden, cell) = self.lstm(embedded) # outputs 形状:(batch_size, seq_length, hidden_size) # hidden 形状:(num_layers, batch_size, hidden_size) # cell 形状:(num_layers, batch_size, hidden_size) # 我们通常使用最终的隐藏状态和细胞状态作为上下文 return hidden, cell class DecoderRNN(nn.Module): def __init__(self, hidden_size, output_size, num_layers=1): super(DecoderRNN, self).__init__() self.hidden_size = hidden_size self.num_layers = num_layers # 注意:output_size 是目标语言的词汇表大小 self.embedding = nn.Embedding(output_size, hidden_size) self.lstm = nn.LSTM( hidden_size, hidden_size, num_layers, batch_first=True ) self.out = nn.Linear(hidden_size, output_size) self.softmax = nn.LogSoftmax(dim=1) # 经常与NLLLoss一起使用 def forward(self, input_token, hidden, cell): # input_token 形状:(batch_size, 1) -> 单个令牌 # hidden 形状:(num_layers, batch_size, hidden_size) # cell 形状:(num_layers, batch_size, hidden_size) embedded = self.embedding(input_token) # embedded 形状:(batch_size, 1, hidden_size) # 上下文向量(编码器最终的隐藏/细胞状态) # 在此处作为初始隐藏/细胞状态传入。 output, (hidden, cell) = self.lstm(embedded, (hidden, cell)) # output 形状:(batch_size, 1, hidden_size) # 将输出重塑为 (batch_size, hidden_size) 以用于 # 线性层 output = output.squeeze(1) output = self.out(output) # output 形状:(batch_size, output_size) # 可选:应用 softmax 以获得 # 概率/对数概率 # output = self.softmax(output) return output, hidden, cell # 示例用法 # input_vocab_size = 10000 # output_vocab_size = 12000 # hidden_dim = 256 # n_layers = 2 # batch_size = 32 # input_length = 50 # encoder = EncoderRNN(input_vocab_size, hidden_dim, n_layers) # decoder = DecoderRNN(hidden_dim, output_vocab_size, n_layers) # 示例输入批次(索引) # input_tensor = torch.randint( # 0, input_vocab_size, (batch_size, input_length) # ) # 传入编码器 # encoder_hidden, encoder_cell = encoder(input_tensor) # 解码器输入以 <SOS> 令牌开始(假设索引为 0) # decoder_input = torch.full((batch_size, 1), 0, dtype=torch.long) # decoder_hidden = encoder_hidden # 使用编码器最终的隐藏状态 # decoder_cell = encoder_cell # 使用编码器最终的细胞状态 # 逐步生成输出序列(简化循环) # max_target_length = 60 # all_decoder_outputs = [] # for _ in range(max_target_length): # decoder_output, decoder_hidden, decoder_cell = decoder( # decoder_input, decoder_hidden, decoder_cell # ) # all_decoder_outputs.append(decoder_output) # # # 获取最有可能的下一个令牌(贪婪解码) # _, top_idx = decoder_output.topk(1) # # 使用预测的令牌作为下一个输入 # decoder_input = top_idx.detach()局限性与后续步骤使用循环神经网络的标准编码器-解码器架构在许多任务中被证明是有效的。然而,它依赖于将整个输入序列压缩成一个单一的固定大小的上下文向量。这会产生一个信息瓶颈,尤其对于长输入序列而言问题更突出。当模型生成输出序列的末尾时,它很难记住长输入开头部分的细节。这一局限性是注意力机制发展的重要推动力。注意力机制让解码器在输出生成过程的每一步,能够选择性地关注输入序列的不同部分,而不是仅仅依赖于单一的上下文向量。这种回顾源输入相关部分的能力大幅提升了机器翻译等任务的性能,并为我们将在下一章中介绍的Transformer架构打下了基础。