• 【深度学习实验】循环神经网络(五):基于GRU的语言模型训练(包括自定义门控循环单元GRU)


      经验是智慧之父,记忆是智慧之母。
    ——谚语

    一、实验介绍

    • 基于门控的循环神经网络(Gated RNN)
      • 门控循环单元(GRU)
        • 门控循环单元(GRU)具有比传统循环神经网络更少的门控单元,因此参数更少,计算效率更高。GRU通过重置门更新门来控制信息的流动,从而改善了传统循环神经网络中的长期依赖问题。
      • 长短期记忆网络(LSTM)
        • 长短期记忆网络(LSTM)是另一种常用的门控循环神经网络结构。LSTM引入了记忆单元输入门输出门以及遗忘门等门控机制,通过这些门控机制可以选择性地记忆、遗忘和输出信息,有效地处理长期依赖和梯度问题。
    • GRU示意图:
      GRU 模块图示

    二、实验环境

      本系列实验使用了PyTorch深度学习框架,相关操作如下:

    1. 配置虚拟环境

    conda create -n DL python=3.7 
    
    • 1
    conda activate DL
    
    • 1
    pip install torch==1.8.1+cu102 torchvision==0.9.1+cu102 torchaudio==0.8.1 -f https://download.pytorch.org/whl/torch_stable.html
    
    • 1
    conda install matplotlib
    
    • 1
     conda install scikit-learn
    
    • 1

    2. 库版本介绍

    软件包本实验版本目前最新版
    matplotlib3.5.33.8.0
    numpy1.21.61.26.0
    python3.7.16
    scikit-learn0.22.11.3.0
    torch1.8.1+cu1022.0.1
    torchaudio0.8.12.0.2
    torchvision0.9.1+cu1020.15.2

    三、实验内容

    (一)自定义门控循环单元(GRU,Gated Recurrent Unit)

    1. get_params

    def get_params(vocab_size, num_hiddens, device):
        num_inputs = num_outputs = vocab_size
    
        def normal(inputs, hiddens):
            ctx = device
            param = torch.rand((inputs, hiddens))
            param.to(ctx)
            return param
        
        def three():
            return (normal(num_inputs, num_hiddens),
                    normal(num_hiddens, num_hiddens),
                    torch.zeros(num_hiddens, device=device))
    
        W_xz, W_hz, b_z = three()  # 更新门参数
        W_xr, W_hr, b_r = three()  # 重置门参数
        W_xh, W_hh, b_h = three()  # 候选隐状态参数
        # 输出层参数
        W_hq = normal(num_hiddens, num_outputs)
        b_q = torch.zeros(num_outputs, device=device)
        # 附加梯度
        params = [W_xz, W_hz, b_z, W_xr, W_hr, b_r, W_xh, W_hh, b_h, W_hq, b_q]
        for param in params:
            param.requires_grad_(True)
        return params
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

      get_params 函数用于初始化模型的参数。它接受三个参数:vocab_size 表示词汇表的大小,num_hiddens 表示隐藏单元的数量,device 表示模型所在的设备(如 CPU 或 GPU)。

    • 首先,根据 vocab_size 初始化 num_inputsnum_outputs,它们的值都等于 vocab_size
    • 然后,定义了一个内部函数 normal,该函数用于生成一个服从均匀分布的随机参数矩阵。这个函数返回一个形状为 (inputs, hiddens) 的随机参数矩阵,并将其移动到指定的设备上。
    • 接下来,定义了另一个内部函数 three,该函数用于生成三个参数组成的元组。这三个参数分别表示更新门参数、重置门参数和候选隐状态参数。
    • 使用 three 函数分别初始化了更新门参数 W_xz, W_hz, b_z,重置门参数 W_xr, W_hr, b_r,候选隐状态参数 W_xh, W_hh, b_h
    • 然后,通过调用 normal 函数初始化了输出层参数 W_hqb_q
    • 最后,将所有的参数放入一个列表 params 中,并设置它们的 requires_grad 属性为 True,表示这些参数需要计算梯度。
    • 返回参数列表 params

    2. init_gru_state

    def init_gru_state(batch_size, num_hiddens, device):
        return (torch.zeros((batch_size, num_hiddens), device=device), )
    
    • 1
    • 2

      init_gru_state 函数用于初始化隐藏状态,作为时间步 t=0 时的输入。它接受三个参数:batch_size 表示批次大小,num_hiddens 表示隐藏单元的数量,device 表示模型所在的设备。

    • 使用 torch.zeros 函数创建并返回一个形状为 (batch_size, num_hiddens) 的全零张量,表示初始的隐藏状态。

    3. gru

    def gru(inputs, state, params):
        W_xz, W_hz, b_z, W_xr, W_hr, b_r, W_xh, W_hh, b_h, W_hq, b_q = params
        H, = state
        outputs = []
        # @符号为矩阵乘法的运算符号
        for X in inputs:
            Z = torch.sigmoid((X @ W_xz) + (H @ W_hz) + b_z)
            R = torch.sigmoid((X @ W_xr) + (H @ W_hr) + b_r)
            H_tilda = torch.tanh((X @ W_xh) + ((R * H) @ W_hh) + b_h)
            H = Z * H + (1 - Z) * H_tilda
            Y = H @ W_hq + b_q
            outputs.append(Y)
        return torch.cat(outputs, dim=0), (H,)
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

       gru 函数是实现门控循环单元的关键部分,接受三个参数:inputs 表示输入序列,state 表示隐藏状态,params 表示模型的参数。

    • 首先,从 params 中解包出更新门参数、重置门参数、候选隐状态参数以及输出层参数。
    • 然后,通过一个循环遍历输入序列 inputs
    • 在每个时间步,根据输入 X 和当前的隐藏状态 H,计算更新门 Z、重置门 R 和候选隐状态 H_tilda
    • 然后,根据门控机制和候选隐状态,计算新的隐藏状态 H
    • 接着,使用隐藏状态 H 计算输出 Y
    • 将输出 Y 添加到输出列表 outputs 中。
    • 循环结束后,使用 torch.cat 函数将输出列表中的所有输出连接起来,得到一个形状为 (seq_length * batch_size, num_outputs) 的张量,表示模型在整个序列上的输出。
    • 最后,返回连接后的输出张量和最终的隐藏状态 (H,)

    (二)创建模型

    0. 超参数

    batch_size, num_steps = 32, 35
    train_iter, vocab = d2l.load_data_time_machine(batch_size, num_steps)
    vocab_size, num_hiddens, num_epochs, lr= 28, 256, 200, 1
    device = try_gpu()
    
    • 1
    • 2
    • 3
    • 4
    • 调用d2l.load_data_time_machine函数加载了训练数据,并设置了一些训练超参数。

    1. 使用上述手动实现的GRU函数

    model = d2l.RNNModelScratch(len(vocab), num_hiddens, device, get_params,
                                init_gru_state, gru)
    d2l.train_ch8(model, train_iter, vocab, lr, num_epochs, device)
    
    • 1
    • 2
    • 3
    • 使用先前定义的参数初始化函数、隐藏状态初始化函数和GRU函数创建自定义的RNN模型model
    • 调用d2l.train_ch8函数对该模型进行训练。

    2. 调用Pytorch库的GRU类

    gru_layer = nn.GRU(vocab_size, num_hiddens)
    model_gru = RNNModel(gru_layer, vocab_size)
    train(model_gru, train_iter, vocab, lr, num_epochs, device)
    
    • 1
    • 2
    • 3
    • 创建了一个使用PyTorch库中的GRU类的model_gru,并对其进行训练。
    • 关于训练过程,请继续阅读

    (三)基于GRU的语言模型训练

    注:本实验使用Pytorch库的GRU类,不使用自定义的GRU函数

    1. RNNModel类

    2. 训练、测试及其余辅助函数

    3. 主函数

    a. 训练
    batch_size, num_steps = 32, 35
    train_iter, vocab = d2l.load_data_time_machine(batch_size, num_steps)
    vocab_size, num_hiddens, num_epochs, lr= 28, 256, 200, 1
    device = try_gpu()
    gru_layer = nn.GRU(vocab_size, num_hiddens)
    model_gru = RNNModel(gru_layer, vocab_size)
    train(model_gru, train_iter, vocab, lr, num_epochs, device)
    
    print(predict('time ', 10, model_gru, vocab, device))
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 训练中每个小批次(batch)的大小和每个序列的时间步数(time step)的值分别为32,25
    • 加载的训练数据迭代器和词汇表
    • vocab_size 是词汇表的大小,num_hiddens 是GRU 隐藏层中的隐藏单元数量,num_epochs 是训练的迭代次数,lr 是学习率
    • 选择可用的 GPU 设备进行训练,如果没有可用的 GPU,则会使用 CPU
    • 训练模型
    • 模型测试
    b. 测试结果

    在这里插入图片描述

    4. 代码整合

    # 导入必要的库
    import torch
    from torch import nn
    import torch.nn.functional as F
    from d2l import torch as d2l
    import math
    
    
    class RNNModel(nn.Module):
    
        def __init__(self, rnn_layer, vocab_size, **kwargs):
            super(RNNModel, self).__init__(**kwargs)
            self.rnn = rnn_layer
            self.vocab_size = vocab_size
            self.num_hiddens = self.rnn.hidden_size
            self.num_directions = 1
            self.linear = nn.Linear(self.num_hiddens, self.vocab_size)
    
        def forward(self, inputs, state):
            X = F.one_hot(inputs.T.long(), self.vocab_size)
            X = X.to(torch.float32)
            Y, state = self.rnn(X, state)
            # 全连接层首先将Y的形状改为(时间步数*批量大小,隐藏单元数)
            # 它的输出形状是(时间步数*批量大小,词表大小)。
            output = self.linear(Y.reshape((-1, Y.shape[-1])))
            return output, state
    
        # 在第一个时间步,需要初始化一个隐藏状态,由此函数实现
        def begin_state(self, device, batch_size=1):
            if not isinstance(self.rnn, nn.LSTM):
                # nn.GRU以张量作为隐状态
                return torch.zeros((self.num_directions * self.rnn.num_layers,
                                    batch_size, self.num_hiddens),
                                   device=device)
            else:
                # nn.LSTM以元组作为隐状态
                return (torch.zeros((
                    self.num_directions * self.rnn.num_layers,
                    batch_size, self.num_hiddens), device=device),
                        torch.zeros((
                            self.num_directions * self.rnn.num_layers,
                            batch_size, self.num_hiddens), device=device))
    
    
    def train(net, train_iter, vocab, lr, num_epochs, device, use_random_iter=False):
        loss = nn.CrossEntropyLoss()
        animator = d2l.Animator(xlabel='epoch', ylabel='perplexity',
                                legend=['train'], xlim=[10, num_epochs])
    
        if isinstance(net, nn.Module):
            updater = torch.optim.SGD(net.parameters(), lr)
        else:
            updater = lambda batch_size: d2l.sgd(net.params, lr, batch_size)
    
        for epoch in range(num_epochs):
            ppl, speed = train_epoch(
                net, train_iter, loss, updater, device, use_random_iter)
            if (epoch + 1) % 10 == 0:
                animator.add(epoch + 1, [ppl])
        print('Train Done!')
        torch.save(net.state_dict(), 'chapter6.pth')
        print(f'困惑度 {ppl:.1f}, {speed:.1f} 词元/秒 {str(device)}')
    
    
    def train_epoch(net, train_iter, loss, updater, device, use_random_iter):
        state, timer = None, d2l.Timer()
        metric = d2l.Accumulator(2)  # 训练损失之和,词元数量
        for X, Y in train_iter:
            if state is None or use_random_iter:
                # 在第一次迭代或使用随机抽样时初始化state
                state = net.begin_state(batch_size=X.shape[0], device=device)
            if isinstance(net, nn.Module) and not isinstance(state, tuple):
                # state对于nn.GRU是个张量
                state.detach_()
            else:
                # state对于nn.LSTM或对于我们从零开始实现的模型是个张量
                for s in state:
                    s.detach_()
            y = Y.T.reshape(-1)
            X, y = X.to(device), y.to(device)
            y_hat, state = net(X, state)
            l = loss(y_hat, y.long()).mean()
            if isinstance(updater, torch.optim.Optimizer):
                updater.zero_grad()
                l.backward()
                grad_clipping(net, 1)
                updater.step()
            else:
                l.backward()
                grad_clipping(net, 1)
                # 因为已经调用了mean函数
                updater(batch_size=1)
            metric.add(l * d2l.size(y), d2l.size(y))
        return math.exp(metric[0] / metric[1]), metric[1] / timer.stop()
    
    
    def predict(prefix, num_preds, net, vocab, device):
        state = net.begin_state(batch_size=1, device=device)
        outputs = [vocab[prefix[0]]]
        get_input = lambda: torch.reshape(torch.tensor(
            [outputs[-1]], device=device), (1, 1))
        for y in prefix[1:]:  # 预热期
            _, state = net(get_input(), state)
            outputs.append(vocab[y])
        for _ in range(num_preds):  # 预测num_preds步
            y, state = net(get_input(), state)
            outputs.append(int(y.argmax(dim=1).reshape(1)))
        return ''.join([vocab.idx_to_token[i] for i in outputs])
    
    
    def grad_clipping(net, theta):
        if isinstance(net, nn.Module):
            params = [p for p in net.parameters() if p.requires_grad]
        else:
            params = net.params
        norm = torch.sqrt(sum(torch.sum((p.grad ** 2)) for p in params))
        if norm > theta:
            for param in params:
                param.grad[:] *= theta / norm
    
    
    def try_gpu(i=0):
        # """如果存在,则返回gpu(i),否则返回cpu()"""
        # # if torch.cuda.device_count() >= i + 1:
        # #     return torch.device(f'cuda:{i}')
        return torch.device('cpu')
    
    
    batch_size, num_steps = 32, 35
    train_iter, vocab = d2l.load_data_time_machine(batch_size, num_steps)
    vocab_size, num_hiddens, num_epochs, lr = 28, 256, 200, 1
    device = try_gpu()
    
    gru_layer = nn.GRU(vocab_size, num_hiddens)
    model_gru = RNNModel(gru_layer, vocab_size)
    train(model_gru, train_iter, vocab, lr, num_epochs, device)
    
    print(predict('time ', 10, model_gru, vocab, device))
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136
    • 137
    • 138
    • 139
  • 相关阅读:
    【2021届】数据结构期末实验考试
    java毕业生设计短视频交流点播系统计算机源码+系统+mysql+调试部署+lw
    .NET 6 AssemblyLoadContext DLL 库 热插拔逻辑实现
    c++一级练习题
    Github每日精选(第28期):Swift图像下载库 Kingfisher
    Springboot多数据源及事务实现方案
    IDEA自定义注释模版
    6个机器学习可解释性框架
    【Java挑战赛】——static、代码块
    米诺地尔行业分析:预计2029年将达到14亿美元
  • 原文地址:https://blog.csdn.net/m0_63834988/article/details/133966412