深度学习每周学习总结N7:seq2seq翻译实战

目录

  • 使用seq2seq实现文本翻译
    • 一、前期准备工作
      • [1. 搭建语言类](#1. 搭建语言类)
      • [2. 文本处理函数](#2. 文本处理函数)
      • [3. 文件读取函数](#3. 文件读取函数)
    • [二、Seq2Seq 模型](#二、Seq2Seq 模型)
      • [1. 编码器(Encoder)](#1. 编码器(Encoder))
      • [2. 解码器(Decoder)](#2. 解码器(Decoder))
    • 三、训练
      • [1. 数据预处理](#1. 数据预处理)
      • [2. 训练函数](#2. 训练函数)
    • 四、训练与评估

总结:
之前有学习过文本预处理的环节,对文本处理的主要方式有以下三种:

1:词袋模型(one-hot编码)

2:TF-IDF

3:Word2Vec(词向量(Word Embedding) 以及Word2vec(Word Embedding 的方法之一))

详细介绍及中英文分词详见pytorch文本分类(一):文本预处理

N1期介绍了one-hot编码

N2期主要介绍Embedding,及EmbeddingBag 使用示例(对词索引向量转化为词嵌入向量)

N3期主要介绍:应用三种模型的英文分类

N4期将主要介绍中文基本分类(熟悉流程)、拓展:textCNN分类(通用模型)、拓展:Bert分类(模型进阶)

N5期主要介绍Word2Vec,和nn.Embedding(), nn.EmbeddingBag()相比都是嵌入技术,用于将离散的词语或符号映射到连续的向量空间。

nn.Embeddingnn.EmbeddingBag 是深度学习框架(如 PyTorch)中的层,直接用于神经网络模型中,而 Word2Vec 是一种独立的词嵌入算法。

使用来说,如果需要在神经网络中处理变长序列的嵌入,可以选择 nn.EmbeddingBag;如果需要预训练词嵌入用于不同任务,可以选择 Word2Vec

N6期主要介绍使用Word2Vec实现文本分类:

与N4文本分类的异同点总结

  • 共同点
    数据加载:都使用了PyTorch的DataLoader来批量加载数据。
    模型训练:训练过程大同小异,都是前向传播、计算损失、反向传播和梯度更新。
  • 不同点
    分词处理
    BERT模型:使用专门的BERT分词器。
    传统嵌入方法:通常使用jieba等工具进行分词。
    Word2Vec模型:假设数据已经分词。
    词向量表示
    BERT模型:使用BERT生成的上下文相关的词向量。
    传统嵌入方法:使用静态预训练词向量。
    Word2Vec模型:训练一个Word2Vec模型生成词向量。
    模型结构
    BERT模型:使用预训练的BERT模型作为编码器。
    传统嵌入方法:一般使用嵌入层+卷积/循环神经网络。
    Word2Vec模型:使用Word2Vec词向量和一个简单的线性分类器。
  • 值得学习的点
    词向量的使用:了解如何使用Word2Vec生成词向量并将其用于下游任务。
    数据预处理:不同方法的数据预处理方式,尤其是分词和词向量化的处理。
    模型训练:标准的模型训练和评估流程,尤其是损失计算、反向传播和梯度更新等步骤。
    超参数选择:注意学习率、批量大小和训练轮数等超参数的选择。
    通过这些比较和分析,可以更好地理解不同文本分类方法的优缺点以及适用场景。

本期(N7期),需要理解RNN 及 seq2seq代码,并在此基础上成功运行代码,理解代码流程

使用seq2seq实现文本翻译

python 复制代码
from __future__ import unicode_literals, print_function, division
from io import open
import unicodedata
import string
import re
import random

import torch
import torch.nn as nn
from torch import optim
import torch.nn.functional as F

device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
print(device)
cuda

一、前期准备工作

1. 搭建语言类

python 复制代码
SOS_token = 0
EOS_token = 1

# 语言类,方便对语料库进行操作
class Lang:
    def __init__(self, name):
        self.name = name
        self.word2index = {}
        self.word2count = {}
        self.index2word = {0: "SOS", 1: "EOS"}
        self.n_words    = 2  # Count SOS and EOS

    def addSentence(self, sentence):
        for word in sentence.split(' '):
            self.addWord(word)

    def addWord(self, word):
        if word not in self.word2index:
            self.word2index[word] = self.n_words
            self.word2count[word] = 1
            self.index2word[self.n_words] = word
            self.n_words += 1
        else:
            self.word2count[word] += 1

2. 文本处理函数

python 复制代码
def unicodeToAscii(s):
    return ''.join(
        c for c in unicodedata.normalize('NFD', s)
        if unicodedata.category(c) != 'Mn'
    )

# 小写化,剔除标点与非字母符号
def normalizeString(s):
    s = unicodeToAscii(s.lower().strip())
    s = re.sub(r"([.!?])", r" \1", s)
    s = re.sub(r"[^a-zA-Z.!?]+", r" ", s)
    return s

3. 文件读取函数

python 复制代码
def readLangs(lang1, lang2, reverse=False):
    print("Reading lines...")

    # 以行为单位读取文件
    lines = open('./data/N9-%s-%s.txt'%(lang1,lang2), encoding='utf-8').\
            read().strip().split('\n')

    # 将每一行放入一个列表中
    # 一个列表中有两个元素,A语言文本与B语言文本
    pairs = [[normalizeString(s) for s in l.split('\t')] for l in lines]

    # 创建Lang实例,并确认是否反转语言顺序
    if reverse:
        pairs       = [list(reversed(p)) for p in pairs]
        input_lang  = Lang(lang2)
        output_lang = Lang(lang1)
    else:
        input_lang  = Lang(lang1)
        output_lang = Lang(lang2)

    return input_lang, output_lang, pairs

.startswith(eng_prefixes) 是字符串方法 startswith() 的调用。它用于检查一个字符串是否以指定的前缀开始。

python 复制代码
MAX_LENGTH = 10      # 定义语料最长长度

eng_prefixes = (
    "i am ", "i m ",
    "he is", "he s ",
    "she is", "she s ",
    "you are", "you re ",
    "we are", "we re ",
    "they are", "they re "
)

def filterPair(p):
    return len(p[0].split(' ')) < MAX_LENGTH and \
           len(p[1].split(' ')) < MAX_LENGTH and p[1].startswith(eng_prefixes)

def filterPairs(pairs):
    # 选取仅仅包含 eng_prefixes 开头的语料
    return [pair for pair in pairs if filterPair(pair)]
python 复制代码
def prepareData(lang1, lang2, reverse=False):
    # 读取文件中的数据
    input_lang, output_lang, pairs = readLangs(lang1, lang2, reverse)
    print("Read %s sentence pairs" % len(pairs))
    
    # 按条件选取语料
    pairs = filterPairs(pairs[:])
    print("Trimmed to %s sentence pairs" % len(pairs))
    print("Counting words...")
    
    # 将语料保存至相应的语言类
    for pair in pairs:
        input_lang.addSentence(pair[0])
        output_lang.addSentence(pair[1])
        
    # 打印语言类的信息    
    print("Counted words:")
    print(input_lang.name, input_lang.n_words)
    print(output_lang.name, output_lang.n_words)
    return input_lang, output_lang, pairs

input_lang, output_lang, pairs = prepareData('eng', 'fra', True)
print(random.choice(pairs))
Reading lines...
Read 135842 sentence pairs
Trimmed to 10599 sentence pairs
Counting words...
Counted words:
fra 4345
eng 2803
['je ne suis pas joueur .', 'i m not a gambler .']

二、Seq2Seq 模型

1. 编码器(Encoder)

python 复制代码
class EncoderRNN(nn.Module):
    def __init__(self, input_size, hidden_size):
        super(EncoderRNN, self).__init__()
        self.hidden_size = hidden_size
        self.embedding   = nn.Embedding(input_size, hidden_size)
        self.gru         = nn.GRU(hidden_size, hidden_size)

    def forward(self, input, hidden):
        embedded       = self.embedding(input).view(1, 1, -1)
        output         = embedded
        output, hidden = self.gru(output, hidden)
        return output, hidden

    def initHidden(self):
        return torch.zeros(1, 1, self.hidden_size, device=device)

2. 解码器(Decoder)

python 复制代码
class DecoderRNN(nn.Module):
    def __init__(self, hidden_size, output_size):
        super(DecoderRNN, self).__init__()
        self.hidden_size = hidden_size
        self.embedding   = nn.Embedding(output_size, hidden_size)
        self.gru         = nn.GRU(hidden_size, hidden_size)
        self.out         = nn.Linear(hidden_size, output_size)
        self.softmax     = nn.LogSoftmax(dim=1)

    def forward(self, input, hidden):
        output         = self.embedding(input).view(1, 1, -1)
        output         = F.relu(output)
        output, hidden = self.gru(output, hidden)
        output         = self.softmax(self.out(output[0]))
        return output, hidden

    def initHidden(self):
        return torch.zeros(1, 1, self.hidden_size, device=device)

三、训练

1. 数据预处理

python 复制代码
# 将文本数字化,获取词汇index
def indexesFromSentence(lang, sentence):
    return [lang.word2index[word] for word in sentence.split(' ')]

# 将数字化的文本,转化为tensor数据
def tensorFromSentence(lang, sentence):
    indexes = indexesFromSentence(lang, sentence)
    indexes.append(EOS_token)
    return torch.tensor(indexes, dtype=torch.long, device=device).view(-1, 1)

# 输入pair文本,输出预处理好的数据
def tensorsFromPair(pair):
    input_tensor  = tensorFromSentence(input_lang, pair[0])
    target_tensor = tensorFromSentence(output_lang, pair[1])
    return (input_tensor, target_tensor)

2. 训练函数

python 复制代码
teacher_forcing_ratio = 0.5

def train(input_tensor, target_tensor, 
          encoder, decoder, 
          encoder_optimizer, decoder_optimizer, 
          criterion, max_length=MAX_LENGTH):
    
    # 编码器初始化
    encoder_hidden = encoder.initHidden()
    
    # grad属性归零
    encoder_optimizer.zero_grad()
    decoder_optimizer.zero_grad()

    input_length  = input_tensor.size(0)
    target_length = target_tensor.size(0)
    
    # 用于创建一个指定大小的全零张量(tensor),用作默认编码器输出
    encoder_outputs = torch.zeros(max_length, encoder.hidden_size, device=device)

    loss = 0
    
    # 将处理好的语料送入编码器
    for ei in range(input_length):
        encoder_output, encoder_hidden = encoder(input_tensor[ei], encoder_hidden)
        encoder_outputs[ei]            = encoder_output[0, 0]
    
    # 解码器默认输出
    decoder_input  = torch.tensor([[SOS_token]], device=device)
    decoder_hidden = encoder_hidden

    use_teacher_forcing = True if random.random() < teacher_forcing_ratio else False
    
    # 将编码器处理好的输出送入解码器
    if use_teacher_forcing:
        # Teacher forcing: Feed the target as the next input
        for di in range(target_length):
            decoder_output, decoder_hidden = decoder(decoder_input, decoder_hidden)
            
            loss         += criterion(decoder_output, target_tensor[di])
            decoder_input = target_tensor[di]  # Teacher forcing
    else:
        # Without teacher forcing: use its own predictions as the next input
        for di in range(target_length):
            decoder_output, decoder_hidden = decoder(decoder_input, decoder_hidden)
            
            topv, topi    = decoder_output.topk(1)
            decoder_input = topi.squeeze().detach()  # detach from history as input

            loss         += criterion(decoder_output, target_tensor[di])
            if decoder_input.item() == EOS_token:
                break

    loss.backward()

    encoder_optimizer.step()
    decoder_optimizer.step()

    return loss.item() / target_length

在序列生成的任务中,如机器翻译或文本生成,解码器(decoder)的输入通常是由解码器自己生成的预测结果,即前一个时间步的输出。然而,这种自回归方式可能存在一个问题,即在训练过程中,解码器可能会产生累积误差,并导致输出与目标序列逐渐偏离。

为了解决这个问题,引入了一种称为"Teacher Forcing"的技术。在训练过程中,Teacher Forcing将目标序列的真实值作为解码器的输入,而不是使用解码器自己的预测结果。这样可以提供更准确的指导信号,帮助解码器更快地学习到正确的输出。

在这段代码中,use_teacher_forcing变量用于确定解码器在训练阶段使用何种策略作为下一个输入。

use_teacher_forcingTrue时,采用"Teacher Forcing"的策略,即将目标序列中的真实标签作为解码器的下一个输入。而当use_teacher_forcingFalse时,采用"Without Teacher Forcing"的策略,即将解码器自身的预测作为下一个输入。

使用use_teacher_forcing的目的是在训练过程中平衡解码器的预测能力和稳定性。以下是对两种策略的解释:

  1. Teacher Forcing: 在每个时间步(di循环中),解码器的输入都是目标序列中的真实标签。这样做的好处是,解码器可以直接获得正确的输入信息,加快训练速度,并且在训练早期提供更准确的梯度信号,帮助解码器更好地学习。然而,过度依赖目标序列可能会导致模型过于敏感,一旦目标序列中出现错误,可能会在解码器中产生累积的误差。

  2. Without Teacher Forcing: 在每个时间步,解码器的输入是前一个时间步的预测输出。这样做的好处是,解码器需要依靠自身的预测能力来生成下一个输入,从而更好地适应真实应用场景中可能出现的输入变化。这种策略可以提高模型的稳定性,但可能会导致训练过程更加困难,特别是在初始阶段。

一般来说,Teacher Forcing策略在训练过程中可以帮助模型快速收敛,而Without Teacher Forcing策略则更接近真实应用中的生成场景。通常会使用一定比例的Teacher Forcing,在训练过程中逐渐减小这个比例,以便模型逐渐过渡到更自主的生成模式。

综上所述,通过使用use_teacher_forcing来选择不同的策略,可以在训练解码器时平衡模型的预测能力和稳定性,同时也提供了更灵活的生成模式选择。

  1. topv, topi = decoder_output.topk(1)

    这一行代码使用.topk(1)函数从decoder_output中获取最大的元素及其对应的索引。decoder_output是一个张量(tensor),它包含了解码器的输出结果,可能是一个概率分布或是其他的数值。.topk(1)函数将返回两个张量:topvtopitopv是最大的元素值,而topi是对应的索引值。

  2. decoder_input = topi.squeeze().detach()

    这一行代码对topi进行处理,以便作为下一个解码器的输入。首先,.squeeze()函数被调用,它的作用是去除张量中维度为1的维度,从而将topi的形状进行压缩。然后,.detach()函数被调用,它的作用是将张量从计算图中分离出来,使得在后续的计算中不会对该张量进行梯度计算。最后,将处理后的张量赋值给decoder_input,作为下一个解码器的输入。

python 复制代码
import time
import math

def asMinutes(s):
    m = math.floor(s / 60)
    s -= m * 60
    return '%dm %ds' % (m, s)

def timeSince(since, percent):
    now = time.time()
    s = now - since
    es = s / (percent)
    rs = es - s
    return '%s (- %s)' % (asMinutes(s), asMinutes(rs))
python 复制代码
def trainIters(encoder,decoder,n_iters,print_every=1000,
               plot_every=100,learning_rate=0.01):
    
    start = time.time()
    plot_losses      = []
    print_loss_total = 0  # Reset every print_every
    plot_loss_total  = 0  # Reset every plot_every

    encoder_optimizer = optim.SGD(encoder.parameters(), lr=learning_rate)
    decoder_optimizer = optim.SGD(decoder.parameters(), lr=learning_rate)
    
    # 在 pairs 中随机选取 n_iters 条数据用作训练集
    training_pairs    = [tensorsFromPair(random.choice(pairs)) for i in range(n_iters)]
    criterion         = nn.NLLLoss()

    for iter in range(1, n_iters + 1):
        training_pair = training_pairs[iter - 1]
        input_tensor  = training_pair[0]
        target_tensor = training_pair[1]

        loss = train(input_tensor, target_tensor, encoder,
                     decoder, encoder_optimizer, decoder_optimizer, criterion)
        print_loss_total += loss
        plot_loss_total  += loss

        if iter % print_every == 0:
            print_loss_avg   = print_loss_total / print_every
            print_loss_total = 0
            print('%s (%d %d%%) %.4f' % (timeSince(start, iter / n_iters),
                                         iter, iter / n_iters * 100, print_loss_avg))

        if iter % plot_every == 0:
            plot_loss_avg = plot_loss_total / plot_every
            plot_losses.append(plot_loss_avg)
            plot_loss_total = 0

    return plot_losses

四、训练与评估

python 复制代码
hidden_size   = 256
encoder1      = EncoderRNN(input_lang.n_words, hidden_size).to(device)
attn_decoder1 = DecoderRNN(hidden_size, output_lang.n_words).to(device)

plot_losses = trainIters(encoder1, attn_decoder1, 20000, print_every=5000)
2m 25s (- 7m 17s) (5000 25%) 2.9208
4m 50s (- 4m 50s) (10000 50%) 2.3893
7m 15s (- 2m 25s) (15000 75%) 2.0605
9m 38s (- 0m 0s) (20000 100%) 1.8302
python 复制代码
import matplotlib.pyplot as plt
#隐藏警告
import warnings
warnings.filterwarnings("ignore")               # 忽略警告信息
# plt.rcParams['font.sans-serif']    = ['SimHei'] # 用来正常显示中文标签
plt.rcParams['axes.unicode_minus'] = False      # 用来正常显示负号
plt.rcParams['figure.dpi']         = 100        # 分辨率

epochs_range = range(len(plot_losses))

plt.figure(figsize=(8, 3))

plt.subplot(1, 1, 1)
plt.plot(epochs_range, plot_losses, label='Training Loss')
plt.legend(loc='upper right')
plt.title('Training Loss')
plt.show()

python 复制代码
相关推荐
深蓝海拓12 分钟前
Pyside6(PyQT5)中的QTableView与QSqlQueryModel、QSqlTableModel的联合使用
数据库·python·qt·pyqt
IE0612 分钟前
深度学习系列76:流式tts的一个简单实现
人工智能·深度学习
GIS数据转换器17 分钟前
城市生命线安全保障:技术应用与策略创新
大数据·人工智能·安全·3d·智慧城市
无须logic ᭄20 分钟前
CrypTen项目实践
python·机器学习·密码学·同态加密
百流32 分钟前
scala文件编译相关理解
开发语言·学习·scala
Channing Lewis33 分钟前
flask常见问答题
后端·python·flask
Channing Lewis34 分钟前
如何保护 Flask API 的安全性?
后端·python·flask
水兵没月1 小时前
钉钉群机器人设置——python版本
python·机器人·钉钉
一水鉴天2 小时前
为AI聊天工具添加一个知识系统 之65 详细设计 之6 变形机器人及伺服跟随
人工智能