当前位置:   article > 正文

自然语言处理——使用seq2seq模型架构实现英译法_自然语言处理 英译法

自然语言处理 英译法

自然语言处理

使用seq2seq模型架构实现英译法

  • seq2seq模型架构图示:

在这里插入图片描述

  • seq2seq模型架构分析:从图中可知,seq2seq模型架构,包括两部分分别是encoder(编码器)和decoder(解码器),编码器和解码器的内部实现都使用了GRU模型,这里它要完成的是一个中文到英文的翻译:欢迎 来 北京 --> welcome to BeiJing 编码器首先处理中文输入"欢迎 来 北京",通过GRU模型获得每个时间步的输出张量,最后将它们拼接成一个中间语义张量c,接着解码器将使用这个中间语义张量c以及每一个时间步的隐层张量,逐个生成对应的翻译语言。

  • 基于GRU的seq2seq模型架构实现翻译过程:

    1. 导入必备工具包
    2. 对持久化文件中数据进行处理,以满足模型训练要求
    3. 构建基于GRU的编码器和译码器
    4. 构建模型训练函数,并进行训练
    5. 构建模型评估函数,并进行测试以及Attention效果分析

导入必备工具包

# 从io工具包导入open方法
from io import open
# 用于字符规范化
import unicodedata
# 用于正则表达式
import re
# 用于随机生成数据
import random
# 用于构建网络结构和函数的torch工具包
import torch
import torch.nn as nn
import torch.nn.functional as F
# torch中预定义的优化方法工具包
from torch import optim
# 设备选择,在cuda上运行代码
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

对持久化文件中数据进行处理,以满足模型训练要求

  • 将指定语言中词汇映射成数值:
# 起始标志
SOS_token = 0
# 结束标志
EOS_token = 1

class lang:
    def __init__(self, name):
        '''
        初始化函数中参数

        Parameters
        ----------
        name : 
            代表传入某种语言的名字.

        Returns
        -------
        None.

        '''
        # 将name传入类中
        self.name = name
        # 初始化词汇对应自然数值的字典
        self.word2index = {}
        # 初始化自然数值对应词汇的字典,其中0,1对应的SOS和EOS已经在里面了
        self.index2word = {0:'SOS', 1:'EOS'}
        # 初始化词汇对应的自然数索引,从2开始,因为0、1已经被开始和结束标志占用了
        self.n_words = 2

    def addSentence(self, sentence):
        '''
        添加一个句子

        Parameters
        ----------
        sentence : 
            句子本身.

        Returns
        -------
        None.

        '''
        # 英法都是以空格分割单词
        # 对句子进行分割得到对应的词汇列表
        for word in sentence.split(' '):
            # 调用addWord
            self.addWord(word)

    def addWord(self, word):
        '''
        添加词汇函数,将词汇转为对应数值

        Parameters
        ----------
        word : 
            单词.

        Returns
        -------
        None.

        '''
        # 首先判断word是否已经在self.word2index字典的key中
        if word not in self.word2index:
            # 不在:加入并对应一个数值
            self.word2index[word] = self.n_words
            # 同时将其反转形式加入self.index2word
            self.index2word[self.n_words] = word
            # self.n_words自加
            self.n_words += 1
            
            
name = 'eng'
sentence = 'Hello I am lancibe'
engl = Lang(name)
engl.addSentence(sentence)
print('word2index:', engl.word2index)
print('index2word:', engl.index2word)
print('n_words:', engl.n_words)

word2index: {'Hello': 2, 'I': 3, 'am': 4, 'lancibe': 5}
index2word: {0: 'SOS', 1: 'EOS', 2: 'Hello', 3: 'I', 4: 'am', 5: 'lancibe'}
n_words: 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
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 字符规范化
# 将unicode转ascii,去掉重音标记
def unicodeToAscii(s):
    return ''.join(c for c in unicodedata.normalize('NFD', s)
                   if unicodedata.category(c) != 'Mn')

