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

  • 🍨 本文为🔗365天深度学习训练营 中的学习记录博客
  • 🍖 原作者:K同学啊

理论基础

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。这决定了神经网络模型将在哪个设备上运行。
from __future__ import unicode_literals, print_function, division
from io import open
import unicodedata
import string
import re
import randomimport torch
import torch.nn as nn
from torch import optim
import torch.nn.functional as Fdevice = torch.device("cuda" if torch.cuda.is_available() else "cpu")
print(device)

输出
cuda

二、前期的语料处理

1.搭建语言类

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

SOS_token = 0
EOS_token = 1

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

class Lang:def __init__(self, name):self.name = nameself.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)。
    def addSentence(self, sentence):for word in sentence.split(' '):self.addWord(word)

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

    def addWord(self, word):if word not in self.word2index:self.word2index[word] = self.n_wordsself.word2count[word] = 1self.index2word[self.n_words] = wordself.n_words += 1else:self.word2count[word] += 1

addWord方法用于将单个单词添加到词汇表中。如果单词不在词汇表中,它会将单词添加到word2index字典中,并为其分配一个新的索引(self.n_words),然后在index2word字典中记录这个索引到单词的映射,并更新n_words计数。如果单词已经在词汇表中,它会更新word2count字典中该单词的计数。
总的来说,Lang类提供了一个方便的方式来构建和处理与特定语言相关的词汇表,这在序列到序列的学习任务(如机器翻译)中是非常重要的。

2.文本处理函数

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

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等效形式。这是因为在分解后,非间距标记会被独立出来,从而可以轻松地被过滤掉。
# 小写化,剔除标点与非字母符号
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、文件读取函数

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的函数,用于读取和预处理一对语言的文本数据。

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

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

    print("Reading lines...")

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

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

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

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

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

    # 创建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是第一种语言。

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

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

    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在教案中是%
但是会出现这样的报错
在这里插入图片描述这个错误信息表明在尝试使用字符串格式化时出现了问题。具体来说,错误发生在这一行代码中:

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() 方法的示例:

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

或者,使用f-strings:

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

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


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

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

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

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

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
def filterPairs(pairs):# 选取仅仅包含 eng_prefixes 开头的语料return [pair for pair in pairs if filterPair(pair)]

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

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, pairsinput_lang, output_lang, pairs = prepareData('eng', 'fra', True)
print(random.choice(pairs))

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

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

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

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

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

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

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

    # 按条件选取语料pairs = filterPairs(pairs[:])

使用filterPairs函数对句子对进行过滤,确保它们满足特定的条件。pairs[:]创建了pairs列表的副本,这样原始的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)

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

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.编码器

class EncoderRNN(nn.Module):def __init__(self, input_size, hidden_size):super(EncoderRNN, self).__init__()self.hidden_size = hidden_sizeself.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         = embeddedoutput, hidden = self.gru(output, hidden)return output, hiddendef initHidden(self):return torch.zeros(1, 1, self.hidden_size, device=device)

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

class EncoderRNN(nn.Module):
def __init__(self, input_size, hidden_size):

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

    super(EncoderRNN, self).__init__()

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

    self.hidden_size = hidden_size

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

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

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

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

这行代码创建了一个GRU(门控循环单元)层(self.gru)。GRU是一种RNN(循环神经网络)的变体,它将传统的RNN的三个门(输入门、遗忘门和输出门)合并为两个门(更新门和重置门)。在这里,GRU的输入大小和隐藏大小都是hidden_size
综上所述,EncoderRNN类的构造函数__init__负责初始化类实例的属性,包括设置隐藏状态的大小、创建嵌入层和GRU层。这些步骤是构建一个序列到序列模型中编码器组件的基础。

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

def forward(self, input, hidden):

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

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

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

    output = embedded

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

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

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

    return output, hidden

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

    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.解码器

class DecoderRNN(nn.Module):def __init__(self, hidden_size, output_size):super(DecoderRNN, self).__init__()self.hidden_size = hidden_sizeself.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, hiddendef initHidden(self):return torch.zeros(1, 1, self.hidden_size, device=device)

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

class DecoderRNN(nn.Module):

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

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

def __init__(self, hidden_size, output_size):

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

    super(DecoderRNN, self).__init__()

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

    self.hidden_size = hidden_size

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

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

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

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

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

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

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

    self.softmax = nn.LogSoftmax(dim=1)

这行代码创建了一个softmax层(self.softmax)。softmax层是一个非线性层,它将输入的向量转换为概率分布。在这里,softmax层的维度是1,这意味着它将每个输出向量转换为大小为1的向量,其中每个元素都是0或1,且所有元素之和为1。这样,GRU的输出就被转换为目标语言词汇表中每个单词的概率分布。
综上所述,DecoderRNN类的构造函数__init__负责初始化类实例的属性,包括设置隐藏状态的大小、创建嵌入层、GRU层、线性层和softmax层。这些步骤是构建序列到序列模型中解码器组件的基础。

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

