人工智能-深度学习-自然语言处理(NLP)-Pytorch:Pytorch创建RNN、LSTM、GRU神经网络模型【人名所属国家分类器】

以一个人名为输入, 使用模型帮助我们判断它最有可能是来自哪一个国家的人名, 这在某些国际化公司的业务中具有重要意义, 在用户注册过程中, 会根据用户填写的名字直接给他分配可能的国家或地区选项, 以及该国家或地区的国旗, 限制手机号码位数等等.

数据下载地址: https://download.pytorch.org/tutorial/data.zip

数据文件预览:

- data/
    - names/
        Arabic.txt
        Chinese.txt
        Czech.txt
        Dutch.txt
        English.txt
        French.txt
        German.txt
        Greek.txt
        Irish.txt
        Italian.txt
        Japanese.txt
        Korean.txt
        Polish.txt
        Portuguese.txt
        Russian.txt
        Scottish.txt
        Spanish.txt
        Vietnamese.txt

Chiness.txt预览:

Ang
Au-Yong
Bai
Ban
Bao
Bei
Bian
Bui
Cai
Cao
Cen
Chai
Chaim
Chan
Chang
Chao
Che
Chen
Cheng

整个案例的实现可分为以下五个步骤:

  • 第一步: 导入必备的工具包.

    • python版本使用3.6.x, pytorch版本使用1.3.1
  • 第二步: 对data文件中的数据进行处理,满足训练要求.

    • 定义数据集路径并获取常用的字符数量.
    • 字符规范化之unicode转Ascii函数unicodeToAscii.
    • 构建一个从持久化文件中读取内容到内存的函数readLines.
    • 构建人名类别(所属的语言)列表与人名对应关系字典
    • 将人名转化为对应onehot张量表示函数lineToTensor
  • 第三步: 构建RNN模型(包括传统RNN, LSTM以及GRU).

    • 构建传统的RNN模型的类class RNN.
    • 构建LSTM模型的类class LSTM.
    • 构建GRU模型的类class GRU.
  • 第四步: 构建训练函数并进行训练.

    • 从输出结果中获得指定类别函数categoryFromOutput.
    • 随机生成训练数据函数randomTrainingExample.
    • 构建传统RNN训练函数trainRNN.
    • 构建LSTM训练函数trainLSTM.
    • 构建GRU训练函数trainGRU.
    • 构建时间计算函数timeSince.
    • 构建训练过程的日志打印函数train.得到损失对比曲线和训练耗时对比图.
  • 第五步: 构建评估函数并进行预测.

    • 构建传统RNN评估函数evaluateRNN.
    • 构建LSTM评估函数evaluateLSTM.
    • 构建GRU评估函数evaluateGRU.
    • 构建预测函数predict.
# 从io中导入文件打开方法
from io import open
# 帮助使用正则表达式进行子目录的查询
import glob
import os
# 用于获得常见字母及字符规范化
import string
import unicodedata
# 导入随机工具random
import random
# 导入时间和数学工具包
import time
import math
# 导入torch工具
import torch
# 导入nn准备构建模型
import torch.nn as nn
# 引入制图工具包
import matplotlib.pyplot as plt

data_path = "./data/names/"  # 下载地址:https://download.pytorch.org/tutorial/data.zip

# 第一步: 对data文件中的数据进行处理,满足训练要求
# 1.1 获取常用的字符数量
all_letters = string.ascii_letters + " .,;'"  # 获取所有常用字符包括字母和常用标点
n_letters = len(all_letters)  # 获取常用字符数量
print("n_letter:", n_letters)


# 1.2 字符规范化之unicode转Ascii函数【关于编码问题我们暂且不去考虑,我们认为这个函数的作用就是去掉一些语言中的重音标记。如: Ślusàrski ---> Slusarski】
def unicodeToAscii(s):
    return ''.join(
        c for c in unicodedata.normalize('NFD', s)
        if unicodedata.category(c) != 'Mn' and c in all_letters
    )


s = "Ślusàrski"
a = unicodeToAscii(s)
print("a = {0}".format(a))  # a = Slusarski


# 1.3 构建一个从持久化文件中读取内容到内存的函数
def readLines(filename):  # 从文件中读取每一行加载到内存中形成列表
    names = open(filename, encoding='utf-8').read().strip().split('\n')  # 打开指定文件并读取所有内容, 使用strip()去除两侧空白符, 然后以'\n'进行切分
    return [unicodeToAscii(name) for name in names]  # 对应每一个names列表中的名字进行Ascii转换, 使其规范化.最后返回一个名字列表


filename = data_path + "Chinese.txt"  # filename是数据集中某个具体的文件, 我们这里选择Chinese.txt
results = readLines(filename)
print("results[:20] = {0}".format(results[:20]))  # results[:20] = ['Ang', 'AuYong', 'Bai', 'Ban', 'Bao', 'Bei', 'Bian', 'Bui', 'Cai', 'Cao', 'Cen', 'Chai', 'Chaim', 'Chan', 'Chang', 'Chao', 'Che', 'Chen', 'Cheng', 'Cheung']

