RNN案例--人名分类器

使⽤RNN模型构建⼈名分类器

  • 学习⽬标:

    • 了解有关⼈名分类问题和有关数据.

    • 掌握使⽤RNN构建⼈名分类器实现过程.


  • 关于⼈名分类问题:

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


  • ⼈名分类数据:

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

导包

# 从io导入文件打开方法
from io import open
# 帮助使用正则表达式进行子目录的查询
import glob
import os
# 用于获得常见字母及字符规范化
import string
import unicodedata
# 导入随机数模块
import random
# 导入时间模块和数字模块
import time
import math
# 导入Pytorch模块
import torch
import torch.nn as nn
# 导入绘图模块
import matplotlib.pyplot as plt

数据处理

获取字符

# 获取所有常用字符包括字母和常用标点
all_letters = string.ascii_letters + ".,;'"
# 获取常用字符数量
n_letters = len(all_letters)

字符规范化:这个函数的作⽤就是去掉⼀些语⾔中的重⾳标记

def unicodeToAscii(S):
return ''.join(
c for c in unicodedata.normalize('NFD',s)
if unicodedata.category(c) != 'Mn' and c in all_letters
)
   
# s = "Ślusàrski"
# a = unicodeToAscii(s)
# print(a)

文件读取

# 首先设置一个文件根目录
data_path = "./data/names"

def readlines(filename):
# 打开文件
# 打开指定文件并读取所有内容,使用stip()去除两侧空白,然后以'\n'进行分割
lines = open(filename,encoding='utf-8').read().strip().split('\n')
# 对应每一个lines列表的名字进行Ascii转换,使其规范化,最后返回一个名字列表
return [unicodeToAscii(line) for line in lines]

# filename是数据集中某个具体的⽂件, 我们这⾥选择Chinese.txt
# filename = data_path + "Chinese.txt"
# lines = readLines(filename)
# print(lines)

构建人名类别

# 构建人名类别(及所属的语言)列表与人名对应关系字典
category_lines = {}
# 构建人名类别(所属的语言)列表
all_categories = []

# 遍历data_path下的所有文件
for filename in glob.glob(data_path+'*.txt'):
# 获取每个文件的文件名,就是对应名字类别
category = os.path.splitext(os.path.basename(filename))[0]
# 将其逐一装到all_categories中
all_categories.append(category)
# 读取每个文件中的内容,也就是对应类别中的所有名字
lines = readLines(filename)
category_lines[category] = lines
   
n_categories = len(all_categories)
print("n_categories:", n_categories)

# 随便查看其中的⼀些内容
print(category_lines['Italian'][:5])

glob模块

是最简单的模块之一,内容非常少。用它可以查找符合特定规则的文件路径名。

import glob

#获取指定目录下的所有图片
print (glob.glob(r"/home/qiaoyunhao/*/*.png"),"\n")#加上r让字符串不转义

#获取上级目录的所有.py文件
print (glob.glob(r'../*.py')) #相对路径

os.path模块

os.path.split(path):找到 path 的最后一个斜杠并对这个斜杠的两边字符串进行切割,得到两部分:斜杠之前的部分和斜杠之后的部分,然后把这两部分组成一个元组而返回

os.path.split('/python/demo/test.py')
>> ('/python/demo', 'test.py')

os.path.split('/python/demo/')
>> ('/python/demo', '')
os.path.dirname('/python/demo/test.py')
>> /python/demo

os.path.basename('/python/demo/test.py')
>> test.py

构建训练数据

将人名转化为对应onehot张量表示

def lineToTensor(line):
"""将人名转化为对应onehot张量表示,参数line是输入的人名,例如'bai' """
   # 首先初始化一个0张量,它的形状(len(line),1,n_lettets)
   tensor = torch.zeros(len(line),1,n_letters)
   for li,letter in enumerate(line):
       # 使用字符串方法find找到每个字符在all_letters中的索引
       # 它也是我们生产onehot张量中1的索引位置
       tensor[li][0][all_letters.find(letter)] = 1
   return tensor
"""
该函数相当于 创建一个全零张量,然后从人名中取出每个字母,然后在全零张量中找到对应的位置,将其置为1
"""
line = "Bai"
line_tensor = lineToTensor(line)

构建RNN模型

使用nn.RNN构建完成传统RNN使用类

