网络主要参数

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类初始化主要参数解释:

  1. input_size: 输入张量x中特征维度的大小., 一个token用来一个几维向量

  2. hidden_size: 隐层张量h中特征维度的大小, RNN的输出维度超参数. 经过RNN 后一个token用一个几维向量表示

  3. num_layers: 隐含层的数量.RNN层数

  4. nonlinearity: 激活函数的选择, 默认是tanh.

  5. bach_first = True: 表示输入序列的维度顺序是(batch_size, seq_len, input_size)

  6. 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}')

Logo

助力广东及东莞地区开发者,代码托管、在线学习与竞赛、技术交流与分享、资源共享、职业发展,成为松山湖开发者首选的工作与学习平台

更多推荐