# 1.4 构建人名类别(所属的语言)列表与人名对应关系字典【构建的category_names形如:{"English":["Lily", "Susan", "Kobe"], "Chinese":["Zhang San", "Xiao Ming"]}】
category_names_dict = {
    
    }
all_categories_list = []  # all_categories形如: ["English",...,"Chinese"]
# 读取指定路径下的txt文件, 使用glob,path中可以使用正则表达式
for filename in glob.glob(data_path + '*.txt'):
    category = os.path.splitext(os.path.basename(filename))[0]  # 获取每个文件的文件名, 就是对应的名字类别
    all_categories_list.append(category)  # 将其逐一装到all_categories列表中
    names = readLines(filename)  # 然后读取每个文件的内容,形成名字列表
    category_names_dict[category] = names  # 按照对应的类别,将名字列表写入到category_names字典中
categories_size = len(all_categories_list)  # 查看类别总数
print("categories_size = {0}, all_categories_list = {1}".format(categories_size, all_categories_list))
print("category_names_dict['Italian'][:5] = {0}".format(category_names_dict['Italian'][:5]))  # 随便查看其中的一些内容【category_names_dict['Italian'][:5] = ['Abandonato', 'Abatangelo', 'Abatantuono', 'Abate', 'Abategiovanni']】


# 1.5 将人名转化为对应onehot张量表示
# 将字符串(单词粒度)转化为张量表示,如:"ab" --->
# tensor([[[1., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
#          0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
#          0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
#          0., 0., 0., 0., 0., 0.]],

#        [[0., 1., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
#          0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
#          0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
#          0., 0., 0., 0., 0., 0.]]])
# 将人名转化为对应onehot张量表示, 参数name是输入的人名
def nameToTensor(name):
    name_tensor = torch.zeros(len(name), 1, n_letters)  # 首先初始化一个0张量, 它的形状(len(name), 1, n_letters),代表人名中的每个字母用一个(1*n_letters)的张量表示.
    for li, letter in enumerate(name):  # 遍历这个人名中的每个字符索引和字符
        name_tensor[li][0][all_letters.find(letter)] = 1  # 使用字符串方法find找到每个字符在all_letters中的索引,它也是我们生成onehot张量中1的索引位置
    return name_tensor


print("nameToTensor('Bai') = {0}".format(nameToTensor('Bai')))


# 第二步: 构建RNN模型
# 2.1 构建传统的RNN模型(使用nn.RNN构建完成传统RNN使用类)
class RNN(nn.Module):
    # 初始化函数中有4个参数,【input_size:代表输入张量的最后一维尺寸;hidden_size:代表RNN的隐层最后一维尺寸;output_size:代表最后线性层的输出维度;num_layers:代表RNN网络的层数】
    def __init__(self, input_size, hidden_size, output_size, num_layers=1):
        super(RNN, self).__init__()
        self.input_size = input_size
        self.hidden_size = hidden_size
        self.output_size = output_size
        self.num_layers = num_layers
        self.rnn = nn.RNN(input_size, hidden_size, num_layers)  # 实例化预定义的nn.RNN, 它的三个参数分别是input_size, hidden_size, num_layers
        self.namear = nn.Linear(hidden_size, output_size)  # 实例化全连接层, 这个线性层用于将nn.RNN的输出维度转化为指定的输出维度
        self.softmax = nn.LogSoftmax(dim=-1)  # 实例化nn中预定的Softmax层, 用于从输出层获得类别结果

    # 完成传统RNN中的主要逻辑【input代表输入张量, 它的形状是1*n_letters;hidden代表RNN的隐层张量, 它的形状是self.num_layers*1*self.hidden_size】
    def forward(self, input, hidden):
        input = input.unsqueeze(0)  # 因为预定义的nn.RNN要求输入维度一定是三维张量, 因此在这里使用unsqueeze(0)扩展一个维度
        # print("input.shape = {0},hidden.shape = {1}".format(input.shape, hidden.shape))
        rnn_output, hidden = self.rnn(input, hidden)  # 将input和hidden输入到传统RNN的实例化对象中,如果num_layers=1, rnn_output恒等于hidden
        namear_output = self.namear(rnn_output)  # 将从rnn中获得的结果通过线性变换转化为指定的输出维度
        softmax_output = self.softmax(namear_output)  # 将线性变换后的输出结果通过softmax返回分类结果
        return softmax_output, hidden  # 返回softmax结果,每次的softmax被下一次覆盖;hidden作为后续rnn的输入

    # 初始化隐层张量:初始化一个(self.num_layers, 1, self.hidden_size)形状的0张量
    def initHidden(self):
        hidden_tensor = torch.zeros(self.num_layers, 1, self.hidden_size)
        return hidden_tensor