class RNN(nn.Module):
def __init__(self,input_size,hidden_size,output_size,num_layers=1):
"""
      初始化
      :param input_size: 输入大小
      :param hidden_size: 隐藏层大小
      :param output_size: 输出大小
      :param num_layers: 层数
      """
       super(RNN,self).__init__()
       # 将hidden_size与num_layers传入RNN类中
       self.hidden_size = hidden_size
       self.num_layers = num_layers
       
       # 实例化预定义的nn.RNN,三个参数分别是input_size,hidden_size,num_layers
       self.rnn = nn.RNN(input_size,hidden_size,num_layers)
       # 实例化预定义的nn.Linear,两个参数分别是hidden_size,output_size,这个线性层用于将nn.RNN输出维度转化为指定的输出维度-全连接层
       self.linear = nn.Linear(hidden_size,output_size)
       # 实例化nn中预定的Softmax层,用于从输出层获得类别结构
       # dim=-1 代表在最后一个维度上求
       # dim=0对每一列的所有元素进行softmax运算,并使得每一列所有元素和为1
       # dim=1:对每一行的所有元素进行softmax运算,并使得每一行所有元素和为1
       self.softmax = nn.LogSoftmax(dim=-1)
       
   def forward(self,input,hidden):
       """
      完成传统RNN中的主要逻辑,输入参数input代表输入张量,它的形状是1 x n_letters
      x 可以理解为名字的字数 如'bai' x=3 所以实际形状是[1,n_letters] 需要扩展一维
      hidden代表隐藏层张量,它的形状是num_layers x 1 x hidden_size
      """
       # 调用时 output, hidden = rnn(line_tensor[i], hidden)
       # 因为预定义的nn.RNN要求输入维度一定是三维张量,因此在这里使用unsqueeze(0)将输入张量的第0维度扩展一个维度
       input = input.unsqueeze(0)
       # 将input和hidden传入nn.RNN中,如果num_layers=1,rr恒等于hn
       rr, hn = self.rnn(input, hidden)
# 将从RNN中获得的结果通过线性变换和softmax返回,同时返回hn作为后续RNN的输⼊
       return self.softmax(self.linear(rr)), hn
 
   def initHidden(self):
       """
      初始化隐藏层张量,它的形状是num_layers x 1 x hidden_size
      """
       # 初始化一个(self.num_layers,1,self.hidden_size) 形状的0张量
       return torch.zeros(self.num_layers, 1, self.hidden_size)
       
       
rnn = RNN(n_letters, n_hidden, n_categories)

使用nn.LSTM构建完成LSTM使用类

class LSTM(nn.Module):
   def __init__(self,input_size,hidden_size,output_size,num_layers=1):
       """
      初始化参数
      :param input_size: 输入大小
      :param hidden_size: 隐藏层大小
      :param output_size: 输出大小
      :param num_layers: 层数
      """
       super(LSTM,self).__init__()
       # 将hidden_size与num_layers传入LSTM类中
       self.hidden_size = hidden_size
       self.num_layers = num_layers
       
       # 实例化预定义的nn.LSTM
       self.lstm = nn.LSTM(input_size,hidden_size,num_layers)
       # 实例化nn.Linear,这个线性层用于将nn.LSTM的输出维度转化为指定的输出维度
       self.linear = nn.Linear(hidden_size,output_size)
       # 实例化预定义的softmax层,用于从输出层获得类别结果
       self.softmax = nn.Logsoftmax(dim=-1)
       
   def forward(self,input,hidden,c):
       """
      在主要逻辑函数中多出一个参数c,也就是LSTM中的细胞状态张量
      :param input:
      :param hidden:
      :param c:
      :return:
      """
       input = input.unsqueeze(0)
       rr,(hn,c) = self.lstm(input,(hidden,c))
       return self.softmax(self.linear(rr)),hn,c
   
   def initHiddenAndC(self):
       c,hidden = torch.zeros(self.num_layers,1,hidden_size)
       return hidden,c

使用nn.GRU构建完成传统RNN使用类

class GRU(nn.Module):
   def __init__(self, input_size, hidden_size, output_size, num_layers=1):
       super(GRU, self).__init__()
       self.hidden_size = hidden_size
       self.num_layers = num_layers

       # 实例化预定义的nn.GRU,它的三个参数分别是input_size, hidden_size,num_layers
       self.gru = nn.GRU(input_size, hidden_size, num_layers)
       self.linear = nn.Linear(hidden_size, output_size)
       self.softmax = nn.LogSoftmax(dim=-1)

   def forward(self, input, hidden):
       input = input.unsqueeze(0)
       rr, hn = self.gru(input, hidden)
       return self.softmax(self.linear(rr)), hn

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

实例化参数

输入的为onehot编码,输入张量的最后一维尺寸就是n_letters

input_size = n_letters

定义隐藏层的最后一维尺寸

# 选择n_hidden为128是一种经验性的选择
n_hidden = 128

输出尺寸为语言类别总数n_categories

output_size = n_categories

num_layer使用默认值,num_layers = 1

输入参数

