第N8周:图解NLP中的注意力机制

一、前期知识储备

注意力机制是一种模拟人类大脑注意力分配方式的计算模型,它能够在处理大量信息时,聚焦于重要的部分,而忽略不重要的信息。以下是注意力机制的详细计算过程:

1. 基本概念

在注意力机制中,通常涉及到以下三个基本概念:

  • Query:当前要处理的目标信息。
  • Key:用于与Query匹配的参考信息。
  • Value:实际要关注的信息内容。

2. 计算步骤

注意力机制的计算过程通常分为以下几个步骤:

步骤一:计算注意力得分(Attention Scores)

首先,计算Query与所有Key之间的相似度或相关性,这通常通过点积(dot product)来实现。对于每个Key,都会得到一个得分,这个得分表示Query与该Key的匹配程度。

步骤二:权重归一化

由于直接得到的注意力得分可能范围很广,不利于后续的计算,因此需要对这些得分进行归一化处理。常用的方法是使用softmax函数,将得分转换为概率形式,即权重。

步骤三:加权求和

最后,根据归一化后的权重 ( \alpha_i ) 对相应的Value ( v_i ) 进行加权求和,得到最终的注意力

3. 应用示例

在神经机器翻译中,Query通常来自于解码器的当前状态,Key和Value则来自于编码器的输出。通过上述计算过程,解码器能够在每个时间步聚焦于源语言序列中与之最相关的部分,从而生成翻译。
注意力机制的这种计算方式,使得模型能够更加灵活和有效地处理序列数据,尤其是在处理长距离依赖问题时表现出色。通过动态地分配权重,注意力机制能够帮助模型捕捉到关键信息,提高整体的性能和准确性。

seq2seq模型在处理长文本模型上的缺点

Seq2seq(Sequence to Sequence)模型是一种经典的编码器-解码器(Encoder-Decoder)架构,广泛用于处理序列到序列的映射问题,如机器翻译、对话系统等。然而,在处理长对话或长序列数据时,seq2seq模型存在以下缺点:

1. 长距离依赖问题

  • 解释:在长序列中,序列的开始部分和结束部分的信息可能存在依赖关系。由于序列长度增加,模型需要捕捉的依赖关系可能跨越更多的步骤。
  • 缺点:传统的循环神经网络(RNN)或长短时记忆网络(LSTM)在捕捉长距离依赖方面存在困难。随着序列长度的增加,梯度消失或梯度爆炸的问题更加明显,导致模型难以学习到长距离的依赖关系。

2. 信息丢失

  • 解释:编码器需要将整个输入序列编码成一个固定长度的向量(通常是最后一个隐藏状态),这个向量需要包含整个序列的所有信息。
  • 缺点:当输入序列很长时,将所有信息压缩到一个固定长度的向量中会导致信息丢失。这就像试图将一本厚书的内容总结成一句话,很多细节和上下文信息可能会丢失。

3. 计算效率低下

  • 解释:在解码阶段,解码器需要依赖于编码器输出的固定长度向量来生成整个输出序列。
  • 缺点:由于信息压缩,解码器在生成每个输出时可能需要重复访问这个固定长度的向量,这增加了计算负担,并且在长序列上效率低下。

4. 难以捕捉局部和全局信息

  • 解释:长对话中可能同时包含局部细节和全局主题。模型需要在理解全局语境的同时,关注到局部的关键信息。
  • 缺点:seq2seq模型往往难以同时捕捉局部和全局信息,尤其是在信息量大的长对话中。

5. 输出多样性不足

  • 解释:在长对话中,可能存在多个合理的回复或信息表达方式。
  • 缺点:传统的seq2seq模型可能在生成输出时缺乏多样性,因为它依赖于一个固定的上下文表示,这限制了输出的灵活性和丰富性。
    为了解决这些问题,研究者们引入了注意力机制(Attention Mechanism)和其他改进措施,如Transformer模型,它们能够更好地处理长序列数据,通过动态地关注输入序列的不同部分来改善长距离依赖的捕捉,并提高模型的性能。

在seq2seq模型中注入注意力机制带来的性能提升

在seq2seq模型中加入注意力机制可以带来以下几方面的提升:

1. 改善长距离依赖的处理

  • 提升:注意力机制允许解码器在生成每个输出时直接关注输入序列的特定部分,而不是依赖于一个固定的上下文向量。这极大地改善了模型处理长距离依赖的能力,因为模型可以“跳过”中间不相关的部分,直接访问需要的信息。

2. 减少信息丢失

  • 提升:由于注意力机制可以为输入序列的不同部分分配不同的权重,因此模型在编码和解码过程中能够更好地保留和利用输入序列的详细信息,减少了信息压缩和丢失的问题。

3. 提高翻译和生成质量

  • 提升:注意力机制使得模型能够更加准确地捕捉到输入和输出之间的对应关系,从而提高了翻译的准确性和生成文本的质量。

4. 增加输出的多样性

  • 提升:注意力机制使得模型在生成输出时能够考虑到输入序列的不同方面,这有助于生成更加多样化和丰富的输出。

5. 提高计算效率

  • 提升:虽然注意力机制本身增加了计算复杂度,但由于它能够更有效地利用输入信息,可以在某些情况下减少解码过程中的重复计算,从而间接提高计算效率。

6. 提升可解释性

  • 提升:注意力权重可以被可视化为热力图,这为理解模型决策过程提供了直观的证据。通过观察注意力权重,我们可以知道模型在解码时关注了输入序列的哪些部分。

7. 适应不同长度的输入

  • 提升:注意力机制使得seq2seq模型能够更好地适应不同长度的输入序列,因为它不再需要将所有信息编码成一个固定长度的向量。
    综上所述,注意力机制的引入是对传统seq2seq模型的重要改进,它通过提供一种更加灵活和动态的信息处理方式,显著提升了seq2seq模型在处理序列数据时的性能和效果。