# 2.2 使用nn.LSTM构建完成LSTM使用类
class LSTM(nn.Module):
    # 初始化函数中有4个参数,【input_size:代表输入张量的最后一维尺寸;hidden_size:代表LSTM的隐层最后一维尺寸;output_size:代表最后线性层的输出维度;num_layers:代表LSTM网络的层数】
    def __init__(self, input_size, hidden_size, output_size, num_layers=1):
        super(LSTM, self).__init__()
        self.input_size = input_size
        self.hidden_size = hidden_size
        self.output_size = output_size
        self.num_layers = num_layers
        self.lstm = nn.LSTM(input_size, hidden_size, num_layers)  # 实例化预定义的nn.LSTM, 它的三个参数分别是input_size, hidden_size, num_layers
        self.namear = nn.Linear(hidden_size, output_size)  # 实例化全连接层, 这个线性层用于将nn.LSTM的输出维度转化为指定的输出维度
        self.softmax = nn.LogSoftmax(dim=-1)  # 实例化nn中预定的Softmax层, 用于从输出层获得类别结果

    # 完成LSTM中的主要逻辑【input代表输入张量, 它的形状是1*n_letters;hidden代表RNN的隐层张量, 它的形状是self.num_layers*1*self.hidden_size;cell代表LSTM中的细胞状态张量】
    def forward(self, input, hidden, cell):
        input = input.unsqueeze(0)  # 因为预定义的nn.LSTM要求输入维度一定是三维张量, 因此在这里使用unsqueeze(0)扩展一个维度
        lstm_output, (hidden, cell) = self.lstm(input, (hidden, cell))  # 将input, hidden以及初始化的cell传入lstm中
        namear_output = self.namear(lstm_output)  # 将从lstm中获得的结果通过线性变换转化为指定的输出维度
        softmax_output = self.softmax(namear_output)  # 将线性变换后的输出结果通过softmax返回分类结果
        return softmax_output, hidden, cell  # 最后返回处理后的softmax_output, 同时返回hidden, cell作为后续lstm的输入

    # 初始化隐层张量:不仅初始化hidden还要初始化细胞状态cell, 它们形状相同"""
    def initHiddenAndCell(self):
        cell_tensor = hidden_tensor = torch.zeros(self.num_layers, 1, self.hidden_size)
        return hidden_tensor, cell_tensor


# 2.3 使用nn.GRU构建完成传统RNN使用类【GRU与传统RNN的外部形式相同, 都是只传递隐层张量, 因此只需要更改预定义层的名字】
class GRU(nn.Module):
    # 初始化函数中有4个参数,【input_size:代表输入张量的最后一维尺寸;hidden_size:代表GRU的隐层最后一维尺寸;output_size:代表最后线性层的输出维度;num_layers:代表GRU网络的层数】
    def __init__(self, input_size, hidden_size, output_size, num_layers=1):
        super(GRU, self).__init__()
        self.input_size = input_size
        self.hidden_size = hidden_size
        self.output_size = output_size
        self.num_layers = num_layers
        self.gru = nn.GRU(input_size, hidden_size, num_layers)  # 实例化预定义的nn.GRU, 它的三个参数分别是input_size, hidden_size, num_layers
        self.namear = nn.Linear(hidden_size, output_size)  # 实例化全连接层, 这个线性层用于将nn.RNN的输出维度转化为指定的输出维度
        self.softmax = nn.LogSoftmax(dim=-1)  # 实例化nn中预定的Softmax层, 用于从输出层获得类别结果

    # 完成GRU中的主要逻辑【input代表输入张量, 它的形状是1*n_letters;hidden代表GRU的隐层张量, 它的形状是self.num_layers*1*self.hidden_size】
    def forward(self, input, hidden):
        input = input.unsqueeze(0)  # 因为预定义的nn.RNN要求输入维度一定是三维张量, 因此在这里使用unsqueeze(0)扩展一个维度
        gru_output, hidden = self.gru(input, hidden)  # 将input和hidden输入到GRU的实例化对象中
        namear_output = self.namear(gru_output)  # 将从gru中获得的结果通过线性变换转化为指定的输出维度
        softmax_output = self.softmax(namear_output)  # 将线性变换后的输出结果通过softmax返回分类结果
        return softmax_output, hidden

    # 初始化隐层张量:初始化一个(self.num_layers, 1, self.hidden_size)形状的0张量
    def initHidden(self):
        hidden_tensor = torch.zeros(self.num_layers, 1, self.hidden_size)
        return hidden_tensor


# 第三步: 测试模型
# 3.1 实例化参数
input_size = n_letters  # 因为是onehot编码, 输入张量最后一维的尺寸就是n_letters
hidden_size = 128  # 定义隐层的最后一维尺寸大小
output_size = categories_size  # 输出尺寸为语言类别总数n_categories
num_layer = 1  # num_layer使用默认值, num_layers = 1
# 3.2 输入参数
input = nameToTensor('B').squeeze(0)  # 假如我们以一个字母B作为RNN的首次输入, 它通过nameToTensor转为张量【因为我们的nameToTensor输出是三维张量, 而RNN类需要的二维张量,因此需要使用squeeze(0)降低一个维度】
cell = hidden = torch.zeros(1, 1, hidden_size)  # 初始化一个三维的隐层0张量, 也是初始的细胞状态张量
# 3.3 实例化模型
rnn = RNN(input_size, hidden_size, output_size)
lstm = LSTM(input_size, hidden_size, output_size)
gru = GRU(input_size, hidden_size, output_size)
# 3.4 调用模型
rnn_output, next_hidden = rnn(input, hidden)
print("\n\nrnn_output.shape = {0}, \nrnn_output = {1}\nnext_hidden.shape = {2}, \nnext_hidden[:,:,:5] = {3}".format(rnn_output.shape, rnn_output, next_hidden.shape, next_hidden[:, :, :5]))
lstm_output, next_hidden, cell = lstm(input, hidden, cell)
print("\n\nlstm_output.shape = {0}, \nlstm_output = {1}\nnext_hidden.shape = {2}, \nnext_hidden[:,:,:5] = {3}\ncell.shape = {2}, \ncell[:,:,:5] = {3}".format(rnn_output.shape, rnn_output, next_hidden.shape, next_hidden[:, :, :5], cell.shape, cell[:, :, :5]))
gru_output, next_hidden = gru(input, hidden)
print("\n\ngru_output.shape = {0}, \ngru_output = {1}\nnext_hidden.shape = {2}, \nnext_hidden[:,:,:5] = {3}".format(gru_output.shape, gru_output, next_hidden.shape, next_hidden[:, :, :5]))