假如以一个字母B作为RNN的首次输入,它通过lineTotensor转为张量,因此我们的lineToTensor输出的是三维张量,而RNN类需要的是二维张量,因此需要squeeze(0)降低一个维度

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

初始化一个三维的隐藏层0张量,也是初始的细胞状态张量

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

实例化

rnn = RNN(n_letters,n_hidden,n_categories)
lstm = LSTM(n_letters,n_hidden,n_categories)
gru = GRU(n_letters,n_hidden,n_categories)

取出结果

rnn_output,next_hidden = rnn(input,hidden)
lstm_output,next_hidden,c = lstm(input,hidden,c)
gru_output,next_hidden = gru(input,hidden)

输出转换函数

def categoryFromOutput(output):
"""从输出结果中获得指定类别,参数为输出张量output"""
   # 从输出张量中返回最大的值和索引对象,我们这里需要这个索引
   """n是值,i是其在tensor中的索引,top(1)指最大,top(3)前三个"""
   # 返回类型为数组
   top_n,top_i = output.topk(1)
   # top_i对象中取出索引的值
   # 使用item()函数取出的元素值的精度更高,所以在求损失函数等时,我们一般采用item()
   category_i = top_i[0].item()
   # 根据索引值获得对应的语言类别,返回语言类别和索引值
   return all_categories[category_i],category_i

输入参数,将上一步中的gru的输出作为函数的输入

output = gru_output
category,category_i = categoryFromOutput(output)

随机产生训练数据

def randomTrainingExample():
"""函数用于随机产生训练数据"""
   # 首先使用random的choice方法all_categories随机选择一个类别
   category = random.choice(all_categories)
   # 然后再通过category_lines字典取category类对应的名字列表
   # 之后再从列表中随机取一个名字
   line = random.choice(category_lines[category])
   # 接着将这个类别在所有类别列表中的‘索引’封装成tensor,得到类别张量category_tensor
   category_tensor = torch.tensor([all_categories.index(category)],dtype=torch.long)
   line_tensor = lineToTensor(line)
   return category,line,category_tensor,line_tensor

使用上述函数

for i in range(2):
category,line,category_tensor,line_tensor = randomTrainExample()
   print('....')

构建传统RNN的训练函数

定义损失函数为nn.NLLLoss(),因为RNN的最后一层是nn.LogSoftmax(), 两者的内部计算逻辑正好能够吻合

criterion = nn.NLLLoss()

设置学习率

learning_rate = 0.005

RNN训练函数

def trainRNN(category_tensor,line_tensor):
   """
  定义训练函数,
  category_tensor 类别的张量表示,相当于训练数据的标签
  line_tensor 名字的张量表示,相当于对应训练数据
  """
   # 在函数中,首先通过实例化对象rnn初始化隐藏层张量
   hidden = rnn.initHidden()
   # 然后将模型结构中的梯度归0
   rnn.zero_grad()
   # 下面开始进行训练,将训练数据line_tensor的每个字符逐个传入rnn中,得到最终结果
   for i in range(line_tensor.size()[0]):
       output,hidden = rnn(line_tensor[i],hidden)

       # 因为我们的rnn对象由nn.RNN实例化得到,最终输出形状是三维张量,为了满足category_tensor的形状
       # 进行对比计算损失,我们需要对输出张量进行squeeze(0)操作,降低一个维度
       # 预测值和真实值进行对比,计算损失
       loss = criterion(output.squeeze(0),category_tensor)
  # 损失进行反向传播
       loss.backward()
       # 更新模型中所有的参数
       for p in rnn.parameters():
           # 将参数的张量表示 与参数的梯度乘以学习率的结果 相加 进行覆盖的更新 覆盖了p.data
           # 梯度下降是负梯度方向,所以是减去
           p.data.add_(p.grad.data, alpha=-learning_rate)
       # 返回结果和损失的值
       return output, loss.item()

构建LSTM训练函数

def trainLSTM(category_tensor, line_tensor):
   # 在函数中,首先通过实例化对象rnn初始化隐层张量,细胞状态c
   hidden, c = lstm.initHiddenAndC()
   # 将模型结构中的梯度归0
   lstm.zero_grad()

   # 下面开始进行训练,将训练数据line_tensor的每个字符逐个传入rnn之中,得到最终结果
   for i in range(line_tensor.size()[0]):
       output, hidden, c = lstm(line_tensor[i], hidden, c)

       loss = criterion(output.squeeze(0), category_tensor)
       loss.backward()

       for p in lstm.parameters():
           p.data.add_(p.grad.data, alpha=-learning_rate)
       return output, loss.item()

构建GRU训练函数