二、代码分析

一.前期准备

  1. __future__:
    • 这个特殊的模块是为了在旧版本的Python中使用新版本中的一些特性。例如,unicode_literals是一个在Python 2中常用的特性,它使得所有的字符串都被当作Unicode字符串处理,这是Python 3中的默认行为。print_function则是将print语句变为一个函数,这也是Python 3中的默认行为。division则是改变了除法操作的默认行为,在Python 2中,整数除以整数默认会向下取整,而在Python 3中则默认返回一个浮点数。
  2. io:
    • io模块提供了Python中处理I/O的接口。open函数是这个模块的一部分,用于打开一个文件,并返回一个文件对象,这个文件对象可以用于读写文件。在Python 2中,直接使用open即可,但在Python 3中,为了明确起见,通常建议从io模块中导入它。
  3. unicodedata:
    • 这个模块提供了对Unicode字符数据库的访问,可以用来查询字符的属性,如是否为数字、字母、标点符号等,还可以进行字符的归一化(比如将字符转换成标准形式)。
  4. string:
    • string模块提供了处理字符串的常量和类。例如,它包含了所有的字母(大小写)、数字以及一些字符串模板类,可以用于创建字符串格式。
  5. re:
    • re模块提供了对正则表达式的支持。使用这个模块可以执行字符串的搜索、替换、匹配等复杂文本操作。正则表达式是一种强大且灵活的工具,用于处理字符串和文本数据。
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.搭建语言类

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

这段代码定义了一个名为Lang的类,用于表示一种语言,并方便地对语料库进行操作。

SOS_token = 0  # 开始符号的索引
EOS_token = 1  # 结束符号的索引

这两行代码定义了两个常量,分别代表开始(Start of Sentence)和结束(End of Sentence)的标记。这些标记在处理序列数据时非常重要,特别是在自然语言处理(NLP)任务中。

class Lang:

这行代码开始定义一个名为Lang的类。

    def __init__(self, name):

这是Lang类的构造函数,它接受一个参数name,表示语言的名称。

        self.name = name

将传入的name参数保存为实例变量。

        self.word2index = {}

初始化一个字典,用于将单词映射到它们在语料库中的索引。

        self.word2count = {}

初始化一个字典,用于记录每个单词在语料库中出现的次数。

        self.index2word = {0: "SOS", 1: "EOS"}

初始化一个字典,用于将索引映射回单词。这里预先填充了开始和结束标记。

        self.n_words = 2 # Count SOS and EOS

设置一个计数器,表示目前语料库中的单词数量,初始值为2,因为已经包含了"SOS"和"EOS"。

    def addSentence(self, sentence):

定义一个方法,用于将一个句子添加到语料库中。

        for word in sentence.split(' '):

将句子按空格分割成单词列表,并对每个单词执行以下操作。

            self.addWord(word)

调用addWord方法将单词添加到语料库中。

    def addWord(self, word):

定义一个方法,用于将单个单词添加到语料库中。

        if word not in self.word2index:

检查单词是否已经存在于word2index字典中。

            self.word2index[word] = self.n_words

如果单词不存在,将其添加到word2index字典中,并分配一个新的索引。

            self.word2count[word] = 1

word2count字典中为该单词设置计数为1。

            self.index2word[self.n_words] = word

index2word字典中添加新的索引到单词的映射。

            self.n_words += 1

增加n_words计数器,以追踪语料库中的单词总数。

        else:
            self.word2count[word] += 1

如果单词已经存在,则增加其在word2count字典中的计数。

2.文本处理函数

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"[.!?]", "  ", s)  
    s = re.sub(r"[^a-zA-Z.!?]+", r" ", s)  
    return s   

这段代码包含了两个函数:unicodeToAsciinormalizeString

def unicodeToAscii(s):  

定义一个函数unicodeToAscii,它接受一个字符串s作为参数。

    return "".join(  

返回一个由迭代器生成的字符串。"".join(...)将迭代器中的所有元素连接成一个字符串。

        c for c in unicodedata.normalize('NFD', s)  

这是一个生成器表达式,它遍历字符串s中的每个字符。unicodedata.normalize('NFD', s)将字符串s标准化为NFD(Normalization Form Decomposition),即将字符分解成基字符和组合字符。

        if unicodedata.category(c) != 'Mn'  
    )  

这是生成器表达式的条件部分。unicodedata.category(c)返回字符c的Unicode类别。这里检查字符是否不是Mn(Mark, Nonspacing),即非间距标记字符。如果字符不是非间距标记字符,则将其包含在最终的字符串中。

# 小写化,剔除标点与非字母符号  
def normalizeString(s):  

定义一个函数normalizeString,它接受一个字符串s作为参数。这个函数的目标是将字符串小写化,并剔除标点符号和非字母字符。

    s = unicodeToAscii(s.lower()).strip()  

首先,将输入字符串s转换为小写,然后使用之前定义的unicodeToAscii函数将其转换为ASCII字符。.strip()方法移除字符串开头和结尾的空白字符。

    s = re.sub(r"[.!?]", "  ", s)  

使用正则表达式re.sub替换字符串中的标点符号(., !, ?)为两个空格" "。这样做的目的是为了在后续步骤中更容易地移除这些符号。

    s = re.sub(r"[^a-zA-Z.!?]+", r" ", s)  

再次使用re.sub,这次是替换所有非字母字符(除了., !, ?,这些已经被替换为空格了)为一个空格。正则表达式[^a-zA-Z.!?]+匹配任何非字母字符序列。

    return s   

返回处理后的字符串s
总结来说,unicodeToAscii函数用于将Unicode字符串转换为ASCII字符串,并去除所有非间距标记字符。normalizeString函数则进一步处理字符串,使其变为小写,移除标点符号,并用空格替换非字母字符,从而得到一个更规范化的字符串形式。

3.文件读取函数

