第N7周:seq2seq翻译实战-pytorch复现-小白版

理论基础

seq2seq(Sequence-to-Sequence)模型是一种用于机器翻译、文本摘要等序列转换任务的框架。它由两个主要的递归神经网络(RNN)组成:一个编码器(Encoder)和一个解码器(Decoder)。下面是seq2seq模型实现翻译的基本原理:

  1. 编码器(Encoder) :
    • 输入:编码器接收一个源语言句子,这个句子已经被分割成一系列的单词或字符,通常表示为( x_1, x_2, ..., x_T )。
    • 处理:编码器逐个处理这些输入,并为每个输入生成一个隐藏状态( h_t )。在这个过程中,编码器会构建一个代表整个输入句子的内部表示(context vector)。
    • 输出:最后,编码器输出一个固定大小的上下文向量( c ),这个向量包含了输入句子的语义信息。
  2. 上下文向量(Context Vector) :
    • 上下文向量是编码器输出的一个汇总,它捕获了整个输入句子的信息。这个向量通常是通过编码器最后一个隐藏状态或者对所有隐藏状态进行池化得到的。
  3. 解码器(Decoder) :
    • 输入:解码器接收上下文向量( c )和之前生成的目标语言句子的一部分作为输入,通常表示为( y_1, y_2, ..., y_{T'} )。
    • 处理:解码器基于当前的目标语言句子部分和上下文向量来生成下一个单词的概率分布。在每一步,解码器都会更新其隐藏状态,并使用它来预测下一个单词。
    • 输出:解码器输出一个概率分布,表示在给定当前输入的情况下,下一个目标语言单词的所有可能性的概率。
  4. 训练过程 :
    • 在训练过程中,seq2seq模型使用最大似然估计来优化模型的参数。这意味着模型试图最大化目标句子在给定源句子的条件下的概率。
    • 通常,解码器在训练时会使用教师强制(Teacher Forcing)策略,即在每一步都提供真实的下一个目标单词作为输入,而不是使用上一步的预测结果。
  5. 推理过程 :
    • 在推理(或测试)时,模型的解码器通常会使用自己上一步的输出作为下一步的输入,直到生成一个结束标记或达到最大输出长度。
      seq2seq模型的关键优势在于它的灵活性:它可以处理任意长度的输入和输出序列。此外,由于编码器和解码器都是RNN,它们能够捕捉到序列中的长距离依赖关系。
      在实际应用中,基础的seq2seq模型可能会遇到一些问题,比如难以处理长序列和缺乏对输入序列的注意力机制。因此,研究者们提出了许多改进版本,如使用长短时记忆网络(LSTM)或门控循环单元(GRU)来替代基本的RNN,以及引入注意力机制(Attention Mechanism)来允许解码器关注输入序列的不同部分。这些改进显著提高了seq2seq模型在机器翻译等任务上的性能。

一、环境准备(导入基本的包以供使用)

  1. __future__: 这个模块允许你使用未来版本的Python特性。在这个例子中,它启用了Python 2的print_function(使得print成为一个函数,而不是一个语句),unicode_literals(使得所有的字符串默认为Unicode),和division(改变了除法的运算规则,在Python 2中,整数相除会得到整数结果,而不是浮点数)。
  2. io.open: 这个模块提供了一个统一的接口来打开文件。导入open函数是为了确保在Python 2和Python 3中打开文件的方式是一致的。
  3. unicodedata: 这个模块提供了对Unicode字符数据库的访问,可以用于检查和处理Unicode字符。
  4. string: 这个模块包含了常用的字符串操作。在这个脚本中,它可能被用来处理字符集或字符串常量。
  5. re: 这是Python的正则表达式模块,用于字符串的搜索和替换操作。
  6. random: 这个模块提供了生成随机数的工具。
  7. torch: 这是PyTorch框架的主要模块,用于构建和训练神经网络。
  8. torch.nn: 这是PyTorch的神经网络模块,提供了创建和训练神经网络所需的所有工具。
  9. torch.optim: 这个模块包含了各种优化算法,用于在训练过程中调整神经网络的权重。
  10. torch.nn.functional: 这个模块提供了神经网络中使用的激活函数和其他功能性函数。
    最后,代码检查了CUDA(一种用于GPU加速计算的框架)是否可用,如果可用,则将PyTorch的设备设置为CUDA,否则使用CPU。这决定了神经网络模型将在哪个设备上运行。
python 复制代码
from __future__ import unicode_literals, print_function, division
from io import open
import unicodedata
import string
import re
import random

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

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

输出

cuda

二、前期的语料处理

1.搭建语言类

这段代码定义了一个名为Lang的类,它用于处理语言相关的数据,例如构建词汇表、将单词映射到索引等。这个类在处理自然语言数据时非常有用,特别是在构建神经机器翻译系统时。

python 复制代码
SOS_token = 0
EOS_token = 1

这两行代码定义了两个特殊标记的整数值,SOS_token代表"开始符"(Start of Sentence),EOS_token代表"结束符"(End of Sentence)。这些标记用于在序列的开头和结尾处标识句子的开始和结束。

python 复制代码
class Lang:
    def __init__(self, name):
        self.name = name
        self.word2index = {}
        self.word2count = {}
        self.index2word = {0: "SOS", 1: "EOS"}
        self.n_words = 2  # Count SOS and EOS

这段代码定义了Lang类的构造函数。它接受一个参数name,表示语言的名称。然后初始化了几个重要的属性:

  • self.name:存储语言的名称。
  • self.word2index:一个字典,用于将单词映射到它们在词汇表中的索引。
  • self.word2count:一个字典,用于记录每个单词在语料库中出现的次数。
  • self.index2word:一个字典,用于将索引映射回单词。初始时,它包含两个特殊标记SOSEOS
  • self.n_words:一个整数,表示词汇表中的单词数量,初始值为2(因为已经包含了SOSEOS)。
python 复制代码
    def addSentence(self, sentence):
        for word in sentence.split(' '):
            self.addWord(word)

这个方法addSentence接受一个句子作为输入,并将其中的每个单词添加到词汇表中。它通过调用addWord方法来实现这一点,该方法将在下一行中定义。

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

addWord方法用于将单个单词添加到词汇表中。如果单词不在词汇表中,它会将单词添加到word2index字典中,并为其分配一个新的索引(self.n_words),然后在index2word字典中记录这个索引到单词的映射,并更新n_words计数。如果单词已经在词汇表中,它会更新word2count字典中该单词的计数。

总的来说,Lang类提供了一个方便的方式来构建和处理与特定语言相关的词汇表,这在序列到序列的学习任务(如机器翻译)中是非常重要的。

2.文本处理函数

这段代码包含两个函数,unicodeToAsciinormalizeString,用于处理和规范化文本数据。

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

这个函数unicodeToAscii接受一个字符串s作为输入,并返回一个仅包含ASCII字符的字符串。它首先使用unicodedata.normalize('NFD', s)将输入字符串分解为组合字符序列。然后,它遍历每个字符,检查其类别是否为'Mn'非间距标记),如果是,则忽略该字符(不加入到最终的字符串中)。否则,将该字符加入到最终的字符串中。这样做是为了去除字符串中的变音符号,如重音字符。

在讨论字符编码和文本处理时,"Mn"(非间距标记)是Unicode字符类别之一,用于表示非间距标记字符。这类字符通常用于与其他字符结合,以形成特定的文字或音标,它们不会占据额外的空间,而是放在基础字符的上方、下方或穿过基础字符。

例如,在法语中,字母"e"上面可能有一个非间距的重音标记(例如,é),这个重音标记就是一个非间距标记字符。在Unicode中,这个重音标记和"e"是分开的字符,但当你将它们放在一起时,它们会显示为一个带有重音的字符。

在文本处理中,有时需要将这些非间距标记与它们的基础字符分开处理,例如,当需要将文本转换为纯ASCII形式时,可能需要去除这些非间距标记。这就是unicodeToAscii函数的目的,它通过移除非间距标记,将文本转换为只包含ASCII字符的形式。
在Python的unicodedata模块中,normalize('NFD', s)函数调用是将字符串s进行Unicode正规化(Normalization)的一种形式。NFD是Normalization Form D的缩写,代表"Normalization Form Canonical Decomposition"。这种正规化形式将每个Unicode字符分解为其组成部分的基本字符(即组合字符序列)。

具体来说,NFD执行以下操作:

  1. 分解(Decomposition):它将所有字符分解为它们的组合部分。例如,一个带重音的字符(如é)会被分解为基本字符(e)和一个非间距标记(´)。
  2. 规范(Canonical) :它确保分解是规范化的,即遵循Unicode标准中定义的官方分解规则。 使用NFD形式的好处是,它可以使得不同的字符表示方式标准化,这样就可以更容易地进行比较和排序。在处理文本数据时,这有助于确保相同的语义内容得到一致的编码。
    unicodeToAscii函数中,使用NFD正规化形式是为了能够识别并去除字符串中的非间距标记(Mn类别的字符),从而将字符转换为它们的ASCII等效形式。这是因为在分解后,非间距标记会被独立出来,从而可以轻松地被过滤掉。
python 复制代码
# 小写化,剔除标点与非字母符号
def normalizeString(s):
    s = unicodeToAscii(s.lower().strip())  #s.lower().strip()将字符串转换为小写,并去除首尾的空白字符。
    s = re.sub(r"([.!?])", r" \1", s)
    s = re.sub(r"[^a-zA-Z.!?]+", r" ", s)
    return s

这个函数normalizeString接受一个字符串s作为输入,并返回一个规范化后的字符串。它首先调用unicodeToAscii函数将输入字符串转换为仅包含ASCII字符的形式,并将其转换为小写,并去除首尾的空白字符。然后,它使用正则表达式re.sub来处理字符串中的标点符号和非字母字符:

  • re.sub(r"([.!?])", r" \1", s):这个表达式在句号、问号和感叹号前面添加一个空格,这样这些标点符号就会被单独视为一个词。
    如果有看不懂这句代码的语法的,这里是详细解释:

这行代码使用Python的re.sub函数来替换字符串中的特定字符。

  • re.sub:这是Python中re模块的一个函数,用于在字符串中查找和替换模式。
  • r:这是一个前缀,表示字符串是原始字符串(raw string),这意味着反斜杠\不会被当作特殊字符处理,而是按照字面意义进行匹配。
  • "([.!?])":这是第一个参数,是一个正则表达式模式:
    • [.!?]:方括号表示一个字符集,匹配方括号内的任意一个字符,这里表示句号、问号或感叹号。
  • r" \1":这是第二个参数,是替换字符串:
    • :这是一个空格字符,表示要在匹配的字符前添加一个空格。
    • \1:这是一个反向引用(backreference),它引用第一个捕获组匹配的文本(即句号、问号或感叹号)。
  • s:这是第三个参数,是要进行替换操作的原始字符串。
  • re.sub(r"[^a-zA-Z.!?]+", r" ", s):这个表达式将所有非字母字符(除了句号、问号和感叹号)替换为单个空格。这样做的目的是去除字符串中的其他标点符号和特殊字符,只保留字母、句号、问号和感叹号。
    总的来说,normalizeString函数的目的是将输入的字符串转换为一种标准格式,以便于后续的处理和分析。

3、文件读取函数

python 复制代码
def readLangs(lang1, lang2, reverse=False):#reverse这个选项的作用,举个例子,就可以很好理解,当训练一个从法语到英语的翻译模型时,有时候需要将数据集中的句子对反转,以便模型学习如何从英语翻译到法语。
    print("Reading lines...")

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

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

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

    return input_lang, output_lang, pairs

这段代码定义了一个名为readLangs的函数,用于读取和预处理一对语言的文本数据。

python 复制代码
def readLangs(lang1, lang2, reverse=False):

定义函数readLangs,它接受三个参数:lang1lang2是两种语言的名称,reverse是一个布尔值,用于指示是否需要反转语言对的顺序。

python 复制代码
    print("Reading lines...")

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

python 复制代码
    # 以行为单位读取文件
    lines = open('eng-fra.txt'.format(lang1,lang2), encoding='utf-8').read().strip().split('\n')

这行代码读取一个文本文件,该文件包含两种语言的句子对。文件名由lang1lang2参数格式化而成,例如eng-fra.txt。文件以UTF-8编码读取,然后去除首尾空白字符,并根据换行符分割成行列表。

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

这行代码使用列表推导式来处理每一行。每行通过制表符\t分割成两个元素,分别代表两种语言的句子。然后,normalizeString函数被应用于每个句子,以进行规范化处理。处理后的句子对被放入一个列表pairs中。

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

如果reverse参数为True,则反转pairs中的每个句子对,并创建Lang实例input_langoutput_lang,其中input_lang是第二种语言,output_lang是第一种语言。

python 复制代码
    else:
        input_lang  = Lang(lang1)
        output_lang = Lang(lang2)

如果reverse参数为False,则保持pairs中的句子对顺序不变,并创建Lang实例input_langoutput_lang,其中input_lang是第一种语言,output_lang是第二种语言。

python 复制代码
    return input_lang, output_lang, pairs

函数返回三个值:input_lang(输入语言)、output_lang(输出语言)和pairs(处理后的句子对列表)。


这里有一个小点,教案给的示例这一句

lines = open('eng-fra.txt'.format (lang1,lang2), encoding='utf-8').read().strip().split('\n')

这里面.format在教案中是%

但是会出现这样的报错
这个错误信息表明在尝试使用字符串格式化时出现了问题。具体来说,错误发生在这一行代码中:

python 复制代码
lines = open('./end-fra.txt'%(lang1,lang2), encoding='utf-8').read().strip().split('\n')

错误的原因是字符串格式化方法使用不当。在这里,'%(lang1,lang2)' 这样的写法是错误的,因为它试图将两个变量 lang1lang2 作为元组进行格式化,而 % 格式化方法不能直接应用于元组。

正确的做法应该是使用 % 格式化方法,将 lang1lang2 作为单独的参数传递,或者使用 .format() 方法,或者如果使用的是Python 3.6以上的版本,可以使用f-strings。以下是使用 .format() 方法的示例:

python 复制代码
lines = open('./end-{}-{}.txt'.format(lang1, lang2), encoding='utf-8').read().strip().split('\n')

或者,使用f-strings:

python 复制代码
lines = open(f'./end-{lang1}-{lang2}.txt', encoding='utf-8').read().strip().split('\n')

这样,lang1lang2 的值就会被正确地插入到文件名中,从而避免了 TypeError


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

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

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

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

这段代码定义了一些常量,并提供了两个函数,用于过滤一对语言的句子对。

python 复制代码
eng_prefixes = (
    "i am ", "i m ",
    "he is", "he s ",
    # ... 其他英文前缀
)

这行代码定义了一个名为eng_prefixes的列表,其中包含了一些英文句子的前缀。这些前缀在英语中很常见,可能出现在翻译任务的数据集中。列表中的每个元素都是一个前缀,后面跟着一个空格。

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

这行代码定义了一个名为filterPair的函数,它接受一个参数p,代表一对句子。这个函数检查以下条件:

  • len(p[0].split(' ')) < MAX_LENGTH:确保第一个句子(源语言句子)的单词数量不超过MAX_LENGTH
  • len(p[1].split(' ')) < MAX_LENGTH:确保第二个句子(目标语言句子)的单词数量不超过MAX_LENGTH
  • p[1].startswith(eng_prefixes):确保第二个句子以eng_prefixes列表中的某个前缀开头。
    如果所有这些条件都满足,函数返回True,表示这对句子应该被保留;否则,返回False
python 复制代码
def filterPairs(pairs):
    # 选取仅仅包含 eng_prefixes 开头的语料
    return [pair for pair in pairs if filterPair(pair)]

这行代码定义了一个名为filterPairs的函数,它接受一个参数pairs,代表一个包含句子对的列表。这个函数使用列表推导式遍历pairs中的每个句子对,并使用filterPair函数检查每个句子对是否满足过滤条件。如果满足条件,句子对会被包含在新的列表中,最终返回这个新列表。

综上所述,这两个函数用于过滤句子对,确保它们满足特定的长度和前缀条件。

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

input_lang, output_lang, pairs = prepareData('eng', 'fra', True)
print(random.choice(pairs))

这段代码定义了一个名为prepareData的函数,用于准备和处理一对语言的句子对数据。

python 复制代码
def prepareData(lang1, lang2, reverse=False):

定义函数prepareData,它接受三个参数:lang1lang2是两种语言的名称,reverse是一个布尔值,用于指示是否需要反转语言对的顺序。

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

调用readLangs函数,它从文件中读取数据并返回输入语言、输出语言和句子对列表。

python 复制代码
    print("Read %s sentence pairs" % len(pairs))

打印一条消息,表示已经读取了指定数量的句子对。

python 复制代码
    # 按条件选取语料
    pairs = filterPairs(pairs[:])

使用filterPairs函数对句子对进行过滤,确保它们满足特定的条件。pairs[:]创建了pairs列表的副本,这样原始的pairs列表不会被修改。

python 复制代码
    print("Trimmed to %s sentence pairs" % len(pairs))

打印一条消息,表示过滤后剩下的句子对数量。

python 复制代码
    print("Counting words...")

打印一条消息,表示开始计数单词。

python 复制代码
    # 将语料保存至相应的语言类
    for pair in pairs:
        input_lang.addSentence(pair[0])
        output_lang.addSentence(pair[1])

遍历过滤后的句子对列表,将每个句子添加到相应的语言类中。

python 复制代码
    # 打印语言类的信息    
    print("Counted words:")
    print(input_lang.name, input_lang.n_words)
    print(output_lang.name, output_lang.n_words)

打印输入语言和输出语言的信息,包括它们的名称和单词数量。

python 复制代码
    return input_lang, output_lang, pairs

函数返回输入语言、输出语言和过滤后的句子对列表。

python 复制代码
input_lang, output_lang, pairs = prepareData('eng', 'fra', True)

调用prepareData函数,传入参数'eng''fra',并设置reverseTrue,表示需要反转句子对。

python 复制代码
print(random.choice(pairs))

打印一个随机选择的句子对,用于验证数据处理是否正确。

综上所述,prepareData函数读取数据,过滤句子对,将句子添加到语言类中,并返回处理后的输入语言、输出语言和句子对列表。

输出

Reading lines...

Read 135842 sentence pairs

Trimmed to 10599 sentence pairs

Counting words...

Counted words:

fra 4345

eng 2803

['vous gaspillez mon temps .', 'you re wasting my time .']

三、seq2seq模型

1.编码器

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

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

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

这段代码定义了一个名为EncoderRNN的类,它是PyTorch中的一个神经网络模块,用于实现序列到序列(seq2seq)模型中的编码器部分。

python 复制代码
class EncoderRNN(nn.Module):
python 复制代码
def __init__(self, input_size, hidden_size):

这行定义了EncoderRNN类的构造函数。它接受两个参数:input_sizehidden_sizeinput_size是输入序列中单词的数量,通常对应于词汇表的大小。hidden_size是GRU单元的隐藏状态的大小,它决定了模型能够学习到的复杂度。

python 复制代码
    super(EncoderRNN, self).__init__()

这行代码调用父类nn.Module的构造函数。nn.Module是PyTorch中所有神经网络模块的基类,它提供了神经网络的基础功能,如参数管理、前向传播和反向传播。通过调用super(EncoderRNN, self).__init__()EncoderRNN类继承了nn.Module类的所有功能。

python 复制代码
    self.hidden_size = hidden_size

这行代码将hidden_size参数设置为EncoderRNN类的属性。这个属性将在后续的代码中用于访问和修改隐藏状态的大小。

python 复制代码
    self.embedding = nn.Embedding(input_size, hidden_size)

这行代码创建了一个嵌入层(self.embedding)。嵌入层是一个线性层,它将输入的整数索引(代表单词)转换为固定大小的向量。在这里,嵌入层的输入大小是input_size,输出大小是hidden_size

python 复制代码
    self.gru = nn.GRU(hidden_size, hidden_size)

这行代码创建了一个GRU(门控循环单元)层(self.gru)。GRU是一种RNN(循环神经网络)的变体,它将传统的RNN的三个门(输入门、遗忘门和输出门)合并为两个门(更新门和重置门)。在这里,GRU的输入大小和隐藏大小都是hidden_size

综上所述,EncoderRNN类的构造函数__init__负责初始化类实例的属性,包括设置隐藏状态的大小、创建嵌入层和GRU层。这些步骤是构建一个序列到序列模型中编码器组件的基础。

在用户引用的对话内容中,我们看到了EncoderRNN类的forward方法。这个方法定义了前向传播的逻辑,即神经网络在输入数据上的计算过程。下面是详细解释:

python 复制代码
def forward(self, input, hidden):

这行定义了EncoderRNN类的forward方法。它接受两个参数:inputhiddeninput是当前时间步的输入序列,通常是单词的索引。hidden是GRU单元的隐藏状态,它是从前一个时间步传递过来的,用于初始化当前时间步的隐藏状态。

python 复制代码
    embedded = self.embedding(input).view(1, 1, -1)

这行代码首先通过嵌入层(self.embedding)将输入的单词索引转换为嵌入向量。嵌入向量是一个固定大小的向量,其长度等于hidden_size。然后,这个嵌入向量被展平成一个三维张量,其形状为(1, 1, -1),其中-1表示自动推断的维度。由于输入通常是一个单词索引,因此嵌入向量只有一行和一列,但有多列(因为每个单词有一个嵌入向量)。

python 复制代码
    output = embedded

这行代码将嵌入向量赋值给output变量。在GRU的第一个时间步,outputembedded是相同的,因为输入只有一个单词。

python 复制代码
    output, hidden = self.gru(output, hidden)

这行代码使用GRU(self.gru)处理嵌入向量,并返回新的输出和隐藏状态。GRU是一种RNN的变体,它通过两个门(更新门和重置门)来处理序列数据。hidden是从前一个时间步传递过来的隐藏状态,用于初始化当前时间步的隐藏状态。GRU返回一个新的隐藏状态,这是下一个时间步的输入。

python 复制代码
    return output, hidden

这行代码返回GRU的输出和新的隐藏状态。输出是当前时间步的GRU输出,它将作为下一个时间步的输入。隐藏状态是当前时间步的GRU隐藏状态,它将被传递到下一个时间步。

综上所述,EncoderRNN类的forward方法定义了前向传播的逻辑,即输入序列如何通过嵌入层和GRU层处理,以及隐藏状态如何在每个时间步传递。这个方法是构建序列到序列模型中编码器组件的关键步骤。

python 复制代码
    def initHidden(self):
        return torch.zeros(1, 1, self.hidden_size, device=device)

这行定义了一个名为initHidden的方法,它用于初始化GRU的隐藏状态。这个方法返回一个全零的三维张量,其形状为(1, 1, self.hidden_size),表示单个时间步的初始隐藏状态。device是一个属性,用于指定模型将在哪个设备上运行,例如CPU或GPU。
综上所述,EncoderRNN类定义了一个编码器RNN,它将输入序列转换为一个连续的隐藏状态序列,这些状态可以用于后续的解码过程。

2.解码器

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

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

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

这段代码定义了一个名为DecoderRNN的类,它是PyTorch中的一个神经网络模块,用于实现序列到序列(seq2seq)模型中的解码器部分。下面是逐行解释:

python 复制代码
class DecoderRNN(nn.Module):

这行定义了一个名为DecoderRNN的类,它继承自nn.Module,这是PyTorch中用于定义神经网络的核心类。

在用户引用的对话内容中,我们看到了DecoderRNN类的构造函数__init__。这个构造函数是Python中类的一个特殊方法,用于在创建类的实例时进行初始化。

python 复制代码
def __init__(self, hidden_size, output_size):

这行定义了DecoderRNN类的构造函数。它接受两个参数:hidden_sizeoutput_sizehidden_size是GRU单元的隐藏状态的大小,它决定了模型能够学习到的复杂度。output_size是输出序列中单词的数量,通常对应于目标语言词汇表的大小。

python 复制代码
    super(DecoderRNN, self).__init__()

这行代码调用父类nn.Module的构造函数。nn.Module是PyTorch中所有神经网络模块的基类,它提供了神经网络的基础功能,如参数管理、前向传播和反向传播。通过调用super(DecoderRNN, self).__init__()DecoderRNN类继承了nn.Module类的所有功能。

python 复制代码
    self.hidden_size = hidden_size

这行代码将hidden_size参数设置为DecoderRNN类的属性。这个属性将在后续的代码中用于访问和修改隐藏状态的大小。

python 复制代码
    self.embedding = nn.Embedding(output_size, hidden_size)

这行代码创建了一个嵌入层(self.embedding)。嵌入层是一个线性层,它将输入的整数索引(代表单词)转换为固定大小的向量。在这里,嵌入层的输入大小是output_size,输出大小是hidden_size。这意味着每个输出单词的索引都会被转换为一个大小为hidden_size的向量。

python 复制代码
    self.gru = nn.GRU(hidden_size, hidden_size)

这行代码创建了一个GRU(门控循环单元)层(self.gru)。GRU是一种RNN(循环神经网络)的变体,它将传统的RNN的三个门(输入门、遗忘门和输出门)合并为两个门(更新门和重置门)。在这里,GRU的输入大小和隐藏大小都是hidden_size。这意味着GRU的输入和输出都是大小为hidden_size的向量。

python 复制代码
    self.out = nn.Linear(hidden_size, output_size)

这行代码创建了一个线性层(self.out)。线性层是一个简单的全连接层,它将输入的向量转换为输出的向量。在这里,线性层的输入大小是hidden_size,输出大小是output_size。这意味着GRU的输出将被转换为大小为output_size的向量,其中output_size是目标语言词汇表的大小。

python 复制代码
    self.softmax = nn.LogSoftmax(dim=1)

这行代码创建了一个softmax层(self.softmax)。softmax层是一个非线性层,它将输入的向量转换为概率分布。在这里,softmax层的维度是1,这意味着它将每个输出向量转换为大小为1的向量,其中每个元素都是0或1,且所有元素之和为1。这样,GRU的输出就被转换为目标语言词汇表中每个单词的概率分布。

综上所述,DecoderRNN类的构造函数__init__负责初始化类实例的属性,包括设置隐藏状态的大小、创建嵌入层、GRU层、线性层和softmax层。这些步骤是构建序列到序列模型中解码器组件的基础。

在用户引用的对话内容中,我们看到了DecoderRNN类的构造函数__init__。这个构造函数是Python中类的一个特殊方法,用于在创建类的实例时进行初始化。

python 复制代码
def __init__(self, hidden_size, output_size):

这行定义了DecoderRNN类的构造函数。它接受两个参数:hidden_sizeoutput_sizehidden_size是GRU单元的隐藏状态的大小,它决定了模型能够学习到的复杂度。output_size是输出序列中单词的数量,通常对应于目标语言词汇表的大小。

python 复制代码
    super(DecoderRNN, self).__init__()

这行代码调用父类nn.Module的构造函数。nn.Module是PyTorch中所有神经网络模块的基类,它提供了神经网络的基础功能,如参数管理、前向传播和反向传播。通过调用super(DecoderRNN, self).__init__()DecoderRNN类继承了nn.Module类的所有功能。

python 复制代码
    self.hidden_size = hidden_size

这行代码将hidden_size参数设置为DecoderRNN类的属性。这个属性将在后续的代码中用于访问和修改隐藏状态的大小。

python 复制代码
    self.embedding = nn.Embedding(output_size, hidden_size)

这行代码创建了一个嵌入层(self.embedding)。嵌入层是一个线性层,它将输入的整数索引(代表单词)转换为固定大小的向量。在这里,嵌入层的输入大小是output_size,输出大小是hidden_size。这意味着每个输出单词的索引都会被转换为一个大小为hidden_size的向量。

python 复制代码
    self.gru = nn.GRU(hidden_size, hidden_size)

这行代码创建了一个GRU(门控循环单元)层(self.gru)。GRU是一种RNN(循环神经网络)的变体,它将传统的RNN的三个门(输入门、遗忘门和输出门)合并为两个门(更新门和重置门)。在这里,GRU的输入大小和隐藏大小都是hidden_size。这意味着GRU的输入和输出都是大小为hidden_size的向量。

python 复制代码
    self.out = nn.Linear(hidden_size, output_size)

这行代码创建了一个线性层(self.out)。线性层是一个简单的全连接层,它将输入的向量转换为输出的向量。在这里,线性层的输入大小是hidden_size,输出大小是output_size。这意味着GRU的输出将被转换为大小为output_size的向量,其中output_size是目标语言词汇表的大小。

python 复制代码
    self.softmax = nn.LogSoftmax(dim=1)

这行代码创建了一个softmax层(self.softmax)。softmax层是一个非线性层,它将输入的向量转换为概率分布。在这里,softmax层的维度是1,这意味着它将每个输出向量转换为大小为1的向量,其中每个元素都是0或1,且所有元素之和为1。这样,GRU的输出就被转换为目标语言词汇表中每个单词的概率分布。

综上所述,DecoderRNN类的构造函数__init__负责初始化类实例的属性,包括设置隐藏状态的大小、创建嵌入层、GRU层、线性层和softmax层。这些步骤是构建序列到序列模型中解码器组件的基础。

python 复制代码
    def initHidden(self):
        return torch.zeros(1, 1, self.hidden_size, device=device)

这行定义了一个名为initHidden的方法,它用于初始化GRU的隐藏状态。这个方法返回一个全零的三维张量,其形状为(1, 1, self.hidden_size),表示单个时间步的初始隐藏状态。device是一个属性,用于指定模型将在哪个设备上运行,例如CPU或GPU。

综上所述,DecoderRNN类定义了一个解码器RNN,它将输入的单词索引转换为输出序列的概率分布,用于生成翻译。通过这个过程,模型可以学习如何从源语言的句子生成目标语言的句子。

四、训练

1.数据预处理

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

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

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

这段代码定义了三个函数,用于将文本数据转化为模型可以处理的数字和Tensor格式。

python 复制代码
def indexesFromSentence(lang, sentence):

这行定义了一个名为indexesFromSentence的函数,它接受两个参数:langsentencelang是一个Lang类的实例,用于处理和存储某种语言的词汇。sentence是一个字符串,代表要转换的句子。

python 复制代码
    return [lang.word2index[word] for word in sentence.split(' ')]

这行代码定义了函数的逻辑。它遍历句子中的每个单词,并使用lang.word2index字典查找每个单词对应的索引。这个字典是由Lang类的addWord方法构建的,它将单词映射到其在词汇表中的索引。每个单词的索引被添加到一个列表中,该列表包含了句子中所有单词的索引。

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

这行定义了一个名为tensorFromSentence的函数,它接受两个参数:langsentencelang是一个Lang类的实例,用于处理和存储某种语言的词汇。sentence是一个字符串,代表要转换的句子。

python 复制代码
    indexes = indexesFromSentence(lang, sentence)
    indexes.append(EOS_token)
    return torch.tensor(indexes, dtype=torch.long, device=device).view(-1, 1)

这行代码定义了函数的逻辑。它首先调用indexesFromSentence函数,获取句子中单词的索引列表。然后,它将EOS(结束符)的索引添加到列表的末尾。最后,它使用torch.tensor函数将索引列表转换为Tensor。dtype=torch.long参数指定Tensor的数据类型为长整型,device=device参数指定Tensor将在哪个设备上运行,例如CPU或GPU。最后,它使用view方法将Tensor的形状重置为(-1, 1),这意味着Tensor的形状可以根据需要自动推断,但是至少有1行和1列。

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

这行定义了一个名为tensorsFromPair的函数,它接受一个参数:pairpair是一个列表,包含两个字符串,分别代表源语言和目标语言的句子。

python 复制代码
    input_tensor  = tensorFromSentence(input_lang, pair[0])
    target_tensor = tensorFromSentence(output_lang, pair[1])
    return (input_tensor, target_tensor)

这行代码定义了函数的逻辑。它首先调用tensorFromSentence函数,将源语言句子转换为Tensor。然后,它调用tensorFromSentence函数,将目标语言句子转换为Tensor。最后,它返回一个包含两个Tensor的元组,分别代表源语言和目标语言的句子。

在连续的参数变化过程中,我们可以看到以下步骤:

  1. indexesFromSentence函数接受一个句子sentence,并通过split(' ')将句子分割成单词列表。
  2. 对于每个单词,word2index字典被用来查找单词的索引,如果单词不在字典中,则添加新单词并为其分配一个索引。
  3. 所有单词的索引被添加到一个列表中。
  4. tensorFromSentence函数接受indexes列表,并将其转换为一个Tensor。
  5. 在转换过程中,EOS标记被添加到列表的末尾。
  6. Tensor的形状被重置为(-1, 1),这意味着Tensor的形状可以根据需要自动推断,但是至少有1行和1列。
  7. tensorsFromPair函数接受一个句子对pair,并分别调用tensorFromSentence函数来转换源语言和目标语言的句子。
  8. 转换后的Tensor被返回为一个元组,包含源语言和目标语言的Tensor。

2.训练函数

python 复制代码
teacher_forcing_ratio = 0.5

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

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

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

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

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

    loss.backward()

    encoder_optimizer.step()
    decoder_optimizer.step()

    return loss.item() / target_length

这里引入一种技术

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

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

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

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

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

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

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

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

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

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

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

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

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

这段代码定义了一个名为train的函数,用于训练一个序列到序列(seq2seq)模型的编码器和解码器。

python 复制代码
teacher_forcing_ratio = 0.5

这行代码定义了一个名为teacher_forcing_ratio的常量,其值为0.5。这个参数用于控制训练过程中是否使用教师强制(teacher forcing)策略。教师强制是一种训练技巧,其中解码器的下一个输入是从目标序列中硬编码的,而不是基于解码器的当前输出。

python 复制代码
def train(input_tensor, target_tensor, 
          encoder, decoder, 
          encoder_optimizer, decoder_optimizer, 
          criterion, max_length=MAX_LENGTH):

这行定义了train函数,它接受多个参数:

  • input_tensor:编码器输入的Tensor。
  • target_tensor:解码器的目标Tensor。
  • encoder:编码器模型。
  • decoder:解码器模型。
  • encoder_optimizer:编码器的优化器。
  • decoder_optimizer:解码器的优化器。
  • criterion:损失函数。
  • max_length:句子的最大长度,默认为MAX_LENGTH
python 复制代码
    encoder_hidden = encoder.initHidden()

这行代码初始化编码器的隐藏状态。encoder.initHidden()返回一个全零的三维张量,其形状为(1, 1, self.hidden_size),表示单个时间步的初始隐藏状态。

python 复制代码
    encoder_optimizer.zero_grad()
    decoder_optimizer.zero_grad()

这行代码将编码器和解码器的梯度属性归零,这是为了确保在反向传播过程中不会累积前一个时间步的梯度。

python 复制代码
   input_length  = input_tensor.size(0)
    target_length = target_tensor.size(0)

这行代码获取输入张量和目标张量的长度。size(0)表示张量的第一个维度,即句子中的单词数量。

python 复制代码
encoder_outputs = torch.zeros(max_length, encoder.hidden_size, device=device)
loss = 0

这行代码创建一个全零的三维张量,用于存储编码器的输出。encoder_outputs张量的形状为(max_length, encoder.hidden_size)loss变量用于累加每个时间步的损失。

python 复制代码
for ei in range(input_length):
    encoder_output, encoder_hidden = encoder(input_tensor[ei], encoder_hidden)
    encoder_outputs[ei]            = encoder_output[0, 0]

这行代码遍历输入张量中的每个单词,并将它们送入编码器。encoder(input_tensor[ei], encoder_hidden)返回编码器的输出和新的隐藏状态。encoder_outputs[ei]被更新为当前时间步的编码器输出。

python 复制代码
    decoder_input  = torch.tensor([[SOS_token]], device=device)
    decoder_hidden = encoder_hidden

这行代码初始化解码器的输入和隐藏状态。decoder_input是一个包含SOS标记的Tensor,表示解码器的第一个输入。decoder_hidden是从编码器传递过来的隐藏状态。

python 复制代码
    use_teacher_forcing = True if random.random() < teacher_forcing_ratio else False

这行代码使用随机数来决定是否使用教师强制策略。如果随机数小于teacher_forcing_ratio,则使用教师强制;否则不使用。

至此,函数的初始化部分已经完成,包括编码器的隐藏状态初始化、优化器梯度清零、输入和目标长度的获取、编码器输出张量的创建、损失变量的初始化等。

下面train函数的剩余部分,用于执行实际的训练过程。

python 复制代码
if use_teacher_forcing:

这行代码检查是否使用教师强制策略。如果use_teacher_forcingTrue,则执行教师强制策略。

这段代码是train函数中的一个循环,用于在训练过程中执行解码器的正向传播,并使用教师强制策略。

python 复制代码
 # Teacher forcing: Feed the target as the next input
for di in range(target_length):

这行代码开始一个循环,循环次数等于目标序列的长度。di是循环变量,用于索引目标序列中的每个单词。

python 复制代码
    decoder_output, decoder_hidden = decoder(decoder_input, decoder_hidden)

这行代码调用解码器(decoder)函数,将当前的输入(decoder_input)和隐藏状态(decoder_hidden)作为输入,并返回解码器的输出(decoder_output)和新的隐藏状态。

python 复制代码
    loss         += criterion(decoder_output, target_tensor[di])

这行代码计算当前时间步的损失。它将解码器的输出与目标序列中对应单词的索引(target_tensor[di])进行比较,并使用损失函数(criterion)计算损失。损失被累加到loss变量中。

python 复制代码
    decoder_input = target_tensor[di]  # Teacher forcing

这行代码更新解码器的下一个输入。在教师强制(Teacher Forcing)的情况下,这个输入是目标序列中下一个单词的索引。这意味着解码器的输入不依赖于解码器自身的输出,而是直接来自目标序列。
综上所述,这段代码定义了一个循环,用于在每个时间步执行解码器的正向传播,并使用教师强制策略。在循环中,解码器接受当前的输入和隐藏状态,生成一个输出,并计算当前时间步的损失。然后,使用目标序列中下一个单词的索引作为下一个输入,准备进入下一个时间步的循环。

这段代码是train函数中的一个循环,用于在训练过程中执行解码器的正向传播,但不使用教师强制策略。

python 复制代码
 # Without teacher forcing: use its own predictions as the next input
for di in range(target_length):

这行代码开始一个循环,循环次数等于目标序列的长度。di是循环变量,用于索引目标序列中的每个单词。

python 复制代码
    decoder_output, decoder_hidden = decoder(decoder_input, decoder_hidden)

这行代码调用解码器(decoder)函数,将当前的输入(decoder_input)和隐藏状态(decoder_hidden)作为输入,并返回解码器的输出(decoder_output)和新的隐藏状态。

python 复制代码
    topv, topi    = decoder_output.topk(1)

这行代码使用decoder_output张量的topk(1)方法来获取概率最高的单词索引。topk(1)返回两个张量:topv包含概率最高的单词的概率值,topi包含概率最高的单词的索引。

python 复制代码
    decoder_input = topi.squeeze().detach()  # detach from history as input

这行代码将topi张量中的索引转换为一个标量,并使用.detach()方法将其从计算图中分离出来,这样它的梯度就不会被反向传播。这允许我们使用解码器输出的预测作为下一个输入,而不需要依赖于目标序列。

python 复制代码
    loss         += criterion(decoder_output, target_tensor[di])

这行代码计算当前时间步的损失。它将解码器的输出与目标序列中对应单词的索引(target_tensor[di])进行比较,并使用损失函数(criterion)计算损失。损失被累加到loss变量中。

python 复制代码
    if decoder_input.item() == EOS_token:
        break

这行代码检查解码器的下一个输入是否为结束符(EOS_token)。如果是,则跳出循环,因为解码器已经完成了整个目标序列的生成。
综上所述,这段代码定义了一个循环,用于在每个时间步执行解码器的正向传播,但不使用教师强制策略。在循环中,解码器接受当前的输入和隐藏状态,生成一个输出,并计算当前时间步的损失。然后,使用解码器输出的预测作为下一个输入,准备进入下一个时间步的循环。如果解码器生成了结束符,循环结束。

python 复制代码
    loss.backward()

这行代码计算损失的梯度。

python 复制代码
    encoder_optimizer.step()
    decoder_optimizer.step()

这行代码执行优化器的更新步骤,以减小损失。

python 复制代码
    return loss.item() / target_length

这行代码返回平均损失值,即总损失除以目标序列的长度。
综上所述,这段代码定义了一个训练循环,其中包含教师强制和不使用教师强制两种情况。在教师强制情况下,解码器的输入来自目标序列;在不使用教师强制的情况下,解码器的输入是基于解码器输出的预测。在循环结束后,损失被反向传播,然后优化器被用来更新模型参数。最后,函数返回平均损失值。

python 复制代码
import time
import math

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

def timeSince(since, percent):
    now = time.time()
    s = now - since
    es = s / (percent)
    rs = es - s
    return '%s (- %s)' % (asMinutes(s), asMinutes(rs))

这段代码定义了两个函数,asMinutestimeSince,用于将时间转换为分钟和秒的格式,以及计算从某个时间点到现在所花费的时间,并以分钟和秒的格式表示。

python 复制代码
import time
import math

这两行代码导入了timemath模块。time模块用于处理时间相关操作,如获取当前时间、计算时间差等。math模块包含数学运算的函数,如math.floor用于向下取整。

python 复制代码
def asMinutes(s):

这行定义了一个名为asMinutes的函数,它接受一个参数s,代表要转换的时间秒数。

python 复制代码
    m = math.floor(s / 60)

这行代码计算总时间秒数除以60,得到总时间的分钟数。使用math.floor函数向下取整,以得到完整的分钟数。

python 复制代码
    s -= m * 60

这行代码从总时间秒数中减去总时间的分钟数乘以60,得到剩余的秒数。

python 复制代码
    return '%dm %ds' % (m, s)

这行代码将总时间的分钟数和剩余的秒数格式化为字符串,并以%dm %ds的格式返回。%dm表示整数分钟数,%ds表示剩余的秒数。

python 复制代码
def timeSince(since, percent):

这行定义了一个名为timeSince的函数,它接受两个参数:sincepercentsince是开始计时的时间戳,percent是完成的时间比例。

python 复制代码
    now = time.time()

这行代码获取当前时间的时间戳。

python 复制代码
    s = now - since

这行代码计算从since时间戳到现在的时间差,得到总时间秒数。

python 复制代码
    es = s / (percent)

这行代码计算完成percent比例所需的时间秒数。

python 复制代码
    rs = es - s

这行代码计算剩余时间秒数,即完成percent比例所需的时间减去已经过去的时间。

python 复制代码
    return '%s (- %s)' % (asMinutes(s), asMinutes(rs))

这行代码将当前时间与since时间的时间差格式化为字符串,并使用asMinutes函数将剩余时间格式化为字符串。最后,以%s (- %s)的格式返回,其中%s表示当前时间与since时间的时间差,%s表示剩余时间。

综上所述,这两个函数分别用于将时间转换为分钟和秒的格式,以及计算从某个时间点到现在所花费的时间,并以分钟和秒的格式表示。

python 复制代码
def trainIters(encoder,decoder,n_iters,print_every=1000,
               plot_every=100,learning_rate=0.01):
    
    start = time.time()
    plot_losses      = []
    print_loss_total = 0  # Reset every print_every
    plot_loss_total  = 0  # Reset every plot_every

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

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

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

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

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

    return plot_losses

这段代码定义了一个名为trainIters的函数,用于训练一个序列到序列(seq2seq)模型的编码器和解码器。下面是代码的分段大致讲解:

  1. 函数定义

    python 复制代码
    def trainIters(encoder, decoder, n_iters, print_every=1000, plot_every=100, learning_rate=0.01):

    这行定义了trainIters函数,它接受五个参数:

    • encoder:编码器模型。
    • decoder:解码器模型。
    • n_iters:训练迭代次数。
    • print_every:每多少次迭代打印一次损失。
    • plot_every:每多少次迭代画一次损失图。
    • learning_rate:学习率。
  2. 初始化

    python 复制代码
    start = time.time()
    plot_losses      = []
    print_loss_total = 0  # Reset every print_every
    plot_loss_total  = 0  # Reset every plot_every

    这行代码初始化了几个变量:

    • start:记录训练开始的时间。
    • plot_losses:存储每次迭代损失的列表,用于绘制损失图。
    • print_loss_total:用于存储打印时的总损失,每print_every次迭代重置为0。
    • plot_loss_total:用于存储绘图时的总损失,每plot_every次迭代重置为0。
  3. 优化器设置

    python 复制代码
    encoder_optimizer = optim.SGD(encoder.parameters(), lr=learning_rate)
    decoder_optimizer = optim.SGD(decoder.parameters(), lr=learning_rate)

    这行代码创建了两个优化器,一个用于编码器,另一个用于解码器,并设置了相同的学习率。

  4. 训练数据准备

    python 复制代码
    training_pairs    = [tensorsFromPair(random.choice(pairs)) for i in range(n_iters)]

    这行代码创建了一个列表training_pairs,其中包含了n_iters个随机选择的句子对。

  5. 损失函数和迭代循环

    python 复制代码
    criterion         = nn.NLLLoss()
    for iter in range(1, n_iters + 1):
        training_pair = training_pairs[iter - 1]
        input_tensor  = training_pair[0]
        target_tensor = training_pair[1]
        loss = train(input_tensor, target_tensor, encoder,
                    decoder, encoder_optimizer, decoder_optimizer, criterion)
        print_loss_total += loss
        plot_loss_total  += loss
        if iter % print_every == 0:
            print_loss_avg   = print_loss_total / print_every
            print_loss_total = 0
            print('%s (%d %d%%) %.4f' % (timeSince(start, iter / n_iters),
                                          iter, iter / n_iters * 100, print_loss_avg))
        if iter % plot_every == 0:
            plot_loss_avg = plot_loss_total / plot_every
            plot_losses.append(plot_loss_avg)
            plot_loss_total = 0

    这行代码定义了损失函数nn.NLLLoss(),并进入了一个循环,循环次数等于n_iters。在每次迭代中,从training_pairs中随机选择一个句子对,并将其转换为Tensor格式。然后,调用train函数进行训练,并更新print_loss_totalplot_loss_total。如果迭代次数满足print_everyplot_every的条件,就会打印或记录当前的平均损失。

  6. 返回损失列表

    python 复制代码
    return plot_losses

    这行代码在循环结束后返回plot_losses

五、训练与评估

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

plot_losses = trainIters(encoder1, attn_decoder1, 20000, print_every=5000)
python 复制代码
hidden_size = 256

这行代码定义了一个名为hidden_size的常量,其值为256。这个值代表编码器和解码器中隐藏层的大小。

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

这行代码创建了一个名为encoder1EncoderRNN对象,它具有input_lang.n_words个输入词汇和hidden_size大小的隐藏层。然后,将这个模型移动到指定的设备上,这里使用device属性来指定设备,例如CPU或GPU。

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

这行代码创建了一个名为attn_decoder1DecoderRNN对象,它具有hidden_size大小的隐藏层和output_lang.n_words个输出词汇。同样,这个模型也被移动到指定的设备上。

python 复制代码
plot_losses = trainIters(encoder1, attn_decoder1, 20000, print_every=5000)

这行代码调用trainIters函数来训练encoder1attn_decoder1模型。trainIters函数接受两个参数:编码器和解码器模型,以及训练迭代次数20000print_every参数设置为5000,意味着每5000次迭代打印一次损失。plot_losses变量用于存储训练过程中的损失值,这些值将在后续的代码中用于绘制损失曲线。

综上所述,这段代码创建了编码器和解码器模型,并将它们移动到指定的设备上,然后调用trainIters函数进行训练,并返回训练过程中的损失值。这些损失值可以用于可视化训练过程,以监控模型的性能。

输出

1m 23s (- 4m 9s) (5000 25%) 2.9220

2m 38s (- 2m 38s) (10000 50%) 2.3415

3m 55s (- 1m 18s) (15000 75%) 2.0470

5m 13s (- 0m 0s) (20000 100%) 1.7842

python 复制代码
import matplotlib.pyplot as plt
#隐藏警告
import warnings
warnings.filterwarnings("ignore")               # 忽略警告信息
# plt.rcParams['font.sans-serif']    = ['SimHei'] # 用来正常显示中文标签
plt.rcParams['axes.unicode_minus'] = False      # 用来正常显示负号
plt.rcParams['figure.dpi']         = 100        # 分辨率

epochs_range = range(len(plot_losses))

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

plt.subplot(1, 1, 1)
plt.plot(epochs_range, plot_losses, label='Training Loss')
plt.legend(loc='upper right')
plt.title('Training Loss')
plt.show()
相关推荐
ersaijun1 小时前
【Obsidian】当笔记接入AI,Copilot插件推荐
人工智能·笔记·copilot
易雪寒1 小时前
Maven从入门到精通(三)
java·python·maven
FreakStudio1 小时前
全网最适合入门的面向对象编程教程:49 Python函数方法与接口-函数与方法的区别和lamda匿名函数
python·嵌入式·面向对象·电子diy
Good_tea_h1 小时前
如何实现Java中的多态性
java·开发语言·python
IT毕设梦工厂2 小时前
计算机毕业设计选题推荐-项目评审系统-Java/Python项目实战
java·spring boot·python·django·毕业设计·源码·课程设计
格林威2 小时前
Baumer工业相机堡盟工业相机如何通过BGAPISDK使用短曝光功能(曝光可设置1微秒)(C语言)
c语言·开发语言·人工智能·数码相机·计算机视觉
学术头条2 小时前
【直播预告】从人工智能到类脑与量子计算:数学与新计算范式
人工智能·科技·安全·语言模型·量子计算
零 度°2 小时前
Qiskit:量子计算的Python工具包
python
有Li2 小时前
《PneumoLLM:利用大型语言模型的力量进行尘肺病诊断》|文献速递--基于深度学习的医学影像病灶分割
人工智能·深度学习·语言模型
飘逸高铁侠2 小时前
使用Python实现多个PDF文件的合并
开发语言·python·pdf