# 第四步: 工具函数
# 4.1 从输出结果中获得指定类别函数, 参数为输出张量output
def categoryFromOutput(output):
    top_values, top_indexs = output.topk(1)  # 调用topk()函数,从输出张量中返回最大的值及其索引对象,从而得到类别信息
    category_index = top_indexs[0].item()  # 从top_indexs对象中取出索引的值
    category_name = all_categories_list[category_index]  # 从前面已经构造好的all_categories数组中根据索引值获得对应语言类别
    return category_name, category_index  # 返回语言类别和索引值


category, category_index = categoryFromOutput(rnn_output)
print("category = {0}----category_index = {1}".format(category, category_index))


# 4.2 随机生成训练数据
def randomTrainingExample():
    category_name_random = random.choice(all_categories_list)  # 首先使用random.choice()方法从all_categories随机选择一个类别
    names = category_names_dict[category_name_random]  # 然后在通过category_names字典取category_name_random类别对应的名字列表
    name = random.choice(names)  # 之后再从名字列表names中随机取一个名字
    category_tensor = torch.tensor([all_categories_list.index(category_name_random)], dtype=torch.long)  # 接着将这个类别在所有类别列表中的索引封装成tensor, 得到类别张量category_tensor
    name_tensor = nameToTensor(name)  # 最后, 将随机取到的名字通过函数 nameToTensor 转化为onehot张量表示
    return category_name_random, category_tensor, name, name_tensor


for i in range(10):  # 随机取出十个进行结果查看
    category, category_tensor, name, name_tensor = randomTrainingExample()
    print("category = {0}, category_tensor = {1}, name = {2}".format(category, category_tensor, name))
print("name_tensor = \n{0}".format(name_tensor))


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


period = timeSince(time.time() - 10 * 60)  # 假定模型训练开始时间是10min之前
print("测试:period = {0}".format(period))

# 第五步: 构建训练函数
criterion = nn.NLLLoss()  # 定义损失函数为nn.NLLLoss,因为RNN的最后一层是nn.LogSoftmax, 两者的内部计算逻辑正好能够吻合.
learning_rate = 0.005  # 设置学习率为0.005


# 5.1 构建传统RNN训练函数【参数category_tensor代表类别的张量表示, 相当于训练数据的标签;name_tensor代表名字的张量表示, 相当于对应训练数据的特征数据值】
def trainRNN(category_tensor, name_tensor):
    hidden_tensor = rnn.initHidden()  # 初始化对象rnn的隐层张量
    rnn.zero_grad()  # 然后将模型结构中的梯度归0
    # 开始进行训练,训练结束后得到最后一次的softmax_output、hidden_tensor
    for i in range(name_tensor.size()[0]):  # 将训练数据name_tensor的每个字符逐个传入rnn之中, 得到最终结果
        softmax_output, hidden_tensor = rnn(name_tensor[i], hidden_tensor)  # 返回的softmax_output被下一次的softmax_output覆盖,hidden_tensor作为下一个时间步的rnn的输入
    loss = criterion(softmax_output.squeeze(0), category_tensor)  # 因为我们的rnn对象由nn.RNN实例化得到, 最终输出形状是三维张量, 为了满足于category_tensor进行对比计算损失, 需要减少第一个维度, 这里使用squeeze()方法
    loss.backward()  # 损失进行反向传播
    for p in rnn.parameters():  # 显示地更新模型中所有的参数【此处没有用自动参数更新函数】
        p.data.add_(-learning_rate, p.grad.data)  # 原地覆盖:将“参数p”+“参数p的梯度×学习率”的结果来更新参数p
    return softmax_output, loss.item()  # 返回结果和损失的值


