简单介绍

  1. Word2Vec 的主要能力是将词汇放在多维空间中,相似的词汇会被放在邻近的位置
  2. Seq2Seq 不仅能理解词汇,还能将词汇串联成完整的句子
  3. Seq2Seq 即从一个序列到另一个序列的转换
    • 不仅仅能理解单词之间的关系,还能把整个句子的意思打包,并解压成另一种形式的表达
  4. Seq2Seq 的核心角色 - 编码器(Encoder) + 解码器(Decoder)
Role Desc
Encoder 理解和压缩信息 - 把一封长信函整理成一个精简的摘要
Decoder 摘要打开,并翻译成另一种语言或形式的完整信息

优缺点

Seq2Seq

固定长度上下文 + 逐步输入(长序列) + 参数规模小

  1. Seq2Seq 是一种比较高级的神经网络模型,适用于语言翻译,甚至是基本的问答系统
  2. Seq2Seq 使用固定上下文长度,因此长距离依赖的能力比较弱
  3. Seq2Seq 的训练推理通常需要逐步处理输入和输出序列,在处理长序列受限
  4. Seq2Seq 的参数量通常较少,在面对复杂场景时,模型性能可能会受限

Word2Vec

image-20240825002026176

基本概念

Seq2Seq 是一种神经网络架构,模型的核心组成 - 编码器(Encoder) + 解码器(Decoder)

seq2seq-arch

编码器

  1. 编码器的任务是读取并理解序列,然后将它转换成一个固定长度上下文向量,即状态向量
  2. 状态向量输入序列的一种内部表示,捕捉了序列的关键信息
  3. 编码器通常是一个 RNN 或其变体 - 如 LSTM 或者 GRU
    • 能够处理不同长度的输入序列,并且记住序列中长期依赖关系

seq2seq-encoder

解码器

  1. 解码器的任务是接收编码器生成的状态向量,并基于该向量生成目标序列

  2. 解码过程是逐步进行

    • 每一步生成的目标序列中的一个元素字符

    • 直到生成特殊的结束符号,代表输出序列的结束

  3. 解码器通常也是一个 RNNLSTMGRU

    • 不仅仅依赖编码器生成的状态向量,还可能依赖解码器之前的输出,来生成一个输出元素

seq2seq-decoder

注意力机制

  1. 在编码器和解码器之间,会有一个注意力机制
  2. 注意力机制使解码器能够在生成每个输出元素时关注输入序列中的不同部分
  3. 注意力机制可以提高模型处理长序列捕捉依赖关系的能力

ef925bd2yyec5f51836262527e5fa03b

工作原理

场景为中英文翻译,训练数据为中英文数据对

数据集

  1. AI Challenger 2017 - https://github.com/AIChallenger/AI_Challenger_2017
  2. 该数据集有 1000 万对中英文数据,从中选取 10000 条英文数据和中文数据进行训练
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
cn_sentences = []
zh_file_path = "train_1w.zh"
# 使用Python的文件操作逐行读取文件,并将每一行的内容添加到列表中
with open(zh_file_path, "r", encoding="utf-8") as file:
for line in file:
# 去除行末的换行符并添加到列表中
cn_sentences.append(line.strip())

en_sentences = []
en_file_path = "train_1w.en"
# 使用Python的文件操作逐行读取文件,并将每一行的内容添加到列表中
with open(en_file_path, "r", encoding="utf-8") as file:
for line in file:
# 去除行末的换行符并添加到列表中
en_sentences.append(line.strip())

训练模型

构建词汇

基于训练数据集构建中文和英文的词汇表,将每个词映射到一个唯一索引(integer)

1
2
3
# cn_sentences 和 en_sentences 分别包含了所有的中文和英文句子
cn_vocab = build_vocab(cn_sentences, tokenize_cn, max_size=10000, min_freq=2)
en_vocab = build_vocab(en_sentences, tokenize_en, max_size=10000, min_freq=2)