def readLangs(lang1, lang2, reverse=False):
    print("Reading lines...")
    
    # 以行为单位读取文件
    lines = open('./%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

这段代码定义了一个名为readLangs的函数,它用于读取两种语言之间的对齐文本文件,并返回相应的语言类实例和配对列表。

def readLangs(lang1, lang2, reverse=False):

定义一个函数readLangs,它接受两个字符串参数lang1lang2,表示两种语言的名称,以及一个布尔参数reverse,表示是否需要反转语言顺序。

    print("Reading lines...")

打印一条消息,表示开始读取文件。

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

打开一个文件,该文件的名称由lang1lang2拼接而成,并使用UTF-8编码读取内容。.read()读取整个文件内容,.strip()移除首尾的空白字符,.split('\n')按行分割字符串,得到一个包含所有行的列表。

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

使用列表推导式,遍历每一行l,将每行按制表符\t分割成两部分,即两种语言的文本。然后对这两部分文本使用normalizeString函数进行标准化处理,并将结果放入一个列表中。最后,将所有这样的列表放入pairs列表中。

    # 创建Lang实例,并确认是否反转语言顺序
    if reverse:

检查reverse参数是否为True

        pairs = [list(reversed(p)) for p in pairs]
        input_lang = Lang(lang2)
        output_lang = Lang(lang1)

如果reverseTrue,则反转pairs列表中每个子列表的元素顺序,并创建Lang实例,将lang2作为输入语言,lang1作为输出语言。

    else:
        input_lang = Lang(lang1)
        output_lang = Lang(lang2)

如果reverseFalse,则直接创建Lang实例,将lang1作为输入语言,lang2作为输出语言。

    return input_lang, output_lang, pairs

返回三个值:输入语言的Lang实例、输出语言的Lang实例以及处理后的语言对列表pairs

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)]

这段代码包含了两个函数filterPairfilterPairs,以及一个常量MAX_LENGTH和一个元组eng_prefixes

MAX_LENGTH = 10  # 定义语料最长长度

定义一个常量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 "
)

定义一个元组eng_prefixes,包含了一些英语句子的常见开头。这些前缀用于筛选句子。

def filterPair(p):

定义一个函数filterPair,它接受一个参数p,这个参数是一个包含两个元素的列表,代表一对语言对齐的句子。

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

返回一个布尔值,表示这对句子是否满足以下条件:

  • 句子p[0](源语言句子)的单词数小于MAX_LENGTH
  • 句子p[1](目标语言句子)的单词数小于MAX_LENGTH
  • 句子p[1]eng_prefixes中的任一前缀开头。
    这里使用了str.split(' ')来按空格分割句子并计算单词数,str.startswith()来检查句子是否以特定前缀开头。
def filterPairs(pairs):

定义一个函数filterPairs,它接受一个参数pairs,这个参数是一个列表,其中包含多个语言对齐的句子对。

    # 选取仅仅包含 eng_prefixes 开头的语料
    return [pair for pair in pairs if filterPair(pair)]

使用列表推导式,遍历pairs列表中的每一对句子,使用filterPair函数检查每对句子是否满足筛选条件。如果满足,则将其包含在返回的新列表中。
总结来说,filterPair函数用于检查单个语言对齐的句子对是否符合特定的长度和前缀条件,而filterPairs函数则用于过滤整个语料库,只保留符合条件的句子对。

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))

这段代码定义了一个名为prepareData的函数,并调用了这个函数来准备数据,然后打印出一个随机选择的句子对。以下是逐行解释:

def prepareData(lang1, lang2, reverse=False):

定义一个函数prepareData,它接受两个字符串参数lang1lang2,表示两种语言的名称,以及一个布尔参数reverse,表示是否需要反转语言顺序。

    # 读取文件中的数据
    input_lang, output_lang, pairs = readLangs(lang1, lang2, reverse)

调用之前定义的readLangs函数,读取语言对齐的文件,并获取输入语言、输出语言和语言对列表。

    print("Read %s sentence pairs" % len(pairs))

打印读取到的句子对的数量。

    # 按条件选取语料
    pairs = filterPairs(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])

遍历筛选后的句子对列表pairs,并将每个句子添加到相应的语言类实例中,以统计单词的出现次数。

    # 打印语言类的信息
    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)

调用prepareData函数,准备数据,这里指定eng为源语言,fra为目标语言,并设置reverseTrue,表示反转语言顺序。

print(random.choice(pairs))

从筛选后的句子对列表pairs中随机选择一个句子对,并打印出来。

输出
Reading lines…
Read 135842 sentence pairs
Trimmed to 10604 sentence pairs
Counting words…
Counted words:
fra 4347
eng 2801
['je ne suis pas votre frere ', 'i m not your brother ']

二、seq2seq模型

1.编码器

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)

这段代码定义了一个名为EncoderRNN的类,它继承自nn.Module,这是PyTorch中用于构建神经网络的基础类。以下是逐行解释:

class EncoderRNN(nn.Module):

定义一个名为EncoderRNN的类,它继承自nn.Module。这个类将实现一个编码器RNN(循环神经网络)。

    def __init__(self, input_size, hidden_size):

定义类的构造函数__init__,它接受两个参数:input_size(输入词汇的大小)和hidden_size(隐藏层的大小)。

        super(EncoderRNN, self).__init__()

调用父类nn.Module的构造函数。

        self.hidden_size = hidden_size

hidden_size保存为实例变量,以便在类的其他方法中使用。

        self.embedding = nn.Embedding(input_size, hidden_size)

创建一个嵌入层embedding,它将输入词汇的索引映射到hidden_size维的嵌入向量。

        self.gru = nn.GRU(hidden_size, hidden_size)

创建一个GRU(门控循环单元)层,它是一个类型的RNN,具有hidden_size大小的输入和输出。

    def forward(self, input, hidden):

定义forward方法,这是nn.Module中必须实现的方法,用于定义网络的前向传播。

        embedded = self.embedding(input).view(1, 1, -1)

