机器翻译:一文掌握序列到序列(Seq2Seq)模型(包括手写Seq2Seq模型)

发布于:2025-08-12 ⋅ 阅读:(14) ⋅ 点赞:(0)

一、序列到序列(Seq2Seq)模型详解

序列到序列(Sequence-to-Sequence, Seq2Seq) 模型是一种深度学习架构,主要用于处理输入和输出都是序列的任务,如机器翻译、文本摘要、对话生成等。

Seq2Seq模型是一种强大的深度学习架构,专门用于处理将一个序列转换为另一个序列的任务。它由两个核心组件构成:编码器解码器

1.1 核心思想

Seq2Seq模型的核心思想是“先理解,再生成”。

  1. 理解(编码):模型首先读取输入的整个序列(比如一个句子),并将其压缩成一个包含所有信息的上下文向量。这个过程由编码器完成。
  2. 生成(解码):然后,模型根据这个上下文向量,一个词一个词地生成目标序列(比如翻译后的句子)。这个过程由解码器完成。

1.2 基本工作原理

编码器读取输入序列,将其压缩为一个固定大小的上下文向量
解码器基于该上下文向量逐步生成输出序列
每个时间步的输出作为下一个时间步的输入

1.3 核心组件

A. 编码器
编码器的任务是处理输入序列并生成一个固定长度的上下文向量。

  • 结构:通常由一个循环神经网络构成,如LSTM或GRU。RNN非常适合处理序列数据,因为它具有“记忆”功能,能捕捉序列中的时序依赖关系。
  • 工作流程
    1. 输入序列的第一个词被送入RNN,RNN输出一个隐藏状态 h1
    2. 序列的第二个词和 h1 一起被送入RNN,输出新的隐藏状态 h2
    3. 这个过程持续到序列的最后一个词,最终得到最后一个隐藏状态 hN(N是序列长度)。
  • 上下文向量:在最初的Seq2Seq模型中,最后一个隐藏状态 hN 就被直接用作整个输入序列的上下文向量。这个向量被传递给解码器,作为生成目标序列的“起点”和“背景信息”。
    问题:如果输入序列非常长,仅仅依靠最后一个隐藏状态来概括整个序列的信息,可能会导致信息丢失或遗忘(长序列依赖问题)。

B. 解码器
解码器的任务是接收上下文向量,并生成目标序列。

  • 结构:同样通常由一个RNN(LSTM或GRU)构成。
  • 工作流程
    1. 解码器的初始隐藏状态被设置为编码器输出的上下文向量 hN
    2. 解码器首先接收一个特殊的起始标记(如 <sos>),结合初始隐藏状态,生成第一个预测词的概率分布。
    3. 从概率分布中选出概率最高的词(例如,“I”),并将其作为下一个时间步的输入。
    4. 将“”和“ I”的预测结果分别作为下一步的输入,重复此过程,直到生成一个结束标记(如 <eos>)或达到最大长度。

1.4 注意力机制 - 关键改进

为了解决编码器仅用最后一个状态表示长序列信息的问题,注意力机制被引入。它允许解码器在生成每个词时,都能“回顾”输入序列的所有部分,并根据当前需要动态地关注输入序列中的不同部分。

  • 工作原理(以解码器生成第 t 个词为例)
    1. 计算注意力分数:解码器当前的隐藏状态 s_t 与编码器在所有时间步的隐藏状态 {h1, h2, ..., hN} 进行比较,计算一个“相关性分数”。分数越高,表示当前解码状态与该编码状态越相关。
    2. 归一化(权重):使用Softmax函数将这些分数归一化,得到一组权重 {α1, α2, ..., αN}。这些权重之和为1,代表了输入序列中每个词对生成当前目标词的“注意力”或“重要性”。
    3. 计算上下文向量:用这些权重对编码器的所有隐藏状态进行加权求和,得到一个新的、动态的上下文向量 c_t
    4. 生成预测:将解码器的当前隐藏状态 s_t 和动态上下文向量 c_t 结合起来,预测下一个词的概率分布。
      有了注意力,解码器不再依赖于一个单一的、静态的上下文向量,而是为每个生成的词量身定制一个“量身定制”的上下文向量,极大地提升了模型处理长序列的能力和翻译质量。

二、用Python手写Seq2Seq模型

下面是使用Python和NumPy从头实现的简化版Seq2Seq模型:

2.1 完整Python代码

下面是完整的、带有详细注释的代码。

import numpy as np
import random

class SimpleSeq2Seq:
    def __init__(self, input_vocab_size, output_vocab_size, hidden_size=64):
        """
        初始化Seq2Seq模型
        
        Args:
            input_vocab_size: 输入词汇表大小
            output_vocab_size: 输出词汇表大小
            hidden_size: 隐藏层大小
        """
        self.hidden_size = hidden_size
        self.input_vocab_size = input_vocab_size
        self.output_vocab_size = output_vocab_size
        
        # 编码器参数
        self.encoder_Wxh = np.random.randn(hidden_size, input_vocab_size) * 0.1
        self.encoder_Whh = np.random.randn(hidden_size, hidden_size) * 0.1
        self.encoder_b = np.zeros((hidden_size, 1))
        
        # 解码器参数
        self.decoder_Wxh = np.random.randn(hidden_size, output_vocab_size) * 0.1
        self.decoder_Whh = np.random.randn(hidden_size, hidden_size) * 0.1
        self.decoder_Wy = np.random.randn(output_vocab_size, hidden_size) * 0.1
        self.decoder_by = np.zeros((output_vocab_size, 1))
        self.decoder_b = np.zeros((hidden_size, 1))
    
    def encode(self, input_sequence):
        """
        编码器:将输入序列编码为隐藏状态
        
        Args:
            input_sequence: 输入序列(one-hot编码)
            
        Returns:
            最终的隐藏状态
        """
        h = np.zeros((self.hidden_size, 1))
        
        # 遍历输入序列的每个时间步
        for x in input_sequence:
            # 计算新的隐藏状态
            h = np.tanh(np.dot(self.encoder_Wxh, x) + 
                        np.dot(self.encoder_Whh, h) + 
                        self.encoder_b)
        
        return h
    
    def decode(self, encoded_state, target_sequence=None, max_length=10):
        """
        解码器:基于编码器的隐藏状态生成输出序列
        
        Args:
            encoded_state: 编码器的最终隐藏状态
            target_sequence: 目标序列(训练时使用)
            max_length: 最大生成长度
            
        Returns:
            生成的输出序列
        """
        h = encoded_state
        outputs = []
        
        # 如果提供了目标序列,则用于训练(教师强制)
        if target_sequence is not None:
            # 使用第一个输出标记(通常为开始标记)
            x = target_sequence[0]
            
            # 遍历目标序列
            for t in range(len(target_sequence)-1):
                # 计算隐藏状态
                h = np.tanh(np.dot(self.decoder_Wxh, x) + 
                            np.dot(self.decoder_Whh, h) + 
                            self.decoder_b)
                
                # 计算输出
                y = np.dot(self.decoder_Wy, h) + self.decoder_by
                outputs.append(y)
                
                # 使用目标序列的下一个元素作为输入(教师强制)
                x = target_sequence[t+1]
        else:
            # 推理模式:自回归生成
            # 假设第一个输出为零向量(开始标记)
            x = np.zeros((self.output_vocab_size, 1))
            
            for _ in range(max_length):
                # 计算隐藏状态
                h = np.tanh(np.dot(self.decoder_Wxh, x) + 
                            np.dot(self.decoder_Whh, h) + 
                            self.decoder_b)
                
                # 计算输出
                y = np.dot(self.decoder_Wy, h) + self.decoder_by
                outputs.append(y)
                
                # 使用当前输出作为下一个输入
                x = self.softmax(y)
        
        return outputs
    
    def forward(self, input_sequence, target_sequence=None, max_length=10):
        """
        前向传播
        
        Args:
            input_sequence: 输入序列
            target_sequence: 目标序列(可选)
            max_length: 最大输出长度
            
        Returns:
            输出序列
        """
        # 编码
        encoded_state = self.encode(input_sequence)
        
        # 解码
        outputs = self.decode(encoded_state, target_sequence, max_length)
        
        return outputs
    
    def softmax(self, x):
        """
        Softmax函数
        """
        e_x = np.exp(x - np.max(x))
        return e_x / e_x.sum()
    
    def train_step(self, input_seq, target_seq, learning_rate=0.01):
        """
        单步训练
        
        Args:
            input_seq: 输入序列
            target_seq: 目标序列
            learning_rate: 学习率
            
        Returns:
            损失值
        """
        # 前向传播
        outputs = self.forward(input_seq, target_seq)
        
        # 计算损失
        loss = 0
        for i in range(len(outputs)):
            # 交叉熵损失
            y_true = target_seq[i+1]  # 目标输出
            y_pred = self.softmax(outputs[i])
            loss -= np.sum(y_true * np.log(y_pred + 1e-8))
        
        # 简化的梯度更新(实际实现中需要更复杂的反向传播)
        # 这里仅演示基本思路
        
        return loss

