Pytorch:解码器端的Attention注意力机制、seq2seq模型架构实现英译法任务

日萌社

人工智能AI:Keras PyTorch MXNet TensorFlow PaddlePaddle 深度学习实战(不定时更新)


Encoder编码器-Decoder解码器框架 + Attention注意力机制

Pytorch:Transformer(Encoder编码器-Decoder解码器、多头注意力机制、多头自注意力机制、掩码张量、前馈全连接层、规范化层、子层连接结构、pyitcast) part1

Pytorch:Transformer(Encoder编码器-Decoder解码器、多头注意力机制、多头自注意力机制、掩码张量、前馈全连接层、规范化层、子层连接结构、pyitcast) part2

Pytorch:使用Transformer构建语言模型

Pytorch:解码器端的Attention注意力机制、seq2seq模型架构实现英译法任务

BahdanauAttention注意力机制、LuongAttention注意力机制

BahdanauAttention注意力机制:基于seq2seq的西班牙语到英语的机器翻译任务、解码器端的Attention注意力机制、seq2seq模型架构

图片的描述生成任务、使用迁移学习实现图片的描述生成过程、CNN编码器+RNN解码器(GRU)的模型架构、BahdanauAttention注意力机制、解码器端的Attention注意力机制

注意力机制、bmm运算

注意力机制 SENet、CBAM

机器翻译 MXNet(使用含注意力机制的编码器—解码器,即 Encoder编码器-Decoder解码器框架 + Attention注意力机制)

基于Seq2Seq的中文聊天机器人编程实践(Encoder编码器-Decoder解码器框架 + Attention注意力机制)

基于Transformer的文本情感分析编程实践(Encoder编码器-Decoder解码器框架 + Attention注意力机制 + Positional Encoding位置编码)

注意:这一文章“基于Transformer的文本情感分析编程实践(Encoder编码器-Decoder解码器框架 + Attention注意力机制 + Positional Encoding位置编码)”
	该文章实现的Transformer的Model类型模型,实际是改造过的特别版的Transformer,因为Transformer的Model类型模型中只实现了Encoder编码器,
	而没有对应实现的Decoder解码器,并且因为当前Transformer的Model类型模型处理的是分类任务,
	所以我们此处只用了Encoder编码器来提取特征,最后通过全连接层网络来拟合分类。

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

  • 学习目标:
    • 更深一步了解seq2seq模型架构和翻译数据集.
    • 掌握使用基于GRU的seq2seq模型架构实现翻译的过程.
    • 掌握Attention机制在解码器端的实现过程.
  • seq2seq模型架构:

  • seq2seq模型架构分析:
    • 从图中可知, seq2seq模型架构, 包括两部分分别是encoder(编码器)和decoder(解码器), 编码器和解码器的内部实现都使用了GRU模型, 这里它要完成的是一个中文到英文的翻译: 欢迎 来 北京 --> welcome to BeiJing. 编码器首先处理中文输入"欢迎 来 北京", 通过GRU模型获得每个时间步的输出张量,最后将它们拼接成一个中间语义张量c, 接着解码器将使用这个中间语义张量c以及每一个时间步的隐层张量, 逐个生成对应的翻译语言.
  • 翻译数据集
    • 下载地址: https://download.pytorch.org/tutorial/data.zip
    • 数据文件预览
- data/
        - eng-fra.txt  
She feeds her dog a meat-free diet. Elle fait suivre à son chien un régime sans viande.
She feeds her dog a meat-free diet. Elle fait suivre à son chien un régime non carné.
She folded her handkerchief neatly. Elle plia soigneusement son mouchoir.
She folded her handkerchief neatly. Elle a soigneusement plié son mouchoir.
She found a need and she filled it. Elle trouva un besoin et le remplit.
She gave birth to twins a week ago. Elle a donné naissance à des jumeaux il y a une semaine.
She gave him money as well as food. Elle lui donna de l'argent aussi bien que de la nourriture.
She gave it her personal attention. Elle y a prêté son attention personnelle.
She gave me a smile of recognition. Elle m'adressa un sourire indiquant qu'elle me reconnaissait.
She glanced shyly at the young man. Elle a timidement jeté un regard au jeune homme.
She goes to the movies once a week. Elle va au cinéma une fois par semaine.
She got into the car and drove off. Elle s'introduisit dans la voiture et partit.
  • 基于GRU的seq2seq模型架构实现翻译的过程:

    • 第一步: 导入必备的工具包.
    • 第二步: 对持久化文件中数据进行处理, 以满足模型训练要求.
    • 第三步: 构建基于GRU的编码器和解码器.
    • 第四步: 构建模型训练函数, 并进行训练.
    • 第五步: 构建模型评估函数, 并进行测试以及Attention效果分析.
  • 第一步: 导入必备的工具包
    • python版本使用3.6.x, pytorch版本使用1.3.1
# 从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或者cpu上运行你的代码
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")

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

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

class Lang:
    def __init__(self, name):
        """初始化函数中参数name代表传入某种语言的名字"""
        # 将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):
        """添加句子函数, 即将句子转化为对应的数值序列, 输入参数sentence是一条句子"""
        # 根据一般国家的语言特性(我们这里研究的语言都是以空格分个单词)
        # 对句子进行分割,得到对应的词汇列表
        for word in sentence.split(' '):
            # 然后调用addWord进行处理
            self.addWord(word)


    def addWord(self, word):
        """添加词汇函数, 即将词汇转化为对应的数值, 输入参数word是一个单词"""
        # 首先判断word是否已经在self.word2index字典的key中
        if word not in self.word2index:
            # 如果不在, 则将这个词加入其中, 并为它对应一个数值,即self.n_words
            self.word2index[word] = self.n_words
            # 同时也将它的反转形式加入到self.index2word中
            self.index2word[self.n_words] = word
            # self.n_words一旦被占用之后,逐次加1, 变成新的self.n_words
            self.n_words += 1
  • 实例化参数
name = "eng"
  • 输入参数
sentence = "hello I am Jay"
  • 调用
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, 'Jay': 5}
index2word: {0: 'SOS', 1: 'EOS', 2: 'hello', 3: 'I', 4: 'am', 5: 'Jay'}
n_words: 6
  • 字符规范化
# 将unicode转为Ascii, 我们可以认为是去掉一些语言中的重音标记:Ślusàrski
def unicodeToAscii(s):
    return ''.join(
        c for c in unicodedata.normalize('NFD', s)
        if unicodedata.category(c) != 'Mn'
    )


def normalizeString(s):
    """字符串规范化函数, 参数s代表传入的字符串"""
    # 使字符变为小写并去除两侧空白符, z再使用unicodeToAscii去掉重音标记
    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?"
  • 调用
nsr = normalizeString(s)
print(nsr)
  • 输出效果
are you kidding me ?
  • 将持久化文件中的数据加载到内存, 并实例化类Lang
data_path = '../Downloads/data/eng-fra.txt'

def readLangs(lang1, lang2):
    """读取语言函数, 参数lang1是源语言的名字, 参数lang2是目标语言的名字
       返回对应的class Lang对象, 以及语言对列表"""
    # 从文件中读取语言对并以/n划分存到列表lines中
    lines = open(data_path, encoding='utf-8').\
        read().strip().split('\n')
    # 对lines列表中的句子进行标准化处理,并以\t进行再次划分, 形成子列表, 也就是语言对
    pairs = [[normalizeString(s) for s in l.split('\t')] for l in lines] 
    # 然后分别将语言名字传入Lang类中, 获得对应的语言对象, 返回结果
    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("input_lang:", input_lang)
print("output_lang:", output_lang)
print("pairs中的前五个:", pairs[:5])
  • 输出效果
input_lang: <__main__.Lang object at 0x11ecf0828>
output_lang: <__main__.Lang object at 0x12d420d68>
pairs中的前五个: [['go .', 'va !'], ['run !', 'cours !'], ['run !', 'courez !'], ['wow !', 'ca alors !'], ['fire !', 'au feu !']]
  • 过滤出符合我们要求的语言对
# 设置组成句子中单词或标点的最多个数
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):
    """语言对过滤函数, 参数p代表输入的语言对, 如['she is afraid.', 'elle malade.']"""
    # p[0]代表英语句子,对它进行划分,它的长度应小于最大长度MAX_LENGTH并且要以指定的前缀开头
    # p[1]代表法文句子, 对它进行划分,它的长度应小于最大长度MAX_LENGTH
    return len(p[0].split(' ')) < MAX_LENGTH and \
        p[0].startswith(eng_prefixes) and \
        len(p[1].split(' ')) < MAX_LENGTH 


def filterPairs(pairs):
    """对多个语言对列表进行过滤, 参数pairs代表语言对组成的列表, 简称语言对列表"""
    # 函数中直接遍历列表中的每个语言对并调用filterPair即可
    return [pair for pair in pairs if filterPair(pair)]
  • 输入参数
# 输入参数pairs使用readLangs函数的输出结果pairs
  • 调用
fpairs = filterPairs(pairs)
print("过滤后的pairs前五个:", fpairs[:5])
  • 输出效果
过滤后的pairs前五个: [['i m .', 'j ai ans .'], ['i m ok .', 'je vais bien .'], ['i m ok .', 'ca va .'], ['i m fat .', 'je suis gras .'], ['i m fat .', 'je suis gros .']]
  • 对以上数据准备函数进行整合, 并使用类Lang对语言对进行数值映射
def prepareData(lang1, lang2):
    """数据准备函数, 完成将所有字符串数据向数值型数据的映射以及过滤语言对
       参数lang1, lang2分别代表源语言和目标语言的名字"""
    # 首先通过readLangs函数获得input_lang, output_lang对象,以及字符串类型的语言对列表
    input_lang, output_lang, pairs = readLangs(lang1, lang2)
    # 对字符串类型的语言对列表进行过滤操作
    pairs = filterPairs(pairs)
    # 对过滤后的语言对列表进行遍历
    for pair in pairs:
        # 并使用input_lang和output_lang的addSentence方法对其进行数值映射
        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_n_words:", input_lang.n_words)
print("output_n_words:", output_lang.n_words)
print(random.choice(pairs))
  • 输出效果
input_n_words: 2803
output_n_words: 4345
pairs随机选择一条: ['you re such an idiot !', 'quelle idiote tu es !']
  • 将语言对转化为模型输入需要的张量
def tensorFromSentence(lang, sentence):
    """将文本句子转换为张量, 参数lang代表传入的Lang的实例化对象, sentence是预转换的句子"""
    # 对句子进行分割并遍历每一个词汇, 然后使用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):
    """将语言对转换为张量对, 参数pair为一个语言对"""
    # 调用tensorFromSentence分别将源语言和目标语言分别处理,获得对应的张量表示
    input_tensor = tensorFromSentence(input_lang, pair[0])
    target_tensor = tensorFromSentence(output_lang, pair[1])
    # 最后返回它们组成的元组
    return (input_tensor, target_tensor)
  • 输入参数
# 取pairs的第一条
pair = pairs[0]
  • 调用
pair_tensor = tensorsFromPair(pair)
print(pair_tensor)
  • 输出效果
(tensor([[2],
        [3],
        [4],
        [1]]), 
 tensor([[2],
        [3],
        [4],
        [5],
        [1]]))
  • 第三步: 构建基于GRU的编码器和解码器
    • 构建基于GRU的编码器
    • 编码器结构图

class EncoderRNN(nn.Module):
    def __init__(self, input_size, hidden_size):
        """它的初始化参数有两个, input_size代表解码器的输入尺寸即源语言的
            词表大小,hidden_size代表GRU的隐层节点数, 也代表词嵌入维度, 同时又是GRU的输入尺寸"""
        super(EncoderRNN, self).__init__()
        # 将参数hidden_size传入类中
        self.hidden_size = hidden_size
        # 实例化nn中预定义的Embedding层, 它的参数分别是input_size, hidden_size
        # 这里的词嵌入维度即hidden_size
        # nn.Embedding的演示在该代码下方
        self.embedding = nn.Embedding(input_size, hidden_size)
        # 然后实例化nn中预定义的GRU层, 它的参数是hidden_size
        # nn.GRU的演示在该代码下方
        self.gru = nn.GRU(hidden_size, hidden_size)

    def forward(self, input, hidden):
        """编码器前向逻辑函数中参数有两个, input代表源语言的Embedding层输入张量
           hidden代表编码器层gru的初始隐层张量"""
        # 将输入张量进行embedding操作, 并使其形状变为(1,1,-1),-1代表自动计算维度
        # 理论上,我们的编码器每次只以一个词作为输入, 因此词汇映射后的尺寸应该是[1, embedding]
        # 而这里转换成三维的原因是因为torch中预定义gru必须使用三维张量作为输入, 因此我们拓展了一个维度
        output = self.embedding(input).view(1, 1, -1)
        # 然后将embedding层的输出和传入的初始hidden作为gru的输入传入其中, 
        # 获得最终gru的输出output和对应的隐层张量hidden, 并返回结果
        output, hidden = self.gru(output, hidden)
        return output, hidden

    def initHidden(self):
        """初始化隐层张量函数"""
        # 将隐层张量初始化成为1x1xself.hidden_size大小的0张量
        return torch.zeros(1, 1, self.hidden_size, device=device)
  • 实例化参数
hidden_size = 25
input_size = 20
  • 输入参数
# pair_tensor[0]代表源语言即英文的句子,pair_tensor[0][0]代表句子中
的第一个词
input = pair_tensor[0][0]
# 初始化第一个隐层张量,1x1xhidden_size的0张量
hidden = torch.zeros(1, 1, hidden_size)
  • 调用
encoder = EncoderRNN(input_size, hidden_size)
encoder_output, hidden = encoder(input, hidden)
print(encoder_output)
  • 输出效果
tensor([[[ 1.9149e-01, -2.0070e-01, -8.3882e-02, -3.3037e-02, -1.3491e-01,
          -8.8831e-02, -1.6626e-01, -1.9346e-01, -4.3996e-01,  1.8020e-02,
           2.8854e-02,  2.2310e-01,  3.5153e-01,  2.9635e-01,  1.5030e-01,
          -8.5266e-02, -1.4909e-01,  2.4336e-04, -2.3522e-01,  1.1359e-01,
           1.6439e-01,  1.4872e-01, -6.1619e-02, -1.0807e-02,  1.1216e-02]]],
       grad_fn=<StackBackward>)
  • 构建基于GRU的解码器

  • 解码器结构图:

class DecoderRNN(nn.Module):
    def __init__(self, hidden_size, output_size):
        """初始化函数有两个参数,hidden_size代表解码器中GRU的输入尺寸,也是它的隐层节点数
           output_size代表整个解码器的输出尺寸, 也是我们希望得到的指定尺寸即目标语言的词表大小"""
        super(DecoderRNN, self).__init__()
        # 将hidden_size传入到类中
        self.hidden_size = hidden_size
        # 实例化一个nn中的Embedding层对象, 它的参数output这里表示目标语言的词表大小
        # hidden_size表示目标语言的词嵌入维度
        self.embedding = nn.Embedding(output_size, hidden_size)
        # 实例化GRU对象,输入参数都是hidden_size,代表它的输入尺寸和隐层节点数相同
        self.gru = nn.GRU(hidden_size, hidden_size)
        # 实例化线性层, 对GRU的输出做线性变化, 获我们希望的输出尺寸output_size
        # 因此它的两个参数分别是hidden_size, output_size
        self.out = nn.Linear(hidden_size, output_size)
        # 最后使用softmax进行处理,以便于分类
        self.softmax = nn.LogSoftmax(dim=1)


    def forward(self, input, hidden):
        """解码器的前向逻辑函数中, 参数有两个, input代表目标语言的Embedding层输入张量
           hidden代表解码器GRU的初始隐层张量"""
        # 将输入张量进行embedding操作, 并使其形状变为(1,1,-1),-1代表自动计算维度
        # 原因和解码器相同,因为torch预定义的GRU层只接受三维张量作为输入
        output = self.embedding(input).view(1, 1, -1)
        # 然后使用relu函数对输出进行处理,根据relu函数的特性, 将使Embedding矩阵更稀疏,以防止过拟合
        output = F.relu(output)
        # 接下来, 将把embedding的输出以及初始化的hidden张量传入到解码器gru中
        output, hidden = self.gru(output, hidden)
        # 因为GRU输出的output也是三维张量,第一维没有意义,因此可以通过output[0]来降维
        # 再传给线性层做变换, 最后用softmax处理以便于分类
        output = self.softmax(self.out(output[0]))
        return output, hidden

    def initHidden(self):
        """初始化隐层张量函数"""
        # 将隐层张量初始化成为1x1xself.hidden_size大小的0张量
        return torch.zeros(1, 1, self.hidden_size, device=device)
  • 实例化参数
hidden_size = 25
output_size = 10
  • 输入参数
# pair_tensor[1]代表目标语言即法文的句子,pair_tensor[1][0]代表句子中的第一个词
input = pair_tensor[1][0]
# 初始化第一个隐层张量,1x1xhidden_size的0张量
hidden = torch.zeros(1, 1, hidden_size)
  • 调用
decoder = DecoderRNN(hidden_size, output_size)
output, hidden = decoder(input, hidden)
print(output)
  • 输出效果
tensor([[-2.3554, -2.3551, -2.4361, -2.2158, -2.2550, -2.6237, -2.2917, -2.2663,
         -2.2409, -2.0783]], grad_fn=<LogSoftmaxBackward>)
  • 构建基于GRU和Attention的解码器

  • 解码器结构图:

class AttnDecoderRNN(nn.Module):
    def __init__(self, hidden_size, output_size, dropout_p=0.1, max_length=MAX_LENGTH):
        """初始化函数中的参数有4个, hidden_size代表解码器中GRU的输入尺寸,也是它的隐层节点数
           output_size代表整个解码器的输出尺寸, 也是我们希望得到的指定尺寸即目标语言的词表大小
           dropout_p代表我们使用dropout层时的置零比率,默认0.1, 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

        # 实例化一个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.hidden_size * 2
        self.attn_combine = nn.Linear(self.hidden_size * 2, self.hidden_size)
        # 接着实例化一个nn.Dropout层,并传入self.dropout_p
        self.dropout = nn.Dropout(self.dropout_p)
        # 之后实例化nn.GRU, 它的输入和隐层尺寸都是self.hidden_size
        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):
        """forward函数的输入参数有三个, 分别是源数据输入张量, 初始的隐层张量, 以及解码器的输出张量"""

        # 根据结构计算图, 输入张量进行Embedding层并扩展维度
        embedded = self.embedding(input).view(1, 1, -1)
        # 使用dropout进行随机丢弃,防止过拟合
        embedded = self.dropout(embedded)

        # 进行attention的权重计算, 哦我们呢使用第一种计算方式:
        # 将Q,K进行纵轴拼接, 做一次线性变化, 最后使用softmax处理获得结果
        attn_weights = F.softmax(
            self.attn(torch.cat((embedded[0], hidden[0]), 1)), dim=1)

        # 然后进行第一步的后半部分, 将得到的权重矩阵与V做矩阵乘法计算, 当二者都是三维张量且第一维代表为batch条数时, 则做bmm运算
        attn_applied = torch.bmm(attn_weights.unsqueeze(0),
                                 encoder_outputs.unsqueeze(0))

        # 之后进行第二步, 通过取[0]是用来降维, 根据第一步采用的计算方法, 需要将Q与第一步的计算结果再进行拼接
        output = torch.cat((embedded[0], attn_applied[0]), 1)

        # 最后是第三步, 使用线性层作用在第三步的结果上做一个线性变换并扩展维度,得到输出
        output = self.attn_combine(output).unsqueeze(0)

        # attention结构的结果使用relu激活
        output = F.relu(output)

        # 将激活后的结果作为gru的输入和hidden一起传入其中
        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):
        """初始化隐层张量函数"""
        # 将隐层张量初始化成为1x1xself.hidden_size大小的0张量
        return torch.zeros(1, 1, self.hidden_size, device=device)
  • 实例化参数
hidden_size = 25
output_size = 10
  • 输入参数
input = pair_tensor[1][0]
hidden = torch.zeros(1, 1, hidden_size)
# encoder_outputs需要是encoder中每一个时间步的输出堆叠而成
# 它的形状应该是10x25, 我们这里直接随机初始化一个张量
encoder_outputs  = torch.randn(10, 25)
  • 调用
decoder = AttnDecoderRNN(hidden_size, output_size)
output, hidden, attn_weights= decoder(input, hidden, encoder_outputs)
print(output)
  • 输出效果
tensor([[-2.3556, -2.1418, -2.2012, -2.5109, -2.4025, -2.2182, -2.2123, -2.4608,
         -2.2124, -2.3827]], grad_fn=<LogSoftmaxBackward>)
  • 第四步: 构建模型训练函数, 并进行训练
    • 什么是teacher_forcing?
      • 它是一种用于序列生成任务的训练技巧, 在seq2seq架构中, 根据循环神经网络理论,解码器每次应该使用上一步的结果作为输入的一部分, 但是训练过程中,一旦上一步的结果是错误的,就会导致这种错误被累积,无法达到训练效果, 因此,我们需要一种机制改变上一步出错的情况,因为训练时我们是已知正确的输出应该是什么,因此可以强制将上一步结果设置成正确的输出, 这种方式就叫做teacher_forcing.
  • teacher_forcing的作用:
    • 能够在训练的时候矫正模型的预测,避免在序列生成的过程中误差进一步放大.
    • teacher_forcing能够极大的加快模型的收敛速度,令模型训练过程更快更平稳.
  • 构建训练函数
# 设置teacher_forcing比率为0.5
teacher_forcing_ratio = 0.5


def train(input_tensor, target_tensor, encoder, decoder, encoder_optimizer, decoder_optimizer, criterion, max_length=MAX_LENGTH):
    """训练函数, 输入参数有8个, 分别代表input_tensor:源语言输入张量,target_tensor:目标语言输入张量,encoder, decoder:编码器和解码器实例化对象
       encoder_optimizer, decoder_optimizer:编码器和解码器优化方法,criterion:损失函数计算方法,max_length:句子的最大长度"""

    # 初始化隐层张量
    encoder_hidden = encoder.initHidden()

    # 编码器和解码器优化器梯度归0
    encoder_optimizer.zero_grad()
    decoder_optimizer.zero_grad()

    # 根据源文本和目标文本张量获得对应的长度
    input_length = input_tensor.size(0)
    target_length = target_tensor.size(0)

    # 初始化编码器输出张量,形状是max_lengthxencoder.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

    # 如果使用teacher_forcing
    if use_teacher_forcing:
        # 循环遍历目标张量索引
        for di in range(target_length):
            # 将decoder_input, decoder_hidden, encoder_outputs即attention中的QKV, 
            # 传入解码器对象, 获得decoder_output, decoder_hidden, decoder_attention
            decoder_output, decoder_hidden, decoder_attention = decoder(
                decoder_input, decoder_hidden, encoder_outputs)
            # 因为使用了teacher_forcing, 无论解码器输出的decoder_output是什么, 我们都只
            # 使用‘正确的答案’,即target_tensor[di]来计算损失
            loss += criterion(decoder_output, target_tensor[di])
            # 并强制将下一次的解码器输入设置为‘正确的答案’
            decoder_input = target_tensor[di]  

    else:
        # 如果不使用teacher_forcing
        # 仍然遍历目标张量索引
        for di in range(target_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)
            # 只不过这里我们将从decoder_output取出答案
            topv, topi = decoder_output.topk(1)
            # 损失计算仍然使用decoder_output和target_tensor[di]
            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
  • 构建时间计算函数
# 导入时间和数学工具包
import time
import math

def timeSince(since):
    "获得每次打印的训练耗时, since是训练开始时间"
    # 获得当前时间
    now = time.time()
    # 获得时间差,就是训练耗时
    s = now - since
    # 将秒转化为分钟, 并取整
    m = math.floor(s / 60)
    # 计算剩下不够凑成1分钟的秒数
    s -= m * 60
    # 返回指定格式的耗时
    return '%dm %ds' % (m, s)
  • 输入参数
# 假定模型训练开始时间是10min之前
since = time.time() - 10*60
  • 调用:
period = timeSince(since)
print(period)
  • 输出效果
10m 0s 
  • 调用训练函数并打印日志和制图
# 导入plt以便绘制损失曲线
import matplotlib.pyplot as plt

def trainIters(encoder, decoder, n_iters, print_every=1000, plot_every=100, learning_rate=0.01):
    """训练迭代函数, 输入参数有6个,分别是encoder, decoder: 编码器和解码器对象,
       n_iters: 总迭代步数, print_every:打印日志间隔, plot_every:绘制损失曲线间隔, learning_rate学习率"""
    # 获得训练开始时间戳
    start = time.time()
    # 每个损失间隔的平均损失保存列表,用于绘制损失曲线
    plot_losses = []

    # 每个打印日志间隔的总损失,初始为0
    print_loss_total = 0  
    # 每个绘制损失间隔的总损失,初始为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))
        # 分别从training_pair中取出输入张量和目标张量
        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
            # 将总损失归0
            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列表
            plot_losses.append(plot_loss_avg)
            # 总损失归0
            plot_loss_total = 0

    # 绘制损失曲线
    plt.figure()  
    plt.plot(plot_losses)
    # 保存到指定路径
    plt.savefig("./s2s_loss.png")
  • 输入参数
# 设置隐层大小为256 ,也是词嵌入维度      
hidden_size = 256
# 通过input_lang.n_words获取输入词汇总数,与hidden_size一同传入EncoderRNN类中
# 得到编码器对象encoder1
encoder1 = EncoderRNN(input_lang.n_words, hidden_size).to(device)

# 通过output_lang.n_words获取目标词汇总数,与hidden_size和dropout_p一同传入AttnDecoderRNN类中
# 得到解码器对象attn_decoder1
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,迭代步数,日志打印间隔传入其中
trainIters(encoder1, attn_decoder1, n_iters, print_every=print_every)
  • 输出效果
3m 35s (5000 6%) 3.4159
7m 12s (10000 13%) 2.7805
10m 46s (15000 20%) 2.4663
14m 23s (20000 26%) 2.1693
18m 6s (25000 33%) 1.9303
21m 44s (30000 40%) 1.7601
25m 23s (35000 46%) 1.6207
29m 8s (40000 53%) 1.4973
32m 44s (45000 60%) 1.3832
36m 22s (50000 66%) 1.2694
40m 6s (55000 73%) 1.1813
43m 51s (60000 80%) 1.0907
47m 29s (65000 86%) 1.0425
51m 10s (70000 93%) 0.9955
54m 48s (75000 100%) 0.9158
  • 损失下降曲线:

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

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

  • 构建模型评估函数:
def evaluate(encoder, decoder, sentence, max_length=MAX_LENGTH):
    """评估函数,输入参数有4个,分别是encoder, decoder: 编码器和解码器对象,
       sentence:需要评估的句子,max_length:句子的最大长度"""

    # 评估阶段不进行梯度计算
    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]
  • 随机选择指定数量的数据进行评估
def evaluateRandomly(encoder, decoder, n=6):
    """随机测试函数, 输入参数encoder, decoder代表编码器和解码器对象,n代表测试数"""
    # 对测试数进行循环
    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('')
  • 调用
# 调用evaluateRandomly进行模型测试,将编码器对象encoder1,码器对象attn_decoder1传入其中
evaluateRandomly(encoder1, attn_decoder1)
  • 输出效果
> i m impressed with your french .
= je suis impressionne par votre francais .
< je suis impressionnee par votre francais . <EOS>

> i m more than a friend .
= je suis plus qu une amie .
< je suis plus qu une amie . <EOS>

> she is beautiful like her mother .
= elle est belle comme sa mere .
< elle est sa sa mere . <EOS>

> you re winning aren t you ?
= vous gagnez n est ce pas ?
< tu restez n est ce pas ? <EOS>

> he is angry with you .
= il est en colere apres toi .
< il est en colere apres toi . <EOS>

> you re very timid .
= vous etes tres craintifs .
< tu es tres craintive . <EOS>
  • 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")
  • 输出效果
['nous', 'sommes', 'toutes', 'deux', 'enseignantes', '.', '<EOS>']
  • Attention可视化

  • 分析:
    • Attention图像的纵坐标代表输入的源语言各个词汇对应的索引, 0-6分别对应["we", "re", "both", "teachers", ".", ""], 纵坐标代表生成的目标语言各个词汇对应的索引, 0-7代表['nous', 'sommes', 'toutes', 'deux', 'enseignantes', '.', ''], 图中浅色小方块(颜色越浅说明影响越大)代表词汇之间的影响关系, 比如源语言的第1个词汇对生成目标语言的第1个词汇影响最大, 源语言的第4,5个词对生成目标语言的第5个词会影响最大, 通过这样的可视化图像, 我们可以知道Attention的效果好坏, 与我们人为去判定到底还有多大的差距. 进而衡量我们训练模型的可用性.

小节总结:

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

  • 基于GRU的seq2seq模型架构实现翻译的过程:
    • 第一步: 导入必备的工具包.
    • 第二步: 对持久化文件中数据进行处理, 以满足模型训练要求.
    • 第三步: 构建基于GRU的编码器和解码器.
    • 第四步: 构建模型训练函数, 并进行训练.
    • 第五步: 构建模型评估函数, 并进行测试以及Attention效果分析.

  • 第一步: 导入必备的工具包
    • python版本使用3.6.x, pytorch版本使用1.3.1

  • 第二步: 对持久化文件中数据进行处理, 以满足模型训练要求
    • 将指定语言中的词汇映射成数值
    • 字符规范化
    • 将持久化文件中的数据加载到内存, 并实例化类Lang
    • 过滤出符合我们要求的语言对
    • 对以上数据准备函数进行整合, 并使用类Lang对语言对进行数值映射
    • 将语言对转化为模型输入需要的张量

  • 第三步: 构建基于GRU的编码器和解码器
    • 构建基于GRU的编码器
    • 构建基于GRU的解码器
    • 构建基于GRU和Attention的解码器

  • 第四步: 构建模型训练函数, 并进行训练
    • 什么是teacher_forcing: 它是一种用于序列生成任务的训练技巧, 在seq2seq架构中, 根据循环神经网络理论,解码器每次应该使用上一步的结果作为输入的一部分, 但是训练过程中,一旦上一步的结果是错误的,就会导致这种错误被累积,无法达到训练效果, 因此,我们需要一种机制改变上一步出错的情况,因为训练时我们是已知正确的输出应该是什么,因此可以强制将上一步结果设置成正确的输出, 这种方式就叫做teacher_forcing.
    • teacher_forcing的作用: 能够在训练的时候矫正模型的预测,避免在序列生成的过程中误差进一步放大. 另外, teacher_forcing能够极大的加快模型的收敛速度,令模型训练过程更快更平稳.
    • 构建训练函数train
    • 构建时间计算函数timeSince
    • 调用训练函数并打印日志和制图
    • 损失曲线分析: 一直下降的损失曲线, 说明模型正在收敛, 能够从数据中找到一些规律应用于数据

  • 第五步: 构建模型评估函数, 并进行测试以及Attention效果分析
    • 构建模型评估函数evaluate
    • 随机选择指定数量的数据进行评估
    • 进行了Attention可视化分析

"""
基于GRU的seq2seq模型架构实现翻译的过程:
    第一步: 导入必备的工具包.
    第二步: 对持久化文件中数据进行处理, 以满足模型训练要求.
    第三步: 构建基于GRU的编码器和解码器.
    第四步: 构建模型训练函数, 并进行训练.
    第五步: 构建模型评估函数, 并进行测试以及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或者cpu上运行你的代码
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")


#构建时间计算函数
# 导入时间和数学工具包
import time
import math

def timeSince(since):
    "获得每次打印的训练耗时, since是训练开始时间"
    # 获得当前时间
    now = time.time()
    # 获得时间差,就是训练耗时
    s = now - since
    # 将秒转化为分钟, 并取整
    m = math.floor(s / 60)
    # 计算剩下不够凑成1分钟的秒数
    s -= m * 60
    # 返回指定格式的耗时
    return '%dm %ds' % (m, s)

# #输入参数:
# # 假定模型训练开始时间是10min之前
# since = time.time() - 10*60
# #调用:
# period = timeSince(since)
# print(period) #10m 0s

"""
第二步: 对持久化文件中数据进行处理, 以满足模型训练要求
        1.将指定语言中的词汇映射成数值
        2.字符规范化
        3.将持久化文件中的数据加载到内存, 并实例化类Lang
        4.过滤出符合我们要求的语言对
        5.对以上数据准备函数进行整合, 并使用类Lang对语言对进行数值映射
        6.将语言对转化为模型输入需要的张量
"""
#======================1.将指定语言中的词汇映射成数值========================#
# 起始标志
SOS_token = 0
# 结束标志
EOS_token = 1

class Lang:
    """初始化函数中参数name代表传入某种语言的名字"""
    def __init__(self, name):
        # 将name传入类中
        self.name = name
        # 初始化词汇对应自然数值的字典
        self.word2index = {}
        # 初始化自然数值对应词汇的字典, 其中0,1对应的SOS和EOS已经在里面了
        self.index2word = {0: "SOS", 1: "EOS"}
        # 初始化词汇对应的自然数索引,这里从2开始,因为0,1已经被开始和结束标志占用了
        self.n_words = 2

    """添加句子函数, 即将句子转化为对应的数值序列, 输入参数sentence是一条句子"""
    def addSentence(self, sentence):
        # 根据一般国家的语言特性(我们这里研究的语言都是以空格分个单词)
        # 对句子进行分割,得到对应的词汇列表
        for word in sentence.split(' '):
            """
            构建 单词-索引的词汇表字典、索引-单词的词汇表字典:
                单词-索引的词汇表字典:key为单词,value为单词对应的索引值
                索引-单词的词汇表字典:key为单词对应的索引值,value为单词
           """
            # 然后调用addWord进行处理
            self.addWord(word)

    """添加词汇函数, 即将词汇转化为对应的数值, 输入参数word是一个单词"""
    def addWord(self, word):
        # 首先判断word是否已经在self.word2index字典的key中
        if word not in self.word2index:
            """
            构建 单词-索引的词汇表字典、索引-单词的词汇表字典
                单词-索引的词汇表字典:key为单词,value为单词对应的索引值
                索引-单词的词汇表字典:key为单词对应的索引值,value为单词
           """
            # 如果不在, 则将这个词加入其中, 并为它对应一个数值,即self.n_words
            self.word2index[word] = self.n_words
            # 同时也将它的反转形式加入到self.index2word中
            self.index2word[self.n_words] = word
            # self.n_words一旦被占用之后,逐次加1, 变成新的self.n_words
            self.n_words += 1

# #实例化参数
# name = "eng"
# #输入参数
# sentence = "hello I am Jay"
# #调用
# engl = Lang(name)
# engl.addSentence(sentence)
# print("word2index:", engl.word2index) #{'hello': 2, 'I': 3, 'am': 4, 'Jay': 5}
# print("index2word:", engl.index2word) #{0: 'SOS', 1: 'EOS', 2: 'hello', 3: 'I', 4: 'am', 5: 'Jay'}
# print("n_words:", engl.n_words) #6

#======================2.字符规范化==============================================#
# 将unicode转为Ascii, 我们可以认为是去掉一些语言中的重音标记:Ślusàrski
def unicodeToAscii(s):
    return ''.join(
        c for c in unicodedata.normalize('NFD', s)
        if unicodedata.category(c) != 'Mn'
    )

def normalizeString(s):
    """字符串规范化函数, 参数s代表传入的字符串"""
    # 使字符变为小写并去除两侧空白符, z再使用unicodeToAscii去掉重音标记
    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?"
# #调用
# nsr = normalizeString(s)
# print(nsr) #are you kidding me ?

#======================3.将持久化文件中的数据加载到内存, 并实例化类Lang=========#
data_path = 'data/eng-fra.txt'

def readLangs(lang1, lang2):
    """读取语言函数, 参数lang1是源语言的名字, 参数lang2是目标语言的名字
       返回对应的class Lang对象, 以及语言对列表"""
    # 从文件中读取语言对并以/n划分存到列表lines中
    lines = open(data_path, encoding='utf-8').read().strip().split('\n')
    # 对lines列表中的句子进行标准化处理,并以\t进行再次划分, 形成子列表, 也就是语言对
    pairs = [[normalizeString(s) for s in l.split('\t')] for l in lines]
    # 然后分别将语言名字传入Lang类中, 获得对应的语言对象, 返回结果
    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("input_lang:", input_lang)#input_lang: <__main__.Lang object at 0x0000026E82DAED48>
# print("output_lang:", output_lang)#output_lang: <__main__.Lang object at 0x0000026E8A427FC8>
# print("pairs中的前五个:", pairs[:5]) #pairs中的前五个: [['go .', 'va !'], ['run !', 'cours !'], ['run !', 'courez !'], ['wow !', 'ca alors !'], ['fire !', 'au feu !']]

#======================4.过滤出符合我们要求的语言对==============================================#
# 设置组成句子中单词或标点的最多个数
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):
    """语言对过滤函数, 参数p代表输入的语言对, 如['she is afraid.', 'elle malade.']"""
    # p[0]代表英语句子,对它进行划分,它的长度应小于最大长度MAX_LENGTH并且要以指定的前缀开头
    # p[1]代表法文句子, 对它进行划分,它的长度应小于最大长度MAX_LENGTH
    return len(p[0].split(' ')) < MAX_LENGTH and \
                p[0].startswith(eng_prefixes) and \
                len(p[1].split(' ')) < MAX_LENGTH

def filterPairs(pairs):
    """对多个语言对列表进行过滤, 参数pairs代表语言对组成的列表, 简称语言对列表"""
    # 函数中直接遍历列表中的每个语言对并调用filterPair即可
    return [pair for pair in pairs if filterPair(pair)]

# #输入参数:输入参数pairs使用readLangs函数的输出结果pairs
# #调用
# fpairs = filterPairs(pairs)
# print("过滤后的pairs前五个:", fpairs[:5]) #过滤后的pairs前五个: [['i m .', 'j ai ans .'], ['i m ok .', 'je vais bien .'], ['i m ok .', 'ca va .'], ['i m fat .', 'je suis gras .'], ['i m fat .', 'je suis gros .']]

#======================5.对以上数据准备函数进行整合, 并使用类Lang对语言对进行数值映射============#
def prepareData(lang1, lang2):
    """数据准备函数, 完成将所有字符串数据向数值型数据的映射以及过滤语言对
       参数lang1, lang2分别代表源语言和目标语言的名字"""
    # 首先通过readLangs函数获得input_lang, output_lang对象,以及字符串类型的语言对列表
    input_lang, output_lang, pairs = readLangs(lang1, lang2)
    # 对字符串类型的语言对列表进行过滤操作
    pairs = filterPairs(pairs)
    # 对过滤后的语言对列表进行遍历
    for pair in pairs:
        # 并使用input_lang和output_lang的addSentence方法对其进行数值映射
        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_n_words:", input_lang.n_words)# 2803
# print("output_n_words:", output_lang.n_words) # 4345
# print(random.choice(pairs)) #['he is the oldest of them all .', 'c est le plus vieux d entre eux .']

#======================6.将语言对转化为模型输入需要的张量==============================================#
def tensorFromSentence(lang, sentence):
    """将文本句子转换为张量, 参数lang代表传入的Lang的实例化对象, sentence是预转换的句子"""
    # 对句子进行分割并遍历每一个词汇, 然后使用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):
    """将语言对转换为张量对, 参数pair为一个语言对"""
    # 调用tensorFromSentence分别将源语言和目标语言分别处理,获得对应的张量表示
    input_tensor = tensorFromSentence(input_lang, pair[0])
    target_tensor = tensorFromSentence(output_lang, pair[1])
    # 最后返回它们组成的元组
    return (input_tensor, target_tensor)

# #输入参数:取pairs的第一条
# pair = pairs[0]
# #调用
# pair_tensor = tensorsFromPair(pair)
# print(pair_tensor)
# # (tensor([[2],[3],[4],[1]], device='cuda:0'),
# #  tensor([[2],[3],[4],[5],[1]], device='cuda:0'))

"""
第三步: 构建基于GRU的编码器和解码器
        1.构建基于GRU的编码器
        2.构建基于GRU的解码器
        3.构建基于GRU和Attention的解码器
"""
#=============================1.构建基于GRU的编码器=============================#
class EncoderRNN(nn.Module):
    def __init__(self, input_size, hidden_size):
        """
        它的初始化参数有两个
            input_size代表整个编码器的输入尺寸,即编码的源语言的词表大小,也是我们希望得到的指定尺寸
            hidden_size代表GRU的隐层节点数, 也代表词嵌入维度, 同时又是GRU的输入尺寸
        """
        super(EncoderRNN, self).__init__()
        # 将参数hidden_size传入类中
        self.hidden_size = hidden_size
        # 实例化nn中预定义的Embedding层, 它的参数分别是input_size, hidden_size
        # 这里的词嵌入维度即hidden_size
        # nn.Embedding的演示在该代码下方
        """
        初始化参数:
            input_size:整个编码器的输入尺寸,即编码器输出语言的词汇表大小。
                         可以是整个训练语料的不同单词的词汇总数,
                         也可以是“只包含至少出现N次的不同单词的”训练语料中至少出现N次的不同单词的词汇总数。
            hidden_size:是隐藏层中神经元数量, 又是embed_dim词嵌入维度。
        
        nn.Embedding(vocab_size 词汇总数, embed_dim 单词嵌入维度)
            vocab_size的多个版本:
                整个编码器的输入尺寸,编码器输入的词嵌入的词汇表大小。
                1.测试代码的时候输入的训练语料只有几个句子时,那么vocab_size为几个句子中可能出现的最多的不同单词的词汇总数,
                  可设置为比一个句子长度还长的大小。
                2.训练的语料为整个训练集时,那么vocab_size为整个数据集中所有不同单词的词汇总数。
                3.如果训练的整个语料数据集中只保留至少出现N次的词,那么vocab_size为整个数据集中至少出现N次的不同单词的词汇总数。
 
        nn.GRU(hidden_size, hidden_size) 多个写法:
            1.nn.GRU(输入数据的词嵌入维度, 隐藏层中神经元数量, 隐藏层层数)
            2.nn.GRU(输入数据的词嵌入维度, 隐藏层中神经元数量) #不写隐藏层层数,缺失值默认为1
        """
        self.embedding = nn.Embedding(input_size, hidden_size)
        # 然后实例化nn中预定义的GRU层, 它的参数是hidden_size
        # nn.GRU的演示在该代码下方
        self.gru = nn.GRU(hidden_size, hidden_size)

    def forward(self, input, hidden):
        """
        编码器前向逻辑函数中参数有两个
            input代表源语言的Embedding层输入张量
            hidden代表编码器层gru的初始隐层张量
        """
        """
        forward函数参数输入
            input:输入数据为句子中的一个单词,shape为torch.Size([1])
            hidden:(1, 1, 25),即 (隐藏层层数, 句子中的一个单词, 隐藏层中神经元数量)
            
        embedding(input).view(1, 1, -1)
            [1]的input经过embedding嵌入化后变成[1, embed_dim],再经过view(1, 1, -1)变成[1,1, embed_dim]
            
        output, hidden = gru(嵌入化的三维张量, hidden)
            hidden:初始化的hidden(1, 1, 25)作为gru节点输入,一个gru节点的隐藏状态输出作为下一个gru节点的隐藏状态输入
            output:(1, 1, 25) 即 (当前批次的样本个数, 当前样本的序列长度(单词个数), 隐藏层中神经元数量)
        """
        # 将输入张量进行embedding操作, 并使其形状变为(1,1,-1),-1代表自动计算维度
        # 理论上,我们的编码器每次只以一个词作为输入, 因此词汇映射后的尺寸应该是[1, embedding]
        # 而这里转换成三维的原因是因为torch中预定义gru必须使用三维张量作为输入, 因此我们拓展了一个维度
        output = self.embedding(input).view(1, 1, -1)
        # 然后将embedding层的输出和传入的初始hidden作为gru的输入传入其中,
        # 获得最终gru的输出output和对应的隐层张量hidden, 并返回结果
        output, hidden = self.gru(output, hidden)
        return output, hidden

    def initHidden(self):
        """初始化隐层张量函数"""
        # 将隐层张量初始化成为1x1xself.hidden_size大小的0张量
        return torch.zeros(1, 1, self.hidden_size, device=device)