def normalizeString(s):
    '''
    字符串规范化函数

    Parameters
    ----------
    s : 
        传入的字符串.

    Returns
    -------
    规范后的字符串.

    '''
    # 使字符变小写并去除两侧空白符,再使用unicodeToAcsii去重音
    s = unicodeToAscii(s.lower().strip())
    # 加入几个字符
    s = re.sub(r'([.!?])', r' \1', s)
    # 使用正则表达式将字符串中不是大小写和正常标点的都替换成空格
    s = re.sub(r'[^a-zA-Z.!?]+', r' ', s)
    return s

s = 'Are you kidding me?'
print(normalizeString(s))


are you kidding me ?
  • 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
  • 将持久化文件中的数据加载到内存,并实例化类Lang
data_path = './data/eng-fra.txt'

def readLangs(lang1, lang2):
    '''
    读取语言函数

    Parameters
    ----------
    lang1 : 
        eng.
    lang2 : 
        fra.

    Returns
    -------
    对应的class Lang对象,以及语言对列表.

    '''
    # 从文件中读取语言对并以\n划分存到列表lines中
    lines = open(data_path, encoding='utf-8').read().strip().split('\n')
    # 对lines列表中的句子进行标准化处理,获得对应语言对象并返回结果
    pairs = [[normalizeString(s) for s in l.split('\t')] for l in lines]
    input_lang = Lang(lang1)
    output_lang = Lang(lang2)
    return input_lang, output_lang, pairs


lang1 = 'eng'
lang2 = 'fra'
input_lang, output_lang, pairs = readLangs(lang1, lang2)
print(pairs[:5])


[['go .', 'va !'], ['run !', 'cours !'], ['run !', 'courez !'], ['wow !', 'ca alors !'], ['fire !', 'au feu !']]
  • 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
  • 过滤出符合要求的语言对:
# 设置组成句子中单词或标点的最多个数
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):
    '''
    语言对过滤

    Parameters
    ----------
    p : 
        输入的语言对.

    Returns
    -------
    返回语言对判别的布尔值.

    '''
    # p[0]->eng, p[1]->fra
    return len(p[0].spit(' ')) < MAX_LENGTH and \
        p[0].startswith(eng_prefixes) and \
        len(p[1].split(' ')) < MAX_LENGTH

def filterPairs(pairs):
    return [pair for pair in pairs if filterPair(pair)]
  • 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
  • 对以上数据准备函数进行整合,并使用Lang对语言对进行数值映射
def prepareData(lang1, lang2):
    '''
    数据准备函数,完成将所有字符串数据向数值型数据的映射以及过滤语言对

    Parameters
    ----------
    lang1 : 
        eng.
    lang2 : 
        fra.

    Returns
    -------
    数据映射后的对象以及过滤后的语言对.

    '''
    # 首先通过readLangs函数获得input_lang,output_lang对象,以及语言对
    input_lang, output_lang, pairs = readLangs(lang1, lang2)
    pairs = filterPairs(pairs)
    for pair in pairs:
        input_lang.addSentence(pair[0])
        output_lang.addSentence(pair[1])
    # 返回数值映射结果
    return input_lang, output_lang, pairs


input_lang, output_lang, pairs = prepareData('eng', 'fra')
print('input_lang words:', input_lang.n_words)
print('output_lang words:', output_lang.n_words)
print(random.choice(pairs))


input_lang words: 2803
output_lang words: 4345
['he is poor .', 'il est pauvre .']
  • 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
  • 将语言对转化为模型输入需要的张量
def tensorFromSentence(lang, sentence):
    '''
    将文本句子转换为张量,

    Parameters
    ----------
    lang : 
        代表传入的Lang的实例化对象.
    sentence : 
        预转换的句子.

    Returns
    -------
    张量.

    '''
    # 对句子进行分割并遍历每一个词汇,然后使用lang的word2index方法找到对应索引
    # 这样就得到了该句子对应的数值列表
    indexes = [lang.word2index[word] for word in sentence.split(' ')]
    # 然后加入句子结束标志
    indexes.append(EOS_token)
    # 将其使用torch.tensor封装成张量,并改变形状为nx1,以方便后续计算
    return torch.tensor(indexes, dtype=torch.long, device=device).view(-1,1)