def __init__(self, hidden_size, output_size):

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

    super(DecoderRNN, self).__init__()

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

    self.hidden_size = hidden_size

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

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

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

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

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

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

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

    self.softmax = nn.LogSoftmax(dim=1)

这行代码创建了一个softmax层(self.softmax)。softmax层是一个非线性层,它将输入的向量转换为概率分布。在这里,softmax层的维度是1,这意味着它将每个输出向量转换为大小为1的向量,其中每个元素都是0或1,且所有元素之和为1。这样,GRU的输出就被转换为目标语言词汇表中每个单词的概率分布。
综上所述,DecoderRNN类的构造函数__init__负责初始化类实例的属性,包括设置隐藏状态的大小、创建嵌入层、GRU层、线性层和softmax层。这些步骤是构建序列到序列模型中解码器组件的基础。

    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.数据预处理

# 将文本数字化,获取词汇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格式。

def indexesFromSentence(lang, sentence):

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

    return [lang.word2index[word] for word in sentence.split(' ')]

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

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

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

    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列。

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

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

    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.训练函数

teacher_forcing_ratio = 0.5def 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_hiddenuse_teacher_forcing = True if random.random() < teacher_forcing_ratio else False# 将编码器处理好的输出送入解码器if use_teacher_forcing:# Teacher forcing: Feed the target as the next inputfor 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 forcingelse:# Without teacher forcing: use its own predictions as the next inputfor 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 inputloss         += criterion(decoder_output, target_tensor[di])if decoder_input.item() == EOS_token:breakloss.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)模型的编码器和解码器。

teacher_forcing_ratio = 0.5

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

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
    encoder_hidden = encoder.initHidden()

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

    encoder_optimizer.zero_grad()decoder_optimizer.zero_grad()

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

   input_length  = input_tensor.size(0)target_length = target_tensor.size(0)

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

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

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

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]被更新为当前时间步的编码器输出。

    decoder_input  = torch.tensor([[SOS_token]], device=device)decoder_hidden = encoder_hidden

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

    use_teacher_forcing = True if random.random() < teacher_forcing_ratio else False

这行代码使用随机数来决定是否使用教师强制策略。如果随机数小于teacher_forcing_ratio,则使用教师强制;否则不使用。
至此,函数的初始化部分已经完成,包括编码器的隐藏状态初始化、优化器梯度清零、输入和目标长度的获取、编码器输出张量的创建、损失变量的初始化等。
下面train函数的剩余部分,用于执行实际的训练过程。

if use_teacher_forcing:

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

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

 # Teacher forcing: Feed the target as the next input
for di in range(target_length):

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

    decoder_output, decoder_hidden = decoder(decoder_input, decoder_hidden)

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

    loss         += criterion(decoder_output, target_tensor[di])

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

    decoder_input = target_tensor[di]  # Teacher forcing

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

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

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

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

    decoder_output, decoder_hidden = decoder(decoder_input, decoder_hidden)

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

    topv, topi    = decoder_output.topk(1)

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

    decoder_input = topi.squeeze().detach()  # detach from history as input

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

    loss         += criterion(decoder_output, target_tensor[di])

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

    if decoder_input.item() == EOS_token:break

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

    loss.backward()

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

    encoder_optimizer.step()decoder_optimizer.step()

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

    return loss.item() / target_length

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

import time
import mathdef asMinutes(s):m = math.floor(s / 60)s -= m * 60return '%dm %ds' % (m, s)def timeSince(since, percent):now = time.time()s = now - sincees = s / (percent)rs = es - sreturn '%s (- %s)' % (asMinutes(s), asMinutes(rs))

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

import time
import math

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

def asMinutes(s):

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

    m = math.floor(s / 60)

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

    s -= m * 60

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

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

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

def timeSince(since, percent):

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

    now = time.time()

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

    s = now - since

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

    es = s / (percent)

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

    rs = es - s

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

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