# #输入参数:取pairs的第一条
# pair = pairs[0]
# #调用
# pair_tensor = tensorsFromPair(pair)
# # print(pair_tensor)
# # (tensor([[2],[3],[4],[1]], device='cuda:0'),
# #  tensor([[2],[3],[4],[5],[1]], device='cuda:0'))
#
# #实例化参数
# hidden_size = 25
# input_size = 20
# #输入参数
# # pair_tensor[0]代表源语言即英文的句子,pair_tensor[0][0]代表句子中的第一个词
# input = pair_tensor[0][0]
# # print("input.shape:",input.shape) #torch.Size([1])
# # 初始化第一个隐层张量,1 x 1 x hidden_size 的0维张量
# #隐藏状态hidden三维张量:(隐藏层层数, 句子中的一个单词, 隐藏层中神经元数量)
# hidden = torch.zeros(1, 1, hidden_size)
# #调用
# encoder = EncoderRNN(input_size, hidden_size).to(device)
# encoder_output, hidden = encoder(input.to(device), hidden.to(device))
# print(encoder_output.shape) #torch.Size([1, 1, 25])
# print(hidden.shape) #torch.Size([1, 1, 25])

#=============================2.构建基于GRU的解码器=============================#

class DecoderRNN(nn.Module):
    def __init__(self, hidden_size, output_size):
        """
        初始化函数有两个参数
            hidden_size代表解码器中GRU的输入尺寸,也是它的隐层节点数
            output_size代表整个解码器的输出尺寸, 也是我们希望得到的指定尺寸即目标语言的词表大小
        """
        """
        初始化参数:
            input_size:整个解码器的输入尺寸,即解码器输出语言的词汇表大小。
                         可以是整个训练语料的不同单词的词汇总数,
                         也可以是“只包含至少出现N次的不同单词的”训练语料中至少出现N次的不同单词的词汇总数。
            hidden_size:是隐藏层中神经元数量, 又是embed_dim词嵌入维度。

        nn.Embedding(vocab_size 词汇总数, embed_dim 单词嵌入维度)
            vocab_size的多个版本:
                整个编码器的输入尺寸,编码器输入的词嵌入的词汇表大小。
                1.测试代码的时候输入的训练语料只有几个句子时,那么vocab_size为几个句子中可能出现的最多的不同单词的词汇总数,
                  可设置为比一个句子长度还长的大小。
                2.训练的语料为整个训练集时,那么vocab_size为整个数据集中所有不同单词的词汇总数。
                3.如果训练的整个语料数据集中只保留至少出现N次的词,那么vocab_size为整个数据集中至少出现N次的不同单词的词汇总数。

        nn.GRU(hidden_size, hidden_size) 多个写法:
            1.nn.GRU(输入数据的词嵌入维度, 隐藏层中神经元数量, 隐藏层层数)
            2.nn.GRU(输入数据的词嵌入维度, 隐藏层中神经元数量) #不写隐藏层层数,缺失值默认为1
            
        Linear(hidden_size输入维度, output_size输出维度) --> nn.LogSoftmax(dim=1)
        先使用Linear把GRU的output输出的(1, 1, 25)中的hidden_size维度25 转换为 output_size维度10,最后用Softmax转换为维度10的概率值
        """
        super(DecoderRNN, self).__init__()
        # 将hidden_size传入到类中
        self.hidden_size = hidden_size
        # 实例化一个nn中的Embedding层对象, 它的参数output这里表示目标语言的词表大小
        # hidden_size表示目标语言的词嵌入维度
        self.embedding = nn.Embedding(output_size, hidden_size)
        # 实例化GRU对象,输入参数都是hidden_size,代表它的输入尺寸和隐层节点数相同
        self.gru = nn.GRU(hidden_size, hidden_size)
        # 实例化线性层, 对GRU的输出做线性变化, 获我们希望的输出尺寸output_size
        # 因此它的两个参数分别是hidden_size, output_size
        self.out = nn.Linear(hidden_size, output_size)
        # 最后使用softmax进行处理,以便于分类
        self.softmax = nn.LogSoftmax(dim=1)

    def forward(self, input, hidden):
        """
        解码器的前向逻辑函数中, 参数有两个
            input代表目标语言的Embedding层输入张量
            hidden代表解码器GRU的初始隐层张量
        """
        """
        forward函数的实现:embedding --> relu --> gru --> Linear --> softmax
        
        forward函数参数输入
            input:输入数据为句子中的一个单词,shape为torch.Size([1])
            hidden:(1, 1, 25),即 (隐藏层层数, 句子中的一个单词, 隐藏层中神经元数量)

        embedding(input).view(1, 1, -1)
            [1]的input经过embedding嵌入化后变成[1, embed_dim],再经过view(1, 1, -1)变成[1,1, embed_dim]

        output, hidden = gru(嵌入化的三维张量, hidden)
            hidden:初始化的hidden(1, 1, 25)作为gru节点输入,一个gru节点的隐藏状态输出作为下一个gru节点的隐藏状态输入
            output:(1, 1, 25) 即 (当前批次的样本个数, 当前样本的序列长度(单词个数), 隐藏层中神经元数量)
        """
        # 将输入张量进行embedding操作, 并使其形状变为(1,1,-1),-1代表自动计算维度
        # 原因和解码器相同,因为torch预定义的GRU层只接受三维张量作为输入
        output = self.embedding(input).view(1, 1, -1)
        # 然后使用relu函数对输出进行处理,根据relu函数的特性, 将使Embedding矩阵更稀疏,以防止过拟合
        output = F.relu(output)
        # 接下来, 将把embedding的输出以及初始化的hidden张量传入到解码器gru中
        output, hidden = self.gru(output, hidden)
        # 因为GRU输出的output也是三维张量,第一维没有意义,因此可以通过output[0]来降维
        # 再传给线性层做变换, 最后用softmax处理以便于分类
        output = self.softmax(self.out(output[0]))
        return output, hidden

    def initHidden(self):
        """初始化隐层张量函数"""
        # 将隐层张量初始化成为1x1xself.hidden_size大小的0张量
        return torch.zeros(1, 1, self.hidden_size, device=device)

# #输入参数:取pairs的第一条
# pair = pairs[0]
# #调用
# pair_tensor = tensorsFromPair(pair)
# # print(pair_tensor)
# # (tensor([[2],[3],[4],[1]], device='cuda:0'),
# #  tensor([[2],[3],[4],[5],[1]], device='cuda:0'))
#
# #实例化参数
# hidden_size = 25
# output_size = 10
# #输入参数
# # pair_tensor[1]代表目标语言即法文的句子,pair_tensor[1][0]代表句子中的第一个词
# input = pair_tensor[1][0]
# # 初始化第一个隐层张量,1 x 1 x hidden_size的0张量
# # 隐藏状态hidden三维张量:(隐藏层层数, 句子中的一个单词, 隐藏层中神经元数量)
# hidden = torch.zeros(1, 1, hidden_size)
# #调用
# decoder = DecoderRNN(hidden_size, output_size).to(device)
# output, hidden = decoder(input.to(device), hidden.to(device))
# print(hidden.shape) #torch.Size([1, 1, 25])
# print(output.shape) #torch.Size([1, 10])

#=============================3.构建基于GRU和Attention的解码器=============================#
class AttnDecoderRNN(nn.Module):
    def __init__(self, hidden_size, output_size, dropout_p=0.1, max_length=MAX_LENGTH):
        """初始化函数中的参数有4个
            hidden_size 代表解码器中GRU的输入尺寸,也是它的隐层节点数
            output_size 代表整个解码器的输出尺寸, 也是我们希望得到的指定尺寸即目标语言的词表大小
            dropout_p 代表我们使用dropout层时的置零比率,默认0.1
            max_length 代表句子的最大长度
        """
        """
        初始化参数:
            output_size:整个解码器的输出尺寸,即解码器输出语言的词汇表大小 4345。
                         可以是整个训练语料的不同单词的词汇总数 4345,
                         也可以是“只包含至少出现N次的不同单词的”训练语料中至少出现N次的不同单词的词汇总数。
            hidden_size:隐藏层中神经元数量 256, 又是embed_dim词嵌入维度 256。
            dropout_p:dropout层的置零比率,默认0.1
            max_length:句子的最大长度 10
            
        nn.Embedding(vocab_size 词汇总数, embed_dim 单词嵌入维度)
            vocab_size的多个版本:
                整个编码器的输入尺寸,编码器输入的词嵌入的词汇表大小。
                1.测试代码的时候输入的训练语料只有几个句子时,那么vocab_size为几个句子中可能出现的最多的不同单词的词汇总数,
                  可设置为比一个句子长度还长的大小。
                2.训练的语料为整个训练集时,那么vocab_size为整个数据集中所有不同单词的词汇总数。
                3.如果训练的整个语料数据集中只保留至少出现N次的词,那么vocab_size为整个数据集中至少出现N次的不同单词的词汇总数。

        nn.GRU(hidden_size, hidden_size) 多个写法:
            1.nn.GRU(输入数据的词嵌入维度, 隐藏层中神经元数量, 隐藏层层数)
            2.nn.GRU(输入数据的词嵌入维度, 隐藏层中神经元数量) #不写隐藏层层数,缺失值默认为1

        Linear(hidden_size输入维度, output_size输出维度) --> nn.LogSoftmax(dim=1)
        先使用Linear把GRU的output输出的(1, 1, 256)中的hidden_size维度256 转换为 output_size维度4345,最后用Softmax转换为维度4345的概率值
        """
        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.hidden_size * 2
        self.attn_combine = nn.Linear(self.hidden_size * 2, self.hidden_size)
        # 接着实例化一个nn.Dropout层,并传入self.dropout_p
        self.dropout = nn.Dropout(self.dropout_p)
        # 之后实例化nn.GRU, 它的输入和隐层尺寸都是self.hidden_size
        self.gru = nn.GRU(self.hidden_size, self.hidden_size)
        # 最后实例化gru后面的线性层,也就是我们的解码器输出层.
        self.out = nn.Linear(self.hidden_size, self.output_size)

    """
    forward函数参数输入
        input:输入数据为句子中的一个单词,shape为torch.Size([1])
        hidden:(1, 1, 256),即 (隐藏层层数, 句子中的一个单词, 隐藏层中神经元数量)
        encoder_outputs:(10, 256),即(句子最大长度MAX_LENGTH, 隐藏层中神经元数量)
        
    forward函数返回gru节点的output输出和hidden输出,还有第一步的前半部分所输出的注意力权重矩阵attn_weights
        
        
    注意力机制实现步骤简略版:
        Q:输入张量进行embedding嵌入化后的张量input
        K:隐藏状态输入hidden
        V:编码器的输出encoder_outputs
        第一步的前半部分:注意力权重矩阵attn_weights = softmax(Linear(cat((Q[0], K[0]), 1)), dim=1)
        第一步的后半部分:attn_applied = bmm(注意力权重矩阵attn_weights.unsqueeze(0), V)
        第二步:output = cat((Q[0], attn_applied[0]), 1)
        第三步:output = Linear(output).unsqueeze(0)
           
    1.embedding(input).view(1, 1, -1)
            [1]的input经过embedding嵌入化后变成[1, embed_dim],再经过view(1, 1, -1)变成[1,1, embed_dim] 即(1, 1, 256)
            
    2.embedded = dropout(embedded)
        dropout前后的形状都不变,embedded依旧为[1,1, embed_dim] 即(1, 1, 256)
        
    3.注意力机制实现 第一步的前半部分
        注意力权重矩阵attn_weights = softmax(Linear_attn(cat((Q[0], K[0]), 1)), dim=1)
            1.Linear_attn(hidden_size * 2, max_length) 输入维度:隐藏层中神经元数量*2。输出维度:句子的最大长度 10。
            2.Linear_attn(cat((embedded[0], hidden[0]), 1))
                embedded[0]:(1, 1, 256) 变成 (1, 256)
                hidden[0]:(1, 1, 256) 变成 (1, 256)
                cat((embedded[0], hidden[0]), 1):(1, 256)和(1, 256)在列维度拼接变成(1, 512)
                Linear_attn 输入(1, 512) 输出(1, 10)
            3.注意力权重矩阵attn_weights = F.softmax(Linear_attn的输出):softmax把(1, 10)转换为概率值的(1, 10)
    
    4.注意力机制实现 第一步的后半部分
        attn_applied = bmm(注意力权重矩阵attn_weights.unsqueeze(0), V)
                attn_applied = torch.bmm(attn_weights.unsqueeze(0), encoder_outputs.unsqueeze(0))
                    注意力权重矩阵attn_weights.unsqueeze(0):(1, 10) 变成 (1, 1, 10) 
                    encoder_outputs.unsqueeze(0):(10, 256)变成 (1, 10, 256) ,即(1, 句子最大长度MAX_LENGTH, 隐藏层中神经元数量)
                    attn_applied结果:bmm((1, 1, 10), (1, 10, 256)) 输出结果为 (1, 1, 256)
                
    5.注意力机制实现 第二步
        output = cat((Q[0], attn_applied[0]), 1)
            output = torch.cat((embedded[0], attn_applied[0]), 1)
                embedded[0]:(1, 1, 256) 变成 (1, 256)
                attn_applied[0]:(1, 1, 256) 变成 (1, 256)
                cat((embedded[0], attn_applied[0]), 1):(1, 256)和(1, 256)在列维度拼接变成(1, 512)的output

    6.注意力机制实现 第三步
        output = Linear(output).unsqueeze(0)
            Linear_attn_combine(hidden_size * 2, hidden_size) 输入维度:隐藏层中神经元数量*2。输出维度:隐藏层中神经元数量。
            output = Linear_attn_combine(output).unsqueeze(0)
                线性层的输入张量output:(1, 512) 即 (1, 隐藏层中神经元数量*2)
                Linear_attn_combine: 把(1, 512) 转换为 (1, 256)
                unsqueeze(0):把(1, 256) 变成 (1, 1, 256)
                线性层的输出张量output:(1, 1, 256)
                
    7.output = relu(output)
        dropout前后的形状都不变,output依旧为(1, 1, 256)
        
    8.output, hidden = gru(output, hidden)
        gru节点输入的output实际为经过注意力机制Attention计算之后的输出张量值,shape为(1, 1, 256)            
        1.gru = nn.GRU(hidden_size, hidden_size)
            nn.GRU(输入数据的词嵌入维度, 隐藏层中神经元数量)  #不写隐藏层层数,缺失值默认为1
            hidden_size为256:既为词嵌入维度,又为隐藏层神经元数量
        2.output, hidden = gru(output, hidden)
            gru节点输入的output:
                经过注意力机制Attention计算之后的输出张量值,shape为(1, 1, 256) 即 (当前批次的样本个数, 当前样本的序列长度(单词个数), 隐藏层中神经元数量)
            hidden:一开始使用初始化的hidden(1, 1, 256)作为第一个gru节点输入,一个gru节点的隐藏状态输出作为下一个gru节点的隐藏状态输入
            gru节点输出的output:shape为(1, 1, 256) (当前批次的样本个数, 当前样本的序列长度(单词个数), 隐藏层中神经元数量)	
                
    9.output = F.log_softmax(Linear_out(output[0]), dim=1)
        1.Linear_out = Linear(hidden_size, output_size) 
            输入维度:隐藏层中神经元数量, 又是embed_dim词嵌入维度。输出维度:output_size,即解码器输出语言的词汇表大小
            最后实例化gru后面的线性层,也就是我们的解码器输出层
        2.output = Linear_out(output[0])
            输入output的shape为(1, 1, 隐藏层中神经元数量256),output[0]为(1, 隐藏层中神经元数量256)
            输出output的shape为(1, 解码器输出语言的词汇表大小 4345)
        3.log_softmax((1, 4345), dim=1)
            softmax前后的形状都不变,把列值上的值转换为概率值
    """
    def forward(self, input, hidden, encoder_outputs):
        """
        forward函数的输入参数有三个, 分别是源数据输入张量input, 初始的隐层张量hidden, 以及编码器的输出张量encoder_outputs
        """
        # 根据结构计算图, 输入张量进行Embedding层并扩展维度
        embedded = self.embedding(input).view(1, 1, -1)
        # print("embedded.shape:",embedded.shape) #torch.Size([1, 1, 256])

        # 使用dropout进行随机丢弃,防止过拟合
        embedded = self.dropout(embedded)
        # print("embedded.shape:",embedded.shape) ##torch.Size([1, 1, 256])

        # 进行attention的权重计算, 哦我们呢使用第一种计算方式:
        # 将Q,K进行纵轴拼接, 做一次线性变化, 最后使用softmax处理获得结果
        attn_weights = F.softmax(self.attn(torch.cat((embedded[0], hidden[0]), 1)), dim=1)
        # print("attn_weights.shape:",attn_weights.shape) #torch.Size([1, 10])

        # 然后进行第一步的后半部分, 将得到的权重矩阵与V做矩阵乘法计算, 当二者都是三维张量且第一维代表为batch条数时, 则做bmm运算
        attn_applied = torch.bmm(attn_weights.unsqueeze(0),encoder_outputs.unsqueeze(0))
        # print("attn_applied.shape:",attn_applied.shape) #torch.Size([1, 1, 256])

        # 之后进行第二步, 通过取[0]是用来降维, 根据第一步采用的计算方法, 需要将Q与第一步的计算结果再进行拼接
        output = torch.cat((embedded[0], attn_applied[0]), 1)
        # print("output.shape:",output.shape) #torch.Size([1, 512])

        # 最后是第三步, 使用线性层作用在第三步的结果上做一个线性变换并扩展维度,得到输出
        output = self.attn_combine(output).unsqueeze(0)
        # print("output.shape:",output.shape) #torch.Size([1, 1, 256])

        # attention结构的结果使用relu激活
        output = F.relu(output)
        # print("output.shape:",output.shape)#torch.Size([1, 1, 256])

        # 将激活后的结果作为gru的输入和hidden一起传入其中
        output, hidden = self.gru(output, hidden)
        # print("output.shape:",output.shape) #torch.Size([1, 1, 256])
        # print("hidden.shape:",hidden.shape)#torch.Size([1, 1, 256])

        # 最后将结果降维并使用softmax处理得到最终的结果
        output = F.log_softmax(self.out(output[0]), dim=1)
        # print("output.shape:",output.shape) #torch.Size([1, 4345])

        # 返回解码器结果,最后的隐层张量以及注意力权重张量
        return output, hidden, attn_weights

    def initHidden(self):
        """初始化隐层张量函数"""
        # 将隐层张量初始化成为1x1xself.hidden_size大小的0张量
        return torch.zeros(1, 1, self.hidden_size, device=device)

# #输入参数:取pairs的第一条
# pair = pairs[0]
# #调用
# pair_tensor = tensorsFromPair(pair)
# # print(pair_tensor)
# # (tensor([[2],[3],[4],[1]], device='cuda:0'),
# #  tensor([[2],[3],[4],[5],[1]], device='cuda:0'))
#
# #实例化参数
# # hidden_size = 25
# # output_size = 10
# hidden_size = 256
# output_size = output_lang.n_words
# #输入参数
# # pair_tensor[1]代表目标语言即法文的句子,pair_tensor[1][0]代表句子中的第一个词
# input = pair_tensor[1][0]
# hidden = torch.zeros(1, 1, hidden_size)
# print("input:",input) #input: tensor([2], device='cuda:0')
"""
编码器输出状态张量:(句子最大长度MAX_LENGTH, 隐藏层中神经元数量)
    1.encoder中每一个时间步的输出outout堆叠而成的, 每一个时间步的输出outout为(1, 256) 即(句子中的一个单词, 隐藏层中神经元数量),
      而案例中设置了一个句子的最大长度为MAX_LENGTH=10,其中清洗好的句子只包含单词字母和标点符号。
    2.一个句子的最大长度为MAX_LENGTH=10,而且每一个时间步的输出outout为(1, 256),因此encoder_outputs编码器输出为(10, 256),
      即 (句子最大长度MAX_LENGTH, 隐藏层中神经元数量),因此一开始先初始化一个(句子最大长度MAX_LENGTH, 隐藏层中神经元数量)的0值的二维张量,
      encoder_outputs = torch.zeros(max_length, encoder.hidden_size, device=device)
"""
# # encoder_outputs需要是encoder中每一个时间步的输出堆叠而成
# # 它的形状应该是10x25, 我们这里直接随机初始化一个张量
# # encoder_outputs = torch.randn(10, 25)
# encoder_outputs = torch.randn(10, hidden_size)
#
# #调用
# decoder = AttnDecoderRNN(hidden_size, output_size).to(device)
# output, hidden, attn_weights= decoder(input.to(device), hidden.to(device), encoder_outputs.to(device))
# print(output.shape) #torch.Size([1, 4345])
# print(hidden.shape) #torch.Size([1, 1, 256])
# print(attn_weights.shape) #torch.Size([1, 10])


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

#=========================== 2.构建训练函数 ===========================#