def tensorsFromPair(pair):
    '''
    将语言对转换为张量对

    Parameters
    ----------
    pair : 
        语言对.

    Returns
    -------
    元组.

    '''
    # 调用tensorFromSentence分别将源语言和目标语言分别处理,获得对应的张量表示
    input_tensor = tensorFromSentence(input_lang, pair[0])
    target_tensor = tensorFromSentence(output_lang, pair[1])
    # 返回他们的元组
    return (input_tensor, target_tensor)

  • 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
  • 40
  • 41
  • 42
  • 43
  • 44

构建基于GRU的编码器和解码器

  • 编码器结构图:

在这里插入图片描述

class EncoderRNN(nn.Module):
    def __init__(self, input_size, hidden_size):
        '''
        初始化

        Parameters
        ----------
        input_size : 
            解码器的输入尺寸即语言的词表大小.
        hidden_size : 
            GRU的隐层节点数,也代表词嵌入维度,同时也是GRU输入尺寸.

        Returns
        -------
        None.

        '''
        super(EncoderRNN, self).__init__()
        self.hidden_size = hidden_size
        # 实例化Embedding层
        self.embedding = nn.Embedding(input_size, hidden_size)
        # 实例化GRU层
        self.gru = nn.GRU(hidden_size, hidden_size)
        
    def forward(self, input, hidden):
        '''
        前向逻辑函数

        Parameters
        ----------
        input : 
            源语言Embedding层输入张量.
        hidden : 
            编码器层gru初始张量.

        Returns
        -------
        output, hidden.

        '''
        output = self.embedding(input).view(1,1,-1)
        output, hidden = self.gru(output, hidden)
        return output, hidden
    
    def initHidden(self):
        return torch.zeros(1, 1, self.hidden_size, device=device)
  • 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
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 解码器结构图:

在这里插入图片描述

class DecoderRNN(nn.Module):
    def __init__(self, hidden_size, output_size):
        '''
        初始化函数

        Parameters
        ----------
        hidden_size : 
            解码器GRU输入尺寸,也是隐层节点数.
        output_size : 
            解码器输出尺寸,也是希望得到的指定尺寸即目标语言词表大小.

        Returns
        -------
        None.

        '''
        super(DecoderRNN, self).__init__()
        self.hidden_size = hidden_size
        # 实例化一个nn中的Embedding层对象,参数output表示目标语言词表大小
        # hidden_size代表目标语言词嵌入维度
        self.embedding = nn.Embedding(output_size, hidden_size)
        # 实例化GRU对象
        self.gru = nn.GRU(hidden_size, hidden_size)
        # 实例化线性层,对GRU的输出做线性变换,输出尺寸为output_size
        self.out = nn.Linear(hidden_size, output_size)
        # 用softmax进行处理,便于分类
        self.softmax = nn.LogSoftmax(dim=1)
        
    def forward(self, input, hidden):
        '''
        解码器的前向逻辑函数

        Parameters
        ----------
        input : 
            目标语言的Embedding层输入张量.
        hidden : 
            解码器GRU的初始隐层张量.

        Returns
        -------
        output, hidden.

        '''
        output = self.embedding(input).view(1,1,-1)
        # 然后使用relu函数对输出进行处理,根据relu函数特性使Embedding层更加稀疏
        output = F.relu(output)
        # 把embedding输出以及初始化的hidden传入解码器gru中
        output, hidden = self.gru(output, hidden)
        # 因为输出output也是三维,第一维没有意义,所以可以通过output[0]降维
        output = self.softmax(self.out(output[0]))
        return output, hidden

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

  • 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
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 构建基于GRU和Attention的解码器:

在这里插入图片描述