将输入input(一个整数,代表词汇索引)通过嵌入层转换为嵌入向量,并使用.view(1, 1, -1)将其形状调整为三维张量,其中第一维是批次大小(这里为1,因为是单个输入),第二维是序列长度(也是1,因为是单个时间步),第三维是嵌入向量的维度。

        output = embedded

初始化输出张量output为嵌入向量。

        output, hidden = self.gru(output, hidden)

将嵌入向量output和上一个时间步的隐藏状态hidden输入到GRU层,并得到当前的输出output和新的隐藏状态hidden

        return output, hidden

返回当前的输出张量output和新的隐藏状态hidden

    def initHidden(self):

定义一个方法initHidden,用于初始化隐藏状态。

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

返回一个全零的三维张量,其形状为(1, 1, self.hidden_size),代表初始隐藏状态。device=device表示这个张量将被创建在之前设置的设备上(比如GPU或CPU)。

2.解码器

class AttnDecoderRNN(nn.Module):
    def __init__(self, hidden_size, output_size, dropout_p=0.1, max_length=MAX_LENGTH):
        super(AttnDecoderRNN, self).__init__()
        self.hidden_size = hidden_size
        self.output_size = output_size
        self.dropout_p = dropout_p
        self.max_length = max_length
        
        self.embedding = nn.Embedding(self.output_size, self.hidden_size)
        self.attn = nn.Linear(self.hidden_size * 2, self.max_length)
        self.attn_combine = nn.Linear(self.hidden_size * 2, self.hidden_size)
        self.dropout = nn.Dropout(self.dropout_p)
        self.gru = nn.GRU(self.hidden_size, self.hidden_size)
        self.out = nn.Linear(self.hidden_size, self.output_size)
    
    def forward(self, input, hidden, encoder_outputs):
        embedded = self.embedding(input).view(1, 1, -1)
        embedded = self.dropout(embedded)
    
        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)
    
        output = F.relu(output)
        output, hidden = self.gru(output, hidden)
    
        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)

这段代码定义了一个名为AttnDecoderRNN的类,它是一个带有注意力机制的解码器RNN(循环神经网络)。以下是逐行解释:

class AttnDecoderRNN(nn.Module):

定义一个名为AttnDecoderRNN的类,它继承自nn.Module

    def __init__(self, hidden_size, output_size, dropout_p=0.1, max_length=MAX_LENGTH):

定义类的构造函数__init__,它接受四个参数:hidden_size(隐藏层的大小)、output_size(输出词汇的大小)、dropout_p(Dropout层概率,默认为0.1)和max_length(最大句子长度,默认为之前定义的MAX_LENGTH)。

        super(AttnDecoderRNN, self).__init__()

调用父类nn.Module的构造函数。

        self.hidden_size = hidden_size
        self.output_size = output_size
        self.dropout_p = dropout_p
        self.max_length = max_length

将传入的参数保存为实例变量。

        self.embedding = nn.Embedding(self.output_size, self.hidden_size)

创建一个嵌入层,将输出词汇的索引映射到hidden_size维的嵌入向量。

        self.attn = nn.Linear(self.hidden_size * 2, self.max_length)

创建一个线性层attn,用于计算注意力权重。

        self.attn_combine = nn.Linear(self.hidden_size * 2, self.hidden_size)

创建一个线性层attn_combine,用于合并嵌入向量和应用了注意力的编码器输出。

        self.dropout = nn.Dropout(self.dropout_p)

创建一个Dropout层,用于在训练过程中随机将输入单元置零,以减少过拟合。

        self.gru = nn.GRU(self.hidden_size, self.hidden_size)

创建一个GRU层,它是一个类型的RNN,具有hidden_size大小的输入和输出。

        self.out = nn.Linear(self.hidden_size, self.output_size)

创建一个线性层out,用于将GRU层的输出转换为最终的输出。

    def forward(self, input, hidden, encoder_outputs):

定义forward方法,这是nn.Module中必须实现的方法,用于定义网络的前向传播。

        embedded = self.embedding(input).view(1, 1, -1)

将输入input(一个整数,代表词汇索引)通过嵌入层转换为嵌入向量,并调整其形状。

        embedded = self.dropout(embedded)

应用Dropout层到嵌入向量上。

        attn_weights = F.softmax(
            self.attn(torch.cat((embedded[0], hidden[0]), 1)), dim=1)

计算注意力权重。首先将嵌入向量和隐藏状态拼接,然后通过线性层attn,最后使用softmax函数进行归一化处理。

        attn_applied = torch.bmm(attn_weights.unsqueeze(0),
                                 encoder_outputs.unsqueeze(0))

应用注意力权重到编码器的输出上。这里使用批矩阵乘法torch.bmm

        output = torch.cat((embedded[0], attn_applied[0]), 1)

将嵌入向量和应用了注意力的编码器输出拼接起来。

        output = self.attn_combine(output).unsqueeze(0)

将拼接后的输出通过线性层attn_combine,并调整其形状。

        output = F.relu(output)

应用ReLU激活函数。

        output, hidden = self.gru(output, hidden)

将处理后的输出和隐藏状态输入到GRU层,得到新的输出和隐藏状态。

        output = F.log_softmax(self.out(output[0]), dim=1)

将GRU层的输出通过线性层out,并应用log-softmax函数,以获得对数概率输出。

        return output, hidden, attn_weights

返回最终的输出、新的隐藏状态和注意力权重。

    def initHidden(self):

定义一个方法initHidden,用于初始化隐藏状态。

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

返回一个全零的三维张量,代表初始隐藏状态。同样,device=device表示这个张量将被创建在之前设置的设备上(比如GPU或CPU)。

三、训练

1.数据预处理

# 将文本数字化,获取词汇index
def indexesFromSentence(lang, sentence):
    return [lang.word2index[word] if word in lang.word2index else lang.word2index["<UNK>"] for word in sentence.split(' ')]
    