构建词汇 - 读入所有句子,循环分词,放入字典(≥ min_freq)

1
2
3
4
5
6
7
8
9
10
11
def build_vocab(sentences, tokenizer, max_size, min_freq):
token_freqs = Counter()
for sentence in sentences:
tokens = tokenizer(sentence)
token_freqs.update(tokens)
vocab = {token: idx + 4 for idx, (token, freq) in enumerate(token_freqs.items()) if freq >= min_freq}
vocab['<unk>'] = 0
vocab['<pad>'] = 1
vocab['<sos>'] = 2
vocab['<eos>'] = 3
return vocab

输出结果

image-20240824223450755

重要部分

Part Desc
<unk> 未知单词,表示在训练数据中没有出现过的单词
当模型在处理输入文本时遇到未知单词时,会用 <unk> 来标记
<pad> 填充单词,用于将不同长度的序列填充相同长度
在处理批次数据时,不同序列的长度可能不同
使用 <pad> 把短序列填充到最长序列相同的长度,以便进行批次处理
<sos> 句子起始标记,表示句子的开始位置
通常会在目标句子开头添加 <sos> 标识,以指示解码器开始生成输出
<eos> 句子结束标记,表示句子的结束位置
通常会在目标句子末尾添加 <eos> 标识,以指示解码器生成结束

创建训练集

将数据处理成方便训练的格式 - 语言序列

1
2
3
# cn_vocab 和 en_vocab 是已经创建的词汇表
dataset = TranslationDataset(cn_sentences, en_sentences, cn_vocab, en_vocab, tokenize_cn, tokenize_en)
train_loader = DataLoader(dataset, batch_size=32, collate_fn=collate_fn)

检测设备

1
2
3
# 检查是否有可用的GPU,如果没有,则使用CPU
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
print("训练设备为:", device)

创建模型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 定义一些超参数
INPUT_DIM = 10000 # 输入语言的词汇量
OUTPUT_DIM = 10000 # 输出语言的词汇量
ENC_EMB_DIM = 256 # 编码器嵌入层大小,即编码器词向量维度
DEC_EMB_DIM = 256 # 解码器嵌入层大小,即解码器词向量维度
HID_DIM = 512 # 隐藏层维度
N_LAYERS = 2 # RNN层的数量
ENC_DROPOUT = 0.5 # 编码器中dropout的比例,编码器神经元输出的数据有 50% 会被随机丢掉
DEC_DROPOUT = 0.5 # 解码器同上

enc = Encoder(INPUT_DIM, ENC_EMB_DIM, HID_DIM, N_LAYERS, ENC_DROPOUT)
dec = Decoder(OUTPUT_DIM, DEC_EMB_DIM, HID_DIM, N_LAYERS, DEC_DROPOUT)

model = Seq2Seq(enc, dec, device).to(device)
# 假定模型已经被实例化并移到了正确的设备上
model.to(device)
# 定义优化器和损失函数
optimizer = optim.Adam(model.parameters())
criterion = nn.CrossEntropyLoss(ignore_index=en_vocab['<pad>']) # 忽略<pad>标记的损失