class AttnDecoderRNN(nn.Module):
    def __init__(self, hidden_size, output_size, dropout_p=0.1, max_length=MAX_LENGTH):
        '''
        初始化函数

        Parameters
        ----------
        hidden_size : 
            解码器中GRU输入尺寸,也是隐层节点数.
        output_size : 
            解码器输出尺寸,希望得到的目标语言的词表大小.
        dropout_p : optional
            使用dropout层时的置零比率. The default is 0.1.
        max_length : optional
            句子最大长度. The default is MAX_LENGTH.

        Returns
        -------
        None.

        '''
        super(AttnDecoderRNN, self).__init__()
        self.hidden_size = hidden_size
        self.output_size = output_size
        self.dropout_p = dropout_p
        self.max_length = max_length
        
        # 实例化一个Embedding层,输入参数是self.output_size和self.hidden_size
        self.embedding = nn.Embedding(self.output_size, self.hidden_size)
        # 根据attention的QKV理论,attention的输入参数为三个Q,K,V,
        # 第一步,使用Q与K进行attention权值计算得到权重矩阵, 再与V做矩阵乘法, 得到V的注意力表示结果.
        # 这里常见的计算方式有三种:
        # 1,将Q,K进行纵轴拼接, 做一次线性变化, 再使用softmax处理获得结果最后与V做张量乘法
        # 2,将Q,K进行纵轴拼接, 做一次线性变化后再使用tanh函数激活, 然后再进行内部求和, 最后使用softmax处理获得结果再与V做张量乘法
        # 3,将Q与K的转置做点积运算, 然后除以一个缩放系数, 再使用softmax处理获得结果最后与V做张量乘法

        # 说明:当注意力权重矩阵和V都是三维张量且第一维代表为batch条数时, 则做bmm运算.

        # 第二步, 根据第一步采用的计算方法, 如果是拼接方法,则需要将Q与第二步的计算结果再进行拼接, 
        # 如果是转置点积, 一般是自注意力, Q与V相同, 则不需要进行与Q的拼接.因此第二步的计算方式与第一步采用的全值计算方法有关.
        # 第三步,最后为了使整个attention结构按照指定尺寸输出, 使用线性层作用在第二步的结果上做一个线性变换. 得到最终对Q的注意力表示.

        # 我们这里使用的是第一步中的第一种计算方式, 因此需要一个线性变换的矩阵, 实例化nn.Linear
        # 因为它的输入是Q,K的拼接, 所以输入的第一个参数是self.hidden_size * 2,第二个参数是self.max_length
        # 这里的Q是解码器的Embedding层的输出, K是解码器GRU的隐层输出,因为首次隐层还没有任何输出,会使用编码器的隐层输出
        # 而这里的V是编码器层的输出
        self.attn = nn.Linear(self.hidden_size*2, self.max_length)
        # 接着我们实例化另外一个线性层, 它是attention理论中的第四步的线性层,用于规范输出尺寸
        # 这里它的输入来自第三步的结果, 因为第三步的结果是将Q与第二步的结果进行拼接, 因此输入维度
        self.attn_combine = nn.Linear(self.hidden_size * 2, self.hidden_size)
        # 接着实例化一个nn.Dropout对象
        self.dropout = nn.Dropout(self.dropout_p)
        # 实例化GRU
        self.gru = nn.GRU(self.hidden_size, self.hidden_size)
        # 最后实例化gru后面的线性层,也就是解码器输出层
        self.out = nn.Linear(self.hidden_size, self.output_size)
        
        
    def forward(self, input, hidden, encoder_outputs):
        '''
        前向函数

        Parameters
        ----------
        input : 
            源数据输入张量.
        hidden : 
            初始隐层张量.
        encoder_outputs : 
            解码器输出张量.

        Returns
        -------
        output, hidden, attn_weights.

        '''
        embedded = self.embedding(input).view(1,1,-1)
        # 使用dropout进行随即丢弃,防止过拟合
        embedded = self.dropout(embedded)
        
        # 进行attention权重计算,我们使用第一种计算方式
        attn_weights = F.softmax(
            self.attn(torch.cat((embedded[0], hidden[0]), 1)), dim=1)
        
        # 进行第一步后半部分
        attn_applied = torch.bmm(attn_weights.unsqueeze(0), encoder_outputs.unsqueeze(0))
        
        # 进行第二步
        output = torch.cat((embedded[0], attn_applied[0]), 1)
        
        # 第三步,使用线性层作用在第三步结果上做一个线性变换并扩展维度,得到输出
        output = self.attn_combine(output).unsqueeze(0)
        
        # attention结构的结果要使用relu激活
        output = F.relu(output)
        
        # 将激活的结果传入gru
        output, hidden = self.gru(output, hidden)
        
        # 最后将结果降维并使用softmax处理
        output = F.log_softmax(self.out(output[0]), dim=1)
        # 返回结果
        return output, hidden, attn_weights
    
    def initHidden(self):
        return torch.zeros(1, 1, self.hidden_size, device=device)

  • 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
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107