input_lang.word2index["<UNK>"] = len(input_lang.word2index)
input_lang.index2word[len(input_lang.word2index) - 1] = "<UNK>"


# 将数字化的文本,转化为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)

这段代码定义了三个函数,用于将文本数字化,并将其转化为可以用于神经网络训练的tensor数据。以下是逐行解释:

# 将文本数字化,获取词汇index
def indexesFromSentence(lang, sentence):

定义一个函数indexesFromSentence,它接受两个参数:一个语言类实例lang和一个字符串sentence

    return [lang.word2index[word] if word in lang.word2index else lang.word2index["<UNK>"] for word in sentence.split(' ')]

返回一个列表,其中包含sentence中的每个单词的索引。如果单词在lang.word2index中存在,则返回其索引;否则,返回lang.word2index["<UNK>"]的值,通常这是UNK(未知)标记的索引。

input_lang.word2index["<UNK>"] = len(input_lang.word2index)

input_lang的语言类实例中,将<UNK>标记的索引设置为当前已知的最大索引值加1。

input_lang.index2word[len(input_lang.word2index) - 1] = "<UNK>"

input_lang的语言类实例中,将最后一个索引值映射到<UNK>标记。

# 将数字化的文本,转化为tensor数据
def tensorFromSentence(lang, sentence):

定义一个函数tensorFromSentence,它接受两个参数:一个语言类实例lang和一个字符串sentence

    indexes = indexesFromSentence(lang, sentence)

调用indexesFromSentence函数获取sentence的索引列表。

    indexes.append(EOS_token)

在索引列表的末尾添加一个EOS_token,表示句子结束。

    return torch.tensor(indexes, dtype=torch.long, device=device).view(-1, 1)

将索引列表转换为PyTorch的tensor对象,其中数据类型为long(整数),设备设置为之前定义的device。然后使用.view(-1, 1)调整tensor的形状,以便在后续的神经网络训练中使用。

# 输入pair文本,输出预处理好的数据
def tensorsFromPair(pair):

定义一个函数tensorsFromPair,它接受一个包含两个字符串的列表pair,代表一对语言对齐的句子。

    input_tensor = tensorFromSentence(input_lang, pair[0])
    target_tensor = tensorFromSentence(output_lang, pair[1])

分别调用tensorFromSentence函数,为输入语言和输出语言的句子生成对应的tensor

    return (input_tensor, target_tensor)

返回一个包含两个tensor的元组,第一个是输入语言的tensor,第二个是输出语言的tensor

2.训练函数

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:
        # 教师强制:将目标作为下一个输入
        for di in range(target_length):
            decoder_output, decoder_hidden, decoder_attention = decoder(
                decoder_input, decoder_hidden, encoder_outputs)
            
            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)
            
            topv, topi = decoder_output.topk(1)
            decoder_input = topi.squeeze().detach()  # 从历史中分离出来作为输入
            
            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

这段代码定义了一个名为train的函数,用于训练一个带有注意力机制的编码器-解码器模型。以下是逐行解释:

teacher_forcing_ratio = 0.5

定义一个变量teacher_forcing_ratio,用于控制是否使用教师强制(Teacher Forcing)的比率。在训练过程中,有时使用目标序列作为解码器的输入,有时使用解码器自己的预测作为输入。teacher_forcing_ratio控制了这两种情况的比例。

def train(input_tensor, target_tensor,
          encoder, decoder,
          encoder_optimizer, decoder_optimizer,
          criterion, max_length=MAX_LENGTH):

定义train函数,它接受七个参数:input_tensor(编码器的输入)、target_tensor(解码器的输入)、encoder(编码器模型)、decoder(解码器模型)、encoder_optimizer(编码器的优化器)、decoder_optimizer(解码器的优化器)和criterion(损失函数)。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)

创建一个全零张量,其大小为(max_length, encoder.hidden_size),用于存储编码器的输出。

    loss = 0

初始化损失变量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

这里计算了一个随机数,如果这个数小于teacher_forcing_ratio,则使用教师强制;否则,不使用教师强制。teacher_forcing_ratio是一个在训练时用于控制是否使用目标序列作为解码器输入的比率。

# 将编码器处理好的输出送入解码器
if use_teacher_forcing:
    # 教师强制:将目标作为下一个输入
    for di in range(target_length):
        decoder_output, decoder_hidden, decoder_attention = decoder(
            decoder_input, decoder_hidden, encoder_outputs)
        
        loss += criterion(decoder_output, target_tensor[di])
        decoder_input = target_tensor[di]  # 教师强制

如果use_teacher_forcingTrue,则使用目标序列作为解码器的输入。在这种情况下,解码器在每个时间步的输入是目标序列中的下一个单词。损失函数计算在每个时间步的解码器输出与目标序列中相应单词的差异,并将这些差异累加到总损失loss中。

else:
    # 不使用教师强制:使用自己的预测作为下一个输入
    for di in range(target_length):
        decoder_output, decoder_hidden, decoder_attention = decoder(
            decoder_input, decoder_hidden, encoder_outputs)
        
        topv, topi = decoder_output.topk(1)
        decoder_input = topi.squeeze().detach()  # 从历史中分离出来作为输入
        
        loss += criterion(decoder_output, target_tensor[di])
        if decoder_input.item() == EOS_token:
            break

如果use_teacher_forcingFalse,则使用解码器自己的预测作为下一个输入。在这种情况下,解码器在每个时间步的输入是其上一步的预测。解码器输出通过.topk(1)获取概率最高的单词索引,并通过.squeeze().detach()将其转换为可用的输入。损失函数计算在每个时间步的解码器输出与目标序列中相应单词的差异,并将这些差异累加到总损失loss中。如果解码器预测的单词是EOS_token(表示句子结束),则循环结束。

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))

这段代码定义了两个函数:asMinutestimeSince。以下是逐行解释:

import time
import math

导入timemath模块,分别用于获取当前时间戳和进行数学运算。

def asMinutes(s):

定义一个函数asMinutes,它接受一个参数s,表示时间(以秒为单位)。

    m = math.floor(s / 60)

计算时间s中包含的分钟数,通过将秒数除以60并使用math.floor向下取整。

    s -= m * 60

从原始时间s中减去计算出的分钟数乘以60,得到剩余的秒数。

    return '%dm %ds' % (m, s)

返回一个字符串,表示总时间为m分钟和s秒。

def timeSince(since, percent):

定义一个函数timeSince,它接受两个参数:since(一个时间戳,表示某个时间点开始)和percent(一个百分比,表示完成的工作比例)。

    now = time.time()

获取当前时间戳。

    s = now - since

计算从since时间点到现在的时间差s

    es = s / (percent)

计算预计完成剩余工作所需的时间es,通过将当前时间差s除以剩余工作比例percent

    rs = es - s

计算剩余时间rs,即预计完成剩余工作所需的时间与当前时间差之差。

    return '%s (- %s)' % (asMinutes(s), asMinutes(rs))

返回一个字符串,表示已经过去的时间(使用asMinutes函数格式化)和剩余时间(使用asMinutes函数格式化)。

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 (%.2f %%) %.4f' % (timeSince(start, iter / n_iters), 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

这段代码定义了一个名为trainIters的函数,用于训练一个带有注意力机制的编码器-解码器模型。以下是逐行详细解释:

def trainIters(encoder, decoder, n_iters, print_every=1000, plot_every=100, learning_rate=0.01):
  • 定义trainIters函数,它接受六个参数:encoder(编码器模型)、decoder(解码器模型)、n_iters(训练迭代次数)、print_every(打印损失的频率)、plot_every(绘制损失曲线的频率)和learning_rate(学习率)。
    start = time.time()
  • 记录开始训练的时间戳。
    plot_losses = [] 
  • 初始化一个空列表plot_losses,用于存储绘图用的损失值。
    print_loss_total = 0  # Reset every print_every
  • 初始化print_loss_total为0,用于在每次打印损失时重置。
    plot_loss_total = 0  # Reset every plot_every
  • 初始化plot_loss_total为0,用于在每次绘制损失曲线时重置。
    encoder_optimizer = optim.SGD(encoder.parameters(), lr=learning_rate)
  • 创建一个SGD优化器,用于优化编码器的参数,学习率为learning_rate
    decoder_optimizer = optim.SGD(decoder.parameters(), lr=learning_rate)
  • 创建一个SGD优化器,用于优化解码器的参数,学习率为learning_rate
    # 在 pairs 中随机选取 n_iters 条数据用作训练集
    training_pairs = [tensorsFromPair(random.choice(pairs)) for i in range(n_iters)]
  • 创建一个列表training_pairs,其中包含n_iters个从pairs中随机选取的训练数据对。
    criterion = nn.NLLLoss()
  • 创建一个NLLLoss损失函数实例,用于计算负对数似然损失。
    for iter in range(1, n_iters + 1):
  • 开始一个循环,遍历从1到n_iters的迭代次数。
        training_pair = training_pairs[iter - 1]
  • training_pairs列表中获取当前迭代次数对应的训练数据对。
        input_tensor = training_pair[0]
  • 获取训练数据对的输入张量。
        target_tensor = training_pair[1]
  • 获取训练数据对的目标张量。
        loss = train(input_tensor, target_tensor, encoder, decoder, encoder_optimizer, decoder_optimizer, criterion)
  • 调用train函数,计算当前训练数据对的损失。
        print_loss_total += loss
  • 将当前损失累加到print_loss_total中。
        plot_loss_total += loss
  • 将当前损失累加到plot_loss_total中。
        if iter % print_every == 0:
  • 如果当前迭代次数是print_every的倍数,则执行以下代码。
            print_loss_avg = print_loss_total / print_every
  • 计算print_loss_total的平均值。
            print_loss_total = 0
  • print_loss_total重置为0。
        if iter % print_every == 0:
  • 检查当前迭代次数iter是否是print_every的倍数。
            print_loss_avg = print_loss_total / print_every
  • 如果iterprint_every的倍数,计算print_loss_total的平均值,即从上次打印以来累计的损失的平均值。
            print_loss_total = 0
  • print_loss_total重置为0,以便在下一次打印时重新开始累加损失。
            print('%s (%.2f %%) %.4f' % (timeSince(start, iter / n_iters), iter / n_iters * 100, print_loss_avg))
  • 如果iterprint_every的倍数,打印以下信息:
    • timeSince(start, iter / n_iters):从开始训练到现在的时间。
    • iter / n_iters * 100:当前迭代次数占总迭代次数的百分比。
    • print_loss_avg:从上次打印以来累计的损失的平均值。
        if iter % plot_every == 0:
  • 检查当前迭代次数iter是否是plot_every的倍数。
            plot_loss_avg = plot_loss_total / plot_every
  • 如果iterplot_every的倍数,计算plot_loss_total的平均值,即从上次绘制损失曲线以来累计的损失的平均值。
            plot_losses.append(plot_loss_avg)
  • plot_loss_avg添加到plot_losses列表中,以便绘制损失曲线。
            plot_loss_total = 0
  • plot_loss_total重置为0,以便在下一次绘制损失曲线时重新开始累加损失。
    return plot_losses
  • 循环结束后,返回plot_losses列表,其中包含了训练过程中的损失值。

3.评估