def train(input_tensor, target_tensor, encoder, decoder, encoder_optimizer, decoder_optimizer, criterion, max_length=MAX_LENGTH):
    """训练函数, 输入参数有8个, 分别代表input_tensor:
            源语言输入张量,target_tensor:目标语言输入张量,encoder, decoder:编码器和解码器实例化对象
            encoder_optimizer, decoder_optimizer:编码器和解码器优化方法,criterion:损失函数计算方法,max_length:句子的最大长度
    """
    """
    input_tensor:shape为[英语句子单词个数, 1]的[[单词对应的索引值]],每个值为英语句子中的每个单词对应在词汇表的索引值
    target_tensor:shape为[法语句子单词个数, 1]的[[单词对应的索引值]],每个值为法语句子中的每个单词对应在词汇表的索引值
    encoder_hidden:(1, 1, hidden_size) 即(1, 1, 256)
    input_length:英语(输入)句子单词个数,即单词的索引值的数量
    target_length:法语(输出)句子单词个数,即单词的索引值的数量
    encoder_outputs:
            初始化编码器的output输出状态,为每一个时间步在最后一层隐藏层的output输出所封装的二维矩阵,
            shape为(句子最大长度max_length, hidden_size) 即(10, 256)
    """
    # 初始化隐层张量
    encoder_hidden = encoder.initHidden()

    # 编码器和解码器优化器梯度归0
    encoder_optimizer.zero_grad()
    decoder_optimizer.zero_grad()

    # 根据源文本和目标文本张量获得对应的长度
    input_length = input_tensor.size(0)
    target_length = target_tensor.size(0)

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

    # 初始设置损失为0
    loss = 0

    """
    for循环遍历出输入的英语句子中每个单词对应在词汇表中的索引值
        1.encoder_output, encoder_hidden = encoder(input_tensor[ei], encoder_hidden)
            先调用EncoderRNN类中的forward函数,先逐个把英语单词对应在词汇表的索引值传入到embedding层中输出嵌入化后的向量,
            shape为(1, embed_dim),再经过view(1, 1, -1)变成(1,1, embed_dim)。
            然后把嵌入化后的三维张量和hidden状态值输入到GRU中输出output和hidden,两者shape均为(1, 1, hidden_size)。
            encoder_hidden:每一层隐藏层的最后一个时间步(最后一个节点)在编码器中隐藏状态值
            encoder_output:每一个时间步(每个单词)在编码器中最后一层隐藏层的output输出值
        2.encoder_outputs[ei] = encoder_output[0, 0]
            encoder_outputs:
                封装每一个时间步(每个单词)在编码器中最后一层隐藏层的output输出值,
                作为整个编码器的output输出值用于输入到解码器中的每个时间步中。
                encoder_output[0, 0]表示把(1, 1, hidden_size)取出为[hidden_size]维度的向量作为每个单词(每个时间步)在编码器中的最终输出值,
                然后存到encoder_outputs用于输入到解码器中的每个时间步中。
    """
    # 循环遍历输入张量索引
    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]
        # print("encoder_output.shape:",encoder_output.shape)#torch.Size([1, 1, 256]) 即(1, 1, hidden_size)
        # print("encoder_hidden.shape:",encoder_hidden.shape)#torch.Size([1, 1, 256]) 即(1, 1, hidden_size)

    """
    1.decoder_input = torch.tensor([[SOS_token]], device=device)
        先创建句子起始标志为0值的[[0]]张量,shape为[1, 1],并作为解码器中第一个时间步的输入
    2.decoder_hidden = encoder_hidden 两者shape为[1, 1, 256]
        encoder_hidden:每一层隐藏层的最后一个时间步(最后一个节点)在编码器中隐藏状态值
        decoder_hidden:把编码器中每一层隐藏层的最后一个时间步(最后一个节点)所封装起来的隐藏状态值 作为 解码器第一个时间步(第一个节点)的初始化隐藏状态值
    3.use_teacher_forcing = True if random.random() < teacher_forcing_ratio else False
        teacher_forcing_ratio=0.5:表示random.random()随机出来的数据有一半机会整个判断表达式返回True或False
        当一半机会判断True则执行teacher_forcing机制,当一半机会判断False则不执行teacher_forcing机制。
    """
    # 初始化解码器的第一个输入,即起始符
    decoder_input = torch.tensor([[SOS_token]], device=device)
    # print("decoder_input.shape:",decoder_input.shape) #torch.Size([1, 1])

    # 初始化解码器的隐层张量即编码器的隐层输出
    decoder_hidden = encoder_hidden
    # print("decoder_hidden.shape:",decoder_hidden.shape) #[1, 1, 256]

    # 根据随机数与teacher_forcing_ratio对比判断是否使用teacher_forcing
    use_teacher_forcing = True if random.random() < teacher_forcing_ratio else False

    """
    使用teacher_forcing机制
        模型预测中,我们需要将解码器在上⼀个时间步的输出作为当前时间步的输⼊。
        与此不同,在训练中我们也可以将标签序列(训练集的真实输出序列)在上⼀个时间步的标签作为解码器在当前时间步的输⼊。
        这叫作强制教学(teacher forcing)。解码器在某时间步的输⼊为样本输出序列在上⼀时间步的词,即强制教学。
        在编码器—解码器的训练中,可以采⽤强制教学。
 
        for index in range(法语(输出)句子单词个数,即单词的索引值的数量):
            # decoder_input:
            #   第一个时间步传入的是shape为[1, 1]的起始符[[SOS_token]],
            #   后面每个时间步传入的是shape为[英语句子单词个数, 1]所遍历的每个单词对应的索引值,即shape为[1, 1]的[[单词对应的索引值]]。
            # decoder_hidden:[1, 1, 256] 
            #   第一个时间步传入的是decoder_hidden=encoder_hidden,即第一个时间步使用的是编码器输出的隐藏状态值。
            #   后面每个时间步传入的是解码器前一时间步输出隐藏状态值。
            # encoder_outputs:[10, 256] 即[max_length, 隐藏神经元数量]
            #   解码器的每个时间步都传入编码器的output输出,该encoder_outputs封装了编码器每一个时间步在最后一层隐藏层的output输出值。
            decoder_output, decoder_hidden, decoder_attention = decoder(decoder_input, decoder_hidden, encoder_outputs)
            
            #target_tensor:shape为[法语(输出)句子单词个数, 1]的[[单词对应的索引值]],每个值为法语(输出)句子中的每个单词对应在词汇表的索引值。
            #target_tensor[index]:从shape为[[单词对应的索引值]]的法语(输出)句子中获取每个单词的索引值即[单词对应的索引值]
            #decoder_output:解码器输出output为经过了softmax概率值转换操作后的shape为[1, 4345]的概率值矩阵,4345为输出语料的词汇表大小
            #loss:使用shape为[1, 4345]的概率值矩阵 和 [单词对应的索引值] 计算loss
            loss += criterion(decoder_output, target_tensor[index])
            
            #target_tensor[index]:从shape为[[单词对应的索引值]]的法语(输出)句子中获取每个单词的索引值即[单词对应的索引值]
            #把下一个单词对应的索引值(真实标签值)作为解码器的下一个时间步的输入
            decoder_input = target_tensor[index]
    """
    # 如果使用teacher_forcing
    if use_teacher_forcing:
            """
            如果是teacher_forcing,那么将会使用真实标签语句中的值作为解码器时间步的输入。
            第一个时间步输入的是起始符SOS_token,然后后面每个时间步的输入都是真实标签语句中的单词索引值。
            真实标签语句中的最后一个单词索引值为1(结束标志EOS_token),那么当前的循环是没有把1(结束标志EOS_token)再输入到解码器中训练的,
            因为当前range(目标语句长度)中的最后一次循环中target_tensor[di]仅仅是把1(结束标志EOS_token)取出来了,
            然后循环就结束了,并没有机会再次输入到解码器中继续训练。
            """
        # 循环遍历目标张量索引
        for di in range(target_length):
            # 将decoder_input, decoder_hidden, encoder_outputs即attention中的QKV,
            # 传入解码器对象, 获得decoder_output, decoder_hidden, decoder_attention
            decoder_output, decoder_hidden, decoder_attention = decoder(
                decoder_input, decoder_hidden, encoder_outputs)
            # print("encoder_outputs.shape:", encoder_outputs.shape) #[10, 256]
            # print("decoder_output.shape:", decoder_output.shape) #[1, 4345]
            # print("decoder_hidden.shape:", decoder_hidden.shape) #[1, 1, 256]
            # print("decoder_attention.shape:", decoder_attention.shape) #[1, 10]
            # print("target_tensor[di].shape:", target_tensor[di].shape) #[1]
            """
            >>> a
            tensor([[0., 0., 1., 0., 0.]])
            >>> a.argmax(1)
            tensor([2])
            >>> a.argmax(-1)
            tensor([2])
            """
            # print("target_tensor[di]:", target_tensor[di]) #一维张量 [一个单词对应的索引值]
            # print("decoder_output:", decoder_output.argmax(-1)) #二维张量 取[1, 4345]中的最后一个维度/第二个维度的最大概率值的索引位置

            # 因为使用了teacher_forcing, 无论解码器输出的decoder_output是什么, 我们都只
            # 使用‘正确的答案’,即target_tensor[di]来计算损失
            loss += criterion(decoder_output, target_tensor[di])
            # 并强制将下一次的解码器输入设置为‘正确的答案’
            decoder_input = target_tensor[di]

    else:
        # 如果不使用teacher_forcing
        # 仍然遍历目标张量索引
        for di in range(target_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)
            """
            #decoder_output:解码器输出output为经过了softmax概率值转换操作后的shape为[1, 4345]的概率值矩阵,4345为输出语料的词汇表大小
            #top_n,top_i = topk(K):从维度4345的概率值的向量值中获取最大概率类别值和对应的索引值,该索引值即代表某单词在词汇表中的索引值。
            #top_n:最大概率值。top_i:最大概率值的索引值。
            topv, topi = decoder_output.topk(1)
            """
            # 只不过这里我们将从decoder_output取出答案
            topv, topi = decoder_output.topk(1)
            # 损失计算仍然使用decoder_output和target_tensor[di]
            loss += criterion(decoder_output, target_tensor[di])
            # 最后如果输出值是终止符,则循环停止
            if topi.squeeze().item() == EOS_token:
                break
            """
            使用teacher_forcing机制
                #topi:从维度4345的概率值的向量值中获取最大概率值的索引值,该索引值即代表某单词在词汇表中的索引值。
                #decoder_input:把下一个单词对应的索引值(真实标签值)作为解码器的下一个时间步的输入
                #detach:具有分离作用使得这个decoder_input与模型构建的张量图无关,相当于全新的外界输入
                decoder_input = topi.squeeze().detach()
            """
            # 否则,并对topi降维并分离赋值给decoder_input以便进行下次运算
            # 这里的detach的分离作用使得这个decoder_input与模型构建的张量图无关,相当于全新的外界输入
            decoder_input = topi.squeeze().detach()

    # 误差进行反向传播
    loss.backward()
    # 编码器和解码器进行优化即参数更新
    encoder_optimizer.step()
    decoder_optimizer.step()

    # 最后返回平均损失
    return loss.item() / target_length

#=========================== 3.调用训练函数并打印日志和制图 ===========================#

# 导入plt以便绘制损失曲线
import matplotlib.pyplot as plt

def trainIters(encoder, decoder, n_iters, print_every=100, plot_every=100, learning_rate=0.0001):
    """
    训练迭代函数, 输入参数有6个,
        分别是 encoder, decoder: 编码器和解码器对象;n_iters: 总迭代步数;
        print_every:打印日志间隔; plot_every:绘制损失曲线间隔; learning_rate:学习率
    """
    # 获得训练开始时间戳
    start = time.time()
    # 每个损失间隔的平均损失保存列表,用于绘制损失曲线
    plot_losses = []
    # 每个打印日志间隔的总损失,初始为0
    print_loss_total = 0
    # 每个绘制损失间隔的总损失,初始为0
    plot_loss_total = 0
    """ optimizer优化器对象 = optim.SGD(继承了“nn.Module”的自定义类对象.parameters(), lr=learning_rate) """
    # 使用预定义的SGD作为优化器,将参数和学习率传入其中
    encoder_optimizer = optim.SGD(encoder.parameters(), lr=learning_rate, momentum=0.9)
    decoder_optimizer = optim.SGD(decoder.parameters(), lr=learning_rate, momentum=0.9)

    """
    nn.LogSoftmax + nn.NLLLoss = 交叉熵Cross Entropy
    F.log_softmax + F.nll_loss = 交叉熵Cross Entropy
    torch.nn.CrossEntropyLoss()(内置softmax,无需显式写softmax层)= 交叉熵Cross Entropy
    """
    # 选择损失函数
    criterion = nn.NLLLoss()

    encoder.load_state_dict(torch.load("./model_save/encoder2_0.4831.pkl"))
    decoder.load_state_dict(torch.load("./model_save/decoder2_0.4831.pkl"))

    # 根据设置迭代步进行循环
    for iter in range(1, n_iters + 1):
        # 每次从语言对列表中随机取出一条作为训练语句
        # 返回的是元祖,元祖中第一个元素为英语句子中的每个单词对应在词汇表的索引值所封装的二维数组,
        # 元祖中第二个元素为法语句子中的每个单词对应在词汇表的索引值所封装的二维数组
        training_pair = tensorsFromPair(random.choice(pairs))
        """
        input_tensor:shape为[英语句子单词个数, 1],每个值为英语句子中的每个单词对应在词汇表的索引值
        target_tensor:shape为[法语句子单词个数, 1],每个值为法语句子中的每个单词对应在词汇表的索引值
        """
        # 分别从training_pair中取出输入张量和目标张量
        input_tensor = training_pair[0]
        target_tensor = training_pair[1]
        # print("input_tensor.shape:",input_tensor.shape) #torch.Size([英语句子单词个数, 1])
        # print("target_tensor.shape:",target_tensor.shape) #torch.Size([法语句子单词个数, 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
            # 将总损失归0
            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列表
            plot_losses.append(plot_loss_avg)
            # 总损失归0
            plot_loss_total = 0

    # 绘制损失曲线
    plt.figure()
    plt.plot(plot_losses)
    # 保存到指定路径
    plt.savefig("./s2s_loss.png")

    torch.save(encoder1.state_dict(), './model_save/encoder.pt')
    torch.save(attn_decoder1.state_dict(), './model_save/attn_decoder.pt')

#输入参数:
# 设置隐层大小为256 ,也是词嵌入维度
hidden_size = 256
# 通过input_lang.n_words获取输入词汇总数 2803,与hidden_size 256一同传入EncoderRNN类中
# 得到编码器对象encoder1
encoder1 = EncoderRNN(input_lang.n_words, hidden_size).to(device)
# 通过output_lang.n_words获取目标词汇总数 4345,与hidden_size 256 和dropout_p一同传入AttnDecoderRNN类中
# 得到解码器对象attn_decoder1
attn_decoder1 = AttnDecoderRNN(hidden_size, output_lang.n_words, dropout_p=0.1).to(device)
# 设置迭代步数
n_iters = 20000
# 设置日志打印间隔
print_every = 100

# 调用trainIters进行模型训练,将编码器对象encoder1,码器对象attn_decoder1,迭代步数,日志打印间隔传入其中
trainIters(encoder1, attn_decoder1, n_iters, print_every=print_every)


"""
第五步: 构建模型评估函数, 并进行测试以及Attention效果分析.
    1.构建模型评估函数
    2.随机选择指定数量的数据进行评估
"""
#============================ 1.构建模型评估函数 =============================================#
def evaluate(encoder, decoder, sentence, max_length=MAX_LENGTH):
    """评估函数,输入参数有4个,分别是encoder, decoder: 编码器和解码器对象,
       sentence:需要评估的句子,max_length:句子的最大长度"""

    # 评估阶段不进行梯度计算
    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 = []
        """
        decoder_attentions = torch.zeros(max_length, max_length)
            因为一个注意力权重矩阵attn_weights的shape为(1, max_length),即(1, 10)。
            而由于for循环要遍历max_length次,即最多遍历10次,如果中途预测为'<EOS>'则提早停止循环。
            注意力权重矩阵attn_weights = F.softmax(Linear_attn的输出):softmax把(1, 10)转换为概率值的(1, 10)
            因此最终初始化的decoder_attentions的shape要为(max_length, max_length),即(10, 10)。
        
        decoder_attentions[di] = decoder_attention.data
            每个时间步输出的decoder_attention的shape为(1, max_length),那么遍历到最多max_length次的话,
            那么[max_length, max_length]的decoder_attentions整个都会被赋值。
        """
        # 初始化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

            """
            #decoder_output:解码器输出output为经过了softmax概率值转换操作后的shape为[1, 4345]的概率值矩阵,4345为输出语料的词汇表大小
            #topv, top_i = topk(K):从维度4345的概率值的向量值中获取最大概率类别值和对应的索引值,该索引值即代表某单词在词汇表中的索引值。
            #topv:最大概率值。top_i:最大概率值的索引值。
            topv, topi = decoder_output.data.topk(1)
            """
            # 从解码器输出中获得概率最高的值及其索引对象
            topv, topi = decoder_output.data.topk(1)
            """ topi最大概率值的索引值 ==  EOS_token即1的话,那么停止遍历,并在输出语句decoded_words中添加结束标志符<EOS> """
            # 从索引对象中取出它的值与结束标志值作对比
            if topi.item() == EOS_token:
                # 如果是结束标志值,则将结束标志装进decoded_words列表,代表翻译结束
                decoded_words.append('<EOS>')
                # 循环退出
                break

            else:
                """
                 decoded_words.append(output_lang.index2word[topi.item()])
                          从index2word字段中根据topi最大概率值的索引值取出对应的单词并添加在输出语句decoded_words中
                """
                # 否则,根据索引找到它在输出语言的index2word字典中对应的单词装进decoded_words
                decoded_words.append(output_lang.index2word[topi.item()])
            """
                #topi:从维度4345的概率值的向量值中获取最大概率值的索引值,该索引值即代表某单词在词汇表中的索引值。
                #decoder_input:把下一个单词对应的索引值(真实标签值)作为解码器的下一个时间步的输入
                #detach:具有分离作用使得这个decoder_input与模型构建的张量图无关,相当于全新的外界输入
                decoder_input = topi.squeeze().detach()
            """
            # 最后将本次预测的索引降维并分离赋值给decoder_input,以便下次进行预测
            decoder_input = topi.squeeze().detach()
        """ decoder_attentions[:di + 1]:因为左闭右开的关系,所以需要+1 """
        # 返回结果decoded_words, 以及完整注意力张量, 把没有用到的部分切掉
        return decoded_words, decoder_attentions[:di + 1]

#============================ 2.随机选择指定数量的数据进行评估 =============================================#

def evaluateRandomly(encoder, decoder, n=6):
    """随机测试函数, 输入参数encoder, decoder代表编码器和解码器对象,n代表测试数"""
    # 对测试数进行循环
    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('')

# 调用evaluateRandomly进行模型测试,将编码器对象encoder1,码器对象attn_decoder1传入其中
evaluateRandomly(encoder1, attn_decoder1)

#绘制 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")
"""
Attention图像的纵坐标代表输入的源语言各个词汇对应的索引, 0-6分别对应["we", "re", "both", "teachers", ".", ""], 
纵坐标代表生成的目标语言各个词汇对应的索引, 0-7代表['nous', 'sommes', 'toutes', 'deux', 'enseignantes', '.', ''], 
图中浅色小方块(颜色越浅说明影响越大)代表词汇之间的影响关系, 比如源语言的第1个词汇对生成目标语言的第1个词汇影响最大, 
源语言的第4,5个词对生成目标语言的第5个词会影响最大, 通过这样的可视化图像, 
我们可以知道Attention的效果好坏, 与我们人为去判定到底还有多大的差距. 进而衡量我们训练模型的可用性.
"""
"""
基于GRU的seq2seq模型架构实现翻译的过程:
    第一步: 导入必备的工具包.
    第二步: 对持久化文件中数据进行处理, 以满足模型训练要求.
    第三步: 构建基于GRU的编码器和解码器.
    第四步: 构建模型训练函数, 并进行训练.
    第五步: 构建模型评估函数, 并进行测试以及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或者cpu上运行你的代码
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")


#构建时间计算函数
# 导入时间和数学工具包
import time
import math

def timeSince(since):
    "获得每次打印的训练耗时, since是训练开始时间"
    # 获得当前时间
    now = time.time()
    # 获得时间差,就是训练耗时
    s = now - since
    # 将秒转化为分钟, 并取整
    m = math.floor(s / 60)
    # 计算剩下不够凑成1分钟的秒数
    s -= m * 60
    # 返回指定格式的耗时
    return '%dm %ds' % (m, s)

# #输入参数:
# # 假定模型训练开始时间是10min之前
# since = time.time() - 10*60
# #调用:
# period = timeSince(since)
# print(period) #10m 0s

"""
第二步: 对持久化文件中数据进行处理, 以满足模型训练要求
        1.将指定语言中的词汇映射成数值
        2.字符规范化
        3.将持久化文件中的数据加载到内存, 并实例化类Lang
        4.过滤出符合我们要求的语言对
        5.对以上数据准备函数进行整合, 并使用类Lang对语言对进行数值映射
        6.将语言对转化为模型输入需要的张量