def trainGRU(category_tensor, line_tensor):
   hidden = gru.initHidden()
   gru.zero_grad()
   for i in range(line_tensor.size()[0]):
       output, hidden = gru(line_tensor[i], hidden)

       loss = criterion(output.squeeze(0), category_tensor)
       loss.backward()
       for p in gru.parameters():
           p.data.add_(p.grad.data, alpha=-learning_rate)
       return output, loss.item()

训练准备

构建模型训练时间获得函数

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

构建训练过程的日志打印函数

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


def train(train_type_fn):
   """训练过程中的日志打印函数,参数train_type_fn代表选择哪种模型训练函数,如trainRNN"""
   # 每个制图间隔损失保存列表
   all_losses = []
   # 获得训练开始时间戳
   start = time.time()
   # 设置初始间隔损失为0
   current_loss = 0
   # 从1开始进行训练迭代,共n_iters次
   for iter in range(1, n_iters + 1):
       # 通过randomTrainingExample函数随机获取一组训练数据和对应的类别
       category, line, category_tensor, line_tensor = randomTrainingExample()
       # 将训练数据和对应类别的张量传入到train函数中
       output, loss = train_type_fn(category_tensor, line_tensor)
       # 计算制图间隔中的总损失
       current_loss += loss
       # 如果迭代数能够整除打印间隔
       if iter % print_every == 0:
           # 取该迭代步上的output通过categoryFromOutput函数获得对应的类别和类别索

           guess, guess_i = categoryFromOutput(output)
           # 然后和真实的类别category做⽐较, 如果相同则打对号, 否则打叉号.
           correct = '✓' if guess == category else '✗ (%s)' % category
           # 打印迭代步, 迭代步百分⽐, 当前训练耗时, 损失, 该步预测的名字, 以及是否正

           print('%d %d%% (%s) %.4f %s / %s %s' % (iter, iter / n_iters *
                                                   100, timeSince(start), loss, line, guess, correct))
       # 如果迭代数能够整除制图间隔
       if iter % plot_every == 0:
           # 将保存该间隔中的平均损失到all_losses列表中
           all_losses.append(current_loss / plot_every)
           # 间隔损失重置为0
           current_loss = 0
   # 返回对应的总损失列表和训练耗时
   return all_losses, int(time.time() - start)

开始训练

# 调⽤train函数, 分别进⾏RNN, LSTM, GRU模型的训练
# 并返回各⾃的全部损失, 以及训练耗时⽤于制图
all_losses1, period1 = train(trainRNN)
all_losses2, period2 = train(trainLSTM)
all_losses3, period3 = train(trainGRU)

构建评估函数

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


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


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

# 调用参数
line = "Bai"
line_tensor = lineToTensor(line)
rnn_output = evaluateRNN(line_tensor)
lstm_output = evaluateLSTM(line_tensor)
gru_output = evaluateGRU(line_tensor)
print("rnn_output:", rnn_output)
print("gru_output:", lstm_output)
print("gru_output:", gru_output)

构建预测函数

# 构建预测函数
def predict(input_line,evaluate,n_predictions=3):
   """预测函数, 输⼊参数input_line代表输⼊的名字,n_predictions代表需要取最有可能的top个"""
   # ⾸先打印输⼊
   print('\n> %s' % input_line)
   # 以下操作的相关张量不进⾏求梯度
   with torch.no_grad():
       # 使输⼊的名字转换为张量表示, 并使⽤evaluate函数获得预测输出
       output = evaluate(lineToTensor(input_line))
       # 从预测的输出中取前3个最⼤的值及其索引
       topv, topi = output.topk(n_predictions, 1, True)
       # 创建盛装结果的列表
       predictions = []
       # 遍历n_predictions
       for i in range(n_predictions):
           # 从topv中取出的output值
           value = topv[0][i].item()
           # 取出索引并找到对应的类别
           category_index = topi[0][i].item()
           # 打印ouput的值, 和对应的类别
           print('(%.2f) %s' % (value, all_categories[category_index]))
           # 将结果装进predictions中
           predictions.append([value, all_categories[category_index]])


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

完整代码

# 从io中导入文件打开方法
from io import open
# 帮助使用正则表达式进行子目录的查询
import glob
import os
# 用于获得常见字母及字符规范化
import string
import unicodedata
# 导入随机数模块
import random
# 导入时间模块和数学模块
import time
import math
# 导入PyTorch模块
import torch
import torch.nn as nn
# 导入绘图模块
import matplotlib.pyplot as plt

# 数据处理
# 获取所有常用字符包括字母和常用标点
all_letters = string.ascii_letters + " .,;'"
# print(all_letters)
# 获取常用字符数量
n_letters = len(all_letters)


# print(n_letters)

