PyTorch实战:Encoder-Decoder模型核心逻辑与实现详解


各位观众老爷, 我是诗人啊_最近在整理RNN的相关知识点, 发现`编码器–解码器,特别容易混淆, 所以想着发一篇文章, 进行梳理, 希望能帮助到大家~
各位观众老爷可以点点关注不咯~ (简单实用, 注释清晰, 看了包会的)

前言

在序列到序列(Sequence-to-Sequence)任务中,Encoder-Decoder架构是解决机器翻译、文本摘要等问题的经典方案。本文将从核心逻辑到代码实现,层层递进地解析基于RNN的编码器(Encoder)和解码器(Decoder),帮助读者快速理解其设计思路与工作原理。

一、Encoder-Decoder架构核心思想

Encoder-Decoder架构的核心是“两步走”:

  1. 编码(Encoder):将输入序列(如“我爱中国”)压缩为包含全局语义的隐藏状态(上下文向量)。
  2. 解码(Decoder):基于隐藏状态,逐步生成目标序列(如“I love China”)。

两者通过隐藏状态传递信息,且共享相同的特征维度(hidden_size),确保语义传递的连贯性。

二、编码器(EncoderRNN):压缩输入序列的语义

1. 设计目标

将变长的输入序列(如源语言句子)转换为固定维度的隐藏状态,浓缩序列的全部语义信息。

2. 核心逻辑拆解

(1)参数定义
  • input_size:输入词汇表大小(如源语言有5000个不同单词)。
  • hidden_size:特征维度(如256),统一词嵌入和RNN的维度,确保数据流通畅。
(2)网络层设计

编码器的网络层需完成“离散索引→连续向量→语义压缩”的转换:

网络层 作用 设计细节
词嵌入层 将单词索引转为稠密向量 nn.Embedding(input_size, hidden_size):索引→hidden_size维向量
GRU层 处理序列,融合上下文信息 nn.GRU(hidden_size, hidden_size, batch_first=True):保持维度一致,适配批量输入
(3)前向传播流程
输入:单词索引序列(如[1, 5, 3],对应“我/爱/中国”)
↓
词嵌入层:索引→向量(形状:[batch, seq_len] → [batch, seq_len, hidden_size])
↓
GRU层:逐词处理序列,更新隐藏状态
↓
输出:所有时间步特征 + 最后一个隐藏状态(核心:此状态将传给解码器)
(4)隐藏状态初始化

为GRU提供初始“记忆”(全零张量),需与模型参数在同一设备(CPU/GPU),避免计算时设备不匹配。

3. 代码实现

import torch
import torch.nn as nn

class EncoderRNN(nn.Module):
    """基于RNN的编码器,压缩输入序列为隐藏状态"""
    def __init__(self, input_size, hidden_size):
        super().__init__()
        self.input_size = input_size    # 输入词汇表大小
        self.hidden_size = hidden_size  # 特征维度(与解码器一致)
        
        # 1. 词嵌入层:索引→向量
        self.embedding = nn.Embedding(input_size, hidden_size)
        # 2. GRU层:处理序列,融合上下文
        self.gru = nn.GRU(hidden_size, hidden_size, batch_first=True)

    def forward(self, input, hidden):
        """前向传播:输入序列→隐藏状态"""
        # 步骤1:词嵌入(索引→向量)
        embedded = self.embedding(input)  # 形状:[batch, seq_len] → [batch, seq_len, hidden_size]
        # 步骤2:GRU处理,输出所有时间步特征和最终隐藏状态
        output, hidden = self.gru(embedded, hidden)
        return output, hidden

    def init_hidden(self):
        """初始化隐藏状态(全零张量,与嵌入层同设备)"""
        return torch.zeros(1, 1, self.hidden_size, device=self.embedding.weight.device)

三、解码器(DecoderRNN):基于语义生成目标序列

1. 设计目标

接收编码器的隐藏状态,从起始符号(如<SOS>)开始,逐词生成目标序列,每一步预测下一个单词。

2. 核心逻辑拆解

(1)参数定义
  • output_size:目标词汇表大小(如目标语言有4345个不同单词)。
  • hidden_size:必须与编码器一致(如256),确保语义状态可传递。
(2)网络层设计

解码器的网络层需完成“生成→更新→预测”的循环:

网络层 作用 设计细节
词嵌入层 目标语言单词索引→向量 nn.Embedding(output_size, hidden_size):与GRU输入维度对齐
ReLU激活 稀疏化特征,防止过拟合 F.relu():将词向量中负数置0,保留正数
GRU层 结合历史信息与当前输入,更新隐藏状态 与编码器GRU参数一致,确保状态维度匹配
线性层+LogSoftmax 将隐藏状态映射为词汇表概率分布 线性层:hidden_sizeoutput_size;LogSoftmax:归一化概率,便于损失计算
(3)前向传播流程
输入:上一步预测的单词索引 + 编码器隐藏状态
↓
词嵌入层:索引→向量(形状:[batch, 1] → [batch, 1, hidden_size])
↓
ReLU激活:稀疏化向量,过滤冗余特征
↓
GRU层:更新隐藏状态(融合历史与当前信息)
↓
线性层+LogSoftmax:隐藏状态→词汇表概率分布(形状:[1, output_size])
↓
输出:概率分布 + 更新后的隐藏状态(用于下一步预测)
(4)循环生成逻辑

解码器通过循环调用前向传播,从<SOS>开始,每次预测一个单词,直到生成<EOS>(结束符号)或达到最大长度。

3. 代码实现

import torch.nn.functional as F

class DecoderRNN(nn.Module):
    """基于RNN的解码器,生成目标序列"""
    def __init__(self, output_size, hidden_size):
        super().__init__()
        self.output_size = output_size  # 目标词汇表大小
        self.hidden_size = hidden_size  # 与编码器共享特征维度
        
        # 1. 词嵌入层:目标语言索引→向量
        self.embedding = nn.Embedding(output_size, hidden_size)
        # 2. GRU层:更新隐藏状态
        self.gru = nn.GRU(hidden_size, hidden_size, batch_first=True)
        # 3. 线性层:映射到词汇表维度
        self.out = nn.Linear(hidden_size, output_size)
        # 4. 归一化:生成概率分布
        self.softmax = nn.LogSoftmax(dim=-1)

    def forward(self, input, hidden):
        """前向传播:输入→下一个单词概率"""
        # 步骤1:词嵌入
        embedded = self.embedding(input)  # 形状:[batch, 1] → [batch, 1, hidden_size]
        # 步骤2:ReLU激活,稀疏化特征
        embedded = F.relu(embedded)
        # 步骤3:GRU更新隐藏状态
        output, hidden = self.gru(embedded, hidden)
        # 步骤4:映射为概率分布(降维→线性层→归一化)
        output = self.softmax(self.out(output[0]))  # 形状:[1, output_size]
        return output, hidden

    def init_hidden(self):
        """初始化隐藏状态(全零张量)"""
        return torch.zeros(1, 1, self.hidden_size, device=torch.device('cuda' if torch.cuda.is_available() else 'cpu'))

四、Encoder与Decoder协同工作流程

  1. 编码阶段

    • 输入序列(如“我爱中国”)通过编码器的词嵌入层转为向量。
    • GRU逐词处理向量,最终输出最后一个隐藏状态(浓缩了整句语义)。
  2. 解码阶段

    • 解码器以编码器的隐藏状态为初始输入,从<SOS>开始生成目标序列。
    • 第1步:输入<SOS>→预测第一个单词(如“I”)。
    • 第2步:输入“I”→预测第二个单词(如“love”)。
    • 循环至生成<EOS>,得到完整目标序列(如“I love China ”)。

五、关键设计要点

  1. 维度一致性hidden_size必须在编码器和解码器中保持一致,否则无法传递隐藏状态。
  2. 设备对齐:隐藏状态初始化时需与模型参数(如词嵌入层权重)在同一设备(CPU/GPU),避免运行时错误。
  3. 稀疏化处理:解码器中ReLU的作用是增强特征稀疏性,减少冗余信息,尤其在训练数据有限时可缓解过拟合。
  4. 概率归一化:LogSoftmax与NLLLoss配合使用,等价于交叉熵损失,且数值计算更稳定。

总结

Encoder-Decoder架构通过“压缩-生成”的两步流程,完美解决了输入与输出序列长度不固定的问题。本文解析的RNN版本是基础框架,实际应用中可结合注意力机制(Attention)进一步提升性能(如Transformer模型)。掌握核心逻辑后,读者可根据具体任务(如翻译、对话生成)调整网络细节,快速落地序列到序列模型。

作者有话:

感谢您观看到这里, AI、人工智能 系列文章(基础向)稳定更新中, 如果您感兴趣, 欢迎一键三连~

我是**诗人啊_程序员**, 我致力于编写出让小白也能轻松上文的技术博客~ 求个关注呗~
在这里插入图片描述

Logo

火山引擎开发者社区是火山引擎打造的AI技术生态平台,聚焦Agent与大模型开发,提供豆包系列模型(图像/视频/视觉)、智能分析与会话工具,并配套评测集、动手实验室及行业案例库。社区通过技术沙龙、挑战赛等活动促进开发者成长,新用户可领50万Tokens权益,助力构建智能应用。

更多推荐