7.1 利用PyTorch构造神经翻译机

Posted 王小小小草

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了7.1 利用PyTorch构造神经翻译机相关的知识,希望对你有一定的参考价值。

欢迎订阅本专栏:《PyTorch深度学习实践》
订阅地址:https://blog.csdn.net/sinat_33761963/category_9720080.html

  • 第二章:认识Tensor的类型、创建、存储、api等,打好Tensor的基础,是进行PyTorch深度学习实践的重中之重的基础。
  • 第三章:学习PyTorch如何读入各种外部数据
  • 第四章:利用PyTorch从头到尾创建、训练、评估一个模型,理解与熟悉PyTorch实现模型的每个步骤,用到的模块与方法。
  • 第五章:学习如何利用PyTorch提供的3种方法去创建各种模型结构。
  • 第六章:利用PyTorch实现简单与经典的模型全过程:简单二分类、手写字体识别、词向量的实现、自编码器实现。
  • 第七章:利用PyTorch实现复杂模型:翻译机(nlp领域)、生成对抗网络(GAN)、强化学习(RL)、风格迁移(cv领域)。
  • 第八章:PyTorch的其他高级用法:模型在不同框架之间的迁移、可视化、多个GPU并行计算。

机器翻译其实是走得比较先的AI应用了,后来的许多自然语言生成的任务多少都在借鉴着机器翻译的一些研究与实践成果。如果你是做自然语言处理的,那么下面这篇论文是必读无疑的,关于sequence to sequence ,关于attention的知识也是必须知道。

论文:Bahdanau, Neural Machine Translation By Jointly Language to Align and Translate

本节就是针对以上论文中模型的一个简单实现。

先导入本节代码需要的包:

from __future__ import unicode_literals, print_function, division
import unicodedata
import re
import random
import torch
import torch.nn as nn
import torch.nn.functional as F

use_cuda = False

7.1.1 准备数据

原始数据

机器翻译模型输入是语言A,输出是翻译后的结果语言B。在本项目的data/translation_data/路径下有一份平行语料,chinese1为中文,english1为英文,每行一个样本,两份数据的行之间是对应的翻译结果。

数据处理

  • (1)预处理数据,将文本从Unicode格式转换为ASCII格式,去除大部分标点符号,大写字母转换成小写字母,这些操作用normaliz_string函数进行封装。
  • (2)过滤数据,过滤掉一些不符合设定标注的句子,比如过滤掉长度大于30的句子, 用filter_pairs函数封装。
  • (3)构建词典,分别对两个语言的语料构建词典,即词对应数字编码,用类LangEmbed()来构建。

下面看看代码具体的实现:

1)预处理数据的函数:

def unicode2ascii(s):
    return ''.join(
        c for c in unicodedata.normalize('NFD', s) if unicodedata.category(c) != 'Mn'
    )

def normalize_string(s):
    s = unicode2ascii(s.lower().strip()) # 小写、去前后空格、转ascii格式
    s = re.sub("[.!?。!?]", "\\1", s)  # 处理标点
    return s

2)过滤数据的函数, 为了简化,我们设置保留长度小于30的句子。

MAX_LENGTH = 10

def filter_pairs(pairs):
    p = []
    for pair in pairs:
        if len(pair[0]) <= MAX_LENGTH and len(pair[1]) <= MAX_LENGTH:
            p.append(pair)
            
    return p

3)构建词典的类

class LangEmbed():
    def __init__(self, name):
        self.name = name
        self.word2index = 
        self.word2count = 
        self.index2word = 0:"SOS", 1:"EOS"
        self.n_word=2
        
    def add_sentence(self, sentence):
        for word in sentence.split():
            self.add_word(word)
        
    def add_word(self, word):
        if word not in self.word2index:
            self.word2index[word] = self.n_word
            self.word2count[word] = 1
            self.index2word[self.n_word] = word
            self.n_word += 1
        else:
            self.word2count[word] += 1

4)读入数据整个整个预处理流程, 返回两个语言的词典,与语料对

def prepare_data():
    ch = open('data/translation_data/chinese1.txt', encoding='utf-8').readlines()
    en = open('data/translation_data/english1.txt', encoding='utf-8').readlines()
    pairs = [[normalize_string(ch[i]), normalize_string(en[i])] for i in range(len(ch)) ]
    print("read %s sentence paris" % (len(pairs)))
    
    pairs = filter_pairs(pairs)
    print("%s sentence after filter" % (len(pairs)))
    
    ch_lang = LangEmbed('ch')
    en_lang = LangEmbed('en')
    for pair in pairs:
        ch_lang.add_sentence(pair[0])
        en_lang.add_sentence(pair[1])
    print('number of chinese words', ch_lang.n_word)
    print('number of english words', en_lang.n_word)
    
    return ch_lang, en_lang, pairs