# 5.2 构建LSTM训练函数【参数category_tensor代表类别的张量表示, 相当于训练数据的标签;name_tensor代表名字的张量表示, 相当于对应训练数据的特征数据值】
def trainLSTM(category_tensor, name_tensor):
    hidden, cell = lstm.initHiddenAndCell()  # 初始化对象rnn的隐层张量
    lstm.zero_grad()  # 然后将模型结构中的梯度归0
    # 开始进行训练,训练结束后得到最后一次的softmax_output、hidden、cell
    for i in range(name_tensor.size()[0]):  # 将训练数据name_tensor的每个字符逐个传入rnn之中, 得到最终结果
        softmax_output, hidden, cell = lstm(name_tensor[i], hidden, cell)  # 返回的softmax_output被下一次的softmax_output覆盖,hidden、cell作为下一个时间步的lstm的输入
    loss = criterion(softmax_output.squeeze(0), category_tensor)
    loss.backward()  # 损失进行反向传播
    for p in lstm.parameters():  # 显示地更新模型中所有的参数【此处没有用自动参数更新函数】
        p.data.add_(-learning_rate, p.grad.data)  # 原地覆盖:将“参数p”+“参数p的梯度×学习率”的结果来更新参数p
    return softmax_output, loss.item()  # 返回结果和损失的值


# 5.3 构建GRU训练函数【参数category_tensor代表类别的张量表示, 相当于训练数据的标签;name_tensor代表名字的张量表示, 相当于对应训练数据的特征数据值】
def trainGRU(category_tensor, name_tensor):
    hidden_tensor = gru.initHidden()  # 初始化对象rnn的隐层张量
    gru.zero_grad()  # 然后将模型结构中的梯度归0
    # 开始进行训练,训练结束后得到最后一次的softmax_output、hidden_tensor
    for i in range(name_tensor.size()[0]):  # 将训练数据name_tensor的每个字符逐个传入rnn之中, 得到最终结果
        softmax_output, hidden_tensor = gru(name_tensor[i], hidden_tensor)  # 返回的softmax_output被下一次的softmax_output覆盖,hidden_tensor作为下一个时间步的gru的输入
    loss = criterion(softmax_output.squeeze(0), category_tensor)  # 因为我们的rnn对象由nn.RNN实例化得到, 最终输出形状是三维张量, 为了满足于category_tensor进行对比计算损失, 需要减少第一个维度, 这里使用squeeze()方法
    loss.backward()  # 损失进行反向传播
    for p in gru.parameters():  # 显示地更新模型中所有的参数【此处没有用自动参数更新函数】
        p.data.add_(-learning_rate, p.grad.data)  # 原地覆盖:将“参数p”+“参数p的梯度×学习率”的结果来更新参数p
    return softmax_output, loss.item()  # 返回结果和损失的值


# 第六步: 构建训练过程
n_iters = 1000  # 设置训练迭代次数
print_every = 50  # 设置结果的打印间隔【每隔50轮打印一次数据】
plot_every = 10  # 设置绘制损失曲线上的制图间隔【每隔10轮采集一次绘图数据】


# 训练过程的日志打印函数, 参数train_type_fn代表选择哪种模型训练函数, 如trainRNN、trainLSTM、trainGRU
def train(train_type_fn):
    all_losses = []  # 每个制图间隔损失保存列表
    start = time.time()  # 获得训练开始时间戳
    current_loss = 0  # 设置初始间隔损失为0
    for iter in range(1, n_iters + 1):  # 从1开始进行训练迭代, 共n_iters次
        category, category_tensor, name, name_tensor = randomTrainingExample()  # 通过randomTrainingExample函数随机获取一组训练数据和对应的类别
        output, loss = train_type_fn(category_tensor, name_tensor)  # 将训练数据和对应类别的张量表示传入到train函数中
        current_loss += loss  # 计算制图间隔中的总损失
        if iter % print_every == 0:  # 如果迭代数能够整除打印间隔
            guess, guess_i = categoryFromOutput(output)  # 取该迭代步上的output通过categoryFromOutput函数获得对应的类别和类别索引
            correct = '✓' if guess == category else '✗ (%s)' % category  # 然后和真实的类别category做比较, 如果相同则打对号, 否则打叉号.
            print('iter = %d %d%%----timeSince(start)= (%s)----loss = %.4f----name = %s / guess = %s----is_correct = %s' % (iter, iter / n_iters * 100, timeSince(start), loss, name, guess, correct))  # 打印迭代步, 迭代步百分比, 当前训练耗时, 损失, 该步预测的名字, 以及是否正确
        if iter % plot_every == 0:  # 如果迭代数能够整除制图间隔
            all_losses.append(current_loss / plot_every)  # 将保存该间隔中的平均损失到all_losses列表中
            current_loss = 0  # 间隔损失重置为0
    return all_losses, int(time.time() - start)  # 返回对应的总损失列表和训练耗时


# 第七步: 开始训练传统RNN, LSTM, GRU模型并制作对比图
# 7.1 调用train函数, 分别进行RNN, LSTM, GRU模型的训练
# 并返回各自的全部损失, 以及训练耗时用于制图
all_losses1, period1 = train(trainRNN)
all_losses2, period2 = train(trainLSTM)
all_losses3, period3 = train(trainGRU)

# 7.2 绘制损失对比曲线, 训练耗时对比柱张图
plt.figure(0)  # 创建画布0
# 绘制损失对比曲线
plt.plot(all_losses1, label="RNN")
plt.plot(all_losses2, color="red", label="LSTM")
plt.plot(all_losses3, color="orange", label="GRU")
plt.legend(loc='upper left')

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