训练过程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
num_epochs = 10  # 训练轮数
for epoch in range(num_epochs):
model.train()
total_loss = 0
for src, trg in train_loader:
src, trg = src.to(device), trg.to(device)
optimizer.zero_grad() # 清空梯度
output = model(src, trg[:-1]) # 输入给模型的是除了最后一个词的目标句子
# Reshape输出以匹配损失函数期望的输入
output_dim = output.shape[-1]
output = output.view(-1, output_dim)
trg = trg[1:].view(-1) # 从第一个词开始的目标句子
loss = criterion(output, trg) # 计算模型输出和实际目标序列之间的损失
loss.backward() # 通过反向传播计算损失相对于模型参数的梯度
optimizer.step() # 根据梯度更新模型参数,这是优化器的一个步骤
total_loss += loss.item()
avg_loss = total_loss / len(train_loader)
print(f'Epoch {epoch + 1}/{num_epochs}, Average Loss: {avg_loss}')
# 可以在这里添加验证步骤
1
2
我 喜欢 学习 机器 学习。
I like studying machine learning
  1. 在开始训练之前,先将原文本转化成对应词汇表里的语言序列
    • 在中文词汇表中,我 喜欢 学习 机器 学习 分别对应的是 1,2,3,4,5
    • 那么转换成的语言序列为 1,2,3,4,5,即 train_loader 中的格式
  2. 编码器接收到语言序列,经过神经网络 GRU 后,生成一个状态向量,作为解码器初始状态
  3. 解码器接收到状态向量作为输入,并根据当前上下文以及已经生成的部分目标语言序列
    • 计算目标词汇表每个单词概率分布
    • 假设在第一个时间步,解码器生成的概率分布
      • "I": 0.3, "like": 0.1, "studying": 0.5, "machine": 0.05, "learning": 0.05
    • 根据解码器生成的概率分布,选择概率最高的词(studying),作为当前时间步输出
  4. 模型将解码器生成的输出词汇目标语言句子当前时间步对应的词汇I)进行对比
    • I like studying machine learning.
    • 解码器输出的是 studying,与目标语言句子中的 I,存在很大差别
  5. 根据解码器输出 studying 和目标语言句子中真实词汇 I 计算损失,并通过反向传播算法计算梯度
    • 损失值是一个衡量模型预测输出真实目标之间差异的指标
    • 根据损失值更新模型参数,使模型能够更准确地预测下一个词汇
  6. 重复以上步骤,直到模型达到指定的训练轮数或者满足其它停止训练的条件
    • 在每次训练迭代中,模型都在尝试调整参数,以使其预测输出更接近真实的目标语言序列

训练轮数非常关键,不能太少,也不能太多

验证模型

推理与训练的区别 - 训练过程中模型会记住参数,而推理过程直接根据参数计算下一个词的概率即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
def translate_sentence(sentence, src_vocab, trg_vocab, model, device, max_len=50):
# 将输入句子进行分词并转换为索引序列
src_tokens = ['<sos>'] + tokenize_cn(sentence) + ['<eos>']
src_indices = [src_vocab[token] if token in src_vocab else src_vocab['<unk>'] for token in src_tokens]
# 将输入句子转换为张量并移动到设备上
src_tensor = torch.LongTensor(src_indices).unsqueeze(1).to(device)
# 将输入句子传递给编码器以获取上下文张量
with torch.no_grad():
encoder_hidden = model.encoder(src_tensor)
# 初始化解码器输入为<sos>
trg_token = '<sos>'
trg_index = trg_vocab[trg_token]
# 存储翻译结果
translation = []
# 解码过程
for _ in range(max_len):
# 将解码器输入传递给解码器,并获取输出和隐藏状态
with torch.no_grad():
trg_tensor = torch.LongTensor([trg_index]).to(device)
output, encoder_hidden = model.decoder(trg_tensor, encoder_hidden)
# 获取解码器输出中概率最高的单词的索引
pred_token_index = output.argmax(dim=1).item()
# 如果预测的单词是句子结束符,则停止解码
if pred_token_index == trg_vocab['<eos>']:
break
# 否则,将预测的单词添加到翻译结果中
pred_token = list(trg_vocab.keys())[list(trg_vocab.values()).index(pred_token_index)]
translation.append(pred_token)
# 更新解码器输入为当前预测的单词
trg_index = pred_token_index
# 将翻译结果转换为字符串并返回
translation = ' '.join(translation)
return translation


sentence = "我喜欢学习机器学习。"
translation = translate_sentence(sentence, cn_vocab, en_vocab, model, device)
print(f"Chinese: {sentence}")
print(f"Translation: {translation}")

输出 - 因训练数据太少,效果不佳

1
2
Chinese: 我喜欢学习机器学习。
Translation: a <unk> <unk> <unk> . . . .