简单介绍
Word2Vec 的主要能力是将词汇 放在多维空间 中,相似的词汇 会被放在邻近的位置
Seq2Seq 不仅能理解词汇 ,还能将词汇串联 成完整的句子
Seq2Seq 即从一个序列到另一个序列的转换
不仅仅能理解单词之间的关系 ,还能把整个句子的意思打包 ,并解压 成另一种形式的表达
Seq2Seq 的核心角色 - 编码器 (Encoder) + 解码器 (Decoder)
Role
Desc
Encoder
理解和压缩信息 - 把一封长信函整理成一个精简的摘要
Decoder
将摘要 打开,并翻译成另一种语言或形式的完整信息
优缺点 Seq2Seq
固定长度上下文 + 逐步输入(长序列) + 参数规模小
Seq2Seq 是一种比较高级 的神经网络模型,适用于语言翻译 ,甚至是基本的问答系统
Seq2Seq 使用固定 的上下文长度 ,因此长距离依赖 的能力比较弱
Seq2Seq 的训练 和推理 通常需要逐步处理 输入和输出序列,在处理长序列 会受限
Seq2Seq 的参数量通常较少 ,在面对复杂场景 时,模型性能 可能会受限
Word2Vec
基本概念
Seq2Seq 是一种神经网络架构,模型的核心组成 - 编码器 (Encoder) + 解码器 (Decoder)
编码器
编码器的任务是读取并理解序列 ,然后将它转换 成一个固定长度 的上下文向量 ,即状态向量
状态向量 是输入序列 的一种内部表示 ,捕捉了序列的关键信息
编码器通常是一个 RNN 或其变体 - 如 LSTM 或者 GRU
能够处理不同长度 的输入序列,并且记住序列中长期依赖关系
解码器
解码器的任务是接收编码器 生成的状态向量 ,并基于该向量生成目标序列
解码过程是逐步进行 的
每一步 生成的目标序列中的一个元素 (词 或字符 )
直到生成特殊的结束符号 ,代表输出序列的结束
解码器通常也是一个 RNN 、LSTM 、GRU
不仅仅依赖编码器生成的状态向量 ,还可能依赖解码器之前的输出 ,来生成一个输出元素
注意力机制
在编码器和解码器之间,会有一个注意力机制
注意力机制使解码器 能够在生成每个输出元素时关注输入序列中的不同部分
注意力机制可以提高模型处理长序列 和捕捉依赖关系 的能力
工作原理
场景为中英文翻译 ,训练数据为中英文数据对
数据集
AI Challenger 2017 - https://github.com/AIChallenger/AI_Challenger_2017
该数据集有 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" 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" 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_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
输出结果
重要部分
Part
Desc
<unk>
未知 单词,表示在训练数据中没有出现过的单词 当模型在处理输入文本时遇到未知单词时,会用 <unk> 来标记
<pad>
填充 单词,用于将不同长度 的序列填充 到相同长度 在处理批次数据 时,不同序列的长度可能不同 使用 <pad> 把短序列填充到最长序列 相同的长度,以便进行批次处理
<sos>
句子起始 标记,表示句子的开始位置 通常会在目标句子开头 添加 <sos> 标识,以指示解码器开始生成输出
<eos>
句子结束 标记,表示句子的结束位置 通常会在目标句子末尾 添加 <eos> 标识,以指示解码器生成结束
创建训练集
将数据处理成方便训练 的格式 - 语言序列
1 2 3 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 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 ENC_DROPOUT = 0.5 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>' ])
训练过程 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 ]) 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,2,3,4,5
那么转换成的语言序列为 1,2,3,4,5,即 train_loader 中的格式
编码器 接收到语言序列 ,经过神经网络 GRU 后,生成一个状态向量 ,作为解码器 的初始状态
解码器 接收到状态向量 作为输入 ,并根据当前上下文 以及已经生成的部分目标语言序列
计算目标词汇表 中每个单词 的概率分布
假设在第一个时间步,解码器生成的概率分布
"I": 0.3, "like": 0.1, "studying": 0.5, "machine": 0.05, "learning": 0.05
根据解码器 生成的概率分布 ,选择概率最高 的词(studying),作为当前时间步 的输出
模型将解码器生成的输出词汇 与目标语言句子 中当前时间步对应的词汇 (I)进行对比
I like studying machine learning.
解码器输出的是 studying,与目标语言句子中的 I,存在很大差别
根据解码器输出 studying 和目标语言句子中真实词汇 I 计算损失 ,并通过反向传播 算法计算梯度
损失值 是一个衡量模型预测输出 与真实目标 之间差异 的指标
根据损失值 更新模型参数 ,使模型能够更准确地预测 下一个词汇
重复以上步骤,直到模型达到指定的训练轮数 或者满足其它停止训练的条件
在每次训练迭代中,模型都在尝试调整参数 ,以使其预测输出更接近真实 的目标语言序列
训练轮数 非常关键,不能太少 ,也不能太多
验证模型
推理与训练的区别 - 训练过程 中模型会记住参数 ,而推理过程 直接根据参数计算 下一个词的概率即可
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) 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> . . . .