# 第八步: 构建评估函数【评估函数, 和训练函数逻辑相同, 参数是name_tensor代表名字的张量表示】
# 8.1 构建传统RNN评估函数
def evaluateRNN(name_tensor):
    hidden = rnn.initHidden()  # 初始化隐层张量
    for i in range(name_tensor.size()[0]):  # 将评估数据name_tensor的每个字符逐个传入rnn之中
        output, hidden = rnn(name_tensor[i], hidden)
    return output.squeeze(0)  # 获得输出结果


# 8.2 构建LSTM评估函数
def evaluateLSTM(line_tensor):
    hidden, cell = lstm.initHiddenAndCell()  # 初始化隐层张量和细胞状态张量
    for i in range(line_tensor.size()[0]):  # 将评估数据line_tensor的每个字符逐个传入lstm之中
        output, hidden, cell = lstm(line_tensor[i], hidden, cell)
    return output.squeeze(0)


# 8.3 构建GRU评估函数
def evaluateGRU(name_tensor):
    hidden = gru.initHidden()
    for i in range(name_tensor.size()[0]):  # 将评估数据name_tensor的每个字符逐个传入gru之中
        output, hidden = gru(name_tensor[i], hidden)
    return output.squeeze(0)


name = "Bai"
name_tensor = nameToTensor(name)

rnn_output = evaluateRNN(name_tensor)
lstm_output = evaluateLSTM(name_tensor)
gru_output = evaluateGRU(name_tensor)
print("rnn_output:", rnn_output)
print("lstm_output:", lstm_output)
print("gru_output:", gru_output)


# 第九步: 构建预测函数【预测函数, 参数input_name:代表输入的名字字符串, 参数evaluate_fn:代表评估的模型函数;参数n_predictions:代表需要取最有可能的top个】
def predict(input_name, evaluate_fn, n_predictions=3):
    print('\n> %s' % input_name)  # 首先打印输入
    # 以下操作的相关张量不进行求梯度
    with torch.no_grad():
        output = evaluate_fn(nameToTensor(input_name))  # 使输入的名字转换为张量表示, 并使用evaluate函数获得预测输出
        top_values, top_indexes = output.topk(n_predictions)     # 从预测的输出中取前3个最大的值及其索引()
        predictions = []  # 创建盛装结果的列表
        for i in range(n_predictions):  # 遍历n_predictions
            value = top_values[0][i].item()   # 从top_values中取出的output值
            category_index = top_indexes[0][i].item()  # 取出索引并找到对应的类别
            print('(%.2f) %s' % (value, all_categories_list[category_index]))   # 打印ouput的值, 和对应的类别
            predictions.append([value, all_categories_list[category_index]])    # 将结果装进predictions中
        return predictions

for evaluate_fn in [evaluateRNN, evaluateLSTM, evaluateGRU]:
    print("-"*50)
    predictions01 = predict('Dovesky', evaluate_fn)
    predictions02 = predict('Jackson', evaluate_fn)
    predictions03 = predict('Satoshi', evaluate_fn)

打印结果:

n_letter: 57
a = Slusarski
results[:20] = ['Ang', 'AuYong', 'Bai', 'Ban', 'Bao', 'Bei', 'Bian', 'Bui', 'Cai', 'Cao', 'Cen', 'Chai', 'Chaim', 'Chan', 'Chang', 'Chao', 'Che', 'Chen', 'Cheng', 'Cheung']
categories_size = 18, all_categories_list = ['Arabic', 'Chinese', 'Czech', 'Dutch', 'English', 'French', 'German', 'Greek', 'Irish', 'Italian', 'Japanese', 'Korean', 'Polish', 'Portuguese', 'Russian', 'Scottish', 'Spanish', 'Vietnamese']
category_names_dict['Italian'][:5] = ['Abandonato', 'Abatangelo', 'Abatantuono', 'Abate', 'Abategiovanni']
nameToTensor('Bai') = tensor([[[0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
          0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 1., 0., 0., 0., 0., 0., 0.,
          0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
          0., 0., 0., 0., 0., 0.]],

        [[1., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
          0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
          0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
          0., 0., 0., 0., 0., 0.]],

        [[0., 0., 0., 0., 0., 0., 0., 0., 1., 0., 0., 0., 0., 0., 0., 0., 0.,
          0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
          0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
          0., 0., 0., 0., 0., 0.]]])


rnn_output.shape = torch.Size([1, 1, 18]), 
rnn_output = tensor([[[-2.8661, -2.8582, -2.9480, -2.9104, -2.8761, -2.8454, -2.8940,
          -2.8492, -2.9057, -2.8037, -2.8347, -2.8989, -2.8913, -2.8549,
          -2.9412, -2.8947, -3.0215, -2.9551]]], grad_fn=<LogSoftmaxBackward>)
next_hidden.shape = torch.Size([1, 1, 128]), 
next_hidden[:,:,:5] = tensor([[[ 0.1841,  0.0722, -0.0517, -0.0985,  0.0389]]],
       grad_fn=<SliceBackward>)