# 字符规范化之unicode转ascii
# # 关于编码问题我们暂且不去考虑
# # 我们认为这个函数的作⽤就是去掉⼀些语⾔中的重⾳标记
def unicodeToAscii(s):
   return ''.join(
       c for c in unicodedata.normalize('NFD', s)
       if unicodedata.category(c) != 'Mn' and c in all_letters
  )


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

# 构建一个从持久化文件中读取内容到内存的函数

data_path = "./data/names/"


def readLines(filename):
   # 打开文件
   # 打开指定⽂件并读取所有内容, 使⽤strip()去除两侧空⽩符, 然后以'\n'进⾏切分
   lines = open(filename, encoding='utf-8').read().strip().split('\n')
   # 对应每⼀个lines列表中的名字进⾏Ascii转换, 使其规范化.最后返回⼀个名字列表
   return [unicodeToAscii(line) for line in lines]


# filename是数据集中某个具体的⽂件, 我们这⾥选择Chinese.txt
# filename = data_path + "Chinese.txt"
# lines = readLines(filename)
# print(lines)

# 构建⼈名类别(所属的语⾔)列表与⼈名对应关系字典:
category_lines = {}

# 构建⼈名类别(所属的语⾔)列表
all_categories = []

# 遍历data_path下的所有⽂件
for filename in glob.glob(data_path + '*.txt'):
   # 获取每个文件的文件名,就是对应名字类别
   category = os.path.splitext(os.path.basename(filename))[0]
   # 将其逐一装到all_categories中
   all_categories.append(category)
   # 读取每个文件中的内容,也就是对应类别中的所有名字
   lines = readLines(filename)
   # 将每个类别中的所有名字装到category_lines中
   category_lines[category] = lines

# 查看类别总数
n_categories = len(all_categories)


# print("n_categories:", n_categories)

# 随便查看其中的⼀些内容
# print(category_lines['Italian'][:5])

# 构建训练数据:将人名转化为对应onehot张量表示

def lineToTensor(line):
   """将人名转化为对应onehot张量表示,参数line是输入的人名"""
   # 首先初始化一个0张量,它的形状(len(line), 1, n_letters),
   # 代表人名中的每个字(不是字母)用一个1 x n_letters的onehot向量表示
   tensor = torch.zeros(len(line), 1, n_letters)
   for li, letter in enumerate(line):
       # 使⽤字符串⽅法find找到每个字符在all_letters中的索引
       # 它也是我们⽣成onehot张量中1的索引位置
       # print(li, letter)
       tensor[li][0][all_letters.find(letter)] = 1
   return tensor


"""
该函数相当于 创建一个全零张量,然后从人名中取出每个字母,然后在全零张量中找到对应的位置,将其置为1
"""
line = "Bai"
line_tensor = lineToTensor(line)


# print("line_tensot:", line_tensor)


# 构建RNN模型
# 使用nn.RNN构建完成传统RNN使用类
class RNN(nn.Module):
   def __init__(self, input_size, hidden_size, output_size, num_layers=1):
       """
      初始化
      :param input_size: 输入大小
      :param hidden_size: 隐藏层大小
      :param output_size: 输出大小
      :param num_layers: 层数
      """
       super(RNN, self).__init__()
       # 将hidden_size与num_layers传入RNN类中
       self.hidden_size = hidden_size
       self.num_layers = num_layers

       # 实例化预定义的nn.RNN,三个参数分别是input_size,hidden_size,num_layers
       self.rnn = nn.RNN(input_size, hidden_size, num_layers)
       # 实例化预定义的nn.Linear,两个参数分别是hidden_size,output_size,这个线性层用于将nn.RNN的输出维度转化为指定的输出维度
       # (隐藏层到输出层)全连接层
       self.linear = nn.Linear(hidden_size, output_size)
       # 实例化nn中预定的Softmax层, ⽤于从输出层获得类别结果
       # dim=-1 代表在最后一个维度上求
       # dim=0对每一列的所有元素进行softmax运算,并使得每一列所有元素和为1
       # dim=1:对每一行的所有元素进行softmax运算,并使得每一行所有元素和为1
       self.softmax = nn.LogSoftmax(dim=-1)

   # 内部会调用的函数
   def forward(self, input, hidden):
       """
      完成传统RNN中的主要逻辑,输入参数input代表输入张量,它的形状是1 x n_letters
      x 可以理解为名字的字数 所以实际形状是[1,n_letters] 需要扩展一维
      hidden代表隐藏层张量,它的形状是num_layers x 1 x hidden_size
      """
       # 因为预定义的nn.RNN要求输入维度一定是三维张量,因此在这里使用unsqueeze(0)将输入张量的第0维度扩展一个维度
       input = input.unsqueeze(0)
       # 将input和hidden传入nn.RNN中,如果num_layers=1, rr恒等于hn
       rr, hn = self.rnn(input, hidden)
       # 将从RNN中获得的结果通过线性变换和softmax返回,同时返回hn作为后续RNN的输⼊
       return self.softmax(self.linear(rr)), hn

   def initHidden(self):
       """
      初始化隐藏层张量,它的形状是num_layers x 1 x hidden_size
      """
       # 初始化⼀个(self.num_layers, 1, self.hidden_size)形状的0张量
       return torch.zeros(self.num_layers, 1, self.hidden_size)


