以⼀个⼈名为输⼊,使⽤模型帮助我们判断它最有可能是来⾃哪⼀个国家的⼈名,在某些国际化公司的业务中具有重要意义,在⽤户注册过程中 , 会根据⽤户填写的名字直接给他分配可能的国家或地区选项 , 以及该国家或地区的国旗 , 限制⼿机号码位数等。
数据下载地址 : https://download.pytorch.org/tutorial/data.zip
python版本使⽤ 3.6.x, pytorch 版本使⽤ 1.3.1
pip install torch==1.3.1
# 从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
获取常⽤的字符数量:
# 获取所有常⽤字符包括字⺟和常⽤标点
all_letters = string.ascii_letters + " .,;'"
# 获取常⽤字符数量
n_letters = len(all_letters)
print("n_letter:", n_letters)
输出效果:
n_letter: 57
字符规范化之 unicode 转 ASCII 函数:
调⽤ :import unicodedata
# 导⼊随机⼯具 random
import random
# 导⼊时间和数学⼯具包
import time
import math
# 导⼊torch ⼯具
import torch
# 导⼊nn 准备构建模型
import torch.nn as nn
# 引⼊制图⼯具包
import matplotlib.pyplot as plt
# 获取所有常⽤字符包括字⺟和常⽤标点
all_letters = string.ascii_letters + " .,;'"
# 获取常⽤字符数量
n_letters = len(all_letters )
print("n_letter:" , n_letters )
n_letter : 57
# 关于编码问题我们暂且不去考虑
# 我们认为这个函数的作⽤就是去掉⼀些语⾔中的重⾳标记
# 如: Ś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)
输出效果:
s = "Ślusàrski"
构建⼀个从持久化⽂件中读取内容到内存的函数:
data_path = "./data/name/"
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形如:{"English":["Lily", "Susan", "Kobe"], "Chinese":
["Zhang San", "Xiao Ming"]}
category_lines = {}
# all_categories形如: ["English",...,"Chinese"]
all_categories = []
# 读取指定路径下的txt⽂件, 使⽤glob,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])
输出效果:
n_categories: 18
['Abandonato', 'Abatangelo', 'Abatantuono', 'Abate', 'Abategiovanni']
将⼈名转化为对应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.]]])
def lineToTensor(line):
"""将⼈名转化为对应onehot张量表示, 参数line是输⼊的⼈名"""
# ⾸先初始化⼀个0张量, 它的形状(len(line), 1, n_letters)
# 代表⼈名中的每个字⺟⽤⼀个1 x n_letters的张量表示.
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
调用:
line = "Bai"
line_tensor = lineToTensor(line)
print("line_tensot:", line_tensor)
# 使用nn.RNN构建完成传统RNN使用类
class RNN(nn.Module):
def __init__(self, input_size, hidden_size, output_size, num_layers=1):
"""初始化函数中有4个参数, 分别代表RNN输入最后一维尺寸, RNN的隐层最后一维尺寸, RNN层数"""
super(RNN, self).__init__()
# 将hidden_size与num_layers传入其中
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, 这个线性层用于将nn.RNN的输出维度转化为指定的输出维度
self.linear = nn.Linear(hidden_size, output_size)
# 实例化nn中预定的Softmax层, 用于从输出层获得类别结果 dim=-1代表 最后一个维度求解
self.softmax = nn.LogSoftmax(dim=-1)
def forward(self, input, hidden):
# print('inputshape1====', input.shape)
"""完成传统RNN中的主要逻辑, 输入参数input代表输入张量, 它的形状是1 x n_letters
hidden代表RNN的隐层张量, 它的形状是self.num_layers x 1 x self.hidden_size"""
# 因为预定义的nn.RNN要求输入维度一定是三维张量, 因此在这里使用unsqueeze(0)扩展一个维度
input = input.unsqueeze(0)
# print('inputshape2====', input.shape)
# 将input和hidden输入到传统RNN的实例化对象中,如果num_layers=1, rr恒等于hn
rr, hn = self.rnn(input, hidden)
# print('rnn_rr===', rr.shape)
# print('self.linearshape===', self.linear(rr).shape) # self.linearshape=== torch.Size([1, 1, 18])
# 将从RNN中获得的结果通过线性变换和softmax返回,同时返回hn作为后续RNN的输入
return self.softmax(self.linear(rr)), hn
def initHidden(self):
"""初始化隐层张量"""
# 初始化一个(self.num_layers, 1, self.hidden_size)形状的0张量
return torch.zeros(self.num_layers, 1, self.hidden_size)
torch.unsqueeze演示:
>>> x = torch.tensor([1, 2, 3, 4])
>>> torch.unsqueeze(x, 0)
tensor([[ 1, 2, 3, 4]])
>>> torch.unsqueeze(x, 1)
tensor([[ 1],
[ 2],
[ 3],
[ 4]])
# 使用nn.LSTM构建完成LSTM使用类
class LSTM(nn.Module):
def __init__(self, input_size, hidden_size, output_size, num_layers=1):
"""初始化函数的参数与传统RNN相同"""
super(LSTM, self).__init__()
# 将hidden_size与num_layers传入其中
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)
def forward(self, input, hidden, c):
"""在主要逻辑函数中多出一个参数c, 也就是LSTM中的细胞状态张量"""
# 使用unsqueeze(0)扩展一个维度
input = input.unsqueeze(0)
# 将input, hidden以及初始化的c传入lstm中
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,
# 使用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)
从输出结果中获得指定类别函数:
# 第四步: 构建训练函数并进行训练.
# 从输出结果中获得指定类别函数
def categoryFromOutput(output):
"""从输出结果中获得指定类别, 参数为输出张量output"""
# 从输出张量中返回最大的值和索引对象, 我们这里主要需要这个索引
top_n, top_i = output.topk(1)
# print('top_n===', top_n)
# print('top_i===', top_i)
# print('top_i[0]===', top_i[0])
'''
top_n=== tensor([[[-2.8123]]], grad_fn=<TopkBackward>)
top_i=== tensor([[[3]]])
top_i[0]=== tensor([[3]])
'''
# top_i对象中取出索引的值
category_i = top_i[0].item()
# category_i = top_i.item() # 这个也是可以的
# 根据索引值获得对应语言类别, 返回语言类别和索引值
return all_categories[category_i], category_i
torch.topk演示:
>>> x = torch.arange(1., 6.)
>>> x
tensor([ 1., 2., 3., 4., 5.])
>>> torch.topk(x, 3)
torch.return_types.topk(values=tensor([5., 4., 3.]), indices=tensor([4, 3,
2]))
输⼊参数:
# 将上⼀步中gru的输出作为函数的输⼊
output = gru_output
# tensor([[[-2.8042, -2.8894, -2.8355, -2.8951, -2.8682, -2.9502, -2.9056,
# -2.8963, -2.8671, -2.9109, -2.9425, -2.8390, -2.9229, -2.8081,
# -2.8800, -2.9561, -2.9205, -2.9546]]], grad_fn=
<LogSoftmaxBackward>)
调⽤:
category, category_i = categoryFromOutput(output)
print("category:", category)
print("category_i:", category_i)
输出效果:
category: Portuguese
category_i: 13
随机⽣成训练数据:
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)
# 最后, 将随机取到的名字通过函数lineToTensor转化为onehot张量表示
line_tensor = lineToTensor(line)
return category, line, category_tensor, line_tensor
调⽤:
# 我们随机取出⼗个进⾏结果查看
for i in range(10):
category, line, category_tensor, line_tensor = randomTrainingExample()
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()
# 然后将模型结构中的梯度归0
rnn.zero_grad()
# 下面开始进行训练, 将训练数据line_tensor的每个字符逐个传入rnn之中, 得到最终结果
# line_tensor.size()[0] 代表一个名字有几个字符,把字符的个数提取出来: 比如'bai'[3, 1, 57], 把3提取出来
for i in range(line_tensor.size()[0]):
output, hidden = rnn(line_tensor[i], hidden)
# 因为我们的rnn对象由nn.RNN实例化得到, 最终输出形状是三维张量, 为了满足于category_tensor
# 进行对比计算损失, 需要减少第一个维度, 这里使用squeeze()方法
# 损失函数计算, 第一个参数是二维的, 第二个参数是一维的, 看源码,函数要求的输入
loss = criterion(output.squeeze(0), category_tensor)
# 损失进行反向传播
loss.backward()
# 更新模型中所有的参数
for p in rnn.parameters():
# 将参数的张量表示与参数的梯度乘以学习率的结果相加以此来更新参数
# p.data.add_(-learning_rate, p.grad.data)
p.data.add_(p.grad.data, alpha=-learning_rate)
# 返回结果和损失的值
return output, loss.item()
torch.add演示:
>>> a = torch.randn(4)
>>> a
tensor([-0.9732, -0.3497, 0.6245, 0.4022])
>>> b = torch.randn(4, 1)
>>> b
tensor([[ 0.3743],
[-1.7724],
[-0.5811],
[-0.8017]])
>>> torch.add(a, b, alpha=10)
tensor([[ 2.7695, 3.3930, 4.3672, 4.1450],
[-18.6971, -18.0736, -17.0994, -17.3216],
[ -6.7845, -6.1610, -5.1868, -5.4090],
[ -8.9902, -8.3667, -7.3925, -7.6147]])
构建LSTM训练函数:
# 与传统RNN相比多出细胞状态c
def trainLSTM(category_tensor, line_tensor):
hidden, c = lstm.initHiddenAndC()
lstm.zero_grad()
for i in range(line_tensor.size()[0]):
# 返回output, hidden以及细胞状态c
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_(-learning_rate, p.grad.data)
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_(-learning_rate, p.grad.data)
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)
输⼊参数:
# 假定模型训练开始时间是10min之前
since = time.time() - 10 * 60
# 调⽤:
period = timeSince(since)
print(period)
# 输出效果:
10m 0s
构建训练过程的日志打印函数:
# 设置训练迭代次数
n_iters = 10000
# 设置结果的打印间隔
print_every = 50
# 设置绘制损失曲线上的制图间隔
plot_every = 10
def train(train_type_fn):
"""训练过程的日志打印函数, 参数train_type_fn代表选择哪种模型训练函数, 如trainRNN"""
# 每个制图间隔损失保存列表
all_losses = []
# 保存制图使用
all_train_acc = []
# 获得训练开始时间戳
start = time.time()
# 设置初始间隔损失为0
current_loss = 0
# 添加======
current_acc = 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
# 改造===
# 取该迭代步上的output通过categoryFromOutput函数获得对应的类别和类别索引
guess, guess_i = categoryFromOutput(output)
current_acc += 1 if guess == category else 0
# 如果迭代数能够整除打印间隔
if iter % print_every == 0:
# 然后和真实的类别category做比较, 如果相同则打对号, 否则打叉号.
correct = '✓' if guess == category else '✗ (%s)' % category
# 打印迭代步, 迭代步百分比, 当前训练耗时, 损失, 该步预测的名字, 以及是否正确
print('%d %d%% (%s) %.4f %s / %s %s|| acc:%.4f' % (
iter, iter / n_iters * 100, timeSince(start), loss, line, guess, correct, current_acc / print_every))
all_train_acc.append(current_acc / print_every)
current_acc = 0
# 如果迭代数能够整除制图间隔
if iter % plot_every == 0:
# 将保存该间隔中的平均损失到all_losses列表中
all_losses.append(current_loss / plot_every)
# 间隔损失重置为0
current_loss = 0
# return current_acc / n_iters
# 返回对应的总损失列表和训练耗时
return all_losses, all_train_acc, int(time.time() - start)
开始训练传统 RNN, LSTM, GRU 模型并制作对⽐图:
# 调用train函数, 分别进行RNN, LSTM, GRU模型的训练
# 并返回各自的全部损失, 以及训练耗时用于制图
all_losses1, all_train_acc1, period1 = train(trainRNN)
all_losses2, all_train_acc2, period2 = train(trainLSTM)
all_losses3, all_train_acc3, 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')
plt.savefig('./img/RNN_LSTM_GRU_loss.png')
# 创建画布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.savefig('./img/RNN_LSTM_GRU_period.png')
损失对⽐曲线:
损失对⽐曲线分析:模型训练的损失降低快慢代表模型收敛程度。由图可知,传统 RNN 的模型收敛情况最好,然后GRU、LSTM、因为我们当前处理的⽂本数据是⼈名,他们的⻓度有限,且⻓距离字⺟间基本⽆特定关联,因此⽆法发挥改进模型 LSTM 和 GRU 的⻓距离捕捉语义关联的优势。所以在以后的模型选⽤时,要通过对任务的分析以及实验对⽐,选择适合模型。
训练耗时对⽐图:
分析:模型训练的耗时⻓短代表模型的计算复杂度。由图可知 ,正如理论分析,传统 RNN 复杂度最低,耗时⼏乎只是后两者⼀半,然后GRU、LSTM。
结论:模型选⽤⼀般应通过实验对⽐,并⾮越复杂或越先进的模型表现越好,⽽是需要结合⾃ ⼰的特定任务,从对数据的分析和实验结果中获得最佳答案。
构建传统 RNN 评估函数:
def evaluateRNN(line_tensor):
"""评估函数, 和训练函数逻辑相同, 参数是line_tensor代表名字的张量表示"""
rnn = RNN(n_letters, n_hidden, n_categories)
rnn.load_state_dict(torch.load(PATHRNN))
# 初始化隐层张量
hidden = rnn.initHidden()
# 将评估数据line_tensor的每个字符逐个传入rnn之中
print('evaluateRNN', line_tensor.size()) # evaluateRNN torch.Size([3, 1, 57])
for i in range(line_tensor.size()[0]):
output, hidden = rnn(line_tensor[i], hidden)
# 获得输出结果
print('rnnoutput====', output.shape)
return output.squeeze(0)
输⼊参数:
line = "Bai"
line_tensor = lineToTensor(line)
调⽤:
rnn_output = evaluateRNN(line_tensor)
构建预测函数:
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个最大的值及其索引 1表示要排序的维度, True表示是否返回最大或是最下的元素
topv, topi = output.topk(n_predictions, 1, True)
# print('topv-=-=-=', topv)
# print('topv.shape-=-=-=', topv.shape)
# print('topi-=-=-=', topi)
# print('topi.shape-=-=-=', topi.shape)
'''
topv-=-=-= tensor([[-2.7748, -2.8000, -2.8019]])
topv.shape-=-=-= torch.Size([1, 3])
topi-=-=-= tensor([[14, 15, 12]])
topi.shape-=-=-= torch.Size([1, 3])
'''
# 创建盛装结果的列表
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]])
return predictions
调⽤:
for evaluate_fn in [evaluateRNN, evaluateLSTM, evaluateGRU]:
print("-"*18)
predict('Dovesky', evaluate_fn)
predict('Jackson', evaluate_fn)
predict('Satoshi', evaluate_fn)
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。