lstm_output.shape = torch.Size([1, 1, 18]), 
lstm_output = tensor([[[-2.8661, -2.8582, -2.9480, -2.9104, -2.8761, -2.8454, -2.8940,
          -2.8492, -2.9057, -2.8037, -2.8347, -2.8989, -2.8913, -2.8549,
          -2.9412, -2.8947, -3.0215, -2.9551]]], grad_fn=<LogSoftmaxBackward>)
next_hidden.shape = torch.Size([1, 1, 128]), 
next_hidden[:,:,:5] = tensor([[[-0.0018, -0.0170,  0.0134,  0.0299, -0.0115]]],
       grad_fn=<SliceBackward>)
cell.shape = torch.Size([1, 1, 128]), 
cell[:,:,:5] = tensor([[[-0.0018, -0.0170,  0.0134,  0.0299, -0.0115]]],
       grad_fn=<SliceBackward>)


gru_output.shape = torch.Size([1, 1, 18]), 
gru_output = tensor([[[-2.8254, -2.8348, -2.9732, -2.8324, -2.7727, -2.8912, -2.8643,
          -2.8390, -2.8996, -2.9456, -2.9254, -2.8220, -2.9603, -2.9446,
          -2.8960, -2.9835, -2.8761, -2.9742]]], grad_fn=<LogSoftmaxBackward>)
next_hidden.shape = torch.Size([1, 1, 128]), 
next_hidden[:,:,:5] = tensor([[[ 0.0380, -0.0449, -0.0123,  0.0079,  0.0166]]],
       grad_fn=<SliceBackward>)
category = Italian----category_index = 9
category = Greek, category_tensor = tensor([7]), name = Kalogeria
category = French, category_tensor = tensor([5]), name = Voclain
category = Spanish, category_tensor = tensor([16]), name = Ybarra
category = Irish, category_tensor = tensor([8]), name = O'Halloran
category = Greek, category_tensor = tensor([7]), name = Comino
category = Korean, category_tensor = tensor([11]), name = Ngai
category = Irish, category_tensor = tensor([8]), name = Lennon
category = Arabic, category_tensor = tensor([0]), name = Koury
category = Vietnamese, category_tensor = tensor([17]), name = Chung
category = Russian, category_tensor = tensor([14]), name = Otov
name_tensor = 
tensor([[[0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
          0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
          0., 0., 0., 0., 0., 0., 1., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
          0., 0., 0., 0., 0., 0.]],

        [[0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
          0., 0., 1., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
          0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
          0., 0., 0., 0., 0., 0.]],

        [[0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 1., 0., 0.,
          0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
          0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
          0., 0., 0., 0., 0., 0.]],

        [[0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
          0., 0., 0., 0., 1., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
          0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
          0., 0., 0., 0., 0., 0.]]])
测试:period = 10m 0s

iter = 100 10%----timeSince(start)= (0m 0s)----loss = 2.9856----name = Bell / guess = Irish----is_correct =(Scottish)
iter = 200 20%----timeSince(start)= (0m 0s)----loss = 2.9410----name = Schoonraad / guess = French----is_correct =(Dutch)
iter = 300 30%----timeSince(start)= (0m 0s)----loss = 2.7686----name = Yuasa / guess = Japanese----is_correct =iter = 400 40%----timeSince(start)= (0m 1s)----loss = 2.9545----name = Salomon / guess = Japanese----is_correct =(Polish)
iter = 500 50%----timeSince(start)= (0m 1s)----loss = 2.7721----name = Kools / guess = Greek----is_correct =(Dutch)
iter = 600 60%----timeSince(start)= (0m 1s)----loss = 3.0601----name = Kinnison / guess = Dutch----is_correct =(English)
iter = 700 70%----timeSince(start)= (0m 2s)----loss = 2.8069----name = Bertsimas / guess = French----is_correct =(Greek)
iter = 800 80%----timeSince(start)= (0m 2s)----loss = 3.0697----name = Mccourt / guess = French----is_correct =(English)
iter = 900 90%----timeSince(start)= (0m 2s)----loss = 2.9705----name = Vinton / guess = Korean----is_correct =(English)
iter = 1000 100%----timeSince(start)= (0m 2s)----loss = 2.8315----name = Sciacchitano / guess = Chinese----is_correct =(Italian)

iter = 100 10%----timeSince(start)= (0m 0s)----loss = 2.9202----name = Steuben / guess = Russian----is_correct =(German)
iter = 200 20%----timeSince(start)= (0m 0s)----loss = 2.9407----name = Jameson / guess = Russian----is_correct =(English)
iter = 300 30%----timeSince(start)= (0m 1s)----loss = 2.9750----name = Tansho / guess = Russian----is_correct =(Japanese)
iter = 400 40%----timeSince(start)= (0m 1s)----loss = 2.9542----name = Yamawaki / guess = Dutch----is_correct =(Japanese)
iter = 500 50%----timeSince(start)= (0m 2s)----loss = 2.9519----name = Martin / guess = Dutch----is_correct =(Scottish)
iter = 600 60%----timeSince(start)= (0m 2s)----loss = 2.8210----name = Patsyna / guess = Dutch----is_correct =(Russian)
iter = 700 70%----timeSince(start)= (0m 2s)----loss = 2.8637----name = Braden / guess = Dutch----is_correct =(Irish)
iter = 800 80%----timeSince(start)= (0m 3s)----loss = 2.9544----name = Ribeiro / guess = Greek----is_correct =(Portuguese)
iter = 900 90%----timeSince(start)= (0m 3s)----loss = 2.8512----name = Pyrchenkov / guess = Greek----is_correct =(Russian)
iter = 1000 100%----timeSince(start)= (0m 4s)----loss = 2.8863----name = Bonhomme / guess = Dutch----is_correct =(French)