构建模型训练函数并进行训练

  • 什么是teacher_forcing:它是一种用于序列生成任务的训练技巧, 在seq2seq架构中,根据循环神经网络理论,解码器每次应该使用上一步的结果作为输入的一部分,但是训练过程中,一旦上一步的结果是错误的,就会导致这种错误被累积,无法达到训练效果,因此,我们需要一种机制改变上一步出错的情况,因为训练时我们是已知正确的输出应该是什么,因此可以强制将上一步结果设置成正确的输出, 这种方式就叫做teacher_forcing。
  • teacher_forcing作用:
    • 在训练时矫正模型预测,避免在序列生成过程中误差进一步放大
    • teacher_forcing能够极大加快模型收敛速度,令模型训练过程更快更平稳。
teacher_forcing_ratio = 0.5

def train(input_tensor, target_tensor, encoder, decoder, encoder_optimizer, decoder_optimizer, criterion, max_length=MAX_LENGTH):
    '''
    模型训练函数

    Parameters
    ----------
    input_tensor : 
        源语言输入张量.
    target_tensor : 
        目标语言输入张量.
    encoder : 
        编码器实例化对象.
    decoder : 
        解码器实例化对象.
    encoder_optimizer : 
        编码器优化方法.
    decoder_optimizer : 
        解码器优化方法.
    criterion : 
        损失函数计算方法.
    max_length : optional
        句子最大长度. The default is MAX_LENGTH.

    Returns
    -------
    平均损失loss.item() / target_length.

    '''
    encoder_hidden = encoder.initHidden
    # 优化器梯度归零
    encoder_optimizer.zero_grad()
    decoder_optimizer.zero_grad()
    
    # 根据原文本和目标文本张量获得对应长度
    input_length = input_tensor.size(0)
    target_length = target_tensor.size(0)
    
    # 初始化编码器输出张量,形状是max_length和encoder.hidden_size的0张量
    encoder_outputs = torch.zeros(max_length, encoder.hidden_size, device=device)
    
    # 设置初始损失0
    loss = 0
    
    # 循环遍历输入张量
    for ei in range(input_length):
        # 根据索引从input_tensor取出对应的单词张量表示,和初始化隐层张量一同传入encoder对象
        encoder_output, encoder_hidden = encoder(
            input_tensor[ei], encoder_hidden)
        # 将每次获得的输出encoder_output(三维张量)使用[0, 0]将两位变成向量依次存入encoder_outputs
        # 这样encoder_outputs每一行存入都是对应句子中每个单词通过编码器输出的结果
        encoder_outputs[ei] = encoder_output[0, 0]
        
    # 初始化解码器的第一个输入,即起始符
    decoder_input = torch.tensor([[SOS_token]], device=device)
    
    # 初始化解码器的隐层张量即编码器的隐层输出
    decoder_hidden = encoder_hidden
    
    # 根据随机数与teacher_forcing_ratio对比判断是否适用teacher_forcing
    use_teacher_forcing = True if random.random() < teacher_forcing_ratio else False
    
    if use_teacher_forcing:
        # 循环遍历目标张量索引
        for di in range(target_length):
            # 将多个参数传入解码器对象,获得三个参数
            decoder_output, decoder_hidden, decoder_attention = decoder(
                decoder_input, decoder_hidden, encoder_outputs)
            # 因为使用teacher_forcing,无论输出decoder_output是什么,都只要正确答案
            loss += criterion(decoder_output, target_tensor[di])
            # 并将下一次解码器输入设置为正确答案
            decoder_input = target_tensor[di]
    else:
        # 仍遍历目标张量索引
        for di in range(target_length):
            decoder_output, decoder_hidden, decoder_attention = decoder(
                decoder_input, decoder_hidden, encoder_outputs)
            # 从decoder_output取出答案
            topv, topi = decoder_output.topk(1)
            loss += criterion(decoder_output, target_tensor[di])
            # 最后如果输出是终止符,则循环停止
            if topi.squeeze().item() == EOS_token:
                break
            # 否则对topi降维并分离赋值给decoder_input进行下次运算
            # 这里的detach分离作用使decoder_input和模型构建的张量图无关
            decoder_input = topi.squeeze().detach()
            
     
    # 误差反向传播
    loss.backward()

    # 编码器译码器进行优化参数更新
    encoder_optimizer.step()
    decoder_optimizer.step()
    
    # 返回平均损失
    return loss.item() / target_length
  • 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
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 构建时间计算函数
import time
import math
def timeSince(since):
    '''
    获得每次打印的训练耗时

    Parameters
    ----------
    since : 
        训练开始的时间.

    Returns
    -------
    指定格式的耗时.

    '''
    now = time.time()
    s = now - since
    m = math.floor(s / 60)
    s -= m*60
    return '%dmins %dsecs' % (m, s)     
        