"""
#======================1.将指定语言中的词汇映射成数值========================#
# 起始标志
SOS_token = 0
# 结束标志
EOS_token = 1

class Lang:
    """初始化函数中参数name代表传入某种语言的名字"""
    def __init__(self, name):
        # 将name传入类中
        self.name = name
        # 初始化词汇对应自然数值的字典
        self.word2index = {}
        # 初始化自然数值对应词汇的字典, 其中0,1对应的SOS和EOS已经在里面了
        self.index2word = {0: "SOS", 1: "EOS"}
        # 初始化词汇对应的自然数索引,这里从2开始,因为0,1已经被开始和结束标志占用了
        self.n_words = 2

    """添加句子函数, 即将句子转化为对应的数值序列, 输入参数sentence是一条句子"""
    def addSentence(self, sentence):
        # 根据一般国家的语言特性(我们这里研究的语言都是以空格分个单词)
        # 对句子进行分割,得到对应的词汇列表
        for word in sentence.split(' '):
            """
            构建 单词-索引的词汇表字典、索引-单词的词汇表字典:
                单词-索引的词汇表字典:key为单词,value为单词对应的索引值
                索引-单词的词汇表字典:key为单词对应的索引值,value为单词
           """
            # 然后调用addWord进行处理
            self.addWord(word)

    """添加词汇函数, 即将词汇转化为对应的数值, 输入参数word是一个单词"""
    def addWord(self, word):
        # 首先判断word是否已经在self.word2index字典的key中
        if word not in self.word2index:
            """
            构建 单词-索引的词汇表字典、索引-单词的词汇表字典
                单词-索引的词汇表字典:key为单词,value为单词对应的索引值
                索引-单词的词汇表字典:key为单词对应的索引值,value为单词
           """
            # 如果不在, 则将这个词加入其中, 并为它对应一个数值,即self.n_words
            self.word2index[word] = self.n_words
            # 同时也将它的反转形式加入到self.index2word中
            self.index2word[self.n_words] = word
            # self.n_words一旦被占用之后,逐次加1, 变成新的self.n_words
            self.n_words += 1

# #实例化参数
# name = "eng"
# #输入参数
# sentence = "hello I am Jay"
# #调用
# engl = Lang(name)
# engl.addSentence(sentence)
# print("word2index:", engl.word2index) #{'hello': 2, 'I': 3, 'am': 4, 'Jay': 5}
# print("index2word:", engl.index2word) #{0: 'SOS', 1: 'EOS', 2: 'hello', 3: 'I', 4: 'am', 5: 'Jay'}
# print("n_words:", engl.n_words) #6

#======================2.字符规范化==============================================#
# 将unicode转为Ascii, 我们可以认为是去掉一些语言中的重音标记:Ślusàrski
def unicodeToAscii(s):
    return ''.join(
        c for c in unicodedata.normalize('NFD', s)
        if unicodedata.category(c) != 'Mn'
    )

def normalizeString(s):
    """字符串规范化函数, 参数s代表传入的字符串"""
    # 使字符变为小写并去除两侧空白符, z再使用unicodeToAscii去掉重音标记
    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?"
# #调用
# nsr = normalizeString(s)
# print(nsr) #are you kidding me ?

#======================3.将持久化文件中的数据加载到内存, 并实例化类Lang=========#
data_path = 'data/eng-fra.txt'

def readLangs(lang1, lang2):
    """读取语言函数, 参数lang1是源语言的名字, 参数lang2是目标语言的名字
       返回对应的class Lang对象, 以及语言对列表"""
    # 从文件中读取语言对并以/n划分存到列表lines中
    lines = open(data_path, encoding='utf-8').read().strip().split('\n')
    # 对lines列表中的句子进行标准化处理,并以\t进行再次划分, 形成子列表, 也就是语言对
    pairs = [[normalizeString(s) for s in l.split('\t')] for l in lines]
    # 然后分别将语言名字传入Lang类中, 获得对应的语言对象, 返回结果
    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("input_lang:", input_lang)#input_lang: <__main__.Lang object at 0x0000026E82DAED48>
# print("output_lang:", output_lang)#output_lang: <__main__.Lang object at 0x0000026E8A427FC8>
# print("pairs中的前五个:", pairs[:5]) #pairs中的前五个: [['go .', 'va !'], ['run !', 'cours !'], ['run !', 'courez !'], ['wow !', 'ca alors !'], ['fire !', 'au feu !']]

#======================4.过滤出符合我们要求的语言对==============================================#
# 设置组成句子中单词或标点的最多个数
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):
    """语言对过滤函数, 参数p代表输入的语言对, 如['she is afraid.', 'elle malade.']"""
    # p[0]代表英语句子,对它进行划分,它的长度应小于最大长度MAX_LENGTH并且要以指定的前缀开头
    # p[1]代表法文句子, 对它进行划分,它的长度应小于最大长度MAX_LENGTH
    return len(p[0].split(' ')) < MAX_LENGTH and \
                p[0].startswith(eng_prefixes) and \
                len(p[1].split(' ')) < MAX_LENGTH

def filterPairs(pairs):
    """对多个语言对列表进行过滤, 参数pairs代表语言对组成的列表, 简称语言对列表"""
    # 函数中直接遍历列表中的每个语言对并调用filterPair即可
    return [pair for pair in pairs if filterPair(pair)]

# #输入参数:输入参数pairs使用readLangs函数的输出结果pairs
# #调用
# fpairs = filterPairs(pairs)
# print("过滤后的pairs前五个:", fpairs[:5]) #过滤后的pairs前五个: [['i m .', 'j ai ans .'], ['i m ok .', 'je vais bien .'], ['i m ok .', 'ca va .'], ['i m fat .', 'je suis gras .'], ['i m fat .', 'je suis gros .']]

#======================5.对以上数据准备函数进行整合, 并使用类Lang对语言对进行数值映射============#
def prepareData(lang1, lang2):
    """数据准备函数, 完成将所有字符串数据向数值型数据的映射以及过滤语言对
       参数lang1, lang2分别代表源语言和目标语言的名字"""
    # 首先通过readLangs函数获得input_lang, output_lang对象,以及字符串类型的语言对列表
    input_lang, output_lang, pairs = readLangs(lang1, lang2)
    # 对字符串类型的语言对列表进行过滤操作
    pairs = filterPairs(pairs)
    # 对过滤后的语言对列表进行遍历
    for pair in pairs:
        # 并使用input_lang和output_lang的addSentence方法对其进行数值映射
        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_n_words:", input_lang.n_words)# 2803
# print("output_n_words:", output_lang.n_words) # 4345
# print(random.choice(pairs)) #['he is the oldest of them all .', 'c est le plus vieux d entre eux .']

#======================6.将语言对转化为模型输入需要的张量==============================================#
def tensorFromSentence(lang, sentence):
    """将文本句子转换为张量, 参数lang代表传入的Lang的实例化对象, sentence是预转换的句子"""
    # 对句子进行分割并遍历每一个词汇, 然后使用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):
#     """将语言对转换为张量对, 参数pair为一个语言对"""
#     # 调用tensorFromSentence分别将源语言和目标语言分别处理,获得对应的张量表示
#     input_tensor = tensorFromSentence(input_lang, pair[0])
#     target_tensor = tensorFromSentence(output_lang, pair[1])
#     # 最后返回它们组成的元组
#     return (input_tensor, target_tensor)

import numpy as np

def tensorsFromPair(pair):
    """将语言对转换为张量对, 参数pair为一个语言对"""
    # 调用tensorFromSentence分别将源语言和目标语言分别处理,获得对应的张量表示
    # input_tensor = tensorFromSentence(input_lang, pair[0])
    # target_tensor = tensorFromSentence(output_lang, pair[1])

    """
    pairs
        pairs的shape为(10599, 2),pairs列表中包含10599个小列表,每个小列表中有两个字符串元素值。
        第一个是英语句子字符串,第二个是法语句子字符串

    DataLoader中设置的是batch_size=64,因此tensorsFromPair函数中传入的pair为(16, 2)
    """
    input_tensor = [entry[0] for entry in pair]  # 英语 输入句子
    target_tensor = [entry[1] for entry in pair]  # 法语 输出句子
    # print("input_tensor:",input_tensor) #64个 英文句子 组成的 一维数组
    # print("target_tensor:",target_tensor) #64个 法语句子 组成的 一维数组
    # print("len(input_tensor):",len(input_tensor)) #64
    # print("len(target_tensor):",len(target_tensor))#64

    input_tensor_list = []
    target_tensor_list = []

    # 遍历 每个 批量大小英语句子的列表 和 法语句子的列表
    for input, target in zip(input_tensor, target_tensor):
        input_tensor = tensorFromSentence(input_lang, input)
        target_tensor = tensorFromSentence(output_lang, target)
        # print("input:",input)
        # print("target:",target)
        # print("input_tensor:",input_tensor)
        # print("target_tensor:",target_tensor)
        input_tensor_list.append(input_tensor)
        target_tensor_list.append(target_tensor)

    # 最后返回它们组成的元组
    return (np.array(input_tensor_list), np.array(target_tensor_list))

# #输入参数:取pairs的第一条
# pair = pairs[0]
# #调用
# pair_tensor = tensorsFromPair(pair)
# print(pair_tensor)
# # (tensor([[2],[3],[4],[1]], device='cuda:0'),
# #  tensor([[2],[3],[4],[5],[1]], device='cuda:0'))

"""
第三步: 构建基于GRU的编码器和解码器
        1.构建基于GRU的编码器
        2.构建基于GRU的解码器
        3.构建基于GRU和Attention的解码器
"""
#=============================1.构建基于GRU的编码器=============================#
class EncoderRNN(nn.Module):
    def __init__(self, input_size, hidden_size):
        """
        它的初始化参数有两个
            input_size代表整个编码器的输入尺寸,即编码的源语言的词表大小,也是我们希望得到的指定尺寸
            hidden_size代表GRU的隐层节点数, 也代表词嵌入维度, 同时又是GRU的输入尺寸
        """
        super(EncoderRNN, self).__init__()
        # 将参数hidden_size传入类中
        self.hidden_size = hidden_size
        # 实例化nn中预定义的Embedding层, 它的参数分别是input_size, hidden_size
        # 这里的词嵌入维度即hidden_size
        # nn.Embedding的演示在该代码下方
        """
        初始化参数:
            input_size:整个编码器的输入尺寸,即编码器输出语言的词汇表大小。
                         可以是整个训练语料的不同单词的词汇总数,
                         也可以是“只包含至少出现N次的不同单词的”训练语料中至少出现N次的不同单词的词汇总数。
            hidden_size:是隐藏层中神经元数量, 又是embed_dim词嵌入维度。
        
        nn.Embedding(vocab_size 词汇总数, embed_dim 单词嵌入维度)
            vocab_size的多个版本:
                整个编码器的输入尺寸,编码器输入的词嵌入的词汇表大小。
                1.测试代码的时候输入的训练语料只有几个句子时,那么vocab_size为几个句子中可能出现的最多的不同单词的词汇总数,
                  可设置为比一个句子长度还长的大小。
                2.训练的语料为整个训练集时,那么vocab_size为整个数据集中所有不同单词的词汇总数。
                3.如果训练的整个语料数据集中只保留至少出现N次的词,那么vocab_size为整个数据集中至少出现N次的不同单词的词汇总数。
 
        nn.GRU(hidden_size, hidden_size) 多个写法:
            1.nn.GRU(输入数据的词嵌入维度, 隐藏层中神经元数量, 隐藏层层数)
            2.nn.GRU(输入数据的词嵌入维度, 隐藏层中神经元数量) #不写隐藏层层数,缺失值默认为1
        """
        self.embedding = nn.Embedding(input_size, hidden_size)
        # 然后实例化nn中预定义的GRU层, 它的参数是hidden_size
        # nn.GRU的演示在该代码下方
        self.gru = nn.GRU(hidden_size, hidden_size)

    def forward(self, input, hidden):
        """
        编码器前向逻辑函数中参数有两个
            input代表源语言的Embedding层输入张量
            hidden代表编码器层gru的初始隐层张量
        """
        """
        forward函数参数输入
            input:输入数据为句子中的一个单词,shape为torch.Size([1])
            hidden:(1, 1, 25),即 (隐藏层层数, 句子中的一个单词, 隐藏层中神经元数量)
            
        embedding(input).view(1, 1, -1)
            [1]的input经过embedding嵌入化后变成[1, embed_dim],再经过view(1, 1, -1)变成[1,1, embed_dim]
            
        output, hidden = gru(嵌入化的三维张量, hidden)
            hidden:初始化的hidden(1, 1, 25)作为gru节点输入,一个gru节点的隐藏状态输出作为下一个gru节点的隐藏状态输入
            output:(1, 1, 25) 即 (当前批次的样本个数, 当前样本的序列长度(单词个数), 隐藏层中神经元数量)
        """
        # 将输入张量进行embedding操作, 并使其形状变为(1,1,-1),-1代表自动计算维度
        # 理论上,我们的编码器每次只以一个词作为输入, 因此词汇映射后的尺寸应该是[1, embedding]
        # 而这里转换成三维的原因是因为torch中预定义gru必须使用三维张量作为输入, 因此我们拓展了一个维度
        output = self.embedding(input).view(1, 1, -1)
        # 然后将embedding层的输出和传入的初始hidden作为gru的输入传入其中,
        # 获得最终gru的输出output和对应的隐层张量hidden, 并返回结果
        output, hidden = self.gru(output, hidden)
        return output, hidden

    def initHidden(self):
        """初始化隐层张量函数"""
        # 将隐层张量初始化成为1x1xself.hidden_size大小的0张量
        return torch.zeros(1, 1, self.hidden_size, device=device)

# #输入参数:取pairs的第一条
# pair = pairs[0]
# #调用
# pair_tensor = tensorsFromPair(pair)
# # print(pair_tensor)
# # (tensor([[2],[3],[4],[1]], device='cuda:0'),
# #  tensor([[2],[3],[4],[5],[1]], device='cuda:0'))
#
# #实例化参数
# hidden_size = 25
# input_size = 20
# #输入参数
# # pair_tensor[0]代表源语言即英文的句子,pair_tensor[0][0]代表句子中的第一个词
# input = pair_tensor[0][0]
# # print("input.shape:",input.shape) #torch.Size([1])
# # 初始化第一个隐层张量,1 x 1 x hidden_size 的0维张量
# #隐藏状态hidden三维张量:(隐藏层层数, 句子中的一个单词, 隐藏层中神经元数量)
# hidden = torch.zeros(1, 1, hidden_size)
# #调用
# encoder = EncoderRNN(input_size, hidden_size).to(device)
# encoder_output, hidden = encoder(input.to(device), hidden.to(device))
# print(encoder_output.shape) #torch.Size([1, 1, 25])
# print(hidden.shape) #torch.Size([1, 1, 25])

#=============================2.构建基于GRU的解码器=============================#

class DecoderRNN(nn.Module):
    def __init__(self, hidden_size, output_size):
        """
        初始化函数有两个参数
            hidden_size代表解码器中GRU的输入尺寸,也是它的隐层节点数
            output_size代表整个解码器的输出尺寸, 也是我们希望得到的指定尺寸即目标语言的词表大小
        """
        """
        初始化参数:
            input_size:整个解码器的输入尺寸,即解码器输出语言的词汇表大小。
                         可以是整个训练语料的不同单词的词汇总数,
                         也可以是“只包含至少出现N次的不同单词的”训练语料中至少出现N次的不同单词的词汇总数。
            hidden_size:是隐藏层中神经元数量, 又是embed_dim词嵌入维度。

        nn.Embedding(vocab_size 词汇总数, embed_dim 单词嵌入维度)
            vocab_size的多个版本:
                整个编码器的输入尺寸,编码器输入的词嵌入的词汇表大小。
                1.测试代码的时候输入的训练语料只有几个句子时,那么vocab_size为几个句子中可能出现的最多的不同单词的词汇总数,
                  可设置为比一个句子长度还长的大小。
                2.训练的语料为整个训练集时,那么vocab_size为整个数据集中所有不同单词的词汇总数。
                3.如果训练的整个语料数据集中只保留至少出现N次的词,那么vocab_size为整个数据集中至少出现N次的不同单词的词汇总数。

        nn.GRU(hidden_size, hidden_size) 多个写法:
            1.nn.GRU(输入数据的词嵌入维度, 隐藏层中神经元数量, 隐藏层层数)
            2.nn.GRU(输入数据的词嵌入维度, 隐藏层中神经元数量) #不写隐藏层层数,缺失值默认为1
            
        Linear(hidden_size输入维度, output_size输出维度) --> nn.LogSoftmax(dim=1)
        先使用Linear把GRU的output输出的(1, 1, 25)中的hidden_size维度25 转换为 output_size维度10,最后用Softmax转换为维度10的概率值
        """
        super(DecoderRNN, self).__init__()
        # 将hidden_size传入到类中
        self.hidden_size = hidden_size
        # 实例化一个nn中的Embedding层对象, 它的参数output这里表示目标语言的词表大小
        # hidden_size表示目标语言的词嵌入维度
        self.embedding = nn.Embedding(output_size, hidden_size)
        # 实例化GRU对象,输入参数都是hidden_size,代表它的输入尺寸和隐层节点数相同
        self.gru = nn.GRU(hidden_size, hidden_size)
        # 实例化线性层, 对GRU的输出做线性变化, 获我们希望的输出尺寸output_size
        # 因此它的两个参数分别是hidden_size, output_size
        self.out = nn.Linear(hidden_size, output_size)
        # 最后使用softmax进行处理,以便于分类
        self.softmax = nn.LogSoftmax(dim=1)

    def forward(self, input, hidden):
        """
        解码器的前向逻辑函数中, 参数有两个
            input代表目标语言的Embedding层输入张量
            hidden代表解码器GRU的初始隐层张量
        """
        """
        forward函数的实现:embedding --> relu --> gru --> Linear --> softmax
        
        forward函数参数输入
            input:输入数据为句子中的一个单词,shape为torch.Size([1])
            hidden:(1, 1, 25),即 (隐藏层层数, 句子中的一个单词, 隐藏层中神经元数量)

        embedding(input).view(1, 1, -1)
            [1]的input经过embedding嵌入化后变成[1, embed_dim],再经过view(1, 1, -1)变成[1,1, embed_dim]

        output, hidden = gru(嵌入化的三维张量, hidden)
            hidden:初始化的hidden(1, 1, 25)作为gru节点输入,一个gru节点的隐藏状态输出作为下一个gru节点的隐藏状态输入
            output:(1, 1, 25) 即 (当前批次的样本个数, 当前样本的序列长度(单词个数), 隐藏层中神经元数量)
        """
        # 将输入张量进行embedding操作, 并使其形状变为(1,1,-1),-1代表自动计算维度
        # 原因和解码器相同,因为torch预定义的GRU层只接受三维张量作为输入
        output = self.embedding(input).view(1, 1, -1)
        # 然后使用relu函数对输出进行处理,根据relu函数的特性, 将使Embedding矩阵更稀疏,以防止过拟合
        output = F.relu(output)
        # 接下来, 将把embedding的输出以及初始化的hidden张量传入到解码器gru中
        output, hidden = self.gru(output, hidden)
        # 因为GRU输出的output也是三维张量,第一维没有意义,因此可以通过output[0]来降维
        # 再传给线性层做变换, 最后用softmax处理以便于分类
        output = self.softmax(self.out(output[0]))
        return output, hidden

    def initHidden(self):
        """初始化隐层张量函数"""
        # 将隐层张量初始化成为1x1xself.hidden_size大小的0张量
        return torch.zeros(1, 1, self.hidden_size, device=device)

# #输入参数:取pairs的第一条
# pair = pairs[0]
# #调用
# pair_tensor = tensorsFromPair(pair)
# # print(pair_tensor)
# # (tensor([[2],[3],[4],[1]], device='cuda:0'),
# #  tensor([[2],[3],[4],[5],[1]], device='cuda:0'))
#
# #实例化参数
# hidden_size = 25
# output_size = 10
# #输入参数
# # pair_tensor[1]代表目标语言即法文的句子,pair_tensor[1][0]代表句子中的第一个词
# input = pair_tensor[1][0]
# # 初始化第一个隐层张量,1 x 1 x hidden_size的0张量
# # 隐藏状态hidden三维张量:(隐藏层层数, 句子中的一个单词, 隐藏层中神经元数量)
# hidden = torch.zeros(1, 1, hidden_size)
# #调用
# decoder = DecoderRNN(hidden_size, output_size).to(device)
# output, hidden = decoder(input.to(device), hidden.to(device))
# print(hidden.shape) #torch.Size([1, 1, 25])
# print(output.shape) #torch.Size([1, 10])

#=============================3.构建基于GRU和Attention的解码器=============================#
class AttnDecoderRNN(nn.Module):
    def __init__(self, hidden_size, output_size, dropout_p=0.1, max_length=MAX_LENGTH):
        """初始化函数中的参数有4个
            hidden_size 代表解码器中GRU的输入尺寸,也是它的隐层节点数
            output_size 代表整个解码器的输出尺寸, 也是我们希望得到的指定尺寸即目标语言的词表大小
            dropout_p 代表我们使用dropout层时的置零比率,默认0.1
            max_length 代表句子的最大长度
        """
        """
        初始化参数:
            output_size:整个解码器的输出尺寸,即解码器输出语言的词汇表大小 4345。
                         可以是整个训练语料的不同单词的词汇总数 4345,
                         也可以是“只包含至少出现N次的不同单词的”训练语料中至少出现N次的不同单词的词汇总数。
            hidden_size:隐藏层中神经元数量 256, 又是embed_dim词嵌入维度 256。
            dropout_p:dropout层的置零比率,默认0.1
            max_length:句子的最大长度 10
            
        nn.Embedding(vocab_size 词汇总数, embed_dim 单词嵌入维度)
            vocab_size的多个版本:
                整个编码器的输入尺寸,编码器输入的词嵌入的词汇表大小。
                1.测试代码的时候输入的训练语料只有几个句子时,那么vocab_size为几个句子中可能出现的最多的不同单词的词汇总数,
                  可设置为比一个句子长度还长的大小。
                2.训练的语料为整个训练集时,那么vocab_size为整个数据集中所有不同单词的词汇总数。
                3.如果训练的整个语料数据集中只保留至少出现N次的词,那么vocab_size为整个数据集中至少出现N次的不同单词的词汇总数。

        nn.GRU(hidden_size, hidden_size) 多个写法:
            1.nn.GRU(输入数据的词嵌入维度, 隐藏层中神经元数量, 隐藏层层数)
            2.nn.GRU(输入数据的词嵌入维度, 隐藏层中神经元数量) #不写隐藏层层数,缺失值默认为1

        Linear(hidden_size输入维度, output_size输出维度) --> nn.LogSoftmax(dim=1)
        先使用Linear把GRU的output输出的(1, 1, 256)中的hidden_size维度256 转换为 output_size维度4345,最后用Softmax转换为维度4345的概率值
        """
        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.hidden_size * 2
        self.attn_combine = nn.Linear(self.hidden_size * 2, self.hidden_size)
        # 接着实例化一个nn.Dropout层,并传入self.dropout_p
        self.dropout = nn.Dropout(self.dropout_p)
        # 之后实例化nn.GRU, 它的输入和隐层尺寸都是self.hidden_size
        self.gru = nn.GRU(self.hidden_size, self.hidden_size)
        # 最后实例化gru后面的线性层,也就是我们的解码器输出层.
        self.out = nn.Linear(self.hidden_size, self.output_size)

    """
    forward函数参数输入
        input:输入数据为句子中的一个单词,shape为torch.Size([1])
        hidden:(1, 1, 256),即 (隐藏层层数, 句子中的一个单词, 隐藏层中神经元数量)
        encoder_outputs:(10, 256),即(句子最大长度MAX_LENGTH, 隐藏层中神经元数量)
        
    forward函数返回gru节点的output输出和hidden输出,还有第一步的前半部分所输出的注意力权重矩阵attn_weights
        
        
    注意力机制实现步骤简略版:
        Q:输入张量进行embedding嵌入化后的张量input
        K:隐藏状态输入hidden
        V:编码器的输出encoder_outputs
        第一步的前半部分:注意力权重矩阵attn_weights = softmax(Linear(cat((Q[0], K[0]), 1)), dim=1)
        第一步的后半部分:attn_applied = bmm(注意力权重矩阵attn_weights.unsqueeze(0), V)
        第二步:output = cat((Q[0], attn_applied[0]), 1)
        第三步:output = Linear(output).unsqueeze(0)
           
    1.embedding(input).view(1, 1, -1)
            [1]的input经过embedding嵌入化后变成[1, embed_dim],再经过view(1, 1, -1)变成[1,1, embed_dim] 即(1, 1, 256)
            
    2.embedded = dropout(embedded)
        dropout前后的形状都不变,embedded依旧为[1,1, embed_dim] 即(1, 1, 256)
        
    3.注意力机制实现 第一步的前半部分
        注意力权重矩阵attn_weights = softmax(Linear_attn(cat((Q[0], K[0]), 1)), dim=1)
            1.Linear_attn(hidden_size * 2, max_length) 输入维度:隐藏层中神经元数量*2。输出维度:句子的最大长度 10。
            2.Linear_attn(cat((embedded[0], hidden[0]), 1))
                embedded[0]:(1, 1, 256) 变成 (1, 256)
                hidden[0]:(1, 1, 256) 变成 (1, 256)
                cat((embedded[0], hidden[0]), 1):(1, 256)和(1, 256)在列维度拼接变成(1, 512)
                Linear_attn 输入(1, 512) 输出(1, 10)
            3.注意力权重矩阵attn_weights = F.softmax(Linear_attn的输出):softmax把(1, 10)转换为概率值的(1, 10)
    
    4.注意力机制实现 第一步的后半部分
        attn_applied = bmm(注意力权重矩阵attn_weights.unsqueeze(0), V)
                attn_applied = torch.bmm(attn_weights.unsqueeze(0), encoder_outputs.unsqueeze(0))
                    注意力权重矩阵attn_weights.unsqueeze(0):(1, 10) 变成 (1, 1, 10) 
                    encoder_outputs.unsqueeze(0):(10, 256)变成 (1, 10, 256) ,即(1, 句子最大长度MAX_LENGTH, 隐藏层中神经元数量)
                    attn_applied结果:bmm((1, 1, 10), (1, 10, 256)) 输出结果为 (1, 1, 256)
                
    5.注意力机制实现 第二步
        output = cat((Q[0], attn_applied[0]), 1)
            output = torch.cat((embedded[0], attn_applied[0]), 1)
                embedded[0]:(1, 1, 256) 变成 (1, 256)
                attn_applied[0]:(1, 1, 256) 变成 (1, 256)
                cat((embedded[0], attn_applied[0]), 1):(1, 256)和(1, 256)在列维度拼接变成(1, 512)的output

    6.注意力机制实现 第三步
        output = Linear(output).unsqueeze(0)
            Linear_attn_combine(hidden_size * 2, hidden_size) 输入维度:隐藏层中神经元数量*2。输出维度:隐藏层中神经元数量。
            output = Linear_attn_combine(output).unsqueeze(0)
                线性层的输入张量output:(1, 512) 即 (1, 隐藏层中神经元数量*2)
                Linear_attn_combine: 把(1, 512) 转换为 (1, 256)
                unsqueeze(0):把(1, 256) 变成 (1, 1, 256)
                线性层的输出张量output:(1, 1, 256)
                
    7.output = relu(output)
        dropout前后的形状都不变,output依旧为(1, 1, 256)
        
    8.output, hidden = gru(output, hidden)
        gru节点输入的output实际为经过注意力机制Attention计算之后的输出张量值,shape为(1, 1, 256)            
        1.gru = nn.GRU(hidden_size, hidden_size)
            nn.GRU(输入数据的词嵌入维度, 隐藏层中神经元数量)  #不写隐藏层层数,缺失值默认为1
            hidden_size为256:既为词嵌入维度,又为隐藏层神经元数量
        2.output, hidden = gru(output, hidden)
            gru节点输入的output:
                经过注意力机制Attention计算之后的输出张量值,shape为(1, 1, 256) 即 (当前批次的样本个数, 当前样本的序列长度(单词个数), 隐藏层中神经元数量)
            hidden:一开始使用初始化的hidden(1, 1, 256)作为第一个gru节点输入,一个gru节点的隐藏状态输出作为下一个gru节点的隐藏状态输入
            gru节点输出的output:shape为(1, 1, 256) (当前批次的样本个数, 当前样本的序列长度(单词个数), 隐藏层中神经元数量)	
                
    9.output = F.log_softmax(Linear_out(output[0]), dim=1)
        1.Linear_out = Linear(hidden_size, output_size) 
            输入维度:隐藏层中神经元数量, 又是embed_dim词嵌入维度。输出维度:output_size,即解码器输出语言的词汇表大小
            最后实例化gru后面的线性层,也就是我们的解码器输出层
        2.output = Linear_out(output[0])
            输入output的shape为(1, 1, 隐藏层中神经元数量256),output[0]为(1, 隐藏层中神经元数量256)
            输出output的shape为(1, 解码器输出语言的词汇表大小 4345)
        3.log_softmax((1, 4345), dim=1)
            softmax前后的形状都不变,把列值上的值转换为概率值
    """
    def forward(self, input, hidden, encoder_outputs):
        """
        forward函数的输入参数有三个, 分别是源数据输入张量input, 初始的隐层张量hidden, 以及编码器的输出张量encoder_outputs
        """
        # 根据结构计算图, 输入张量进行Embedding层并扩展维度
        embedded = self.embedding(input).view(1, 1, -1)
        # print("embedded.shape:",embedded.shape) #torch.Size([1, 1, 256])

        # 使用dropout进行随机丢弃,防止过拟合
        embedded = self.dropout(embedded)
        # print("embedded.shape:",embedded.shape) ##torch.Size([1, 1, 256])

        # 进行attention的权重计算, 哦我们呢使用第一种计算方式:
        # 将Q,K进行纵轴拼接, 做一次线性变化, 最后使用softmax处理获得结果
        attn_weights = F.softmax(self.attn(torch.cat((embedded[0], hidden[0]), 1)), dim=1)
        # print("attn_weights.shape:",attn_weights.shape) #torch.Size([1, 10])

        # 然后进行第一步的后半部分, 将得到的权重矩阵与V做矩阵乘法计算, 当二者都是三维张量且第一维代表为batch条数时, 则做bmm运算
        attn_applied = torch.bmm(attn_weights.unsqueeze(0),encoder_outputs.unsqueeze(0))
        # print("attn_applied.shape:",attn_applied.shape) #torch.Size([1, 1, 256])

        # 之后进行第二步, 通过取[0]是用来降维, 根据第一步采用的计算方法, 需要将Q与第一步的计算结果再进行拼接
        output = torch.cat((embedded[0], attn_applied[0]), 1)
        # print("output.shape:",output.shape) #torch.Size([1, 512])

        # 最后是第三步, 使用线性层作用在第三步的结果上做一个线性变换并扩展维度,得到输出
        output = self.attn_combine(output).unsqueeze(0)
        # print("output.shape:",output.shape) #torch.Size([1, 1, 256])

        # attention结构的结果使用relu激活
        output = F.relu(output)
        # print("output.shape:",output.shape)#torch.Size([1, 1, 256])

        # 将激活后的结果作为gru的输入和hidden一起传入其中
        output, hidden = self.gru(output, hidden)
        # print("output.shape:",output.shape) #torch.Size([1, 1, 256])
        # print("hidden.shape:",hidden.shape)#torch.Size([1, 1, 256])

        # 最后将结果降维并使用softmax处理得到最终的结果
        output = F.log_softmax(self.out(output[0]), dim=1)
        # print("output.shape:",output.shape) #torch.Size([1, 4345])

        # 返回解码器结果,最后的隐层张量以及注意力权重张量
        return output, hidden, attn_weights

    def initHidden(self):
        """初始化隐层张量函数"""
        # 将隐层张量初始化成为1x1xself.hidden_size大小的0张量
        return torch.zeros(1, 1, self.hidden_size, device=device)

# #输入参数:取pairs的第一条
# pair = pairs[0]
# #调用
# pair_tensor = tensorsFromPair(pair)
# # print(pair_tensor)
# # (tensor([[2],[3],[4],[1]], device='cuda:0'),
# #  tensor([[2],[3],[4],[5],[1]], device='cuda:0'))
#
# #实例化参数
# # hidden_size = 25
# # output_size = 10
# hidden_size = 256
# output_size = output_lang.n_words
# #输入参数
# # pair_tensor[1]代表目标语言即法文的句子,pair_tensor[1][0]代表句子中的第一个词
# input = pair_tensor[1][0]
# hidden = torch.zeros(1, 1, hidden_size)
# print("input:",input) #input: tensor([2], device='cuda:0')
"""
编码器输出状态张量:(句子最大长度MAX_LENGTH, 隐藏层中神经元数量)
    1.encoder中每一个时间步的输出outout堆叠而成的, 每一个时间步的输出outout为(1, 256) 即(句子中的一个单词, 隐藏层中神经元数量),
      而案例中设置了一个句子的最大长度为MAX_LENGTH=10,其中清洗好的句子只包含单词字母和标点符号。
    2.一个句子的最大长度为MAX_LENGTH=10,而且每一个时间步的输出outout为(1, 256),因此encoder_outputs编码器输出为(10, 256),
      即 (句子最大长度MAX_LENGTH, 隐藏层中神经元数量),因此一开始先初始化一个(句子最大长度MAX_LENGTH, 隐藏层中神经元数量)的0值的二维张量,
      encoder_outputs = torch.zeros(max_length, encoder.hidden_size, device=device)
