网络主要参数
RNN及其变体的参数基本一致:
"""
nn.RNN(input_size, hiddden_size, num_layers, batch_first, bidrectional)
input_size:
输入张量维度
hidden_size:
数据经RNN后的输出张量维度
num_layers:
网络层数, 一般设置为1就可以
batch_first:
布尔类型, True表示前向传播时, RNN的输入是(batch_size, sql_len, input_size)
False(默认), RNN的输入是(sql_len, batch_size, input_size)
batch_size
句子数量
sql_len
句子内词的数量
input_size
一个词用一个几维向量表示
bidrectional:
布尔类型, 表示是否是双向循环网络
out_put, hn = self.rnn(input, h0)
假设: nn.RNN(input_size=10, hiddden_size=128, num_layers=1, batch_first=True, bidrectional=True)
input
调用rnn时的输入信息
batch_size: 句子数量
sql_len:句子中词的数量(token数量)
input_size:词向量维度(embedding_dim=10)
h0
初始化隐藏层状态(默认初始化全零)
num_layers=初始化时num_layers*bideractional(True:2, False:1)
batch_size=input中的句子数量
hidden_size=数据经过RNN的输出维度
output
RNN的输出信息
batch_size:句子数量
sel_len:句子中token数量
hidden_size:数据经过RNN的输出维度
hn
hn隐藏层的维度不变, 与初始化的维度一样, 但是张量内的数据变化
(2, 1, 128)
"""
一. 传统RNN
两个线性层: 当前时间步输入和隐藏状态都要经过线性层
原理
RNN(Recurrent Neural Network), 中文称作循环神经网络, 它一般以序列数据为输入, 通过网络内部的结构设计有效捕捉序列之间的关系特征(句法结构, 语义信息)
, 一般也是以序列形式进行输出.
公式
两个线性层
h_t = \tanh(x_t W*{ih}^T + b* {ih} + h*{t-1}W*{hh}^T + b_{hh})
其中 :
math:'h_t' 是时间 't' 的隐藏状态
math:'x_t' 是时间 't' 的输入
math:'h_{(t-1)}' 是前一层在时间 't-1' 或初始隐藏状态在时间 '0' 。
math: tanh用于添加非线性因素, 帮助调节流经网络的值, tanh函数将值压缩在-1和1之间.
import torch
import torch.nn as nn
def dm_rnn_for_base():
"""
展示如何使用PyTorch中的RNN模块。
本函数旨在演示如何创建一个简单的RNN模型,并打印其权重参数。
同时,本函数还会生成一个随机输入序列和初始隐藏状态,并通过RNN模型进行前向计算,
最后打印出前向计算的输出和最终隐藏状态的形状。
"""
# 创建一个RNN实例,输入大小为5,隐藏层大小为6,层数为1
rnn = nn.RNN(5, 6, 1)
# 打印RNN模型的所有权重参数
print(rnn.all_weights)
# 打印第一个权重参数张量的形状
# [6, 5] 中的 6 表示输出特征的数量(即隐藏层的维度),5 表示输入特征的数量(即输入层的维度)。
print(rnn.all_weights[0][0].shape) # torch.Size([6, 5])
# 打印第一个偏置参数张量的形状
# 这个权重矩阵用于将前一个时间步的隐藏状态(6维)映射到当前时间步的隐藏状态(6维)。
print(rnn.all_weights[0][1].shape) # torch.Size([6, 6])
# 生成一个随机输入序列,形状为(4, 2, 5),表示序列长度为4,批量大小为2,输入特征维度为5
input = torch.randn(4, 2, 5)
# 生成一个随机的初始隐藏状态,形状为(1, 2, 6),表示层数为1,批量大小为2,隐藏层维度为6
h0 = torch.randn(1, 2, 6)
# 使用RNN模型进行前向计算,得到输出序列和最终隐藏状态
out, hn = rnn(input, h0)
# 打印输出序列的形状
print(out.shape)
# 打印最终隐藏状态的形状
print(hn.shape)
if __name__ == '__main__':
dm_rnn_for_base()
图解
代码
import torch
import torch.nn as nn
def dm_rnn_for_base():
"""
展示如何使用PyTorch中的RNN模块。
本函数旨在演示如何创建一个简单的RNN模型,并打印其权重参数。
同时,本函数还会生成一个随机输入序列和初始隐藏状态,并通过RNN模型进行前向计算,
最后打印出前向计算的输出和最终隐藏状态的形状。
"""
# 创建一个RNN实例,输入大小为5,隐藏层大小为6,层数为1
rnn = nn.RNN(5, 6, 1)
# 打印RNN模型的所有权重参数
print(rnn.all_weights)
# 打印第一个权重参数张量的形状
# [6, 5] 中的 6 表示输出特征的数量(即隐藏层的维度),5 表示输入特征的数量(即输入层的维度)。
print(rnn.all_weights[0][0].shape) # torch.Size([6, 5])
# 打印第一个偏置参数张量的形状
# 这个权重矩阵用于将前一个时间步的隐藏状态(6维)映射到当前时间步的隐藏状态(6维)。
print(rnn.all_weights[0][1].shape) # torch.Size([6, 6])
# 生成一个随机输入序列,形状为(4, 2, 5),表示序列长度为4,批量大小为2,输入特征维度为5
input = torch.randn(4, 2, 5)
# 生成一个随机的初始隐藏状态,形状为(1, 2, 6),表示层数为1,批量大小为2,隐藏层维度为6
h0 = torch.randn(1, 2, 6)
# 使用RNN模型进行前向计算,得到输出序列和最终隐藏状态
out, hn = rnn(input, h0)
# 打印输出序列的形状
print(out.shape) # torch.Size([4, 2, 6])
# 打印最终隐藏状态的形状
print(hn.shape) # torch.Size([1, 2, 6])
def dm_rnn_batch_first():
# bach_first = True: 表示输入序列的维度顺序是(batch_size, seq_len, input_size)
# bidirectional = True: 表示使用双向RNN,
# 输出为两个方向的隐藏状态拼接在一起,
# 初始化隐藏层等于2倍num_layers, 输出维度为2*hidden_size
rnn = nn.RNN(5, 6, 1, batch_first=True, bidirectional=True)
print(rnn.all_weights)
# print(rnn.all_weights[0][0].shape)
# print(rnn.all_weights[0][1].shape)
input = torch.randn(2, 4, 5)
h0 = torch.randn(2, 2, 6)
out, hn = rnn(input, h0)
# print(out.shape) # torch.Size([2, 4, 12])
# print(hn.shape) # torch.Size([2, 2, 6])
if __name__ == '__main__':
# dm_rnn_for_base()
dm_rnn_batch_first()
优缺点
传统RNN的优势
由于内部结构简单, 对计算资源要求低
, 相比之后我们要学习的RNN变体:LSTM和GRU模型参数总量少了很多, 在短序列任务上性能和效果都表现优异.
传统RNN的缺点
- 传统RNN在解决长序列之间的关联时, 通过实践,证明经典RNN表现很差, 原因是
在进行反向传播的时候, 过长的序列导致梯度的计算异常, 发生梯度消失或爆炸.
梯度消失或爆炸介绍
根据反向传播算法和链式法则, 梯度的计算可以简化为以下公式
Dn=σ′(z1)w1⋅σ′(z2)w2⋅⋯⋅σ′(zn)wnDn=σ′(z1)w1⋅σ′(z2)w2⋅⋯⋅σ′(zn)wn
-
其中sigmoid的导数值域是固定的, 在[0, 0.25]之间, 而一旦公式中的w也小于1, 那么通过这样的公式连乘后, 最终的梯度就会变得非常非常小, 这种现象称作梯度消失. 反之, 如果我们人为的增大w的值, 使其大于1, 那么连乘够就可能造成梯度过大, 称作梯度爆炸.
-
梯度消失或爆炸的危害:
- 如果在训练过程中发生了梯度消失,权重无法被更新,最终导致训练失败; 梯度爆炸所带来的梯度过大,大幅度更新网络参数,在极端情况下,结果会溢出(NaN值).
总结
nn.RNN类初始化主要参数解释:
-
input_size: 输入张量x中特征维度的大小., 一个token用来一个几维向量
-
hidden_size: 隐层张量h中特征维度的大小,
RNN的输出维度
超参数.经过RNN 后一个token用一个几维向量表示
-
num_layers: 隐含层的数量.
RNN层数
-
nonlinearity: 激活函数的选择, 默认是tanh.
-
bach_first = True: 表示输入序列的维度顺序是(batch_size, seq_len, input_size)
-
bidirectional = True: 表示使用双向RNN,输出为两个方向的隐藏状态拼接在一起,初始化隐藏层等于2倍num_layers, 输出维度为2*hidden_size
nn.RNN类实例化对象主要参数解释:
-
input: 输入张量x.
-
h0: 初始化的隐层张量h.
def dm_rnn_batch_first():
# bach_first = True: 表示输入序列的维度顺序是(batch_size, seq_len, input_size)
# bidirectional = True: 表示使用双向RNN,
# 输出为两个方向的隐藏状态拼接在一起,
# 初始化隐藏层等于2倍num_layers, 输出维度为2*hidden_size
rnn = nn.RNN(5, 6, 1, batch_first=True, bidirectional=True)
print(rnn.all_weights)
print(rnn.all_weights[0][0].shape)
print(rnn.all_weights[0][1].shape)
input = torch.randn(2, 4, 5)
h0 = torch.randn(2, 2, 6)
out, hn = rnn(input, h0)
print(out.shape) # torch.Size([2, 4, 12])
print(hn.shape) # torch.Size([2, 2, 6])
二. LSTM模型
总共8个线性层:
介绍
LSTM(Long Short-Term Memory)也称长短时记忆结构, 它是传统RNN的变体, 与经典RNN相比能够有效捕捉长序列之间的语义关联
, 缓解 梯度消失或爆炸现象
. 同时LSTM的结构更复杂, 它的核心结构可以分为四个部分去解析:
-
遗忘门
-
输入门
-
细胞状态
-
输出门
LSTM内部结构
遗忘门结构
与传统RNN的内部结构计算非常相似, 首先将当前时间步输入x(t)与上一个时间步隐含状态h(t-1)拼接, 得到[x(t), h(t-1)], 然后通过一个全连接层做变换, 最后通过sigmoid函数进行激活得到f(t), 我们可以将f(t)看作是门值, 好比一扇门开合的大小程度, 门值都将作用在通过该扇门的张量, 遗忘门门值将作用的上一层的细胞状态上, 代表遗忘过去的多少信息, 又因为遗忘门门值是由x(t), h(t-1)计算得来的, 因此整个公式意味着根据当前时间步输入和上一个时间步隐含状态h(t-1)来决定遗忘多少上一层的细胞状态所携带的过往信息.
sigmoid: 用于帮助调节流经网络的值, sigmoid函数将值压缩在0和1之间.
输入门结构
我们看到输入门的计算公式有两个, 第一个就是产生输入门门值的公式, 它和遗忘门公式几乎相同, 区别只是在于它们之后要作用的目标上. 这个公式意味着输入信息有多少需要进行过滤. 输入门的第二个公式是与传统RNN的内部结构计算相同. 对于LSTM来讲, 它得到的是当前的细胞状态(未更新的), 而不是像经典RNN一样得到的是隐含状态.
细胞状态更新
细胞更新的结构与计算公式非常容易理解, 这里没有全连接层, 只是将刚刚得到的遗忘门门值与上一个时间步得到的C(t-1)相乘, 再加上输入门门值与当前时间步得到的未更新C(t)相乘的结果. 最终得到更新后的C(t)作为下一个时间步输入的一部分. 整个细胞状态更新过程就是对遗忘门和输入门的应用.
输出门结构
输出门部分的公式也是两个, 第一个即是计算输出门的门值, 它和遗忘门,输入门计算方式相同. 第二个即是使用这个门值产生隐含状态h(t), 他将作用在更新后的细胞状态C(t)上, 并做tanh激活, 最终得到h(t)作为下一时间步输入的一部分. 整个输出门的过程, 就是为了产生隐含状态h(t).
Bi-LSTM
Bi-LSTM即双向LSTM, 它没有改变LSTM本身任何的内部结构, 只是将LSTM应用两次且方向不同, 再将两次得到的LSTM结果进行拼接作为最终输出.
代码
import torch
import torch.nn as nn
def dm_lstm_base():
lstm = nn.LSTM(5, 3, 1)
input = torch.randn(4, 2, 5)
h0 = torch.randn(1, 2, 3)
c0 = torch.randn(1, 2, 3)
for name, params in lstm.named_parameters():
print(name, params.shape)
out, (h_n, c_n) = lstm(input, (h0, c0))
print(out.shape) # torch.Size([4, 2, 6])
print(h_n.shape) # torch.Size([1, 2, 6])
print(c_n.shape) # torch.Size([1, 2, 6])
def dm_lstm_pro():
lstm = nn.LSTM(5, 6, 1, batch_first=True, bidirectional=True)
input = torch.randn(2, 4, 5)
h0 = torch.randn(2, 2, 6)
c0 = torch.randn(2, 2, 6)
out, (h_n, c_n) = lstm(input, (h0, c0))
print(out.shape) # torch.Size([2, 4, 256])
print(h_n.shape) # torch.Size([2, 2, 128])
print(c_n.shape) # torch.Size([2, 2, 128])
if __name__ == '__main__':
dm_lstm_base()
# dm_lstm_pro()
LSTM优缺点
因为门控结构, 缓解长文本序列的问题(梯度消失或爆炸)
-
STM优势:
LSTM的门结构能够有效减缓长序列问题中可能出现的梯度消失或爆炸, 虽然并不能杜绝这种现象, 但在更长的序列问题上表现优于传统RNN.
-
LSTM缺点:
由于内部结构相对较复杂, 因此训练效率在同等算力下较传统RNN低很多.
三. GRU模型
介绍
GRU(Gated Recurrent Unit)也称门控循环单元结构, 它也是传统RNN的变体, 同LSTM一样能够有效捕捉长序列之间的语义关联, 缓解梯度消失或爆炸现象. 同时它的结构和计算要比LSTM更简单, 它的核心结构可以分为两个部分去解析:
-
更新门
-
重置门
GRU内部结构
和之前分析过的LSTM中的门控一样, 首先计算更新门和重置门的门值, 分别是z(t)和r(t), 计算方法就是使用X(t)与h(t-1)拼接进行线性变换, 再经过sigmoid激活. 之后重置门门值作用在了h(t-1)上, 代表控制上一时间步传来的信息有多少可以被利用. 接着就是使用这个重置后的h(t-1)进行基本的RNN计算, 即与x(t)拼接进行线性变化, 经过tanh激活, 得到新的h(t). 最后更新门的门值会作用在新的h(t),而1-门值会作用在h(t-1)上, 随后将两者的结果相加, 得到最终的隐含状态输出h(t), 这个过程意味着更新门有能力保留之前的结果, 当门值趋于1时, 输出就是新的h(t), 而当门值趋于0时, 输出就是上一时间步的h(t-1).
Bi-GRU
Bi-GRU与Bi-LSTM的逻辑相同, 都是不改变其内部结构, 而是将模型应用两次且方向不同, 再将两次得到的LSTM结果进行拼接作为最终输出. 具体参见上小节中的Bi-LSTM.
代码
import torch
import torch.nn as nn
def dm_gru_base():
gru = nn.GRU(4, 5, 2, batch_first=True, bidirectional=True)
# 输入尺寸: [batch_size(句子数量), seq_len(句中token数), input_size(token维度)]
input = torch.randn(2, 4, 4)
# 初始化num_layers = 实例化模型的num_layers * 方向数(单向: 1/ 双向: 2)
h0 = torch.randn(4, 2, 5)
out, h_n = gru(input, h0)
print(out.shape)
print(h_n.shape)
if __name__ == '__main__':
dm_gru_base()
GRU优缺点
-
GRU的优势:
- GRU和LSTM作用相同, 在捕捉长序列语义关联时, 能有效抑制梯度消失或爆炸, 效果都优于传统RNN且计算复杂度相比LSTM要小.
-
GRU的缺点:
- GRU仍然不能完全解决梯度消失问题, 同时其作用RNN的变体, 有着RNN结构本身的一大弊端, 即不可并行计算, 这在数据量和模型体量逐步增大的未来, 是RNN发展的关键瓶颈.
四. RNN案例-人名分类器
导包
# 导入torch工具
import torch
# 导入nn准备构建模型
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
# 导入torch的数据源 数据迭代器工具包
from torch.utils.data import Dataset, DataLoader
# 用于获得常见字母及字符规范化
import string
# 导入时间工具包
import time
# 引入制图工具包
import matplotlib.pyplot as plt
# 从io中导入文件打开方法
from io import open
数据预处理
获取常用字符数量
# 1. 获取常用的字符标点
all_letters = string.ascii_letters + " .,;'"
print(all_letters)
n_letters = len(all_letters)
print('词表长度: ', n_letters)
国家种类数和个数
# 2. 获取国家个数
# 国家名 种类数
categories = ['Italian', 'English', 'Arabic', 'Spanish', 'Scottish', 'Irish', 'Chinese', 'Vietnamese', 'Japanese',
'French', 'Greek', 'Dutch', 'Korean', 'Polish', 'Portuguese', 'Russian', 'Czech', 'German']
# 国家名 个数
category_num = len(categories)
print('国家种类数: ', categories)
print('国家个数数: ', category_num)
读取数据
# 3. 读取数据, 获取x, y
def read_data(filename):
my_list_x, my_list_y = [], []
with open(filename, 'r', encoding='utf-8') as f:
# 逐行读取人名和国家名
for line in f.readlines():
# 检测异常样本
if len(line) <= 5:
continue
# 去除首尾空格, 按制表符分割
name, category = line.strip().split('\t')
my_list_x.append(name)
my_list_y.append(category)
return my_list_x, my_list_y
构建数据源
# 4. 构建数据源对象
class NameClassDataset(Dataset):
def __init__(self, my_list_x, my_list_y):
self.x = my_list_x
self.y = my_list_y
self.len = len(self.x)
def __len__(self):
return self.len
def __getitem__(self, idx):
# 标准化索引位置
idx = min(max(idx, 0), self.len - 1)
# 根据idx获取人名和国家名
x = self.x[idx]
y = self.y[idx]
# 获取特征矩阵->one-hot(字母表示token)
tensor_x = torch.zeros(len(x), n_letters)
for li, latter in enumerate(x):
# 遍历名字中的每个字母, 将字母对应词表的索引位置置为1
tensor_x[li][all_letters.find(latter)] = 1
# 获取标签向量
tensor_y = torch.tensor(categories.index(y), dtype=torch.long)
# 在国家列表中的索引值
# print(categories.index(y))
return tensor_x, tensor_y
构建dataloader
# 5. 构建dataloader
def get_dataloader():
x, y = read_data('data/name_classfication.txt')
my_dataset = NameClassDataset(x, y)
my_dataloader = DataLoader(
dataset=my_dataset,
batch_size=1,
shuffle=True,
drop_last=True, # 去除不满足批次大小的数据
collate_fn=True, # 将同一个批次的输入整理成相同维度
)
x, y = next(iter(my_dataloader))
print(x)
print(x.shape)
print(y)
return my_dataloader
模型构建
RNN
# 6. 构建RNN模型
class MyRNN(nn.Module):
def __init__(self, input_size, hidden_size, output_size, n_layers=1):
super(MyRNN, self).__init__()
self.input_size = input_size
self.hidden_size = hidden_size
self.output_size = output_size
self.n_layers = n_layers
self.rnn = nn.RNN(input_size, hidden_size, n_layers, batch_first=True)
self.linear = nn.Linear(hidden_size, output_size)
self.softmax = nn.LogSoftmax(dim=-1)
def forward(self, input):
# input.shape = (1, 9, 57)
# hidden.shape = (1, 1, 128)
# rnn_outputs.shape = (1, 9, 128)
# hn.shape = (1, 1, 128)
# 模型会自动初始化全零的隐藏层状态
rnn_output, rnn_hn = self.rnn(input)
# output.shape = (1, 18)
# output = self.linear(rnn_output[0][-1].unsqueeze(0))
output = self.linear(rnn_hn[0])
output = self.softmax(output)
return output, rnn_hn
LSTM
# 8. 构建LSTM模型
class MyLSTM(nn.Module):
def __init__(self, input_size, hidden_size, output_size, n_layers=1):
super(MyLSTM, self).__init__()
self.input_size = input_size
self.hidden_size = hidden_size
self.output_size = output_size
self.n_layers = n_layers
self.lstm = nn.LSTM(input_size, hidden_size, n_layers, batch_first=True)
self.linear = nn.Linear(hidden_size, output_size)
# 线性层输出为(1, 18), 使用LogSoftmax激活最后一维
self.softmax = nn.LogSoftmax(dim=-1)
def forward(self, input):
# input.shape = (1, 9, 57)
# hidden_size = (1, 1, 128) # 128表示经过lstm层的输出维度
# lstm_output.shape = (1, 9, 128)
# hn.shape = (1, 1, 128)
# cn.shape = (1, 1, 128)
lstm_output, (hn, cn) = self.lstm(input)
output = self.linear(lstm_output[0][-1].unsqueeze(0))
output = self.softmax(output)
return output, (hn, cn)
GRU
# 12. 构建GRU模型
class MyGRU(nn.Module):
def __init__(self, input_size, hidden_size, output_size, n_layers=1):
super(MyGRU, self).__init__()
self.input_size = input_size
self.hidden_size = hidden_size
self.output_size = output_size
self.n_layers = n_layers
self.gru = nn.GRU(input_size, hidden_size, n_layers, batch_first=True)
self.linear = nn.Linear(hidden_size, output_size)
self.softmax = nn.LogSoftmax(dim=-1)
def forward(self, input):
# input.shape = (1, 9, 57)
# hidden.shape = (1, 1, 128)
# gru_outputs.shape = (1, 9, 128)
# hn.shape = (1, 1, 128)
gru_output, gru_hn = self.gru(input)
# output.shape = (1, 18)
# output = self.linear(gru_output[0][-1].unsqueeze(0))
output = self.linear(gru_hn[0])
output = self.softmax(output)
return output, gru_hn
模型训练
RNN
# 10. 训练RNN模型
def dm_rnn_train(epochs=1, lr=1e-3):
epochs = epochs
my_lr = lr
# 数据加载器
my_dataloader = get_dataloader()
# 模型参数
input_size = n_letters
hidden_size = 128
output_size = category_num
# 实例化模型
my_model = MyRNN(input_size, hidden_size, output_size).to('cuda')
# 优化器
optimizer = optim.Adam(my_model.parameters(), lr=my_lr)
# 损失函数
my_crossentropy = nn.NLLLoss()
# 输出日志信息
# 训练批次
total_iter = 0
# 定义损失值
total_loss = 0
total_loss_list = []
# 定义预测准确率
total_acc = 0
total_acc_list = []
# 总时间
all_time = time.time()
# 遍历epochs轮
for epo in range(epochs):
# 获取当前时间
start_time = time.time()
for i, (x, y) in enumerate(my_dataloader):
output, hn = my_model(x.to('cuda'))
loss = my_crossentropy(output, y.to('cuda'))
optimizer.zero_grad()
loss.backward()
optimizer.step()
# 累计批次
total_iter += 1
# 累计损失值
total_loss += loss.item()
# 累计准确率
my_acc = 1 if torch.argmax(output, dim=-1).item() == y.item() else 0
total_acc += my_acc
if total_iter % 100 == 0:
# 计算累计平均损失值和准确率, 并添加到列表中
avg_loss = total_loss / total_iter
avg_acc = total_acc / total_iter
total_loss_list.append(avg_loss)
total_acc_list.append(avg_acc)
avg_loss = total_loss / total_iter
avg_acc = total_acc / total_iter
print(
f'epoch: {epo + 1}, loss: {avg_loss:.5f}, acc: {avg_acc:.5f}, time: {time.time() - start_time:.3f}s')
torch.save(my_model.state_dict(), 'model/rnn_model%d.pth' % epochs)
total_time = time.time() - all_time
return total_loss_list, total_acc_list, total_time
LSTM
# 11. 训练LSTM模型
def dm_lstm_train(epochs=1, lr=1e-3):
epochs = epochs
my_lr = lr
# 数据加载器
my_dataloader = get_dataloader()
# 模型参数
input_size = n_letters
hidden_size = 128
output_size = category_num
# 实例化模型
my_model = MyLSTM(input_size, hidden_size, output_size).to('cuda')
# 优化器
optimizer = optim.Adam(my_model.parameters(), lr=my_lr)
# 损失函数
my_crossentropy = nn.NLLLoss()
# 用于返回损失和准确率
total_iter = 0
total_loss = 0
total_acc = 0
total_loss_list = []
total_acc_list = []
# 总时间
all_time = time.time()
for epo in range(epochs):
start_time = time.time()
for i, (x, y) in enumerate(my_dataloader):
output, (hn, cn) = my_model(x.to('cuda'))
loss = my_crossentropy(output, y.to('cuda'))
optimizer.zero_grad()
loss.backward()
optimizer.step()
total_iter += 1
total_loss += loss.item()
my_acc = 1 if torch.argmax(output, dim=-1).item() == y.item() else 0
total_acc += my_acc
if total_iter % 100 == 0:
avg_loss = total_loss / total_iter
avg_acc = total_acc / total_iter
total_loss_list.append(avg_loss)
total_acc_list.append(avg_acc)
# 输出批次的平均损失和准确率
avg_loss = total_loss / total_iter
avg_acc = total_acc / total_iter
print(
f'epoch: {epo + 1}, loss: {avg_loss:.5f}, acc: {avg_acc:.5f}, time: {time.time() - start_time:.3f}s')
torch.save(my_model.state_dict(), 'model/lstm_model%d.pth' % epochs)
total_time = time.time() - all_time
return total_loss_list, total_acc_list, total_time
GRU
# 14. 训练GRU模型
def dm_gru_train(epochs=1, lr=1e-3):
epochs = epochs
my_lr = lr
# 数据加载器
my_dataloader = get_dataloader()
# 模型参数
input_size = n_letters
hidden_size = 128
output_size = category_num
# 实例化模型
my_model = MyGRU(input_size, hidden_size, output_size).to('cuda')
# 优化器
optimizer = optim.Adam(my_model.parameters(), lr=my_lr)
# 损失函数
my_crossentropy = nn.NLLLoss()
# 输出日志
total_iter = 0
total_loss = 0
total_acc = 0
total_loss_list = []
total_acc_list = []
# 总时间
all_time = time.time()
for epo in range(epochs):
# 日志输出
start_time = time.time()
for i, (x, y) in enumerate(my_dataloader):
output, hn = my_model(x.to('cuda'))
loss = my_crossentropy(output, y.to('cuda'))
optimizer.zero_grad()
loss.backward()
optimizer.step()
total_iter += 1
total_loss += loss.item()
my_acc = 1 if torch.argmax(output, dim=-1).item() == y.item() else 0
total_acc += my_acc
if total_iter % 100 == 0:
avg_loss = total_loss / total_iter
avg_acc = total_acc / total_iter
total_loss_list.append(avg_loss)
total_acc_list.append(avg_acc)
# 输出批次的平均损失和准确率
avg_loss = total_loss / total_iter
avg_acc = total_acc / total_iter
print(
f'epoch: {epo + 1}, loss: {avg_loss:.5f}, acc: {avg_acc:.5f}, time: {time.time() - start_time:.3f}s')
torch.save(my_model.state_dict(), 'model/gru_model%d.pth' % epochs)
total_time = time.time() - all_time
return total_loss_list, total_acc_list, total_time
模型训练过程分析
保存模型评估指标
# 15. 保存模型训练数据
def dm_save_loss_acc(epochs=10):
# 1. 训练模型, 得到需要的结果
rnn_total_loss_list, rnn_total_acc_list, rnn_total_time = dm_rnn_train(epochs=epochs)
print('rnn训练完成: ', '*' * 50)
lstm_total_loss_list, lstm_total_acc_list, lstm_total_time = dm_lstm_train(epochs=epochs)
print('lstm训练完成: ', '*' * 50)
gru_total_loss_list, gru_total_acc_list, gru_total_time = dm_gru_train(epochs=epochs)
print('gru训练完成: ', '*' * 50)
# 2. 定义字典
dict_rnn = {
'loss': rnn_total_loss_list,
'acc': rnn_total_acc_list,
'time': rnn_total_time
}
dict_lstm = {
'loss': lstm_total_loss_list,
'acc': lstm_total_acc_list,
'time': lstm_total_time
}
dict_gru = {
'loss': gru_total_loss_list,
'acc': gru_total_acc_list,
'time': gru_total_time
}
# 3. 保存成json
with open('data/rnn_loss_acc%d.json' % epochs, 'w') as f_rnn:
f_rnn.write(json.dumps(dict_rnn))
with open('data/lstm_loss_acc%d.json' % epochs, 'w') as f_lstm:
f_lstm.write(json.dumps(dict_lstm))
with open('data/gru_loss_acc%d.json' % epochs, 'w') as f_gru:
f_gru.write(json.dumps(dict_gru))
绘图分析
# 16. 读取模型训练数据json
def read_json(data_path):
with open(data_path, 'r') as f:
# '{a:1, b:2}' --> json字符串形式json.loads()
# json.load() --> 加载json文件
return json.load(f)
# 17. 绘图
def draw_loss_acc():
# 读取json数据
rnn_data = read_json('data/rnn_loss_acc.json')
rnn_total_loss_list, rnn_total_acc_list, rnn_total_time = rnn_data['loss'], rnn_data['acc'], rnn_data['time']
lstm_data = read_json('data/lstm_loss_acc.json')
lstm_total_loss_list, lstm_total_acc_list, lstm_total_time = lstm_data['loss'], lstm_data['acc'], lstm_data['time']
gru_data = read_json('data/gru_loss_acc.json')
gru_total_loss_list, gru_total_acc_list, gru_total_time = gru_data['loss'], gru_data['acc'], gru_data['time']
# 绘制loss对比曲线图
plt.figure(0)
plt.plot(rnn_total_loss_list, label='RNN')
plt.plot(lstm_total_loss_list, label='LSTM', color='red')
plt.plot(gru_total_loss_list, label='GRU', color='orange')
plt.legend(loc='upper right')
plt.title('Loss')
plt.savefig('picture/loss.png')
plt.show()
# 绘制耗时柱状图
plt.figure(1)
x_data = ['RNN', 'LSTM', 'GRU']
y_data = [rnn_total_time, lstm_total_time, gru_total_time]
# range(len(x_data))生成x轴的索引,
# y_data是y轴的数据,
# tick_label=x_data设置x轴的标签
plt.bar(range(len(x_data)), y_data, tick_label=x_data)
plt.title('Time')
plt.savefig('picture/time.png')
plt.show()
# 绘制acc对比曲线图
plt.figure(2)
plt.plot(rnn_total_acc_list, label='RNN')
plt.plot(lstm_total_acc_list, label='LSTM', color='red')
plt.plot(gru_total_acc_list, label='GRU', color='orange')
plt.legend(loc='upper left')
plt.title('ACC')
plt.savefig('picture/acc.png')
plt.show()
模型预测
输入转tensor
# 18. 模型预测-输入转tensor
def line2tensor(x):
tensor_x = torch.zeros(len(x), n_letters)
for i, letter in enumerate(x):
tensor_x[i][all_letters.find(letter)] = 1
return tensor_x
RNN预测
# 19. 模型预测-预测
def rnn_predict(x):
# 输入转tensor
x_tensor = line2tensor(x)
# 实例化模型
my_model = MyRNN(n_letters, 128, category_num).to('cuda')
my_model.load_state_dict(torch.load('model/rnn_model10.pth', weights_only=True))
# 预测
with torch.no_grad(): # 预测时不使用梯度
# 模型输入的维度是三维, 因此需要升维
input0 = x_tensor.unsqueeze(0)
output, hn = my_model(input0.to('cuda'))
topv, topi = output.topk(3, 1, True)
# 打印topk个
for i in range(3):
value = topv[0][i]
index = topi[0][i]
cate = categories[index]
print(f'名字为:{x}, 国家可能是: {cate}')
LSTM预测
# 20. 模型预测-LSTM预测
def lstm_predict(x):
# 输入转tensor
x_tensor = line2tensor(x)
# 实例化模型
my_model = MyLSTM(n_letters, 128, category_num).to('cuda')
my_model.load_state_dict(torch.load('model/lstm_model10.pth', weights_only=True))
# 预测
with torch.no_grad(): # 预测时不使用梯度
# 模型输入的维度是三维, 因此需要升维
input0 = x_tensor.unsqueeze(0)
output, (hn, cn) = my_model(input0.to('cuda'))
topv, topi = output.topk(3, 1, True)
for i in range(3):
value = topv[0][i]
index = topi[0][i]
cate = categories[index]
print(f'名字为:{x}, 国家可能是: {cate}')
GRU预测
# 21. 模型预测-GRU预测
def gru_predict(x):
# 输入转tensor
x_tensor = line2tensor(x)
# 实例化模型
my_model = MyGRU(n_letters, 128, category_num).to('cuda')
my_model.load_state_dict(torch.load('model/gru_model10.pth', weights_only=True))
# 预测
with torch.no_grad(): # 预测时不使用梯度
# 模型输入的维度是三维, 因此需要升维
input0 = x_tensor.unsqueeze(0)
output, hn = my_model(input0.to('cuda'))
topv, topi = output.topk(3, 1, True)
for i in range(3):
value = topv[0][i]
index = topi[0][i]
cate = categories[index]
print(f'名字为:{x}, 国家可能是: {cate}')