since = time.time() - 620

period = timeSince(since)
print(period)  


10mins 20secs
  • 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
  • 调用训练函数并打印日志和制图:
import matplotlib.pyplot as plt

def trainIters(encoder, decoder, n_iters, print_every=1000, plot_every=100, learning_rate=0.01):
    '''
    训练迭代函数

    Parameters
    ----------
    encoder : 
        编码器对象.
    decoder : 
        解码器对象.
    n_iters : 
        总迭代次数.
    print_every : optional
        打印日志间隔. The default is 1000.
    plot_every : optional
        绘制损失曲线间隔. The default is 100.
    learning_rate : optional
        学习率. The default is 0.01.

    Returns
    -------
    None.

    '''
    start = time.time()
    plot_losses=[]
    
    print_loss_total = 0
    plot_loss_total = 0
        
    # 使用预定义的SGD优化器,将参数学习率传入
    encoder_optimizer = optim.SGD(encoder.parameters(), lr=learning_rate)
    decoder_optimizer = optim.SGD(decoder.parameters(), lr=learning_rate)
    
    # 选择损失函数
    criterion = nn.NLLLoss()
    
    # 根据设置迭代步进行循环
    for iter in range(1, n_iters + 1):
        # 每次从语言对列表中随机取一条作为训练语句
        training_pair = tensorsFromPair(random.choice(pairs))
        # 获取输入张量目标张量
        input_tensor = training_pair[0]
        target_tensor = training_pair[1]
        
        # 通过train函数获得模型运行的损失
        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, 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
    
    plt.figure()
    plt.plot(plot_losses)
    plt.savefig('./s2s_loss.png')
  • 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
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 训练模型:
hidden_size = 256
encoder1 = EncoderRNN(input_lang.n_words, hidden_size).to(device)
attn_decoder1 = AttnDecoderRNN(hidden_size, output_lang.n_words, dropout_p=0.1).to(device)

n_iters = 75000
print_every = 5000

trainIters(encoder1, attn_decoder1, n_iters)



2mins 18secs (1000 1%) 4.0683
4mins 14secs (2000 2%) 3.5248
6mins 10secs (3000 4%) 3.2639
8mins 30secs (4000 5%) 3.0902
10mins 27secs (5000 6%) 2.9951
...
104mins 0secs (72000 96%) 1.0743
105mins 21secs (73000 97%) 0.9989
107mins 2secs (74000 98%) 0.9682
109mins 10secs (75000 100%) 1.0222
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

在这里插入图片描述

  • 一直下降的损失曲线说明模型正在收敛,能够从数据中找到一些规律应用于数据。

构建模型评估函数,并进行测试以及Attention效果分析

  • 构建模型评估函数:
def evaluate(encoder, decoder, sentence, max_length=MAX_LENGTH):
    '''
    评估函数

    Parameters
    ----------
    encoder : 
        编码器对象.
    decoder : 
        译码器对象.
    sentence : 
        句子本身.
    max_length : , optional
        最大长度. The default is MAX_LENGTH.

    Returns
    -------
    decoded_words, decoder_attentions[:di + 1]

    '''

    # 评估阶段不进行梯度计算
    with torch.no_grad():
        # 对输入的句子进行张量表示
        input_tensor = tensorFromSentence(input_lang, sentence)
        # 获得输入的句子长度
        input_length = input_tensor.size()[0]
        # 初始化编码器隐层张量
        encoder_hidden = encoder.initHidden()

        # 初始化编码器输出张量,是max_lengthxencoder.hidden_size的0张量
        encoder_outputs = torch.zeros(max_length, encoder.hidden_size, device=device)

        # 循环遍历输入张量索引
        for ei in range(input_length):
             # 根据索引从input_tensor取出对应的单词的张量表示,和初始化隐层张量一同传入encoder对象中
            encoder_output, encoder_hidden = encoder(input_tensor[ei],
                                                     encoder_hidden)
            #将每次获得的输出encoder_output(三维张量), 使用[0, 0]降两维变成向量依次存入到encoder_outputs
            # 这样encoder_outputs每一行存的都是对应的句子中每个单词通过编码器的输出结果
            encoder_outputs[ei] += encoder_output[0, 0]

        # 初始化解码器的第一个输入,即起始符
        decoder_input = torch.tensor([[SOS_token]], device=device) 
        # 初始化解码器的隐层张量即编码器的隐层输出
        decoder_hidden = encoder_hidden

        # 初始化预测的词汇列表
        decoded_words = []
        # 初始化attention张量
        decoder_attentions = torch.zeros(max_length, max_length)
        # 开始循环解码
        for di in range(max_length):
            # 将decoder_input, decoder_hidden, encoder_outputs传入解码器对象
            # 获得decoder_output, decoder_hidden, decoder_attention
            decoder_output, decoder_hidden, decoder_attention = decoder(
                decoder_input, decoder_hidden, encoder_outputs)

            # 取所有的attention结果存入初始化的attention张量中
            decoder_attentions[di] = decoder_attention.data
            # 从解码器输出中获得概率最高的值及其索引对象
            topv, topi = decoder_output.data.topk(1)
            # 从索引对象中取出它的值与结束标志值作对比
            if topi.item() == EOS_token:
                # 如果是结束标志值,则将结束标志装进decoded_words列表,代表翻译结束
                decoded_words.append('<EOS>')
                # 循环退出
                break

            else:
                # 否则,根据索引找到它在输出语言的index2word字典中对应的单词装进decoded_words
                decoded_words.append(output_lang.index2word[topi.item()])

            # 最后将本次预测的索引降维并分离赋值给decoder_input,以便下次进行预测
            decoder_input = topi.squeeze().detach()
        # 返回结果decoded_words, 以及完整注意力张量, 把没有用到的部分切掉
        return decoded_words, decoder_attentions[:di + 1]

  • 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
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 构建随机测试函数:
def evaluateRandomly(encoder, decoder, n=6):
    '''
    随机测试函数

    '''
    # 对测试数进行循环
    for i in range(n):
        # 从pairs随机选择语言对
        pair = random.choice(pairs)
        # > 代表输入
        print('>', pair[0])
        # = 代表正确的输出
        print('=', pair[1])
        # 调用evaluate进行预测
        output_words, attentions = evaluate(encoder, decoder, pair[0])
        # 将结果连成句子
        output_sentence = ' '.join(output_words)
        # < 代表模型的输出
        print('<', output_sentence)
        print('')
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • Attention张量制图:
sentence = "we re both teachers ."
# 调用评估函数
output_words, attentions = evaluate(
encoder1, attn_decoder1, sentence)
print(output_words)
# 将attention张量转化成numpy, 使用matshow绘制
plt.matshow(attentions.numpy())
# 保存图像
plt.savefig("./s2s_attn.png")
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

在这里插入图片描述

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/盐析白兔/article/detail/557466
推荐阅读
相关标签
  

闽ICP备14008679号