这行代码将当前时间与since时间的时间差格式化为字符串,并使用asMinutes函数将剩余时间格式化为字符串。最后,以%s (- %s)的格式返回,其中%s表示当前时间与since时间的时间差,%s表示剩余时间。
综上所述,这两个函数分别用于将时间转换为分钟和秒的格式,以及计算从某个时间点到现在所花费的时间,并以分钟和秒的格式表示。

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_everyplot_loss_total  = 0  # Reset every plot_everyencoder_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 += lossplot_loss_total  += lossif iter % print_every == 0:print_loss_avg   = print_loss_total / print_everyprint_loss_total = 0print('%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_everyplot_losses.append(plot_loss_avg)plot_loss_total = 0return plot_losses

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

  1. 函数定义
    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. 初始化
    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. 优化器设置
    encoder_optimizer = optim.SGD(encoder.parameters(), lr=learning_rate)
    decoder_optimizer = optim.SGD(decoder.parameters(), lr=learning_rate)
    
    这行代码创建了两个优化器,一个用于编码器,另一个用于解码器,并设置了相同的学习率。
  4. 训练数据准备
    training_pairs    = [tensorsFromPair(random.choice(pairs)) for i in range(n_iters)]
    
    这行代码创建了一个列表training_pairs,其中包含了n_iters个随机选择的句子对。
  5. 损失函数和迭代循环
    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 += lossplot_loss_total  += lossif iter % print_every == 0:print_loss_avg   = print_loss_total / print_everyprint_loss_total = 0print('%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_everyplot_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. 返回损失列表
    return plot_losses
    
    这行代码在循环结束后返回plot_losses

五、训练与评估

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)
hidden_size = 256

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

encoder1 = EncoderRNN(input_lang.n_words, hidden_size).to(device)

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

attn_decoder1 = DecoderRNN(hidden_size, output_lang.n_words).to(device)

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

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

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

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/pingmian/41321.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【leetcode】双指针算法题

文章目录 1.算法思想2.移动零3.复写零方法一方法二 4.快乐数5.盛水最多的容器方法一&#xff08;暴力求解&#xff09;方法二&#xff08;左右指针&#xff09; 6.有效三角形的个数方法一&#xff08;暴力求解&#xff09;方法二&#xff08;左右指针&#xff09; 7.两数之和8.…

CNN文献综述

卷积神经网络&#xff08;Convolutional Neural Networks&#xff0c;简称CNN&#xff09;是深度学习领域中的一种重要模型&#xff0c;主要用于图像识别和计算机视觉任务。其设计灵感来自于生物学中视觉皮层的工作原理&#xff0c;能够高效地处理图像和语音等数据。 基本原理…

UVa1265/LA4848 Tour Belt

UVa1265/LA4848 Tour Belt 题目链接题意分析AC 代码 题目链接 本题是2010年icpc亚洲区域赛大田赛区的F题 题意 给出一个有n个结点m条边的加权无向图G&#xff08;2≤n≤5000&#xff0c;1≤m≤n(n-1)/2&#xff09;&#xff0c;满足如下条件的结点集B&#xff08;2≤|B|≤n&am…

剪画小程序:手机制作音乐串烧,用它,真的很简单!

Hello&#xff0c;大家好呀&#xff0c;我是不会画画的小画。 相伴关注歌手的小伙伴们&#xff0c;上周五的《歌手 2024》第八期大家看了吧&#xff01;那期节目里有好几首歌都让我沉醉其中&#xff0c;像汪苏泷的《听见下雨的声音》、谭维维的《兰花花儿》等等。 为了能让大…

c++之旅第十一弹——顺序表

大家好啊&#xff0c;这里是c之旅第十一弹&#xff0c;跟随我的步伐来开始这一篇的学习吧&#xff01; 如果有知识性错误&#xff0c;欢迎各位指正&#xff01;&#xff01;一起加油&#xff01;&#xff01; 创作不易&#xff0c;希望大家多多支持哦&#xff01; 一,数据结构…

基于docker环境及Harbor部署{很简短一点了,耐心看吧}

用到的环境&#xff1a; docker 、nacos、compose、harbor&#xff08;自行安装 ,以下连接作为参考&#xff09; nacos&#xff1a;史上最全整合nacos单机模式整合哈哈哈哈哈_nacos 源码启动 单机模式-CSDN博客 docker、compose、harbor:史上最全的整合Harbor安装教程&#…

数据结构之顺序表专题

在学习数据结构之前我们要先了解什么是数据结构&#xff1f; 1.数据结构相关概念 1.什么是数据结构&#xff1f; 数据结构是由“数据”和“结构”两词组合而来。 什么是数据?常见的数值1、2、3、4.、教务系统里保存的用户信息(姓名、性别、年龄、学历等等)、网页里肉眼可以…

TensorBoard进阶

文章目录 TensorBoard进阶1.设置TensorBoard2.图像数据在TensorBoard中可视化3.模型结构在TensorBoard中可视化&#xff08;重点✅&#xff09;4.高维数据在TensorBoard中低维可视化5.利用TensorBoard跟踪模型的训练过程&#xff08;重点✅&#xff09;6.利用TensorBoard给每个…

complex复数库学习