"""
# # encoder_outputs需要是encoder中每一个时间步的输出堆叠而成
# # 它的形状应该是10x25, 我们这里直接随机初始化一个张量
# # encoder_outputs = torch.randn(10, 25)
# encoder_outputs = torch.randn(10, hidden_size)
#
# #调用
# decoder = AttnDecoderRNN(hidden_size, output_size).to(device)
# output, hidden, attn_weights= decoder(input.to(device), hidden.to(device), encoder_outputs.to(device))
# print(output.shape) #torch.Size([1, 4345])
# print(hidden.shape) #torch.Size([1, 1, 256])
# print(attn_weights.shape) #torch.Size([1, 10])


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

#=========================== 2.构建训练函数 ===========================#

def train(batch_input_tensor, batch_target_tensor, encoder, decoder, encoder_optimizer, decoder_optimizer, criterion, max_length=MAX_LENGTH):
    """训练函数, 输入参数有8个, 分别代表input_tensor:
            源语言输入张量,target_tensor:目标语言输入张量,encoder, decoder:编码器和解码器实例化对象
            encoder_optimizer, decoder_optimizer:编码器和解码器优化方法,criterion:损失函数计算方法,max_length:句子的最大长度
    """
    """
    input_tensor:shape为[英语句子单词个数, 1]的[[单词对应的索引值]],每个值为英语句子中的每个单词对应在词汇表的索引值
    target_tensor:shape为[法语句子单词个数, 1]的[[单词对应的索引值]],每个值为法语句子中的每个单词对应在词汇表的索引值
    encoder_hidden:(1, 1, hidden_size) 即(1, 1, 256)
    input_length:英语(输入)句子单词个数,即单词的索引值的数量
    target_length:法语(输出)句子单词个数,即单词的索引值的数量
    encoder_outputs:
            初始化编码器的output输出状态,为每一个时间步在最后一层隐藏层的output输出所封装的二维矩阵,
            shape为(句子最大长度max_length, hidden_size) 即(10, 256)
    """

    # # 编码器和解码器优化器梯度归0
    encoder_optimizer.zero_grad()
    decoder_optimizer.zero_grad()

    """
    for循环遍历出输入的英语句子中每个单词对应在词汇表中的索引值
        1.encoder_output, encoder_hidden = encoder(input_tensor[ei], encoder_hidden)
            先调用EncoderRNN类中的forward函数,先逐个把英语单词对应在词汇表的索引值传入到embedding层中输出嵌入化后的向量,
            shape为(1, embed_dim),再经过view(1, 1, -1)变成(1,1, embed_dim)。
            然后把嵌入化后的三维张量和hidden状态值输入到GRU中输出output和hidden,两者shape均为(1, 1, hidden_size)。
            encoder_hidden:每一层隐藏层的最后一个时间步(最后一个节点)在编码器中隐藏状态值
            encoder_output:每一个时间步(每个单词)在编码器中最后一层隐藏层的output输出值
        2.encoder_outputs[ei] = encoder_output[0, 0]
            encoder_outputs:
                封装每一个时间步(每个单词)在编码器中最后一层隐藏层的output输出值,
                作为整个编码器的output输出值用于输入到解码器中的每个时间步中。
                encoder_output[0, 0]表示把(1, 1, hidden_size)取出为[hidden_size]维度的向量作为每个单词(每个时间步)在编码器中的最终输出值,
                然后存到encoder_outputs用于输入到解码器中的每个时间步中。
    """

    batch_target_length = 0 # 批量样本的所有每个句子的总时间步
    loss = 0 #批量样本的所有句子中的每个时间步的loss总和

    """
    遍历批量样本数据中的每个句子
    """
    for batch_index in range(BATCH_SIZE):
        input_tensor = batch_input_tensor[batch_index]
        target_tensor = batch_target_tensor[batch_index]

        # 根据源文本和目标文本张量获得对应的长度
        input_length = input_tensor.size(0)
        target_length = target_tensor.size(0)
        # 每个句子的总时间步加到一起,即求批量样本的所有每个句子的总时间步
        # batch_target_length:最终值即为批量样本所有句子的单词(时间步)的总数
        batch_target_length += target_length

        """
        遍历每个句子时都重新初始化一次 encoder_hidden、encoder_outputs
        """
        # 初始化隐层张量
        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]
            # print("encoder_output.shape:",encoder_output.shape)#torch.Size([1, 1, 256]) 即(1, 1, hidden_size)
            # print("encoder_hidden.shape:",encoder_hidden.shape)#torch.Size([1, 1, 256]) 即(1, 1, hidden_size)

        """
        1.decoder_input = torch.tensor([[SOS_token]], device=device)
            先创建句子起始标志为0值的[[0]]张量,shape为[1, 1],并作为解码器中第一个时间步的输入
        2.decoder_hidden = encoder_hidden 两者shape为[1, 1, 256]
            encoder_hidden:每一层隐藏层的最后一个时间步(最后一个节点)在编码器中隐藏状态值
            decoder_hidden:把编码器中每一层隐藏层的最后一个时间步(最后一个节点)所封装起来的隐藏状态值 作为 解码器第一个时间步(第一个节点)的初始化隐藏状态值
        3.use_teacher_forcing = True if random.random() < teacher_forcing_ratio else False
            teacher_forcing_ratio=0.5:表示random.random()随机出来的数据有一半机会整个判断表达式返回True或False
            当一半机会判断True则执行teacher_forcing机制,当一半机会判断False则不执行teacher_forcing机制。
        """
        # 初始化解码器的第一个输入,即起始符
        decoder_input = torch.tensor([[SOS_token]], device=device)
        # print("decoder_input.shape:",decoder_input.shape) #torch.Size([1, 1])

        # 初始化解码器的隐层张量即编码器的隐层输出
        decoder_hidden = encoder_hidden
        # print("decoder_hidden.shape:",decoder_hidden.shape) #[1, 1, 256]

        # 根据随机数与teacher_forcing_ratio对比判断是否使用teacher_forcing
        use_teacher_forcing = True if random.random() < teacher_forcing_ratio else False

        """
        使用teacher_forcing机制
            模型预测中,我们需要将解码器在上⼀个时间步的输出作为当前时间步的输⼊。
            与此不同,在训练中我们也可以将标签序列(训练集的真实输出序列)在上⼀个时间步的标签作为解码器在当前时间步的输⼊。
            这叫作强制教学(teacher forcing)。解码器在某时间步的输⼊为样本输出序列在上⼀时间步的词,即强制教学。
            在编码器—解码器的训练中,可以采⽤强制教学。
     
            for index in range(法语(输出)句子单词个数,即单词的索引值的数量):
                # decoder_input:
                #   第一个时间步传入的是shape为[1, 1]的起始符[[SOS_token]],
                #   后面每个时间步传入的是shape为[英语句子单词个数, 1]所遍历的每个单词对应的索引值,即shape为[1, 1]的[[单词对应的索引值]]。
                # decoder_hidden:[1, 1, 256] 
                #   第一个时间步传入的是decoder_hidden=encoder_hidden,即第一个时间步使用的是编码器输出的隐藏状态值。
                #   后面每个时间步传入的是解码器前一时间步输出隐藏状态值。
                # encoder_outputs:[10, 256] 即[max_length, 隐藏神经元数量]
                #   解码器的每个时间步都传入编码器的output输出,该encoder_outputs封装了编码器每一个时间步在最后一层隐藏层的output输出值。
                decoder_output, decoder_hidden, decoder_attention = decoder(decoder_input, decoder_hidden, encoder_outputs)
                
                #target_tensor:shape为[法语(输出)句子单词个数, 1]的[[单词对应的索引值]],每个值为法语(输出)句子中的每个单词对应在词汇表的索引值。
                #target_tensor[index]:从shape为[[单词对应的索引值]]的法语(输出)句子中获取每个单词的索引值即[单词对应的索引值]
                #decoder_output:解码器输出output为经过了softmax概率值转换操作后的shape为[1, 4345]的概率值矩阵,4345为输出语料的词汇表大小
                #loss:使用shape为[1, 4345]的概率值矩阵 和 [单词对应的索引值] 计算loss
                loss += criterion(decoder_output, target_tensor[index])
                
                #target_tensor[index]:从shape为[[单词对应的索引值]]的法语(输出)句子中获取每个单词的索引值即[单词对应的索引值]
                #把下一个单词对应的索引值(真实标签值)作为解码器的下一个时间步的输入
                decoder_input = target_tensor[index]
        """

        """
        遍历输出句子中的每个单词作为一个时间步输入到解码器
        """
        # 如果使用teacher_forcing
        if use_teacher_forcing:
            """
            如果是teacher_forcing,那么将会使用真实标签语句中的值作为解码器时间步的输入。
            第一个时间步输入的是起始符SOS_token,然后后面每个时间步的输入都是真实标签语句中的单词索引值。
            真实标签语句中的最后一个单词索引值为1(结束标志EOS_token),那么当前的循环是没有把1(结束标志EOS_token)再输入到解码器中训练的,
            因为当前range(目标语句长度)中的最后一次循环中target_tensor[di]仅仅是把1(结束标志EOS_token)取出来了,
            然后循环就结束了,并没有机会再次输入到解码器中继续训练。
            """
            # 循环遍历目标张量索引
            for di in range(target_length):
                # 将decoder_input, decoder_hidden, encoder_outputs即attention中的QKV,
                # 传入解码器对象, 获得decoder_output, decoder_hidden, decoder_attention
                decoder_output, decoder_hidden, decoder_attention = decoder(
                    decoder_input, decoder_hidden, encoder_outputs)
                # print("encoder_outputs.shape:", encoder_outputs.shape) #[10, 256]
                # print("decoder_output.shape:", decoder_output.shape) #[1, 4345]
                # print("decoder_hidden.shape:", decoder_hidden.shape) #[1, 1, 256]
                # print("decoder_attention.shape:", decoder_attention.shape) #[1, 10]
                # print("target_tensor[di].shape:", target_tensor[di].shape) #[1]

                # print("target_tensor[di]:", target_tensor[di]) #一维张量 [一个单词对应的索引值]
                # print("decoder_output:", decoder_output.argmax(-1)) #二维张量 取[1, 4345]中的最后一个维度/第二个维度的最大概率值的索引位置

                # 因为使用了teacher_forcing, 无论解码器输出的decoder_output是什么, 我们都只
                # 使用‘正确的答案’,即target_tensor[di]来计算损失
                loss += criterion(decoder_output, target_tensor[di])
                # 并强制将下一次的解码器输入设置为‘正确的答案’
                decoder_input = target_tensor[di]

        else:
            # 如果不使用teacher_forcing
            # 仍然遍历目标张量索引
            for di in range(target_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)
                """
                #decoder_output:解码器输出output为经过了softmax概率值转换操作后的shape为[1, 4345]的概率值矩阵,4345为输出语料的词汇表大小
                #top_n,top_i = topk(K):从维度4345的概率值的向量值中获取最大概率类别值和对应的索引值,该索引值即代表某单词在词汇表中的索引值。
                #top_n:最大概率值。top_i:最大概率值的索引值。
                topv, topi = decoder_output.topk(1)
                """
                # 只不过这里我们将从decoder_output取出答案
                topv, topi = decoder_output.topk(1)
                # 损失计算仍然使用decoder_output和target_tensor[di]
                loss += criterion(decoder_output, target_tensor[di])
                # 最后如果输出值是终止符,则循环停止
                if topi.squeeze().item() == EOS_token:
                    break
                """
                使用teacher_forcing机制
                    #topi:从维度4345的概率值的向量值中获取最大概率值的索引值,该索引值即代表某单词在词汇表中的索引值。
                    #decoder_input:把下一个单词对应的索引值(真实标签值)作为解码器的下一个时间步的输入
                    #detach:具有分离作用使得这个decoder_input与模型构建的张量图无关,相当于全新的外界输入
                    decoder_input = topi.squeeze().detach()
                """
                # 否则,并对topi降维并分离赋值给decoder_input以便进行下次运算
                # 这里的detach的分离作用使得这个decoder_input与模型构建的张量图无关,相当于全新的外界输入
                decoder_input = topi.squeeze().detach()

    """
    使用批量样本数据所有输出句子中的所有每个单词(时间步)对应的解码器训练输出的loss损失值的总和的值来求反导梯度值,用于更新参数
    """
    # 误差进行反向传播
    loss.backward()
    # 编码器和解码器进行优化即参数更新
    encoder_optimizer.step()
    decoder_optimizer.step()

    """
    batch_target_length:最终值即为批量样本所有句子的单词(时间步)的总数
    loss/batch_target_length:求出每个单词(时间步)的平均loss损失
    """
    # 计算每个时间步(每个单词)的平均损失
    return loss.item() / batch_target_length

#=========================== 3.调用训练函数并打印日志和制图 ===========================#

# 导入plt以便绘制损失曲线
import matplotlib.pyplot as plt
# 导入torch中的数据加载器方法
from torch.utils.data import DataLoader

def trainIters(encoder, decoder, n_iters, print_every=1000, plot_every=100 ):
    """
    训练迭代函数, 输入参数有6个,
        分别是 encoder, decoder: 编码器和解码器对象;n_iters: 总迭代步数;
        print_every:打印日志间隔; plot_every:绘制损失曲线间隔; learning_rate:学习率
    """

    """ 
    1.可先用 SGD/Adam(lr=0.01) batch size为32/64 等这样的参数优化 loss到1.0左右,然后保存模型到本地文件
    2.然后加载上一次训练的文件,换用更小的优化率SGD/Adam(lr=0.001)来进行下一步更新
    optimizer优化器对象 = optim.Adam(继承了“nn.Module”的自定义类对象.parameters(), lr=learning_rate) """
    # 使用预定义的SGD作为优化器,将参数和学习率传入其中
    encoder_optimizer = optim.Adam(encoder.parameters(), lr=0.001)
    decoder_optimizer = optim.Adam(decoder.parameters(), lr=0.001)

    """
    nn.LogSoftmax + nn.NLLLoss = 交叉熵Cross Entropy
    F.log_softmax + F.nll_loss = 交叉熵Cross Entropy
    torch.nn.CrossEntropyLoss()(内置softmax,无需显式写softmax层)= 交叉熵Cross Entropy
    """
    # 选择损失函数
    criterion = nn.NLLLoss()

    # 获得训练开始时间戳
    start = time.time()
    # 每个损失间隔的平均损失保存列表,用于绘制损失曲线
    plot_losses = []

    encoder.load_state_dict(torch.load("./model_save/encoder2_0.4831.pkl"))
    decoder.load_state_dict(torch.load("./model_save/decoder2_0.4831.pkl"))

    # 根据设置迭代步进行循环
    for iter in range(1, n_iters + 1):
        times = 0
        # 每个打印日志间隔的总损失,初始为0
        print_loss_total = 0

        # 每次从语言对列表中随机取出一条作为训练语句
        # 返回的是元祖,元祖中第一个元素为英语句子中的每个单词对应在词汇表的索引值所封装的二维数组,
        # 元祖中第二个元素为法语句子中的每个单词对应在词汇表的索引值所封装的二维数组
            # training_pair = tensorsFromPair(random.choice(pairs))
        """
        pairs
            pairs的shape为(10599, 2),pairs列表中包含10599个小列表,每个小列表中有两个字符串元素值。
            第一个是英语句子字符串,第二个是法语句子字符串
        training_pair:
            处理后完的批量数据的维度为[batch_size, 句子单词个数],其中句子单词个数中每个数值实际为单词对应的索引值
        """
        # drop_last=True:丢弃不满足批量大小的批量数据,一般会是最后一个批量数据中的样本数可能不满足于批量大小,因此需要丢弃以防止报错
        training_pair = DataLoader(pairs, batch_size=BATCH_SIZE, shuffle=True, collate_fn=tensorsFromPair, drop_last=True)
        # print("len(training_pair):",len(training_pair)) #165 表示一共有 165 个 批量大小为64的 批量数据

        #遍历每个批量数据
        for i, (english, french) in enumerate(training_pair):
            loss = train(english, french, encoder, decoder, encoder_optimizer, decoder_optimizer, criterion)

            # 将损失进行累和
            print_loss_total += loss
            # print(input_tensor)
            times += 1  # 记录迭代次数

            # 将平均损失装进plot_losses列表
            plot_losses.append(loss)
            # 打印日志,日志内容分别是:训练耗时,当前迭代步,当前进度百分比,当前平均损失
            print('耗时:%s epoch次数:%d 第%d个批次 loss:%.4f 平均loss:%.4f' % (timeSince(start), iter, i, loss, print_loss_total / times))

        # 打印日志,日志内容分别是:训练耗时,当前迭代步,当前进度百分比,当前平均损失
        print('耗时:%s epoch次数:%d 总批量个数%d 平均loss:%.4f' % (timeSince(start), iter, i, print_loss_total / times))

    # 绘制损失曲线
    plt.figure()
    plt.plot(plot_losses)
    # 保存到指定路径
    plt.savefig("./"+str(iter)+"_loss.png")

    torch.save(encoder1.state_dict(), './model_save/encoder1.pt')
    torch.save(attn_decoder1.state_dict(), './model_save/attn_decoder1.pt')


#输入参数:
# 设置隐层大小为256 ,也是词嵌入维度
hidden_size = 256
# 通过input_lang.n_words获取输入词汇总数 2803,与hidden_size 256一同传入EncoderRNN类中
# 得到编码器对象encoder1
encoder1 = EncoderRNN(input_lang.n_words, hidden_size).to(device)
# 通过output_lang.n_words获取目标词汇总数 4345,与hidden_size 256 和dropout_p一同传入AttnDecoderRNN类中
# 得到解码器对象attn_decoder1
attn_decoder1 = AttnDecoderRNN(hidden_size, output_lang.n_words, dropout_p=0.1).to(device)
# 设置迭代步数
n_iters = 10
BATCH_SIZE = 64
# 设置日志打印间隔
print_every = 5000

# 调用trainIters进行模型训练,将编码器对象encoder1,码器对象attn_decoder1,迭代步数,日志打印间隔传入其中
trainIters(encoder1, attn_decoder1, n_iters, print_every=print_every)


"""
第五步: 构建模型评估函数, 并进行测试以及Attention效果分析.
    1.构建模型评估函数
    2.随机选择指定数量的数据进行评估