iter = 100 10%----timeSince(start)= (0m 0s)----loss = 2.8053----name = Fotopoulos / guess = English----is_correct =(Greek)
iter = 200 20%----timeSince(start)= (0m 0s)----loss = 2.7995----name = Pahlke / guess = English----is_correct =(German)
iter = 300 30%----timeSince(start)= (0m 1s)----loss = 2.8021----name = Antonopoulos / guess = English----is_correct =(Greek)
iter = 400 40%----timeSince(start)= (0m 1s)----loss = 2.8211----name = Lyon / guess = English----is_correct =(French)
iter = 500 50%----timeSince(start)= (0m 1s)----loss = 2.8197----name = Uzunov / guess = English----is_correct =(Russian)
iter = 600 60%----timeSince(start)= (0m 2s)----loss = 2.8837----name = Tong / guess = English----is_correct =(Chinese)
iter = 700 70%----timeSince(start)= (0m 2s)----loss = 2.9241----name = Taidhg / guess = English----is_correct =(Irish)
iter = 800 80%----timeSince(start)= (0m 3s)----loss = 2.8355----name = Favager / guess = English----is_correct =(French)
iter = 900 90%----timeSince(start)= (0m 3s)----loss = 2.8069----name = Gavrilopoulos / guess = English----is_correct =(Greek)
iter = 1000 100%----timeSince(start)= (0m 4s)----loss = 2.8909----name = O'Mahoney / guess = English----is_correct =(Irish)

rnn_output: tensor([[-2.8367, -2.7073, -3.1202, -2.7890, -3.0393, -2.8151, -3.0920, -2.9447, -2.9071, -2.7546, -2.8206, -2.7822, -2.8891, -2.9345, -2.8056, -2.9625, -3.0591, -2.8892]], grad_fn=<SqueezeBackward1>)
lstm_output: tensor([[-2.9428, -2.8900, -2.8169, -2.7529, -2.9216, -2.8817, -2.9201, -2.7735, -2.8706, -2.9447, -2.8947, -2.8616, -2.9547, -2.9307, -2.8692, -2.9436, -2.8546, -3.0433]], grad_fn=<SqueezeBackward1>)
gru_output: tensor([[-2.8713, -2.9555, -2.9238, -2.8854, -2.7455, -2.8857, -2.7851, -2.8328, -2.9240, -2.9653, -2.8776, -2.8990, -2.8710, -2.9643, -2.8889, -3.0186, -2.8719, -2.8967]], grad_fn=<SqueezeBackward1>)

--------------------------------------------------

> Dovesky
(-2.66) Dutch
(-2.74) Russian
(-2.78) French

> Jackson
(-2.71) Dutch
(-2.74) Korean
(-2.76) Irish

> Satoshi
(-2.73) Chinese
(-2.75) Dutch
(-2.77) Italian
--------------------------------------------------

> Dovesky
(-2.76) Dutch
(-2.77) Greek
(-2.82) Czech

> Jackson
(-2.76) Greek
(-2.76) Dutch
(-2.81) Czech

> Satoshi
(-2.75) Dutch
(-2.77) Greek
(-2.82) Czech
--------------------------------------------------

> Dovesky
(-2.68) English
(-2.79) German
(-2.83) Greek

> Jackson
(-2.68) English
(-2.81) German
(-2.84) Greek

> Satoshi
(-2.71) English
(-2.77) German
(-2.84) Greek

Process finished with exit code 0

损失对比曲线(n_iters =100000):
在这里插入图片描述
损失对比曲线分析:

  • 模型训练的损失降低快慢代表模型收敛程度,
  • 由图可知, 传统RNN的模型收敛情况最好, 然后是GRU, 最后是LSTM, 这是因为: 我们当前处理的文本数据是人名, 他们的长度有限, 且长距离字母间基本无特定关联, 因此无法发挥改进模型LSTM和GRU的长距离捕捉语义关联的优势.
  • 所以在以后的模型选用时, 要通过对任务的分析以及实验对比, 选择最适合的模型.

训练耗时对比图:
在这里插入图片描述
训练耗时对比图分析:

  • 模型训练的耗时长短代表模型的计算复杂度,
  • 由图可知, 也正如我们之前的理论分析, 传统RNN复杂度最低, 耗时几乎只是后两者的一半, 然后是GRU, 最后是复杂度最高的LSTM.

模型选用一般应通过实验对比, 并非越复杂或越先进的模型表现越好, 而是需要结合自己的特定任务, 从对数据的分析和实验结果中获得最佳答案.

猜你喜欢

转载自blog.csdn.net/u013250861/article/details/114004034