文章目录
-
- 一、序列到序列(Seq2Seq)模型详解
-
- [1.1 核心思想](#1.1 核心思想)
- [1.2 基本工作原理](#1.2 基本工作原理)
- [1.3 核心组件](#1.3 核心组件)
- [1.4 注意力机制 - 关键改进](#1.4 注意力机制 - 关键改进)
- 二、用Python手写Seq2Seq模型
-
- [2.1 完整Python代码](#2.1 完整Python代码)
- [2.3 执行结果打印](#2.3 执行结果打印)
- [2.3 模型特点说明](#2.3 模型特点说明)
一、序列到序列(Seq2Seq)模型详解
序列到序列(Sequence-to-Sequence, Seq2Seq) 模型是一种深度学习架构,主要用于处理输入和输出都是序列的任务,如机器翻译、文本摘要、对话生成等。
Seq2Seq模型是一种强大的深度学习架构,专门用于处理将一个序列转换为另一个序列的任务。它由两个核心组件构成:编码器 和 解码器。
1.1 核心思想
Seq2Seq模型的核心思想是"先理解,再生成"。
- 理解(编码) :模型首先读取输入的整个序列(比如一个句子),并将其压缩成一个包含所有信息的上下文向量 。这个过程由编码器完成。
- 生成(解码) :然后,模型根据这个上下文向量,一个词一个词地生成目标序列(比如翻译后的句子)。这个过程由解码器完成。
1.2 基本工作原理
编码器读取输入序列,将其压缩为一个固定大小的上下文向量
解码器基于该上下文向量逐步生成输出序列
每个时间步的输出作为下一个时间步的输入
1.3 核心组件
A. 编码器
编码器的任务是处理输入序列并生成一个固定长度的上下文向量。
- 结构 :通常由一个循环神经网络构成,如LSTM或GRU。RNN非常适合处理序列数据,因为它具有"记忆"功能,能捕捉序列中的时序依赖关系。
- 工作流程 :
- 输入序列的第一个词被送入RNN,RNN输出一个隐藏状态
h1
。 - 序列的第二个词和
h1
一起被送入RNN,输出新的隐藏状态h2
。 - 这个过程持续到序列的最后一个词,最终得到最后一个隐藏状态
hN
(N是序列长度)。
- 输入序列的第一个词被送入RNN,RNN输出一个隐藏状态
- 上下文向量 :在最初的Seq2Seq模型中,最后一个隐藏状态
hN
就被直接用作整个输入序列的上下文向量 。这个向量被传递给解码器,作为生成目标序列的"起点"和"背景信息"。
问题:如果输入序列非常长,仅仅依靠最后一个隐藏状态来概括整个序列的信息,可能会导致信息丢失或遗忘(长序列依赖问题)。
B. 解码器
解码器的任务是接收上下文向量,并生成目标序列。
- 结构:同样通常由一个RNN(LSTM或GRU)构成。
- 工作流程 :
- 解码器的初始隐藏状态被设置为编码器输出的上下文向量
hN
。 - 解码器首先接收一个特殊的起始标记 (如
<sos>
),结合初始隐藏状态,生成第一个预测词的概率分布。 - 从概率分布中选出概率最高的词(例如,"I"),并将其作为下一个时间步的输入。
- 将""和" I"的预测结果分别作为下一步的输入,重复此过程,直到生成一个结束标记 (如
<eos>
)或达到最大长度。
- 解码器的初始隐藏状态被设置为编码器输出的上下文向量
1.4 注意力机制 - 关键改进
为了解决编码器仅用最后一个状态表示长序列信息的问题,注意力机制 被引入。它允许解码器在生成每个词时,都能"回顾"输入序列的所有部分,并根据当前需要动态地关注输入序列中的不同部分。
- 工作原理(以解码器生成第
t
个词为例) :- 计算注意力分数 :解码器当前的隐藏状态
s_t
与编码器在所有时间步的隐藏状态{h1, h2, ..., hN}
进行比较,计算一个"相关性分数"。分数越高,表示当前解码状态与该编码状态越相关。 - 归一化(权重) :使用Softmax函数将这些分数归一化,得到一组权重
{α1, α2, ..., αN}
。这些权重之和为1,代表了输入序列中每个词对生成当前目标词的"注意力"或"重要性"。 - 计算上下文向量 :用这些权重对编码器的所有隐藏状态进行加权求和,得到一个新的、动态的上下文向量
c_t
。 - 生成预测 :将解码器的当前隐藏状态
s_t
和动态上下文向量c_t
结合起来,预测下一个词的概率分布。
有了注意力,解码器不再依赖于一个单一的、静态的上下文向量,而是为每个生成的词量身定制一个"量身定制"的上下文向量,极大地提升了模型处理长序列的能力和翻译质量。
- 计算注意力分数 :解码器当前的隐藏状态
二、用Python手写Seq2Seq模型
下面是使用Python和NumPy从头实现的简化版Seq2Seq模型:
2.1 完整Python代码
下面是完整的、带有详细注释的代码。
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)训练
推理模式:支持自回归生成输出序列