ch_lang, en_lang, pairs = prepare_data()
print(pairs[:5])
for i in range(10):
    print(i, ":", ch_lang.index2word[i])
read 10009 sentence paris
36 sentence after filter
number of chinese words 102
number of english words 93
[['" 导弹 起 竖 完毕 \\x01', '" missile erection is ready \\x01'], ['" " 发射 \\x01 "', '" fire \\x01 "'], ['赶快 行动 吧 \\x01 回到 主页', 'get into action at once \\x01'], ['第一 , 它 是 轻 度 的 \\x01', 'it is mild \\x01'], ['这是 为什麽 \\x01', 'why \\x01']]
0 : SOS
1 : EOS
2 : "
3 : 导弹
4 : 起
5 : 竖
6 : 完毕
7 : 
8 : 发射
9 : 赶快

7.1.2 构建模型

分别构建encoder与decoder两部分

1)encoder编码器的构建

  • 这里我们选择GRU单元,它有2个输入:当前步的词向量,上一步的隐向量;有1个输出:当前步的隐向量。使用nn.GRUCell来创建。

  • 输入GRU单元的是词嵌入向量,即输入的词one-hot向量先乘以词嵌入矩阵,得到词嵌入向量。注意这个词嵌入矩阵可以在创建时随机初始化,并通过训练来更新矩阵的值,也可以是直接使用外部的训练好的词嵌入矩阵,并选择训练时不更新该矩阵。这里选择的是第一种方式。词嵌入矩阵用nn.embedding来创建,第一个参数是输入的维度,第二个参数是输出的维度.

  • init_hidden函数是为了在外部调用初始化第一步时输入的hidden用的,这里初始化值为0,大小为(1,hidden_size)的矩阵, 第一维是batch_size,因为每一句的长度不一样,我们一次只输入一句话的一个词。

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.GRUCell(hidden_size, hidden_size)
        
    def forward(self, inputs, hidden):
        emb = self.embedding(inputs)
        hidden = self.gru(emb, hidden)  # gru有两个输入一个输出
        return hidden
    
    def init_hidden(self):
        h0 = torch.zeros(1, self.hidden_size)
        if use_cuda:
            return h0.cuda
        else:
            return h0

2)decoder解码器的构建

  • 仍然使用gru单元,gru当前步的输入是上一步的输出y, 和上一步的隐向量,gru的输出是当前步的隐向量
  • gru输出的隐向量需要过一个线性计算和softmax, 使得输出为词典大小的向量,是当前步对每个词的预测概率
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.GRUCell(hidden_size, hidden_size)
        self.out = nn.Linear(hidden_size, output_size)
        
    def forward(self, inputs, hidden):
        emb = self.embedding(inputs)
        emb = F.relu(emb)
        hidden = self.gru(emb, hidden)
        output = F.log_softmax(self.out(hidden))
        return output, hidden
        
    def init_hidden(self):
        h0 = torch.zeros(1, self.hidden_size)
        if use_cuda:
            return h0.cuda
        else:
            return h0
        

7.1.3 训练

1)首先我们构建一个函数,它的功能是对输入的一个样本进行前向后向并输出损失。

teacher_forcing_ratio = 0.5
SOS_token = 0
EOS_token = 1