"""
#============================ 1.构建模型评估函数 =============================================#
def evaluate(encoder, decoder, sentence, max_length=MAX_LENGTH):
    """评估函数,输入参数有4个,分别是encoder, decoder: 编码器和解码器对象,
       sentence:需要评估的句子,max_length:句子的最大长度"""

    # 评估阶段不进行梯度计算
    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 = []
        """
        decoder_attentions = torch.zeros(max_length, max_length)
            因为一个注意力权重矩阵attn_weights的shape为(1, max_length),即(1, 10)。
            而由于for循环要遍历max_length次,即最多遍历10次,如果中途预测为'<EOS>'则提早停止循环。
            注意力权重矩阵attn_weights = F.softmax(Linear_attn的输出):softmax把(1, 10)转换为概率值的(1, 10)
            因此最终初始化的decoder_attentions的shape要为(max_length, max_length),即(10, 10)。
        
        decoder_attentions[di] = decoder_attention.data
            每个时间步输出的decoder_attention的shape为(1, max_length),那么遍历到最多max_length次的话,
            那么[max_length, max_length]的decoder_attentions整个都会被赋值。
        """
        # 初始化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

            """
            #decoder_output:解码器输出output为经过了softmax概率值转换操作后的shape为[1, 4345]的概率值矩阵,4345为输出语料的词汇表大小
            #topv, top_i = topk(K):从维度4345的概率值的向量值中获取最大概率类别值和对应的索引值,该索引值即代表某单词在词汇表中的索引值。
            #topv:最大概率值。top_i:最大概率值的索引值。
            topv, topi = decoder_output.data.topk(1)
            """
            # 从解码器输出中获得概率最高的值及其索引对象
            topv, topi = decoder_output.data.topk(1)
            """ topi最大概率值的索引值 ==  EOS_token即1的话,那么停止遍历,并在输出语句decoded_words中添加结束标志符<EOS> """
            # 从索引对象中取出它的值与结束标志值作对比
            if topi.item() == EOS_token:
                # 如果是结束标志值,则将结束标志装进decoded_words列表,代表翻译结束
                decoded_words.append('<EOS>')
                # 循环退出
                break

            else:
                """
                 decoded_words.append(output_lang.index2word[topi.item()])
                          从index2word字段中根据topi最大概率值的索引值取出对应的单词并添加在输出语句decoded_words中
                """
                # 否则,根据索引找到它在输出语言的index2word字典中对应的单词装进decoded_words
                decoded_words.append(output_lang.index2word[topi.item()])
            """
                #topi:从维度4345的概率值的向量值中获取最大概率值的索引值,该索引值即代表某单词在词汇表中的索引值。
                #decoder_input:把下一个单词对应的索引值(真实标签值)作为解码器的下一个时间步的输入
                #detach:具有分离作用使得这个decoder_input与模型构建的张量图无关,相当于全新的外界输入
                decoder_input = topi.squeeze().detach()
            """
            # 最后将本次预测的索引降维并分离赋值给decoder_input,以便下次进行预测
            decoder_input = topi.squeeze().detach()
        """ decoder_attentions[:di + 1]:因为左闭右开的关系,所以需要+1 """
        # 返回结果decoded_words, 以及完整注意力张量, 把没有用到的部分切掉
        return decoded_words, decoder_attentions[:di + 1]

#============================ 2.随机选择指定数量的数据进行评估 =============================================#

def evaluateRandomly(encoder, decoder, n=6):
    """随机测试函数, 输入参数encoder, decoder代表编码器和解码器对象,n代表测试数"""
    # 对测试数进行循环
    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('')

# 调用evaluateRandomly进行模型测试,将编码器对象encoder1,码器对象attn_decoder1传入其中
evaluateRandomly(encoder1, attn_decoder1)

#绘制 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")
"""
Attention图像的纵坐标代表输入的源语言各个词汇对应的索引, 0-6分别对应["we", "re", "both", "teachers", ".", ""], 
纵坐标代表生成的目标语言各个词汇对应的索引, 0-7代表['nous', 'sommes', 'toutes', 'deux', 'enseignantes', '.', ''], 
图中浅色小方块(颜色越浅说明影响越大)代表词汇之间的影响关系, 比如源语言的第1个词汇对生成目标语言的第1个词汇影响最大, 
源语言的第4,5个词对生成目标语言的第5个词会影响最大, 通过这样的可视化图像, 
我们可以知道Attention的效果好坏, 与我们人为去判定到底还有多大的差距. 进而衡量我们训练模型的可用性.
"""

from io import open
import glob
import os
import string
import unicodedata
import random
import time
import math
import torch
import torch.nn as nn       
import matplotlib.pyplot as plt

all_letters = string.ascii_letters + " .,;'"
n_letters = len(all_letters)
# print("n_letters:", n_letters)

# 函数的作用是去掉一些语言中的重音标记
def unicodeToAscii(s):
    return ''.join(c for c in unicodedata.normalize('NFD', s)
                   if unicodedata.category(c) != 'Mn' 
                   and c in all_letters)

s = "Ślusàrski"
a = unicodeToAscii(s)
# print(a)

data_path = "./data/names/"

def readLines(filename):
    # 打开指定的文件并读取所有的内容, 使用strip()去除掉两侧的空白符, 然后以'\n'为换行符进行切分
    lines = open(filename, encoding='utf-8').read().strip().split('\n')
    return [unicodeToAscii(line) for line in lines]

filename = data_path + "Chinese.txt"
result = readLines(filename)
# print(result[:20])


# 构建一个人名类别与具体人名对应关系的字典
category_lines = {}

# 构建所有类别的列表
all_categories = []

# 遍历所有的文件, 使用glob.glob中可以利用正则表达式的便利
for filename in glob.glob(data_path + "*.txt"):
    # 获取每个文件的文件名, 起始就是得到名字的类别
    category = os.path.splitext(os.path.basename(filename))[0]
    # 逐一将其装入所有类别的列表中
    all_categories.append(category)
    # 然后读取每个文件的内容, 形成名字的列表
    lines = readLines(filename)
    # 按照对应的类别, 将名字列表写入到category_lines字典中
    category_lines[category] = lines

n_categories = len(all_categories)
# print("n_categories:", n_categories)

# print(category_lines['Italian'][:10])

def lineToTensor(line):
    # 首先初始化一个全0的张量, 这个张量的形状是(len(line), 1, n_letters)
    # 代表人名中的每一个字母都用一个(1 * n_letters)张量来表示
    tensor = torch.zeros(len(line), 1, n_letters)
    # 遍历每个人名中的每个字符, 并搜索其对应的索引, 将该索引位置置1
    for li, letter in enumerate(line):
        tensor[li][0][all_letters.find(letter)] = 1
    
    return tensor

line = "Bai"
line_tensor = lineToTensor(line)
# print("line_tensor:", line_tensor)


# x = torch.tensor([1, 2, 3, 4])
# print(x.shape)
# y = torch.unsqueeze(x, 0)
# print(y.shape)
# z = torch.unsqueeze(x, 1)
# print(z)
# print(z.shape)

class RNN(nn.Module):
    def __init__(self, input_size, hidden_size, output_size, num_layers=1):
        # input_size: 代表RNN输入的最后一个维度
        # hidden_size: 代表RNN隐藏层的最后一个维度
        # output_size: 代表RNN网络最后线性层的输出维度
        # num_layers: 代表RNN网络的层数
        super(RNN, self).__init__()
        self.input_size = input_size
        self.hidden_size = hidden_size
        self.output_size = output_size
        self.num_layers = num_layers
        
        # 实例化预定义的RNN,三个参数分别是input_size, hidden_size, num_layers
        self.rnn = nn.RNN(input_size, hidden_size, num_layers)
        # 实例化全连接线性层, 作用是将RNN的输出维度转换成指定的输出维度
        self.linear = nn.Linear(hidden_size, output_size)
        # 实例化nn中预定义的Softmax层, 用于从输出层中获得类别的结果
        self.softmax = nn.LogSoftmax(dim=-1)
        
    def forward(self, input1, hidden):
        # input1: 代表人名分类器中的输入张量, 形状是1 * n_letters
        # hidden: 代表RNN的隐藏层张量, 形状是 self.num_layers * 1 * self.hidden_size
        # 注意一点: 输入到RNN中的张量要求是三维张量, 所以需要用unsqueeze()函数扩充维度
        input1 = input1.unsqueeze(0)
        # 将input1和hidden输入到RNN的实例化对象中, 如果num_layers=1, rr恒等于hn
        rr, hn = self.rnn(input1, hidden)
        # 将从RNN中获得的结果通过线性层的变换和softmax层的处理, 最终返回
        return self.softmax(self.linear(rr)), hn
    
    def initHidden(self):
        # 本函数的作用是用来初始化一个全零的隐藏层张量, 维度是3
        return torch.zeros(self.num_layers, 1, self.hidden_size)


class LSTM(nn.Module):
    def __init__(self, input_size, hidden_size, output_size, num_layers=1):
        # input_size: 代表输入张量x中最后一个维度
        # hidden_size: 代表隐藏层张量的最后一个维度
        # output_size: 代表线性层最后的输出维度
        # num_layers: 代表LSTM网络的层数
        super(LSTM, self).__init__()
        self.input_size = input_size
        self.hidden_size = hidden_size
        self.output_size = output_size
        self.num_layers = num_layers
        
        # 实例化LSTM对象
        self.lstm = nn.LSTM(input_size, hidden_size, num_layers)
        # 实例化线性层, 作用是将LSTM网络的输出维度转换成指定的输出维度
        self.linear = nn.Linear(hidden_size, output_size)
        # 实例化nn中预定义的Softmax层, 作用从输出层的张量中得到类别结果
        self.softmax = nn.LogSoftmax(dim=-1)
    
    def forward(self, input1, hidden, c):
        # 注意: LSTM网络的输入有3个张量,尤其不要忘记细胞状态c
        input1 = input1.unsqueeze(0)
        # 将3个参数输入到LSTM对象中
        rr, (hn, cn) = self.lstm(input1, (hidden, c))
        # 最后将3个张量结果全部返回, 同时rr要经过线性层和softmax的处理
        return self.softmax(self.linear(rr)), hn, cn
    
    def initHiddenAndC(self):
        # 注意: 对于LSTM来说, 初始化的时候要同时初始化hidden和细胞状态c
        # hidden和c的形状保持一致
        c = hidden = torch.zeros(self.num_layers, 1, self.hidden_size)
        return hidden, c


class GRU(nn.Module):
    def __init__(self, input_size, hidden_size, output_size, num_layers=1):
        # input_size: 代表输入张量x最后一个维度
        # hidden_size: 代表隐藏层最后一个维度
        # output_size: 代表指定的线性层输出的维度
        # num_layers: 代表GRU网络的层数
        super(GRU, self).__init__()
        self.input_size = input_size
        self.hidden_size = hidden_size
        self.output_size = output_size
        self.num_layers = num_layers
        
        # 实例化GRU对象
        self.gru = nn.GRU(input_size, hidden_size, num_layers)
        # 实例化线性层的对象
        self.linear = nn.Linear(hidden_size, output_size)
        # 定义softmax对象, 作用是从输出张量中得到类别分类
        self.softmax = nn.LogSoftmax(dim=-1)
    
    def forward(self, input1, hidden):
        input1 = input1.unsqueeze(0)
        rr, hn = self.gru(input1, hidden)
        return self.softmax(self.linear(rr)), hn
    
    def initHidden(self):
        return torch.zeros(self.num_layers, 1, self.hidden_size)


# 参数
input_size = n_letters

n_hidden = 128

output_size = n_categories

input1 = lineToTensor('B').squeeze(0)

hidden = c = torch.zeros(1, 1, n_hidden)

rnn = RNN(input_size, n_hidden, output_size)
lstm = LSTM(input_size, n_hidden, output_size)
gru = GRU(input_size, n_hidden, output_size)

rnn_output, next_hidden = rnn(input1, hidden)
# print('rnn:', rnn_output)
# print('rnn_shape:', rnn_output.shape)
# print('***********')

lstm_output, next_hidden1, c = lstm(input1, hidden, c)
# print('lstm:', lstm_output)
# print('lstm_shape:', lstm_output.shape)
# print('***********')

gru_output, next_hidden2 = gru(input1, hidden)
# print('gru:', gru_output)
# print('gru_shape:', gru_output.shape)


def categoryFromOutput(output):
    # output: 从输出结果中得到指定的类别
    # 需要调用topk()函数, 得到最大的值和索引, 作为我们的类别信息
    top_n, top_i = output.topk(1)
    # 从top_i中取出索引的值
    category_i = top_i[0].item()
    # 从前面已经构造号的all_categories中得到对应语言的类别,返回类别和索引
    return all_categories[category_i], category_i

# x = torch.arange(1, 6)
# print(x)
# res = torch.topk(x, 3)
# print(res)

# category, category_i = categoryFromOutput(gru_output)
# print('category:', category)
# print('category_i:', category_i)


def randomTrainingExample():
    # 该函数的作用用于随机产生训练数据
    # 第一步使用random.choice()方法从all_categories中随机选择一个类别
    category = random.choice(all_categories)
    # 第二步通过category_lines字典取出category类别对应的名字列表
    line = random.choice(category_lines[category])
    # 第三步将类别封装成tensor
    category_tensor = torch.tensor([all_categories.index(category)], dtype=torch.long)
    # 将随机取到的名字通过函数lineToTensor()转换成onehot张量
    line_tensor = lineToTensor(line)
    return category, line, category_tensor, line_tensor


# for i in range(10):
#     category, line, category_tensor, line_tensor = randomTrainingExample()
#     print('category = ', category, ' / line = ', line, ' / category_tensor = ', category_tensor)
# print('line_tensor = ', line_tensor)


# a = torch.randn(4)
# print(a)
# b = torch.randn(4, 1)
# print(b)
# print('---------------')
# c = torch.add(a, b)
# print(c)
# d = torch.add(a, b, alpha=10)
# print(d)

# 定义损失函数, nn.NLLLoss()函数, 因为和RNN最后一层的nn.LogSoftmax()逻辑匹配
criterion = nn.NLLLoss()

# 设置学习率为0.005
learning_rate = 0.005

def trainRNN(category_tensor, line_tensor):
    # category_tensor: 代表训练数据的标签
    # line_tensor: 代表训练数据的特征
    # 第一步要初始化一个RNN隐藏层的张量
    hidden = rnn.initHidden()
    
    # 关键的一步: 将模型结构中的梯度归零
    rnn.zero_grad()
    
    # 循环遍历训练数据line_tensor中的每一个字符, 传入RNN中, 并且迭代更新hidden
    for i in range(line_tensor.size()[0]):
        output, hidden = rnn(line_tensor[i], hidden)
        
    # 因为RNN的输出是三维张量, 为了满足category_tensor, 需要进行降维操作
    loss = criterion(output.squeeze(0), category_tensor)
    
    # 进行反向传播
    loss.backward()
    
    # 显示的更新模型中所有的参数
    for p in rnn.parameters():
        # 将参数的张量标识与参数的梯度进行乘法运算并乘以学习率, 结果加到参数上, 并进行覆盖更新
        p.data.add_(-learning_rate, p.grad.data)
    
    # 返回RNN最终的输出结果output, 和模型的损失loss
    return output, loss.item()


def trainLSTM(category_tensor, line_tensor):
    # 初始化隐藏层张量, 以及初始化细胞状态
    hidden, c = lstm.initHiddenAndC()
    # 先要将LSTM网络的梯度归零
    lstm.zero_grad()
    # 遍历所有的输入时间步的xi
    for i in range(line_tensor.size()[0]):
        # 注意LSTM每次输入包含3个张量
        output, hidden, c = lstm(line_tensor[i], hidden, c)
    
    # 将预测张量, 和目标标签张量输入损失函数中
    loss = criterion(output.squeeze(0), category_tensor)
    # 进行反向传播
    loss.backward()
    # 进行参数的显示更新
    for p in lstm.parameters():
        p.data.add_(-learning_rate, p.grad.data)
    return output, loss.item()


def trainGRU(category_tensor, line_tensor):
    # 注意GRU网络初始化的时候只需要初始化一个隐藏层的张量
    hidden = gru.initHidden()
    # 首先将GRU网络的梯度进行清零
    gru.zero_grad()
    # 遍历所有的输入时间步的xi
    for i in range(line_tensor.size()[0]):
        output, hidden = gru(line_tensor[i], hidden)
    
    # 将预测的张量值和真实的张量标签传入损失函数中
    loss = criterion(output.squeeze(0), category_tensor)
    # 进行反向传播
    loss.backward()
    # 进行参数的显示更新
    for p in gru.parameters():
        p.data.add_(-learning_rate, p.grad.data)
    return output, loss.item()


def timeSince(since):
    # 本函数的作用是打印每次训练的耗时, since是训练开始的时间
    # 第一步获取当前的时间
    now = time.time()
    # 第二步得到时间差
    s = now - since
    # 第三步计算得到分钟数
    m = math.floor(s / 60)
    # 第四步得到秒数
    s -= m * 60
    # 返回指定格式的耗时
    return '%dm %ds' % (m, s)

# since = time.time() - 10 * 60
# period = timeSince(since)
# print(period)


# 设置训练的迭代次数
n_iters = 1000
# 设置结果的打印间隔
print_every = 50
# 设置绘制损失曲线上的制图间隔
plot_every = 10

def train(train_type_fn):
    # train_type_fn代表选择哪种模型来训练函数, 比如选择trainRNN
    # 初始化存储每个制图间隔损失的列表
    all_losses = []
    # 获取训练开始的时间
    start = time.time()
    # 设置初始间隔的损失值等于0
    current_loss = 0
    # 迭代训练
    for iter in range(1, n_iters + 1):
        # 通过randomTrainingExample()函数随机获取一组训练数据和标签
        category, line, category_tensor, line_tensor = randomTrainingExample()
        # 将训练特征和标签张量传入训练函数中, 进行模型的训练
        output, loss = train_type_fn(category_tensor, line_tensor)
        # 累加损失值
        current_loss += loss
        
        # 如果到了迭代次数的打印间隔
        if iter % print_every == 0:
            # 取该迭代步的output通过函数categoryFromOutput()获取对应的类别和索引
            guess, guess_i = categoryFromOutput(output)
            # 判断和真实的类别标签进行比较, 如果相同则为True,如果不同则为False
            correct = 'True' if guess == category else 'False (%s)' % category
            # 打印若干信息
            print('%d %d%% (%s) %.4f %s / %s %s' % (iter, iter/n_iters*100, timeSince(start), loss, line, guess, correct))
        
        # 如果到了迭代次数的制图间隔
        if iter % plot_every == 0:
            # 将过去若干轮的平均损失值添加到all_losses列表中
            all_losses.append(current_loss / plot_every)
            # 将间隔损失值重置为0
            current_loss = 0
    
    # 返回对应的总损失列表, 并返回训练的耗时
    return all_losses, int(time.time() - start)


# 调用train函数, 分别传入RNN, LSTM, GRU的训练函数
# 返回的损失列表, 以及训练时间
# all_losses1, period1 = train(trainRNN)
# all_losses2, period2 = train(trainLSTM)
# all_losses3, period3 = train(trainGRU)

# 绘制损失对比曲线
# plt.figure(0)
# plt.plot(all_losses1, label="RNN")
# plt.plot(all_losses2, color="red", label="LSTM")
# plt.plot(all_losses3, color="orange", label="GRU")
# plt.legend(loc="upper left")

# 绘制训练耗时的柱状图
# plt.figure(1)
# x_data = ["RNN", "LSTM", "GRU"]
# y_data = [period1, period2, period3]
# plt.bar(range(len(x_data)), y_data, tick_label=x_data)


def evaluateRNN(line_tensor):
    # 评估函数, 仅有一个参数, line_tensor代表名字的张量标识
    # 初始化一个隐藏层的张量
    hidden = rnn.initHidden()
    # 将评估数据line_tensor中的每一个字符逐个传入RNN中
    for i in range(line_tensor.size()[0]):
        output, hidden = rnn(line_tensor[i], hidden)
    # 返回整个RNN的输出output
    return output.squeeze(0)


def evaluateLSTM(line_tensor):
    # 评估函数, 针对LSTM模型, 仅有一个参数, line_tensor代表名字的张量表示
    # 初始化一个隐藏层的张量, 同时再初始化一个细胞状态
    hidden, c = lstm.initHiddenAndC()
    # 将评估数据line_tensor中的每一个字符逐个传入LSTM中
    for i in range(line_tensor.size()[0]):
        output, hidden, c = lstm(line_tensor[i], hidden, c)
    # 返回整个LSTM的输出output, 同时完成降维的操作
    return output.squeeze(0)


def evaluateGRU(line_tensor):
    # 评估函数, 针对GRU模型, 仅有一个参数, line_tensor代表名字的张量表示
    # 初始化一个隐藏层的张量
    hidden = gru.initHidden()
    # 将评估数据line_tensor中的每一个字符逐个传入GRU中
    for i in range(line_tensor.size()[0]):
        output, hidden = gru(line_tensor[i], hidden)
    # 返回整个GRU的输出output, 同时完成降维的操作
    return output.squeeze(0)


# line = "Bai"
# line_tensor = lineToTensor(line)

# rnn_output = evaluateRNN(line_tensor)
# lstm_output = evaluateLSTM(line_tensor)
# gru_output = evaluateGRU(line_tensor)
# print('rnn_output:', rnn_output)
# print('lstm_output:', lstm_output)
# print('gru_output:', gru_output)


def predict(input_line, evaluate_fn, n_predictions=3):
    # input_line: 代表输入的字符串名字
    # evaluate_fn: 代表评估的模型函数, RNN, LSTM, GRU
    # n_predictions: 代表需要取得最有可能的n_predictions个结果
    # 首先将输入的名字打印出来
    print('\n> %s' % input_line)
    
    # 注意: 所有的预测函数都不能改变模型的参数
    with torch.no_grad():
        # 使用输入的人名转换成张量, 然后调用评估模型函数得到预测的结果
        output = evaluate_fn(lineToTensor(input_line))
        
        # 从预测的结果中取出top3个最大值及其索引
        topv, topi = output.topk(n_predictions, 1, True)
        # 初始化结果的列表
        predictions = []
        # 遍历3个最可能的结果
        for i in range(n_predictions):
            # 首先从topv中取出概率值
            value = topv[0][i].item()
            # 然后从topi中取出索引值
            category_index = topi[0][i].item()
            # 打印概率值及其对应的真实国家名称
            print('(%.2f) %s' % (value, all_categories[category_index]))
            # 将结果封装成列表格式, 添加到最终的结果列表中
            predictions.append([value, all_categories[category_index]])
        
        return predictions


# for evaluate_fn in [evaluateRNN, evaluateLSTM, evaluateGRU]:
#     print('-'*20)
#     predict('Dovesky', evaluate_fn)
#     predict('Jackson', evaluate_fn)
#     predict('Satoshi', evaluate_fn)

 

# 导入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
# 导入优化方法的工具包
from torch import optim
# 设备的选择, 可以选择在GPU上运行或者在CPU上运行
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")


# 定义起始标志
SOS_token = 0
# 定义结束标志
EOS_token = 1

class Lang():
    def __init__(self, name):
        # 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):
        # 添加句子的函数, 将整个句子中所有的单词依次添加到字典中
        # 因为英文, 法文都是空格进行分割的语言, 直接进行分词就可以
        for word in sentence.split(' '):
            self.addWord(word)
    
    def addWord(self, word):
        # 添加单词到类内字典中, 将单词转换为数字
        # 首先判断word是否已经在self.word2index字典的key中
        if word not in self.word2index:
            # 添加的时候, 索引值取当前类中单词的总数量
            self.word2index[word] = self.n_words
            # 再添加翻转的字典
            self.index2word[self.n_words] = word
            # 第三步更新类内的单词总数量
            self.n_words += 1


# name = "eng"
# sentence = "hello I am Jay"

# eng1 = Lang(name)
# eng1.addSentence(sentence)
# print("word2index:", eng1.word2index)
# print("index2word:", eng1.index2word)
# print("n_words:", eng1.n_words)


# 将unicode字符串转换为ASCII字符串, 主要用于将法文的重音符号去除掉
def unicodeToAscii(s):
    return ''.join(c for c in unicodedata.normalize('NFD', s)
                    if unicodedata.category(c) != 'Mn')


# 定义字符串规范化函数
def normalizeString(s):
    # 第一步使字符转变为小写并去除掉两侧的空白符, 再调用上面的函数转换为ASCII字符串
    s = unicodeToAscii(s.lower().strip())
    # 在.!?前面加一个空格
    s = re.sub(r"([.!?])", r" \1", s)
    # 使用正则表达式将字符串中不是大小写字符和正常标点符号的全部替换成空格
    s = re.sub(r"[^a-zA-Z.!?]+", r" ", s)
    return s


# s1 = "Are you kidding me?"
# res = normalizeString(s1)
# print(res)


# 明确一下数据文件的存放地址
data_path = '../data/eng-fra.txt'

# 读取原始数据并实例化源语言+目标语言的类对象
def readLangs(lang1, lang2):
    # lang1: 代表源语言的名字
    # lang2: 代表目标语言的名字
    # 整个函数返回对应的两个类对象, 以及语言对的列表
    lines = open(data_path, encoding='utf-8').read().strip().split('\n')
    # 对lines列表中的句子进行标准化处理, 并以\t进行再次划分, 形成子列表
    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("input_lang:", input_lang)
# print("output_lang:", output_lang)
# print("pairs[:5]:", pairs[:5])


# 设置组成句子中单词或标点的最多个数
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(pair):
    # 当前传入的pair是一个语言对的形式
    # pair[0]代表英文源语句, 长度应小于MAX_LENGTH, 并且以指定前缀开始
    # pair[1]代表法文源语句, 长度应小于MAX_LENGTH
    return len(pair[0].split(' ')) < MAX_LENGTH and \
            pair[0].startswith(eng_prefixes) and \
            len(pair[1].split(' ')) < MAX_LENGTH


# 过滤语言对的函数
def filterPairs(pairs):
    # 函数直接遍历列表中的每个语言字符串并调用filterPair()函数即可
    return [pair for pair in pairs if filterPair(pair)]


# fpairs = filterPairs(pairs)
# print("fpairs[:5]:", fpairs[:5])


# 整合数据预处理的函数
def prepareData(lang1, lang2):
    # lang1: 代表源语言的名字, 英文
    # lang2: 代表目标语言的名字, 法文
    # 第一步通过调用readLangs()函数得到两个类对象, 并得到字符串类型的语言对的列表
    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))


def tensorFromSentence(lang, sentence):
    # lang: 代表是Lang类的实例化对象
    # sentence: 代表传入的语句
    indexes = [lang.word2index[word] for word in sentence.split(' ')]
    # 注意要在最后添加一个语句的结束符号
    indexes.append(EOS_token)
    # 使用torch.tensor对列表进行封装, 并将其形状改变成 n*1
    return torch.tensor(indexes, dtype=torch.long, device=device).view(-1, 1)


def tensorsFromPair(pair):
    # pair: 代表一个语言对(英文, 法文)
    # 依次调用具体的处理函数, 分别处理源语言和目标语言
    input_tensor = tensorFromSentence(input_lang, pair[0])
    output_tensor = tensorFromSentence(output_lang, pair[1])
    return (input_tensor, output_tensor)


# pair_tensor = tensorsFromPair(pairs[0])
# print(pair_tensor)


# 构建编码器类
class EncoderRNN(nn.Module):
    def __init__(self, input_size, hidden_size):
        # input_size: 代表编码器输入尺寸, 就是英文的词表大小
        # hidden_size: 代表GRU的隐藏层神经单元数, 同时也是词嵌入的维度
        super(EncoderRNN, self).__init__()
        # 将参数传入类中
        self.input_size = input_size
        self.hidden_size = hidden_size
        # 实例化Embedding层, 输入参数分别是词表单词总数, 和词嵌入的维度
        self.embedding = nn.Embedding(input_size, hidden_size)
        # 实例化GRU, 参数也是hidden_size
        self.gru = nn.GRU(hidden_size, hidden_size)
    
    def forward(self, input1, hidden):
        # input1: 代表源语言中的输入张量
        # hidden: 代表初始化的隐藏层张量
        # 注意: 经过EMbedding处理后, 张量是一个二维张量, 但是GRU要求输入是三维张量,
        # 所以要对结果进行扩展维度 view(), 同时让任意单词映射后的尺寸是[1, embedding]
        output = self.embedding(input1).view(1, 1, -1)
        # 将output和hidden传入GRU单元中, 得到返回结果
        output, hidden = self.gru(output, hidden)
        return output, hidden
    
    def initHidden(self):
        # 将隐藏层张量初始化为1*1*self.hidden_size大小的张量
        return torch.zeros(1, 1, self.hidden_size, device=device)


hidden_size = 25
input_size = 20
# input1 = pair_tensor[0][0]
# hidden = torch.zeros(1, 1, hidden_size)

# encoder = EncoderRNN(input_size, hidden_size)
# output, hidden = encoder(input1, hidden)
# print(output)
# print(output.shape)


# 构建解码器类
class DecoderRNN(nn.Module):
    def __init__(self, hidden_size, output_size):
        # hidden_size: 代表隐藏层的神经元个数, 同时也是解码器的输入尺寸
        # output_size: 代表整个解码器的输出尺寸, 指定的尺寸也就是目标语言的单词总数
        super(DecoderRNN, self).__init__()
        # 将参数传入类中
        self.hidden_size = hidden_size
        self.output_size = output_size
        # 实例化Embedding对象, 输入参数分别是目标语言的单词总数, 和词嵌入的维度
        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, input1, hidden):
        # input1: 代表目标语言的输入张量
        # hidden: 代表初始化的GRU隐藏层张量
        # 经历了Embedding层处理后, 要将张量形状改变为三维张量
        output = self.embedding(input1).view(1, 1, -1)
        # 使用relu函数对输出进行处理, 使得EMbedding矩阵更稀疏, 防止过拟合
        output = F.relu(output)
        # 将张量传入GRU解码器中
        output, hidden = self.gru(output, hidden)
        # 经历GRU处理后的张量是三维张量, 但是全连接层需要二维张量, 利用output[0]来降维
        output = self.softmax(self.out(output[0]))
        return output, hidden
    
    def initHidden(self):
        # 初始化隐藏层张量, 形状为1*1*self.hidden_size
        return torch.zeros(1, 1, self.hidden_size, device=device)


# hidden_size = 25
# output_size = 10
# input1 = pair_tensor[1][0]
# hidden = torch.zeros(1, 1, hidden_size)

# decoder = DecoderRNN(hidden_size, output_size)
# output, hidden = decoder(input1, hidden)
# print(output)
# print(output.shape)


# 构建基于GRU和Attention的解码器类
class AttnDecoderRNN(nn.Module):
    def __init__(self, hidden_size, output_size, dropout_p=0.1, max_length=MAX_LENGTH):
        # hidden_size: 代表解码器的GRU输出尺寸, 就是隐藏层的神经元个数
        # output_size: 指定的网络输出尺寸, 代表目标语言的词汇总数(法文)
        # dropout_p: 使用Dropout层的置零比例
        # 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
        
        # 实例化一个Embedding对象, 参数是目标语言的词汇总数和词嵌入的维度
        self.embedding = nn.Embedding(output_size, hidden_size)
        
        # 实例化第一个注意力层, 注意输入是两个张量的合并
        self.attn = nn.Linear(self.hidden_size * 2, self.max_length)
        
        # 实例化第二个注意力层, 注意输入也是两个张量的合并, 同时输出要进入GRU中
        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, input1, hidden, encoder_output):
        # input1: 源数据的输入张量
        # hidden: 初始化的隐藏层张量
        # encoder_output: 代表编码器的输出张量
        # 对输入input1进行词嵌入处理, 并扩展维度
        embedded = self.embedding(input1).view(1, 1, -1)
        # 紧接着将其输入dropout层, 防止过拟合
        embedded = self.dropout(embedded)
        
        # 在进行第一个注意力层处理前, 要将Q, K进行纵轴拼接
        attn_weights = F.softmax(self.attn(torch.cat((embedded[0], hidden[0]), 1)), dim=1)
        
        # 进行bmm操作, 注意要将二维张量扩展成三维张量
        attn_applied = torch.bmm(attn_weights.unsqueeze(0), encoder_output.unsqueeze(0))
        
        # 再次进行拼接, 顺便要进行一次降维
        output = torch.cat((embedded[0], attn_applied[0]), 1)
        
        # 将output输入第二个注意力层
        output = self.attn_combine(output).unsqueeze(0)
        
        # 使用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):
        # 初始化一个全零的隐藏层张量, 形状为 1*1*self.hidden_size
        return torch.zeros(1, 1, self.hidden_size, device=device)


# hidden_size = 25
# output_size = 10
# input1 = pair_tensor[1][0]
# hidden = torch.zeros(1, 1, hidden_size)
# encoder_output = torch.randn(10, 25)

# decoder_attn = AttnDecoderRNN(hidden_size, output_size)
# output, hidden, attn_weights = decoder_attn(input1, hidden, encoder_output)
# print(output)
# print(output.shape)
# print(hidden.shape)
# print(attn_weights)
# print(attn_weights.shape)


# 设定一下teacher_forcing的比率, 在多大的概率下使用这个策略进行训练
teacher_forcing_ratio = 0.5

def train(input_tensor, target_tensor, encoder, decoder, 
          encoder_optimizer, decoder_optimizer, criterion, max_length=MAX_LENGTH):
    # input_tensor: 代表源语言的输入张量
    # target_tensor: 代表目标语言的输入张量
    # encoder: 代表编码器的实例化对象
    # decoder: 代表解码器的实例化对象
    # encoder_optimizer: 代表编码器优化器
    # decoder_optimizer: 代表解码器优化器
    # criterion: 损失函数
    # max_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
    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_output是三维张量, 使用[0,0]进行降维到一维列表, 赋值给输出张量
        encoder_outputs[ei] = encoder_output[0, 0]
    
    # 初始化解码器的第一个输入字符
    decoder_input = torch.tensor([[SOS_token]], device=device)
    
    # 初始化解码器的隐藏层张量, 赋值给最后一次编码器的隐藏层张量
    decoder_hidden = encoder_hidden
    
    # 判断是否使用teacher_forcing
    use_teacher_forcing = True if random.random() < teacher_forcing_ratio else False
    
    # 如果使用teacher_forcing
    if use_teacher_forcing:
            """
            如果是teacher_forcing,那么将会使用真实标签语句中的值作为解码器时间步的输入。
            第一个时间步输入的是起始符SOS_token,然后后面每个时间步的输入都是真实标签语句中的单词索引值。
            真实标签语句中的最后一个单词索引值为1(结束标志EOS_token),那么当前的循环是没有把1(结束标志EOS_token)再输入到解码器中训练的,
            因为当前range(目标语句长度)中的最后一次循环中target_tensor[di]仅仅是把1(结束标志EOS_token)取出来了,
            然后循环就结束了,并没有机会再次输入到解码器中继续训练。
            """
        # 遍历目标张量, 进行解码
        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])
            # 因为使用了teacher_forcing, 所以将下一步的解码器输入强制设定为“正确的答案”
            decoder_input = target_tensor[di]
    # 如果不适用teacher_forcing
    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)
            # 使用损失函数计算损失值, 并进行累加
            loss += criterion(decoder_output, target_tensor[di])
            # 如果某一步的解码结果是句子终止符号, 则解码直接结束, 跳出循环
            if topi.squeeze().item() == EOS_token:
                break
            # 下一步解码器的输入要设定为当前步最大概率值的那一个
            decoder_input = topi.squeeze().detach()
    
    # 应用反向传播进行梯度计算
    loss.backward()
    # 利用编码器和解码器的优化器进行参数的更新
    encoder_optimizer.step()
    decoder_optimizer.step()
    
    # 返回平均损失
    return loss.item() / target_length


# 构建时间计算的辅助函数
def timeSince(since):
    # since: 代表模型训练的开始时间
    # 首先获取当前时间
    now = time.time()
    # 计算得到时间差
    s = now - since
    # 将s转换为分钟, 秒的形式
    m = math.floor(s / 60)
    # 计算余数的秒
    s -= m * 60
    # 按照指定的格式返回时间差
    return '%dm %ds' % (m, s)


# since = time.time() - 620

# period = timeSince(since)
# print(period)


import matplotlib.pyplot as plt

def trainIters(encoder, decoder, n_iters, print_every=1000, plot_every=100, learning_rate=0.01):
    # encoder: 编码器的实例化对象
    # decoder: 解码器的实例化对象
    # n_iters: 训练的总迭代步数
    # print_every: 每隔多少轮次进行一次训练日志的打印
    # plot_every: 每隔多少轮次进行一次损失值的添加, 为了后续绘制损失曲线
    # learning_rate: 学习率
    # 获取训练开始的时间
    start = time.time()
    # 初始化存放平均损失值的列表
    plot_losses = []
    # 每隔打印间隔的总损失值
    print_loss_total = 0
    # 每个绘制曲线损失值的列表
    plot_loss_total = 0
    
    # 定义编码器和解码器的优化器
    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")


# 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 = 5000
# print_every = 500

# if __name__ == '__main__':
#     trainIters(encoder1, attn_decoder1, n_iters, print_every=print_every)


def evaluate(encoder, decoder, sentence, max_length=MAX_LENGTH):
    # encoder: 代表编码器对象
    # decoder: 代表解码器对象
    # sentence: 待评估的源语句
    # max_length: 句子的最大长度
    # 注意: 整个评估过程梯度不进行改变
    with torch.no_grad():
        # 对输入语句进行张量表示
        input_tensor = tensorFromSentence(input_lang, sentence)
        # 获得输入的句子长度
        input_length = input_tensor.size(0)
        # 初始化编码器的隐藏层张量
        encoder_hidden = encoder.initHidden()
        
        # 初始化编码器的输出张量, 矩阵的形状max_length * hidden_size
        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
            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_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]


def evaluateRandomly(encoder, decoder, n=6):
    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(' ')


# if __name__ == '__main__':
#     evaluateRandomly(encoder1, attn_decoder1)


sentence = "we re both teachers ."
output_words, attention = evaluate(encoder1, attn_decoder1, sentence)
print(output_words)
plt.figure()
plt.matshow(attention.numpy())
plt.savefig("./s2s_attn.png")

 

  • 1
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

あずにゃん

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值