def evaluate(encoder, decoder, sentence, max_length=MAX_LENGTH):
    with torch.no_grad():
        input_tensor = tensorFromSentence(input_lang, sentence)
        input_length = input_tensor.size()[0]
        encoder_hidden = encoder.initHidden()

        encoder_outputs = torch.zeros(max_length, encoder.hidden_size, device=device)

        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)  # SOS

        decoder_hidden = encoder_hidden

        decoded_words = []
        decoder_attentions = torch.zeros(max_length, max_length)

        for di in range(max_length):
            decoder_output, decoder_hidden, decoder_attention = decoder(
                decoder_input, decoder_hidden, encoder_outputs)

            decoder_attentions[di] = decoder_attention.data
            topv, topi = decoder_output.data.topk(1)

            if topi.item() == EOS_token:
                decoded_words.append('<EOS>')
                break
            else:
                decoded_words.append(output_lang.index2word[topi.item()])

            decoder_input = topi.squeeze().detach()

    return decoded_words, decoder_attentions[di + 1]

这段代码定义了一个名为evaluate的函数,用于评估一个带有注意力机制的编码器-解码器模型。以下是逐行详细解释:

def evaluate(encoder, decoder, sentence, max_length=MAX_LENGTH):
  • 定义evaluate函数,它接受四个参数:encoder(编码器模型)、decoder(解码器模型)、sentence(要翻译的句子)和max_length(句子允许的最大长度)。
    with torch.no_grad():
  • 使用torch.no_grad()上下文管理器,确保在评估过程中不计算梯度,这样可以加快计算速度。
        input_tensor = tensorFromSentence(input_lang, sentence)
  • 调用tensorFromSentence函数,将输入句子转换为tensor。
        input_length = input_tensor.size()[0]
  • 计算输入句子的长度。
        encoder_hidden = encoder.initHidden()
  • 初始化编码器的隐藏状态。
        encoder_outputs = torch.zeros(max_length, encoder.hidden_size, device=device)
  • 创建一个全零张量,其大小为(max_length, encoder.hidden_size),用于存储编码器的输出。
        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)  # SOS
  • 初始化解码器的输入,SOS标记表示开始一个新句子。
        decoder_hidden = encoder_hidden
  • 初始化解码器的隐藏状态,与编码器的隐藏状态相同。
        decoded_words = []
  • 初始化一个空列表decoded_words,用于存储解码器生成的单词。
        decoder_attentions = torch.zeros(max_length, max_length)
  • 创建一个全零张量,其大小为(max_length, max_length),用于存储解码器在每个时间步的注意力分布。
        for di in range(max_length):
            decoder_output, decoder_hidden, decoder_attention = decoder(
                decoder_input, decoder_hidden, encoder_outputs)
            decoder_attentions[di] = decoder_attention.data
            topv, topi = decoder_output.data.topk(1)
            if topi.item() == EOS_token:
                decoded_words.append('<EOS>')
                break
            else:
                decoded_words.append(output_lang.index2word[topi.item()])
            decoder_input = topi.squeeze().detach()
  • 遍历最大长度的时间步,使用解码器生成单词。在每个时间步,计算解码器的输出、隐藏状态和注意力分布,然后选择概率最高的单词作为下一个输入。如果生成的单词是EOS标记,则停止生成,并添加EOS标记到decoded_words列表中。
    return decoded_words, decoder_attentions[di + 1]
  • 返回解码器生成的单词列表和最后一个时间步的注意力分布。
def evaluateRandomly(encoder, decoder, n=5):
    for i in range(n):
        pair = random.choice(pairs)
        print('>', pair[0])
        print('=', pair[1])
        output_words, attentions = evaluate(encoder, decoder, pair[0])
        output_sentence = ''.join(output_words)
        print('<', output_sentence)
        print('')

这段代码定义了一个名为evaluateRandomly的函数,用于评估一个带有注意力机制的编码器-解码器模型,并打印随机选择的句子对及其翻译结果。以下是逐行详细解释:

def evaluateRandomly(encoder, decoder, n=5):
  • 定义evaluateRandomly函数,它接受三个参数:encoder(编码器模型)、decoder(解码器模型)和n(要评估的句子对的数量,默认为5)。
    for i in range(n):
  • 开始一个循环,遍历从0到n-1的迭代次数。
        pair = random.choice(pairs)
  • pairs列表中随机选择一个句子对。
        print('>', pair[0])
  • 打印句子对的源语言句子。
        print('=', pair[1])
  • 打印句子对的目标语言句子。
        output_words, attentions = evaluate(encoder, decoder, pair[0])
  • 调用evaluate函数,使用选择的句子对作为输入,获取翻译结果和注意力分布。
        output_sentence = ''.join(output_words)
  • 将解码器生成的单词列表转换为一个字符串,以便打印。
        print('<', output_sentence)
  • 打印解码器生成的翻译结果。
        print('')
  • 打印一个空行,以便在输出中分隔不同的句子对。
    整个函数的主要作用是在循环中随机选择句子对,然后使用evaluate函数进行翻译,并打印源语言句子、目标语言句子和翻译结果。

四、训练与评估

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)

plot_losses = trainIters(encoder1, attn_decoder1, 10000, print_every=5000)

输出
2m 35s (- 2m 35s) (50.00 %) 2.8107
5m 8s (- 0m 0s) (100.00 %) 2.2775
这段代码定义了一个名为trainIters的函数,用于训练一个带有注意力机制的编码器-解码器模型,并调用这个函数来开始训练过程。以下是逐行详细解释:

hidden_size = 256
  • 定义一个变量hidden_size,其值为256,表示编码器和解码器的隐藏层大小。
encoder1 = EncoderRNN(input_lang.n_words, hidden_size).to(device)
  • 创建一个EncoderRNN实例,其中input_lang.n_words是输入语言的词汇量,hidden_size是隐藏层大小。然后将这个实例移动到之前定义的device上,以便在相应的设备上进行训练。
attn_decoder1 = AttnDecoderRNN(hidden_size, output_lang.n_words, dropout_p=0.1).to(device)
  • 创建一个AttnDecoderRNN实例,其中hidden_size是隐藏层大小,output_lang.n_words是输出语言的词汇量,dropout_p是dropout概率。然后将这个实例移动到之前定义的device上,以便在相应的设备上进行训练。
