循环神经网络及其变体:RNN, LSTM, GRU
根据反向传播算法和链式法则, 梯度的计算可以简化为以下公式其中sigmoid的导数值域是固定的, 在[0, 0.25]之间, 而一旦公式中的w也小于1, 那么通过这样的公式连乘后, 最终的梯度就会变得非常非常小, 这种现象称作梯度消失. 反之, 如果我们人为的增大w的值, 使其大于1, 那么连乘够就可能造成梯度过大, 称作梯度爆炸.梯度消失或爆炸的危害:如果在训练过程中发生了梯度消失,权重无法被更
网络主要参数
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}')
更多推荐
所有评论(0)