def train(inputs, targets, encoder, decoder, encoder_optimizer, decoder_optimizer, criterion, max_len=MAX_LENGTH):
    # 1.前期工作==================================================
    encoder_hidden = encoder.init_hidden() # 初始化encoder的gru参数
    
    encoder_optimizer.zero_grad()  # encoder梯度置零
    decoder_optimizer.zero_grad()  # decoder梯度置零
    
    input_length = inputs.size()[0]   # 输入句子的词数
    target_length = targets.size()[0]  # 输出句子的词数
    
    loss = 0
    
    
    # 2.句子进行encoder端的计算==================================
    # 2.1初始化encoder端的输出:输出每步的隐向量,n个词即n步
    encoder_outputs = torch.zeros(max_len, encoder.hidden_size)
    if use_cuda:
        encoder_outputs = encoder_outputs.cuda()
    
    # 2.2将一句话中的每个词依次输入enocder中,前一步的隐向量是当前步的输入
    for ei in range(input_length):
        encoder_hidden = encoder(inputs[ei], encoder_hidden)
     
    
    # 3.句子进行decoder端的计算==================================
    # 3.1 设置第一步的输入x,即预测第一个词时的输入为句子起始标识符SOS_token
    decoder_input = torch.LongTensor([SOS_token])
    if use_cuda:
        decoder_input = decoder_inputs.cuda()
        
    decoder_hidden = encoder_hidden # encoder端的最后一步输出隐向量,是decoder端的第一步的输入隐向量
        
    # 3.2设置是否设置teacher_forcing
    if random.random() < teacher_forcing_ratio:
        use_teacher_forcing = True
    else:
        use_teacher_forcing = False
        
    # 3.3 进行decoder端的每步循环
    if use_teacher_forcing:
        for di in range(target_length): # 循环每步
            decoder_output, decoder_hidden = decoder(decoder_input, decoder_hidden) # 经过decoder计算
            loss += criterion(decoder_output, targets[di])  # 直接用decoder的输出去求损失
            decoder_input = targets[di] # decoder的输出直接作为encoder的输入
    else:
        for di in range(target_length):
            decoder_output, decoder_hidden = decoder(decoder_input, decoder_hidden)# 经过decoder计算(与上不变)
            topv, topi = decoder_output.data.topk(1)  # 获得概率最大的值,及其索引
            ni = topi[0][0]  # 获取其在词典中对应的索引,因为decoder_output是二维(1,output_size)
            decoder_input = torch.LongTensor([ni]) # 下一步的input,是上一步的output的概率最大词的索引
            if use_cuda:
                decoder_input = decoder_input.cuda()
            loss += criterion(decoder_output, targets[di])
            if ni == EOS_token:
                break
      
    
    # 3.4 反向传播与更新参数=======================================
    loss.backward()
    encoder_optimizer.step()
    decoder_optimizer.step()
    
    return loss.item() / target_length
  • 解释一下topk(i),是找出tensor中值最大的那个数,并返回它的值和索引。所以上面代码上topv, topi分别指decoder输出向量中的最大值与其所在的索引位置
a = torch.Tensor([[2,4,3,1]])
print(a.topk(1))
(tensor([[4.]]), tensor([[1]]))
  • 解释一下teacher_forcing(重要)

设置了一定的比例的数据是进行teacher_forcing,一部分不进行teacher_forcing。两者的区别是,a.前者的当前步骤的input是真实目标句子中的前一个词(相当于是用真实的结果当老师去提点了),后者的当前步骤的input是上一步预测出概率最大的词。 b.后者设置了循环终止条件。

  • 解释一下损失

函数最终返回的是总损失除以目标句子的词数。

2)接着构建一个迭代处理所有句子的训练函数

先提前设置一个有用的功能函数:index_from_pairs(),即将输入的句子对,根据词典将文字转换成索引的形式。

def sentence2index(lang, sentence):
    return [lang.word2index[w] for w in sentence.split()]

def index_from_sentence(lang, sentence):
    index = sentence2index(lang, sentence)
    index.append(EOS_token) # 追加一个句子结尾符
    re = torch.LongTensor(index).view(-1, 1)  # 增加一个维度
    if use_cuda:
        re = re.cuda()
        
    return re

def index_from_pairs(pair):
    inputs = index_from_sentence(ch_lang, pair[0])
    targets = index_from_sentence(en_lang, pair[1])
    return [inputs, targets]

现在来写完整训练的函数

def train_iters( n_iter, print_every=500, learning_rate=0.01, hidden_size=256):
    print_loss_total = 0
    
    # 模型、优化器、损失
    encoder = EncoderRnn(ch_lang.n_word, hidden_size)
    decoder = DecoderRnn(hidden_size, en_lang.n_word)
    if use_cuda:
        encoder = encoder.cuda()
        decoder = decoder.cuda()
    
    encoder_optimizer = torch.optim.SGD(encoder.parameters(), lr=learning_rate)
    decoder_optimizer = torch.optim.SGD(decoder.parameters(), lr=learning_rate)
    
    criterion = nn.NLLLoss()
    
    # 循环迭代,每次迭代为一个样本
    for i in range(1, n_iter+1):
        random.shuffle(pairs)
        training_pairs = [index_from_pairs(pair) for pair in pairs]  # 所有句子转换成索引形式
        
        for idx, training_pair in enumerate(training_pairs):
            input_index = training_pair[0]
            target_index = training_pair[1]
            loss = train(input_index, target_index, encoder, decoder, encoder_optimizer, decoder_optimizer, criterion) # 调用train
            print_loss_total += loss
            
            if idx % print_every == 0:
                print_loss_avg = print_loss_total / print_every
                print_loss_total = 0
                print('iteration:%s, idx:%d, average loss:%.4f' % (i, idx, print_loss_avg))
                

train_iters(n_iter=10)
        