# 使用nn.LSTM构建完成LSTM使用类
class LSTM(nn.Module):
   def __init__(self, input_size, hidden_size, output_size, num_layers=1):
       """
      初始化参数
      :param input_size: 输入大小
      :param hidden_size: 隐藏层大小
      :param output_size: 输出大小
      :param num_layers: 层数
      """
       super(LSTM, self).__init__()
       # 将hidden_size与num_layers传入LSTM类中
       self.hidden_size = hidden_size
       self.num_layers = num_layers

       # 实例化预定义的nn.LSTM
       self.lstm = nn.LSTM(input_size, hidden_size, num_layers)
       # 实例化nn.linear,这个线性层用于将nn.RNN的输出维度转化为指定的输出维度
       self.linear = nn.Linear(hidden_size, output_size)
       # 实例化nn中预定的softmax层,⽤于从输出层获得类别结果
       self.softmax = nn.LogSoftmax(dim=-1)
       # dim=-1 代表在最后一个维度上求
       # dim=0对每一列的所有元素进行softmax运算,并使得每一列所有元素和为1
       # dim=1:对每一行的所有元素进行softmax运算,并使得每一行所有元素和为1

   def forward(self, input, hidden, c):
       """
      在主要逻辑函数中多出一个参数c,也就是LSTM中的细胞状态张量
      :param input:
      :param hidden:
      :param c:
      :return:
      """
       input = input.unsqueeze(0)
       rr, (hn, c) = self.lstm(input, (hidden, c))
       # 最后返回处理后的rr,hn,c
       return self.softmax(self.linear(rr)), hn, c

   def initHiddenAndC(self):
       """
      初始化函数不仅要初始化hidden还要初始化细胞状态c,他们形状相同
      """
       c = hidden = torch.zeros(self.num_layers, 1, self.hidden_size)
       return hidden, c


# 使⽤nn.GRU构建完成传统RNN使⽤类
# GRU与传统RNN的外部形式相同, 都是只传递隐层张量, 因此只需要更改预定义层的名字
class GRU(nn.Module):
   def __init__(self, input_size, hidden_size, output_size, num_layers=1):
       super(GRU, self).__init__()
       self.hidden_size = hidden_size
       self.num_layers = num_layers

       # 实例化预定义的nn.GRU,它的三个参数分别是input_size, hidden_size,num_layers
       self.gru = nn.GRU(input_size, hidden_size, num_layers)
       self.linear = nn.Linear(hidden_size, output_size)
       self.softmax = nn.LogSoftmax(dim=-1)

   def forward(self, input, hidden):
       input = input.unsqueeze(0)
       rr, hn = self.gru(input, hidden)
       return self.softmax(self.linear(rr)), hn

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


# 实例化参数

# 因为是onehot编码,输入张量最后一维的尺寸就是n_letters
input_size = n_letters
# 定义隐藏层的最后一维尺寸
n_hidden = 128
# 输出尺寸为语言类别总数n_categories
output_size = n_categories
# num_layer使用默认值,num_layers = 1

# 输入参数
"""
假如以一个字母B作为RNN的首次输入,它通过lineToTensor转为张量
因此我们的lineToTensor输出的是三位张量,而RNN类需要的是二维张量
因此需要使用sqeuuze(0)降低一个维度
"""
input = lineToTensor('B').squeeze(0)
# 初始化一个三维的隐藏层0张量,也是初始的细胞状态张量
hidden = c = torch.zeros(1, 1, n_hidden)

rnn = RNN(n_letters, n_hidden, n_categories)
lstm = LSTM(n_letters, n_hidden, n_categories)
gru = GRU(n_letters, n_hidden, n_categories)

# rnn_output, next_hidden = rnn(input, hidden)
# # print("rnn:", rnn_output)
# lstm_output, next_hidden, c = lstm(input, hidden, c)
# # print("lstm:", lstm_output)
# gru_output, next_hidden = gru(input, hidden)


# print("gru:", gru_output)