plot_losses = trainIters(encoder1, attn_decoder1, 10000, print_every=5000)
  • 调用trainIters函数,使用创建的encoder1attn_decoder1实例开始训练。训练的迭代次数为10000次,每5000次迭代打印一次损失。trainIters函数返回一个包含训练过程中损失值的列表plot_losses
evaluateRandomly(encoder1, attn_decoder1)

输出

在这里插入图片描述

1.Loss图

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()

在这里插入图片描述

2.可视化注意力

import matplotlib.pyplot as plt
output_words, attentions = evaluate(encoder1, attn_decoder1, "je suis trop froid.")
plt.matshow(attentions.numpy())

这段代码使用了matplotlib库来绘制一个注意力分布图。以下是逐行详细解释:

import matplotlib.pyplot as plt
  • 导入matplotlib.pyplot模块,并将其别名设置为plt
output_words, attentions = evaluate(encoder1, attn_decoder1, "je suis trop froid.")
  • 调用evaluate函数,使用创建的encoder1attn_decoder1实例来评估输入句子"je suis trop froid."。函数返回翻译结果的单词列表output_words和注意力分布attentions
plt.matshow(attentions.numpy())
  • 使用plt.matshow函数来绘制注意力分布图。attentions.numpy()将注意力分布张量转换为NumPy数组,以便matplotlib可以处理。这个函数会显示一个矩阵,其中每个元素表示解码器在生成输出单词时对源语言句子中相应单词的关注程度。
import matplotlib.ticker as ticker #隐藏警告 
import warnings 
warnings.filterwarnings("ignore") # 忽略警告信息 
def showAttention(input_sentence, output_words, attentions): # 
    fig = plt.figure() 
    ax = fig.add_subplot(111) 
    cax = ax.matshow(attentions.numpy(), cmap='bone') 
    fig.colorbar(cax) # 
    ax.set_xticklabels([''] + input_sentence.split(' ') + ['<EOS>'], rotation=90) 
    ax.set_yticklabels([''] + output_words) # 
    ax.xaxis.set_major_locator(ticker.MultipleLocator(1)) 
    ax.yaxis.set_major_locator(ticker.MultipleLocator(1))
    plt.show() 
def evaluateAndShowAttention(input_sentence): 
    output_words, attentions = evaluate(encoder1, attn_decoder1, input_sentence) 
    print('input =', input_sentence) 
    print('output =', ' '.join(output_words)) 
    showAttention(input_sentence, output_words, attentions) 
    evaluateAndShowAttention("elle a cinq ans de moins que moi.") 
    evaluateAndShowAttention("elle est trop petit.") 
    evaluateAndShowAttention("je ne crains pas de mourir.") 
    evaluateAndShowAttention("c est un jeune directeur plein de talent.")

这段代码定义了两个函数:showAttentionevaluateAndShowAttention,用于展示一个句子对齐的注意力分布。以下是逐行详细解释:

import matplotlib.ticker as ticker # 隐藏警告
  • 导入matplotlib.ticker模块,并将其别名设置为ticker
import warnings 
warnings.filterwarnings("ignore") # 忽略警告信息
  • 导入warnings模块,并使用filterwarnings函数忽略所有警告信息。
def showAttention(input_sentence, output_words, attentions): # 
  • 定义showAttention函数,它接受三个参数:input_sentence(输入句子)、output_words(输出单词列表)和attentions(注意力分布矩阵)。
    fig = plt.figure() 
  • 创建一个新的matplotlib图形窗口。
    ax = fig.add_subplot(111) 
  • 在图形窗口中添加一个子图,这个子图是图形窗口中的唯一子图。
    cax = ax.matshow(attentions.numpy(), cmap='bone') 
  • 使用matshow函数绘制注意力分布矩阵。attentions.numpy()将注意力分布张量转换为NumPy数组,以便matplotlib可以处理。cmap='bone'是颜色映射,用于表示注意力值。
    fig.colorbar(cax) # 
  • 在图形窗口中添加一个颜色条,以便用户可以理解注意力值的含义。
    ax.set_xticklabels([''] + input_sentence.split(' ') + ['<EOS>'], rotation=90) 
  • 设置x轴的刻度标签,这些标签是输入句子中的单词加上<EOS>标记。rotation=90表示刻度标签将以90度角旋转,以便在垂直的矩阵中更清晰地显示。
    ax.set_yticklabels([''] + output_words) # 
  • 设置y轴的刻度标签,这些标签是输出单词列表。
    ax.xaxis.set_major_locator(ticker.MultipleLocator(1)) 
    ax.yaxis.set_major_locator(ticker.MultipleLocator(1))
  • 设置x轴和y轴的主刻度间隔为1,这样可以更清晰地显示注意力值。
    plt.show() 
  • 显示图形窗口。
def evaluateAndShowAttention(input_sentence): 
  • 定义evaluateAndShowAttention函数,它接受一个参数:input_sentence(输入句子)。
    output_words, attentions = evaluate(encoder1, attn_decoder1, input_sentence) 
  • 调用evaluate函数,使用创建的encoder1attn_decoder1实例来评估输入句子。函数返回翻译结果的单词列表output_words和注意力分布attentions
    print('input =', input_sentence) 
    print('output =', ' '.join(output_words)) 
  • 打印输入句子和翻译结果。
    showAttention(input_sentence, output_words, attentions) 
  • 调用showAttention函数,显示注意力分布图。
    evaluateAndShowAttention("elle a cinq ans de moins que moi.") 
    evaluateAndShowAttention("elle est trop petit.") 
    evaluateAndShowAttention("je ne crains pas de mourir.") 
    evaluateAndShowAttention("c est un jeune directeur plein de talent.")
  • 调用evaluateAndShowAttention函数,评估并显示多个输入句子的翻译结果和注意力分布。
  • 20
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值