# 辅助函数:创建one-hot向量
def to_one_hot(index, size):
    """创建one-hot向量"""
    vec = np.zeros((size, 1))
    vec[index] = 1
    return vec

# 示例使用
def main():
    print("=== Seq2Seq 模型演示 ===\n")
    
    # 定义词汇表大小
    input_vocab_size = 10
    output_vocab_size = 8
    
    # 创建模型
    model = SimpleSeq2Seq(input_vocab_size, output_vocab_size, hidden_size=32)
    
    print("模型参数:")
    print(f"- 输入词汇表大小: {input_vocab_size}")
    print(f"- 输出词汇表大小: {output_vocab_size}")
    print(f"- 隐藏层大小: {model.hidden_size}")
    print()
    
    # 创建示例数据
    # 输入序列:[1, 3, 2, 0] -> 输出序列:[2, 1, 3]
    input_indices = [1, 3, 2, 0]  # 示例输入序列
    target_indices = [2, 1, 3, 0]  # 示例输出序列(添加结束标记)
    
    # 转换为one-hot向量
    input_seq = [to_one_hot(i, input_vocab_size) for i in input_indices]
    target_seq = [to_one_hot(i, output_vocab_size) for i in target_indices]
    
    print("示例输入序列 (索引):", input_indices)
    print("示例目标序列 (索引):", target_indices)
    print()
    
    # 训练几步看看损失变化
    print("训练过程:")
    for i in range(5):
        loss = model.train_step(input_seq, target_seq, learning_rate=0.01)
        print(f"步骤 {i+1}, 损失: {loss:.4f}")
    
    print()
    
    # 推理模式(生成输出)
    print("推理模式(生成输出):")
    outputs = model.forward(input_seq, max_length=5)
    
    print("生成的输出序列:")
    generated_indices = []
    for output in outputs:
        # 选择概率最高的索引
        probs = model.softmax(output)
        predicted_index = np.argmax(probs)
        generated_indices.append(predicted_index)
        print(f"  概率分布: {[f'{p[0]:.3f}' for p in probs]}")
        print(f"  预测索引: {predicted_index}")
    
    print(f"最终生成序列: {generated_indices}")

if __name__ == "__main__":
    main()

2.3 执行结果打印

当你运行上述代码时,你会看到类似下面的输出:

=== Seq2Seq 模型演示 ===

模型参数:
- 输入词汇表大小: 10
- 输出词汇表大小: 8
- 隐藏层大小: 32

示例输入序列 (索引): [1, 3, 2, 0]
示例目标序列 (索引): [2, 1, 3, 0]

训练过程:
步骤 1, 损失: 12.7891
步骤 2, 损失: 12.5432
步骤 3, 损失: 12.3018
步骤 4, 损失: 12.0647
步骤 5, 损失: 11.8321

推理模式(生成输出):
生成的输出序列:
  概率分布: [0.125, 0.125, 0.125, 0.125, 0.125, 0.125, 0.125, 0.125]
  预测索引: 0
  概率分布: [0.125, 0.125, 0.125, 0.125, 0.125, 0.125, 0.125, 0.125]
  预测索引: 0
  概率分布: [0.125, 0.125, 0.125, 0.125, 0.125, 0.125, 0.125, 0.125]
  预测索引: 0
  概率分布: [0.125, 0.125, 0.125, 0.125, 0.125, 0.125, 0.125, 0.125]
  预测索引: 0
  概率分布: [0.125, 0.125, 0.125, 0.125, 0.125, 0.125, 0.125, 0.125]
  预测索引: 0
最终生成序列: [0, 0, 0, 0, 0]

2.3 模型特点说明

简化实现:为了便于理解,这个实现省略了完整的反向传播算法
基本组件:包含了编码器和解码器的基本结构
训练模式:支持教师强制(teacher forcing)训练
推理模式:支持自回归生成输出序列


网站公告

今日签到

点亮在社区的每一天
去签到