C:\\Users\\CC\\Anaconda3\\lib\\site-packages\\ipykernel_launcher.py:13: UserWarning: Implicit dimension choice for log_softmax has been deprecated. Change the call to include dim=X as an argument.
  del sys.path[0]


iteration:1, idx:0, average loss:0.0093
iteration:2, idx:0, average loss:0.1971
iteration:3, idx:0, average loss:0.2324
iteration:4, idx:0, average loss:0.2300
iteration:5, idx:0, average loss:0.2226
iteration:6, idx:0, average loss:0.2226
iteration:7, idx:0, average loss:0.2126
iteration:8, idx:0, average loss:0.2066
iteration:9, idx:0, average loss:0.2026
iteration:10, idx:0, average loss:0.1983

7.1.4 评估

def evaluate(encoder, decoder, sentence, max_length=MAX_LENGTH):
    inputs = index_from_sentence(ch_lang, sentence)
    inputs_length = inputs.size()[0]
    
    encoder_hidden = encoder.init_hidden()
    
    encoder_outputs = torch.zeros(max_length, encoder.hidden_size)
    encoder_outputs = encoder_outputs.cuda() if use_cuda else encoder_outputs
    
    for ei in range(inputs_length):
        encoder_hidden = encoder(inputs[ei], encoder_hidden)
        
    decoder_inputs = torch.LongTensor([SOS_token])
    decoder_inputs = decoder_inputs.cuda() if use_cuda else decoder_inputs
    
    decoder_hidden = encoder_hidden
    
    decoder_words = []
    
    for di in range(max_length):
        decoder_output, decoder_hidden = decoder(decoder_inputs, decoder_hidden)
        topv, topi = decoder_output.data.topk(1)
        ni = topi[0][0].item()
        
        if ni == EOS_token:
            decoder_words.append('<EOS>')
            break
        else:
            decoder_words.append(en_lang.index2word[ni])
        
        decoder_input = torch.LongTensor([[ni]])
        decoder_inputs = decoder_inputs.cuda() if use_cuda else decoder_inputs
        
    return decoder_words
        
def evaluat_randomly(encoder, decoder, n=10):
    for i in range(n):
        pair = random.choice(pairs)
        print('source:', pair[0])
        print('target:', pair[1])
        output_word = evaluate(encoder, decoder, pair[0])
        output_word = ' '.join(output_word)
        print('predict:', output_word)
        print()

encoder = EncoderRnn(ch_lang.n_word, 256)
decoder = DecoderRnn(256, en_lang.n_word)
if use_cuda:
    encoder = encoder.cuda()
    decoder = decoder.cuda()
evaluat_randomly(encoder, decoder)

source: " 这就是 我们 的 底线 
target: this is our bottom line 
predict: raised raised always always always always always always always always

source: 这 又是 为什麽 
target: again , why is that 
predict: raised raised always always always always always always always always

source: 我爱你中国
target: i love you ,china
predict: ready raised always always always always always always always always

source: 这 叫 什 麽 
target: what is that called 
predict: raised raised always always always always always always always always

source: 素质 是 高 还是 低 
target: are they competent 
predict: raised raised raised always always always always always always always

source: 素质 是 高 还是 低 
target: are they competent 
predict: raised raised raised always always always always always always always

source: 是否 先进 
target: are they advanced weapons 
predict: raised raised raised always always always always always always always

source: 一 只 只 手 高高 举起 
target: a hand was raised high 
predict: kosovo raised always always always always always always always always

source: 再说 科索沃 
target: or take kosovo 
predict: kosovo raised always always always always always always always always

source: " " 发射  "
target: " fire  "
predict: raised raised always always always always always always always always



C:\\Users\\CC\\Anaconda3\\lib\\site-packages\\ipykernel_launcher.py:13: UserWarning: Implicit dimension choice for log_softmax has been deprecated. Change the call to include dim=X as an argument.
  del sys.path[0]

以上结果还不成形,因为迭代次数和语料的问题,大家可以拿更好的语料,并将迭代次数增多进行测试

7.1.5 Attention机制

以上完成了一个翻译机的全部过程,但这是非常基础的网络结构,通常情况下我们会加入attention机制,那么模型结构encoder与decoder需要稍作改写。

encoder端的改写:主要是将gru的outputs也作为输出返回,这个outputs会用于decoder端的attention计算相似分值

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)  # 直接调用GRU
        
    PyTorch基础——机器翻译的神经网络实现

翻译: 2.7. 如何利用帮助文档 深入神经网络 pytorch

处理语料篇

词向量原理

神经机器翻译任务中的句子指示

从两个文本文件(平行语料库)中随机抽样N行