# 构建训练函数并进行训练
# 从输出结果中获得指定类别函数
def categoryFromOutput(output):
   """从输出结果中获得指定类别,参数为输出张量output"""
   # 从输出张量中返回最大的值和索引对象,我们这里主要需要这个索引
   """ n是值 i是其在tensor中的索引 top(1)指最大 top(3)前三个最大的"""
   # 返回类型都为数组
   top_n, top_i = output.topk(1)
   # top_i对象中取出索引的值
   # 使用item()函数取出的元素值的精度更高,所以在求损失函数等时我们一般用item()
   category_i = top_i[0].item()
   # 根据索引值获得对应语言类别,返回语言类别和索引值
   return all_categories[category_i], category_i


# 输入参数
# 将上一步中gru的输出作为函数的输入
# output = gru_output
# # 调用
# category, category_i = categoryFromOutput(output)


# print('category:', category)
# print('category_i:', category_i)


# 随机生产训练数据
def randomTrainingExample():
   """该函数用于随机产生训练数据"""
   # 首先使用random的choice方法从all_categorys随机选择一个类别
   category = random.choice(all_categories)
   # 然后在通过category_lines字典取category类别对应的名字列表
   # 之后再从列表中随机取一个名字
   line = random.choice(category_lines[category])
   # 接着将这个类别在所有类别列表中的 '索引' 封装成tensor,得到类别张量category_tensor
   category_tensor = torch.tensor([all_categories.index(category)], dtype=torch.long)
   line_tensor = lineToTensor(line)
   return category, line, category_tensor, line_tensor


for i in range(2):
   category, line, category_tensor, line_tensor = randomTrainingExample()
   # print(line_tensor)
   # print('here',line_tensor.size()[0])
   print('category =', category, '/ line =', line, '/ category_tensor =',
         category_tensor)

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


def trainRNN(category_tensor, line_tensor):
   """
  定义训练函数,它的两个参数是category_tensor类别的张量表示,相当于训练数据的标签
  line_tensor名字的张量表示,相当于对应训练数据
  """
   # 在函数中,首先通过实例化对象rnn初始化隐层张量
   hidden = rnn.initHidden()
   # print(hidden)

   # 然后将模型结构中的梯度归0
   rnn.zero_grad()

   # 下面开始进行训练,将训练数据line_tensor的每个字符逐个传入rnn之中,得到最终结果
   for i in range(line_tensor.size()[0]):
       output, hidden = rnn(line_tensor[i], hidden)
       # print(hidden)

       # 因为我们的rnn对象由nn.RNN实例化得到,最终输出形状是三维张量,为了满足于category_tensor的形状
       # 进行对比计算损失,我们需要对输出张量进行squeeze(0)操作,降低一个维度
       # 预测值和真实值进行对比,计算损失
       loss = criterion(output.squeeze(0), category_tensor)
       # 损失进行反向传播
       loss.backward()
       # 更新模型中所有的参数
       for p in rnn.parameters():
           # 将参数的张量表示 与参数的梯度乘以学习率的结果 相加 进行覆盖的更新 覆盖了p.data
           # 梯度下降是负梯度方向,所以是减去
           p.data.add_(p.grad.data, alpha=-learning_rate)
       # 返回结果和损失的值
       return output, loss.item()


# 构建LSTM训练函数
# 与传统RNN相比多出细胞状态c
def trainLSTM(category_tensor, line_tensor):
   # 在函数中,首先通过实例化对象rnn初始化隐层张量,细胞状态c
   hidden, c = lstm.initHiddenAndC()
   # 将模型结构中的梯度归0
   lstm.zero_grad()

   # 下面开始进行训练,将训练数据line_tensor的每个字符逐个传入rnn之中,得到最终结果
   for i in range(line_tensor.size()[0]):
       output, hidden, c = lstm(line_tensor[i], hidden, c)

   loss = criterion(output.squeeze(0), category_tensor)
   loss.backward()

   for p in lstm.parameters():
       p.data.add_(p.grad.data, alpha=-learning_rate)
   return output, loss.item()


# 构建GRU训练函数
# 与传统RNN完全相同,只是名字改为GRU
def trainGRU(category_tensor, line_tensor):
   hidden = gru.initHidden()
   gru.zero_grad()
   for i in range(line_tensor.size()[0]):
       output, hidden = gru(line_tensor[i], hidden)

       loss = criterion(output.squeeze(0), category_tensor)
       loss.backward()
       for p in gru.parameters():
           p.data.add_(p.grad.data, alpha=-learning_rate)
       return output, loss.item()


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


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