此头文件是数值库的一部分。本篇介绍complex的基本用法。 常用的API如下&#xff1a; 运算 real 返回实部 (函数模板) imag 返回虚部 (函数模板) abs(std::complex) 返回复数的模 (函数模板) arg 返回辐角 (函数模板) norm 返回模(范数)的平方 (函数模板) conj 返回复共轭 (函…

桌面保存的Word文件删除怎么找回?超实用的三个方法?

在日常工作和学习中&#xff0c;我们经常会使用Word文档进行文字编辑和文件保存。但是&#xff0c;有时由于操作失误或系统故障&#xff0c;我们会不小心将存放在电脑桌面重要的Word文件删除了。导致无法挽回的损失&#xff0c;但幸运的是&#xff0c;有一些方法可以帮助我们找…

源代码防泄漏的制胜法宝——沙箱

沙箱技术作为现代信息安全领域的一种重要手段&#xff0c;其在源代码防泄密方面的应用愈发受到业界的关注。源代码作为企业或组织的核心资产&#xff0c;一旦泄露&#xff0c;不仅可能导致知识产权的流失&#xff0c;还可能对企业运营造成重大影响。因此&#xff0c;利用沙箱技…

跨境干货|最新注册Google账号方法分享

谷歌账号对做跨境外贸业务的人来说是刚需&#xff0c;目前来说大部分的海外社媒平台、工具都可以用谷歌账号来注册。但是仍然有很多朋友并不知道如何注册这个谷歌账号&#xff0c;今天就来给大家分享2个注册谷歌账号的方法&#xff0c;一个是手机号注册&#xff0c;一个是如何跳…

面向对象案例:电影院

TOC 思路 代码 结构 具体代码 Movie.java public class Movie {//一共七个private int id;private String name;private double price;private double score;private String director;private String actors;private String info;//get和setpublic int getId() {return id;…

opencv概念以及安装方法

#opencv相关概念介绍 Open Source Computer Vision Library 缩写 opencv 翻译&#xff1a;开源的计算机视觉库 &#xff0c;英特尔公司发起并开发&#xff0c;支持多种编程语言&#xff08;如C、Python、Java等&#xff09;&#xff0c;支持计算机视觉和机器学习等众多算法&a…

如何让自动化测试更加灵活简洁?

简化的架构对于自动化测试和主代码一样重要。冗余和不灵活性可能会导致一些问题&#xff1a;比如 UI 中的任何更改都需要更新多个文件&#xff0c;测试可能在功能上相互重复&#xff0c;并且支持新功能可能会变成一项耗时且有挑战性的工作来适应现有测试。 页面对象模式如何理…

DataWhale-吃瓜教程学习笔记 (七)

学习视频**&#xff1a;第6章-支持向量机_哔哩哔哩_bilibili 西瓜书对应章节&#xff1a; 第六章 支持向量机 - 算法原理 几何角度 对于线性可分数据集&#xff0c;找距离正负样本距离都最远的超平面&#xff0c;解是唯一的&#xff0c;泛化性能较好 - 超平面 - 几何间隔 例…

MSPM0G3507——读取引脚的高低电平方法(数字信号循迹模块)

SYSCFG配置 代码部分 //第一个传感器if( DL_GPIO_readPins(xunji_PORT_PIN1_PORT , xunji_PORT_PIN1_PIN )xunji_PORT_PIN1_PIN) //黑&#xff0c;不亮 高{a1;}if( DL_GPIO_readPins(xunji_PORT_PIN1_PORT , xunji_PORT…

从 Keycloak 导出和导入 Realm 和用户

1. 首先对keycloak 命令有所了解 需要将 Keycloak 中的 Realm 导出或导入时&#xff0c;您可以使用 JSON 文件进行操作。以下是一些有关导出和导入 Realm 的方法&#xff1a; 导出 Realm 到目录&#xff1a; 使用 export 命令将 Realm 导出到目录。在执行此命令时&#xff0c;…

技术分享:直播平台如何开发并接入美颜SDK

本篇文章&#xff0c;笔者将分享直播平台如何开发并接入美颜SDK的技术细节与步骤。 一、选择合适的美颜SDK 首先&#xff0c;选择一款适合的美颜SDK非常重要。市面上有很多优秀的美颜SDK供应商&#xff0c;选择时应考虑以下因素&#xff1a; 功能丰富性&#xff1a;支持美白…

短视频文案提取神器怎么提取抖音视频文案!

很多编导以及视频内容创作者为了提高自己的工作效率还会使用视频转文字提取神器&#xff0c;我们都清楚短视频领域每个平台人群熟悉都有所不同&#xff0c;在分发内容的时候也会调整内容已符合平台属性。 短视频文案提取神器怎么提取抖音视频文案 短视频常见的平台有抖音、西瓜…