- #从io中导入打开方式
- from io import open
- #帮助使用正则表达式进行子目录的查询
- import glob
- import os
- #用于获得常见字母及字符规范化
- import string
- import unicodedata
- #导入随机工具
- 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)
all_letters
- # 关于编码问题我们暂且不去考虑
- #我们认为这个函数的作用就是去掉一些语言中的重音标记
- #如: Ś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)
- 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形如:{"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'][:10])
- 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_tensor:",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层, 用于从输出层获得类别结果
- self.softmax = nn.LogSoftmax(dim=-1)
-
-
- def forward(self, input, hidden):
- """完成传统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)
- # 将input和hidden输入到传统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):
- """初始化隐层张量"""
- # 初始化一个(self.num_layers, 1, self.hidden_size)形状的0张量
- return torch.zeros(self.num_layers, 1, self.hidden_size)
- x = torch.tensor([1,2,3,4])
- torch.unsqueeze(x,0)
torch.unsqueeze(x,1)
构建LSTM模型:
- # 使用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, 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类需要的二维张量
- # 因此需要使用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)
- 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"""
- # 从输出张量中返回最大的值和索引对象, 我们这里主要需要这个索引
- top_n, top_i = output.topk(1)
- # top_i对象中取出索引的值
- category_i = top_i[0].item()
- # 根据索引值获得对应语言类别, 返回语言类别和索引值
- return all_categories[category_i], category_i
torch.topk演示:
- x = torch.arange(1., 6.)
- x
torch.topk(x, 3)
输入参数:
- # 将上一步中gru的输出作为函数的输入
- output = gru_output
- output
调用
- category, category_i = categoryFromOutput(output)
- print("category:", category)
- print("category_i:", category_i)
随机生成训练数据:
torch.Tensor和torch.tensor的区别
- 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之中, 得到最终结果
- 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)
- # 返回结果和损失的值
- return output, loss.item()
torch.add演示:
- a = torch.randn(4)
- a
- b = torch.randn(4, 1)
- b
torch.add(a, b, alpha=10) # alpha的作用是 alpha * b + a
构建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)
- 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)
- 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)
构建训练过程的日志打印函数:
- 设置训练迭代次数
- n_iters = 100000
- # 设置结果的打印间隔
- print_every = 5000
- # 设置绘制损失曲线上的制图间隔
- 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模型的训练
- # 并返回各自的全部损失, 以及训练耗时用于制图
- print("传统RNN训练日志输出:")
- all_losses1, period1 = train(trainRNN)
- print("传统LSTM训练日志输出:")
- all_losses2, period2 = train(trainLSTM)
- print("传统GRU训练日志输出:")
- 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)
- # 绘制损失对比曲线, 训练耗时对比柱张图
- # 创建画布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)