- 🍨 本文为🔗365天深度学习训练营 中的学习记录博客
- 🍖 原作者:K同学啊
理论基础
seq2seq(Sequence-to-Sequence)模型是一种用于机器翻译、文本摘要等序列转换任务的框架。它由两个主要的递归神经网络(RNN)组成:一个编码器(Encoder)和一个解码器(Decoder)。下面是seq2seq模型实现翻译的基本原理:
- 编码器(Encoder):
- 输入:编码器接收一个源语言句子,这个句子已经被分割成一系列的单词或字符,通常表示为( x_1, x_2, …, x_T )。
- 处理:编码器逐个处理这些输入,并为每个输入生成一个隐藏状态( h_t )。在这个过程中,编码器会构建一个代表整个输入句子的内部表示(context vector)。
- 输出:最后,编码器输出一个固定大小的上下文向量( c ),这个向量包含了输入句子的语义信息。
- 上下文向量(Context Vector):
- 上下文向量是编码器输出的一个汇总,它捕获了整个输入句子的信息。这个向量通常是通过编码器最后一个隐藏状态或者对所有隐藏状态进行池化得到的。
- 解码器(Decoder):
- 输入:解码器接收上下文向量( c )和之前生成的目标语言句子的一部分作为输入,通常表示为( y_1, y_2, …, y_{T’} )。
- 处理:解码器基于当前的目标语言句子部分和上下文向量来生成下一个单词的概率分布。在每一步,解码器都会更新其隐藏状态,并使用它来预测下一个单词。
- 输出:解码器输出一个概率分布,表示在给定当前输入的情况下,下一个目标语言单词的所有可能性的概率。
- 训练过程:
- 在训练过程中,seq2seq模型使用最大似然估计来优化模型的参数。这意味着模型试图最大化目标句子在给定源句子的条件下的概率。
- 通常,解码器在训练时会使用教师强制(Teacher Forcing)策略,即在每一步都提供真实的下一个目标单词作为输入,而不是使用上一步的预测结果。
- 推理过程:
- 在推理(或测试)时,模型的解码器通常会使用自己上一步的输出作为下一步的输入,直到生成一个结束标记或达到最大输出长度。
seq2seq模型的关键优势在于它的灵活性:它可以处理任意长度的输入和输出序列。此外,由于编码器和解码器都是RNN,它们能够捕捉到序列中的长距离依赖关系。
在实际应用中,基础的seq2seq模型可能会遇到一些问题,比如难以处理长序列和缺乏对输入序列的注意力机制。因此,研究者们提出了许多改进版本,如使用长短时记忆网络(LSTM)或门控循环单元(GRU)来替代基本的RNN,以及引入注意力机制(Attention Mechanism)来允许解码器关注输入序列的不同部分。这些改进显著提高了seq2seq模型在机器翻译等任务上的性能。
- 在推理(或测试)时,模型的解码器通常会使用自己上一步的输出作为下一步的输入,直到生成一个结束标记或达到最大输出长度。
一、环境准备(导入基本的包以供使用)
__future__
: 这个模块允许你使用未来版本的Python特性。在这个例子中,它启用了Python 2的print_function
(使得print
成为一个函数,而不是一个语句),unicode_literals
(使得所有的字符串默认为Unicode),和division
(改变了除法的运算规则,在Python 2中,整数相除会得到整数结果,而不是浮点数)。io.open
: 这个模块提供了一个统一的接口来打开文件。导入open
函数是为了确保在Python 2和Python 3中打开文件的方式是一致的。unicodedata
: 这个模块提供了对Unicode字符数据库的访问,可以用于检查和处理Unicode字符。string
: 这个模块包含了常用的字符串操作。在这个脚本中,它可能被用来处理字符集或字符串常量。re
: 这是Python的正则表达式模块,用于字符串的搜索和替换操作。random
: 这个模块提供了生成随机数的工具。torch
: 这是PyTorch框架的主要模块,用于构建和训练神经网络。torch.nn
: 这是PyTorch的神经网络模块,提供了创建和训练神经网络所需的所有工具。torch.optim
: 这个模块包含了各种优化算法,用于在训练过程中调整神经网络的权重。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 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
的类,它用于处理语言相关的数据,例如构建词汇表、将单词映射到索引等。这个类在处理自然语言数据时非常有用,特别是在构建神经机器翻译系统时。
SOS_token = 0
EOS_token = 1
这两行代码定义了两个特殊标记的整数值,SOS_token
代表“开始符”(Start of Sentence),EOS_token
代表“结束符”(End of Sentence)。这些标记用于在序列的开头和结尾处标识句子的开始和结束。
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
:一个字典,用于将索引映射回单词。初始时,它包含两个特殊标记SOS
和EOS
。self.n_words
:一个整数,表示词汇表中的单词数量,初始值为2(因为已经包含了SOS
和EOS
)。
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_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.文本处理函数
这段代码包含两个函数,unicodeToAscii
和normalizeString
,用于处理和规范化文本数据。
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
执行以下操作:
- 分解(Decomposition):它将所有字符分解为它们的组合部分。例如,一个带重音的字符(如é)会被分解为基本字符(e)和一个非间距标记(´)。
- 规范(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
,它接受三个参数:lang1
和lang2
是两种语言的名称,reverse
是一个布尔值,用于指示是否需要反转语言对的顺序。
print("Reading lines...")
打印一条消息,表示开始读取文件。
# 以行为单位读取文件
lines = open('eng-fra.txt'.format(lang1,lang2), encoding='utf-8').read().strip().split('\n')
这行代码读取一个文本文件,该文件包含两种语言的句子对。文件名由lang1
和lang2
参数格式化而成,例如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_lang
和output_lang
,其中input_lang
是第二种语言,output_lang
是第一种语言。
else:
input_lang = Lang(lang1)
output_lang = Lang(lang2)
如果reverse
参数为False
,则保持pairs
中的句子对顺序不变,并创建Lang
实例input_lang
和output_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)'
这样的写法是错误的,因为它试图将两个变量 lang1
和 lang2
作为元组进行格式化,而 %
格式化方法不能直接应用于元组。
正确的做法应该是使用 %
格式化方法,将 lang1
和 lang2
作为单独的参数传递,或者使用 .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')
这样,lang1
和 lang2
的值就会被正确地插入到文件名中,从而避免了 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, pairs
input_lang, output_lang, pairs = prepareData('eng', 'fra', True)
print(random.choice(pairs))
这段代码定义了一个名为prepareData
的函数,用于准备和处理一对语言的句子对数据。
def prepareData(lang1, lang2, reverse=False):
定义函数prepareData
,它接受三个参数:lang1
和lang2
是两种语言的名称,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'
,并设置reverse
为True
,表示需要反转句子对。
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_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)模型中的编码器部分。
class EncoderRNN(nn.Module):
def __init__(self, input_size, hidden_size):
这行定义了EncoderRNN
类的构造函数。它接受两个参数:input_size
和hidden_size
。input_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
方法。它接受两个参数:input
和hidden
。input
是当前时间步的输入序列,通常是单词的索引。hidden
是GRU单元的隐藏状态,它是从前一个时间步传递过来的,用于初始化当前时间步的隐藏状态。
embedded = self.embedding(input).view(1, 1, -1)
这行代码首先通过嵌入层(self.embedding
)将输入的单词索引转换为嵌入向量。嵌入向量是一个固定大小的向量,其长度等于hidden_size
。然后,这个嵌入向量被展平成一个三维张量,其形状为(1, 1, -1)
,其中-1
表示自动推断的维度。由于输入通常是一个单词索引,因此嵌入向量只有一行和一列,但有多列(因为每个单词有一个嵌入向量)。
output = embedded
这行代码将嵌入向量赋值给output
变量。在GRU的第一个时间步,output
和embedded
是相同的,因为输入只有一个单词。
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_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)模型中的解码器部分。下面是逐行解释:
class DecoderRNN(nn.Module):
这行定义了一个名为DecoderRNN
的类,它继承自nn.Module
,这是PyTorch中用于定义神经网络的核心类。
在用户引用的对话内容中,我们看到了DecoderRNN
类的构造函数__init__
。这个构造函数是Python中类的一个特殊方法,用于在创建类的实例时进行初始化。
def __init__(self, hidden_size, output_size):
这行定义了DecoderRNN
类的构造函数。它接受两个参数:hidden_size
和output_size
。hidden_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_size
和output_size
。hidden_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
的函数,它接受两个参数:lang
和sentence
。lang
是一个Lang
类的实例,用于处理和存储某种语言的词汇。sentence
是一个字符串,代表要转换的句子。
return [lang.word2index[word] for word in sentence.split(' ')]
这行代码定义了函数的逻辑。它遍历句子中的每个单词,并使用lang.word2index
字典查找每个单词对应的索引。这个字典是由Lang
类的addWord
方法构建的,它将单词映射到其在词汇表中的索引。每个单词的索引被添加到一个列表中,该列表包含了句子中所有单词的索引。
# 将数字化的文本,转化为tensor数据
def tensorFromSentence(lang, sentence):
这行定义了一个名为tensorFromSentence
的函数,它接受两个参数:lang
和sentence
。lang
是一个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
的函数,它接受一个参数:pair
。pair
是一个列表,包含两个字符串,分别代表源语言和目标语言的句子。
input_tensor = tensorFromSentence(input_lang, pair[0])
target_tensor = tensorFromSentence(output_lang, pair[1])
return (input_tensor, target_tensor)
这行代码定义了函数的逻辑。它首先调用tensorFromSentence
函数,将源语言句子转换为Tensor。然后,它调用tensorFromSentence
函数,将目标语言句子转换为Tensor。最后,它返回一个包含两个Tensor的元组,分别代表源语言和目标语言的句子。
在连续的参数变化过程中,我们可以看到以下步骤:
indexesFromSentence
函数接受一个句子sentence
,并通过split(' ')
将句子分割成单词列表。- 对于每个单词,
word2index
字典被用来查找单词的索引,如果单词不在字典中,则添加新单词并为其分配一个索引。 - 所有单词的索引被添加到一个列表中。
tensorFromSentence
函数接受indexes
列表,并将其转换为一个Tensor。- 在转换过程中,EOS标记被添加到列表的末尾。
- Tensor的形状被重置为
(-1, 1)
,这意味着Tensor的形状可以根据需要自动推断,但是至少有1行和1列。 tensorsFromPair
函数接受一个句子对pair
,并分别调用tensorFromSentence
函数来转换源语言和目标语言的句子。- 转换后的Tensor被返回为一个元组,包含源语言和目标语言的Tensor。
2.训练函数
teacher_forcing_ratio = 0.5
def train(input_tensor, target_tensor,
encoder, decoder,
encoder_optimizer, decoder_optimizer,
criterion, max_length=MAX_LENGTH):
# 编码器初始化
encoder_hidden = encoder.initHidden()
# grad属性归零
encoder_optimizer.zero_grad()
decoder_optimizer.zero_grad()
input_length = input_tensor.size(0)
target_length = target_tensor.size(0)
# 用于创建一个指定大小的全零张量(tensor),用作默认编码器输出
encoder_outputs = torch.zeros(max_length, encoder.hidden_size, device=device)
loss = 0
# 将处理好的语料送入编码器
for ei in range(input_length):
encoder_output, encoder_hidden = encoder(input_tensor[ei], encoder_hidden)
encoder_outputs[ei] = encoder_output[0, 0]
# 解码器默认输出
decoder_input = torch.tensor([[SOS_token]], device=device)
decoder_hidden = encoder_hidden
use_teacher_forcing = True if random.random() < teacher_forcing_ratio else False
# 将编码器处理好的输出送入解码器
if use_teacher_forcing:
# 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_forcing
为True
时,采用"Teacher Forcing"的策略,即将目标序列中的真实标签作为解码器的下一个输入。而当use_teacher_forcing
为False
时,采用"Without Teacher Forcing"的策略,即将解码器自身的预测作为下一个输入。
使用use_teacher_forcing
的目的是在训练过程中平衡解码器的预测能力和稳定性。以下是对两种策略的解释:
-
Teacher Forcing: 在每个时间步(
di
循环中),解码器的输入都是目标序列中的真实标签。这样做的好处是,解码器可以直接获得正确的输入信息,加快训练速度,并且在训练早期提供更准确的梯度信号,帮助解码器更好地学习。然而,过度依赖目标序列可能会导致模型过于敏感,一旦目标序列中出现错误,可能会在解码器中产生累积的误差。 -
Without Teacher Forcing: 在每个时间步,解码器的输入是前一个时间步的预测输出。这样做的好处是,解码器需要依靠自身的预测能力来生成下一个输入,从而更好地适应真实应用场景中可能出现的输入变化。这种策略可以提高模型的稳定性,但可能会导致训练过程更加困难,特别是在初始阶段。
一般来说,Teacher Forcing策略在训练过程中可以帮助模型快速收敛,而Without Teacher Forcing策略则更接近真实应用中的生成场景。通常会使用一定比例的Teacher Forcing,在训练过程中逐渐减小这个比例,以便模型逐渐过渡到更自主的生成模式。
综上所述,通过使用use_teacher_forcing
来选择不同的策略,可以在训练解码器时平衡模型的预测能力和稳定性,同时也提供了更灵活的生成模式选择。
-
topv, topi = decoder_output.topk(1)
这一行代码使用
.topk(1)
函数从decoder_output
中获取最大的元素及其对应的索引。decoder_output
是一个张量(tensor),它包含了解码器的输出结果,可能是一个概率分布或是其他的数值。.topk(1)
函数将返回两个张量:topv
和topi
。topv
是最大的元素值,而topi
是对应的索引值。 -
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_forcing
为True
,则执行教师强制策略。
这段代码是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 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))
这段代码定义了两个函数,asMinutes
和timeSince
,用于将时间转换为分钟和秒的格式,以及计算从某个时间点到现在所花费的时间,并以分钟和秒的格式表示。
import time
import math
这两行代码导入了time
和math
模块。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
的函数,它接受两个参数:since
和percent
。since
是开始计时的时间戳,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_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)模型的编码器和解码器。下面是代码的分段大致讲解:
- 函数定义:
这行定义了def trainIters(encoder, decoder, n_iters, print_every=1000, plot_every=100, learning_rate=0.01):
trainIters
函数,它接受五个参数:encoder
:编码器模型。decoder
:解码器模型。n_iters
:训练迭代次数。print_every
:每多少次迭代打印一次损失。plot_every
:每多少次迭代画一次损失图。learning_rate
:学习率。
- 初始化:
这行代码初始化了几个变量:start = time.time() plot_losses = [] 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。
- 优化器设置:
这行代码创建了两个优化器,一个用于编码器,另一个用于解码器,并设置了相同的学习率。encoder_optimizer = optim.SGD(encoder.parameters(), lr=learning_rate) decoder_optimizer = optim.SGD(decoder.parameters(), lr=learning_rate)
- 训练数据准备:
这行代码创建了一个列表training_pairs = [tensorsFromPair(random.choice(pairs)) for i in range(n_iters)]
training_pairs
,其中包含了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
nn.NLLLoss()
,并进入了一个循环,循环次数等于n_iters
。在每次迭代中,从training_pairs
中随机选择一个句子对,并将其转换为Tensor格式。然后,调用train
函数进行训练,并更新print_loss_total
和plot_loss_total
。如果迭代次数满足print_every
或plot_every
的条件,就会打印或记录当前的平均损失。 - 返回损失列表:
这行代码在循环结束后返回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)
这行代码创建了一个名为encoder1
的EncoderRNN
对象,它具有input_lang.n_words
个输入词汇和hidden_size
大小的隐藏层。然后,将这个模型移动到指定的设备上,这里使用device
属性来指定设备,例如CPU或GPU。
attn_decoder1 = DecoderRNN(hidden_size, output_lang.n_words).to(device)
这行代码创建了一个名为attn_decoder1
的DecoderRNN
对象,它具有hidden_size
大小的隐藏层和output_lang.n_words
个输出词汇。同样,这个模型也被移动到指定的设备上。
plot_losses = trainIters(encoder1, attn_decoder1, 20000, print_every=5000)
这行代码调用trainIters
函数来训练encoder1
和attn_decoder1
模型。trainIters
函数接受两个参数:编码器和解码器模型,以及训练迭代次数20000
。print_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()