def train(train_type_fn):
   """训练过程中的日志打印函数,参数train_type_fn代表选择哪种模型训练函数,如trainRNN"""
   # 每个制图间隔损失保存列表
   all_losses = []
   # 获得训练开始时间戳
   start = time.time()
   # 设置初始间隔损失为0
   current_loss = 0
   # 从1开始进行训练迭代,共n_iters次
   for iter in range(1, n_iters + 1):
       # 通过randomTrainingExample函数随机获取一组训练数据和对应的类别
       category, line, category_tensor, line_tensor = randomTrainingExample()
       # 将训练数据和对应类别的张量传入到train函数中
       output, loss = train_type_fn(category_tensor, line_tensor)
       # 计算制图间隔中的总损失
       current_loss += loss
       # 如果迭代数能够整除打印间隔
       if iter % print_every == 0:
           # 取该迭代步上的output通过categoryFromOutput函数获得对应的类别和类别索

           guess, guess_i = categoryFromOutput(output)
           # 然后和真实的类别category做⽐较, 如果相同则打对号, 否则打叉号.
           correct = '✓' if guess == category else '✗ (%s)' % category
           # 打印迭代步, 迭代步百分⽐, 当前训练耗时, 损失, 该步预测的名字, 以及是否正

           print('%d %d%% (%s) %.4f %s / %s %s' % (iter, iter / n_iters *
                                                   100, timeSince(start), loss, line, guess, correct))
       # 如果迭代数能够整除制图间隔
       if iter % plot_every == 0:
           # 将保存该间隔中的平均损失到all_losses列表中
           all_losses.append(current_loss / plot_every)
           # 间隔损失重置为0
           current_loss = 0
   # 返回对应的总损失列表和训练耗时
   return all_losses, int(time.time() - start)


# 开始训练传统RNN, LSTM, GRU模型并制作对⽐图
# 调⽤train函数, 分别进⾏RNN, LSTM, GRU模型的训练
# 并返回各⾃的全部损失, 以及训练耗时⽤于制图

all_losses1, period1 = train(trainRNN)
all_losses2, period2 = train(trainLSTM)
all_losses3, period3 = train(trainGRU)
# 绘制损失对⽐曲线, 训练耗时对⽐柱张图
# 创建画布0
plt.figure(0)
# 绘制损失对⽐曲线
plt.plot(all_losses1, label="RNN")
plt.plot(all_losses2, color="red", label="LSTM")
plt.plot(all_losses3, color="orange", label="GRU")
plt.legend(loc='upper left')
# 创建画布1
plt.figure(1)
x_data = ["RNN", "LSTM", "GRU"]
y_data = [period1, period2, period3]
# 绘制训练耗时对⽐柱状图
plt.bar(range(len(x_data)), y_data, tick_label=x_data)
plt.show()


# 构建评估函数并进行预测
def evaluateRNN(line_tensor):
   """
  评估函数,和训练函数逻辑相同,参数是line_tensor代表名字的张量表示
  :param line_tensor:
  :return:
  """
   # 初始化隐藏层张量
   hidden = rnn.initHidden()
   # 将评估数据line_tensor每个字符逐个传入rnn中
   for i in range(line_tensor.size()[0]):
       output,hidden = rnn(line_tensor[i],hidden)
   # 获得输出结果
   return output.squeeze(0)


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


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

# 调用参数
line = "Bai"
line_tensor = lineToTensor(line)
rnn_output = evaluateRNN(line_tensor)
lstm_output = evaluateLSTM(line_tensor)
gru_output = evaluateGRU(line_tensor)
print("rnn_output:", rnn_output)
print("gru_output:", lstm_output)
print("gru_output:", gru_output)


# 构建预测函数
def predict(input_line,evaluate,n_predictions=3):
   """预测函数, 输⼊参数input_line代表输⼊的名字,n_predictions代表需要取最有可能的top个"""
   # ⾸先打印输⼊
   print('\n> %s' % input_line)
   # 以下操作的相关张量不进⾏求梯度
   with torch.no_grad():
       # 使输⼊的名字转换为张量表示, 并使⽤evaluate函数获得预测输出
       output = evaluate(lineToTensor(input_line))
       # 从预测的输出中取前3个最⼤的值及其索引
       topv, topi = output.topk(n_predictions, 1, True)
       # 创建盛装结果的列表
       predictions = []
       # 遍历n_predictions
       for i in range(n_predictions):
           # 从topv中取出的output值
           value = topv[0][i].item()
           # 取出索引并找到对应的类别
           category_index = topi[0][i].item()
           # 打印ouput的值, 和对应的类别
           print('(%.2f) %s' % (value, all_categories[category_index]))
           # 将结果装进predictions中
           predictions.append([value, all_categories[category_index]])


for evaluate_fn in [evaluateRNN, evaluateLSTM, evaluateGRU]:
    print("-"*18)
    predict('Dovesky', evaluate_fn)
    predict('Jackson', evaluate_fn)
    predict('Satoshi', evaluate_fn)
 
posted @ 2023-06-24 10:05  qfzwy  阅读(107)  评论(0)    收藏  举报