NLP学习记录三:循环神经网络RNN

一、何为隐状态?

有时,我们想让模型当前t时刻的输出不仅取决于当前时刻的输入xt,还取决于t时刻以前的输入,但如果把xt-1,xt-2,...,x1都考虑进来,词表中的词汇量将会变得很大,而且也很难设计这样一个如此大的模型。因此,我们便会想让t时刻之前的输入加权和并到一个隐藏变量(hidden variable)中,这个隐藏变量也称为隐状态(hidden state),存储了t时刻之前的序列信息。通常我们可以基于当前输入xt和先前隐状态ht−1来计算时间步t处的任何时间的隐状态:

 循环神经网络(recurrent neural networks,RNNs) 是具有隐状态的神经网络。

二、无隐状态的神经网络

在介绍具有隐状态的神经网络之前,先回顾一下无隐状态的神经网络。经典的多层感知机MLP就是一种无隐状态的神经网络。在MLP的隐藏层中,我们会使用如下式子表示其输入输出关系:

上式中,Φ为隐藏层的激活函数,假设批量大小为n,那么输入X的维度为n×x,输出H的维度为n×h。

同理,输出层的输入输出关系如下式所示:

式中各符号意义已显而易见。

三、有隐状态的循环神经网络

不同于无隐状态的神经网络,在有隐状态的循环循环神经网络中,我们首先需要考虑以另一种形式表示当前时刻的隐藏变量:

上式中,输入Xt的维度为n×x,n表示批量大小,x可以理解为一个token进行独热编码后的长度;隐状态H的维度为n×h,n表示批量大小,h为隐藏单元的数目。

 可以看到,我们引入了一个新的权重参数Whh,使得当前时刻的隐藏变量不仅与当前输入有关,还与之前输入造成的影响因子有关。在循环神经网络中,执行上述步骤的网络层称为循环层

循环神经网络的输出层与MLP相似:

 具有隐状态的循环神经网络的网络结构如下图所示:

四、损失函数

语言模型可以视作一个分类模型,预测出来的词可以理解为就是一个类别。对应分类模型,我们可以使用交叉熵来衡量模型质量:

由于历史原因,自然语言处理的科学家更喜欢使用一个叫做困惑度(perplexity)的量:

五、梯度裁剪 

隐状态的存在意味着假如我们向模型输入了T个token,那么就要反向传播时就会产生长度为O(T)的乘法链,乘法链太长的话就容易出现梯度爆炸或梯度消失的情况。

为了防止梯度爆炸情况的出现,反向传播时我们会加入一个取最小值的判断操作,防止梯度过大:

 上式中,g是所有层计算得到的梯度组成的一个向量,如果这个向量的二范数(平方和开根)大于阈值θ,那么它或被缩小移动倍数,否则保持不变。

六、RNN的应用

六、从零开始实现RNN

在项目中引入utils.py:

https://blog.csdn.net/scongx/article/details/141394511?spm=1001.2014.3001.5502icon-default.png?t=O83Ahttps://blog.csdn.net/scongx/article/details/141394511?spm=1001.2014.3001.5502

rnnModel.py:

导入依赖包:

import math
import torch
import seqDataLoader as loader
from utils import Timer, Accumulator, Animator, sgd, show
from torch import nn
from torch.nn import functional as F

初始化模型参数:

def get_params(vocab_size, num_hiddens, device):
    # 输入到rnn的都是一个个token,每个token经过one-hot编码后,会生成一个与vocab长度相同的向量
    # 所以输入输出大小等于vocab_size
    num_inputs = num_outputs = vocab_size

    def normal(shape):
        return torch.randn(size=shape, device=device) * 0.01 # 方差为0.01

    # 隐藏层参数
    W_xh = normal((num_inputs, num_hiddens))
    W_hh = normal((num_hiddens, num_hiddens))
    b_h = torch.zeros(num_hiddens, device=device)

    # 输出层参数
    W_hq = normal((num_hiddens, num_outputs))
    b_q = torch.zeros(num_outputs, device=device)

    # 附加梯度
    params = [W_xh, W_hh, b_h, W_hq, b_q]
    for param in params:
        param.requires_grad_(True)

    return params

由于零时刻时没有上一时刻的隐藏状态,所以定义一个函数用来初始化隐藏状态:

def init_rnn_state(batch_size, num_hiddens, device):
    return (torch.zeros((batch_size, num_hiddens), device=device), )

定义一个函数用于计算一个时间步内的隐状态和输出:

def rnn(inputs, state, params):

    # inputs的维度:(num_steps,batch_size,vocab_size)
    W_xh, W_hh, b_h, W_hq, b_q = params
    H, = state
    outputs = []

    # X的维度:(batch_size,vocab_size)
    for X in inputs:

        # W_xh的维度:(vocab_size * num_hiddens)
        # torch.mm(X, W_xh)是矩阵乘法操作,返回矩阵的尺度为 batch_size * num_hiddens
        # tanh用于作非线性变换
        H = torch.tanh(torch.mm(X, W_xh) + torch.mm(H, W_hh) + b_h)

        Y = torch.mm(H, W_hq) + b_q

        # 循环结束后,outputs的形状为:(num_steps,batch_size,vocab_size)
        outputs.append(Y)

    # 返回前向推理一次后网络的输出和隐状态
    # torch.cat(outputs, dim=0)使得outputs的形状变为 (num_steps * batch_size,num_outputs)
    return torch.cat(outputs, dim=0), (H,) 

创建一个类来封装上面定义的函数:

class RNNModelScratch:
    def __init__(self, vocab_size, num_hiddens, device, get_params, init_state, forward_fn):
        self.vocab_size, self.num_hiddens = vocab_size, num_hiddens

        # 初始化模型参数结构
        self.params = get_params(vocab_size, num_hiddens, device)

        # 定义初始隐藏状态、rnn函数
        self.init_state, self.forward_fn = init_state, forward_fn

    def __call__(self, X, state):

        # X的维度:(batch_size,num_steps)
        # 独热编码处理并设置为浮点型
        X = F.one_hot(X.T, self.vocab_size).type(torch.float32)

        # 返回一次前向推导结果
        return self.forward_fn(X, state, self.params)

    # 隐藏层参数初始化
    def begin_state(self, batch_size, device):
        return self.init_state(batch_size, self.num_hiddens, device)

 接下来定义一个预测函数。在函数的输入参数中,prefix是给定的句子开头;num_preds表示想要预测几步;net是网络模型,在这里其实就是前面定义的RNNModelScratch的实例:

def predictRNN(prefix, num_preds, net, vocab, device):

    # 生成初始的隐藏状态
    state = net.begin_state(batch_size=1, device=device)

    # prefix[0]是一个token,通过vocab实例的__getitem__方法可以获取到这个token在词典中的索引
    outputs = [vocab[prefix[0]]]

    # outputs[-1]表示当前outpus列表中的最后一个元素
    # get_input函数用于获取outpus列表中的最后一个token,经过reshape这个token的维度为(1,1,vocab_size)
    get_input = lambda: torch.tensor([outputs[-1]], device=device).reshape((1, 1))

    # prefix是由多个token组成的一个句子,这个循环在依次遍历prefix中的token去更新网络中的隐状态
    for y in prefix[1:]:

        # 这里是调用了RNNModelScratch中的__call__函数
        _, state = net(get_input(), state)

        # 输出的前num_steps-1个token和输入是一致的
        outputs.append(vocab[y])

    for _ in range(num_preds):  # 预测num_preds步
        
        # y的维度为(1,vocab_size)
        y, state = net(get_input(), state)

        # 提取除y最大元素的index并转成int类型,添加到outputs列表中(独热码转换成原来编码)
        outputs.append(int(y.argmax(dim=1).reshape(1))) 

    # 最后将输出列表中的所有token组合成一个字符串返回
    return ''.join([vocab.idx_to_token[i] for i in outputs])

梯度裁剪函数,梯度裁剪原理与第五节中的公式相同:

def grad_clipping(net, theta):

    # 判断现在使用的网络是torch已经实现的网络还是我们自定义的网络
    if isinstance(net, nn.Module):
        params = [p for p in net.parameters() if p.requires_grad]
    else:

        # params = [W_xh, W_hh, b_h, W_hq, b_q]
        params = net.params

    # 求g的二范数(平方和开根)
    norm = torch.sqrt(sum(torch.sum((p.grad ** 2)) for p in params))

    # 如果 theta/norm < 1,那么缩小梯度
    if norm > theta:
        for param in params:
            param.grad[:] *= theta / norm

定义模型训练函数,用于迭代一次进行训练:

def train_epoch(net, train_iter, loss, updater, device, use_random_iter):
    state, timer = None, Timer()

    # 训练损失之和
    metric = Accumulator(2)

    # Y是X的延时,也就是真实值。从train_iter中遍历获取所有的batch进行训练
    for X, Y in train_iter:

        # (batch_size,num_steps),torch.Size([3, 10])
        # print(X.shape)

        # 第一次迭代或使用随机抽样时初始化state
        if state is None or use_random_iter:
            state = net.begin_state(batch_size=X.shape[0], device=device)
        else:
            if isinstance(net, nn.Module) and not isinstance(state, tuple):
                # 释放之前的计算图,但是state中还保存着之前更新的结果
                state.detach_()
            else:
                # state对于我们从零开始实现的模型来说是个张量
                for s in state:
                    s.detach_()

        y = Y.T.reshape(-1)

        # 数据转移到gpu上
        X, y = X.to(device), y.to(device)

        # forward预测,这里是调用了RNNModelScratch中的__call__函数
        y_hat, state = net(X, state)

        # 计算损失,这里调用mean方法应该是取了 batch * num_steps 个token的评价损失
        l = loss(y_hat, y.long()).mean()

        # 当张量的requires_grad属性被注册后,调用l.backward()能够根据自动微分机制计算出损失函数l相对于图中所有需要梯度的张量的梯度
        if isinstance(updater, torch.optim.Optimizer):
            updater.zero_grad()
            l.backward()
            grad_clipping(net, 1)  # 梯度剪裁
            updater.step()
        else:
            l.backward()
            grad_clipping(net, 1)
            updater(batch_size=1)

        metric.add(l * y.numel(), y.numel())

    # 计算困惑度,metric[0]表示loss的累加,metric[1]表示样本总数
    return math.exp(metric[0] / metric[1]), metric[1] / timer.stop()

定义用于给外部调用的训练接口函数:

def trainRNN(net, train_iter, vocab, lr, num_epochs, device, use_random_iter=False):
    # 多分类问题采用交叉熵损失
    loss = nn.CrossEntropyLoss()

    animator = 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: sgd(net.params, lr, batch_size)

    # 参数5表示往后预测5个词元
    predict = lambda prefix: predictRNN(prefix, 5, net, vocab, device)

    # 训练和预测
    for epoch in range(num_epochs):
        # ppl:用指数衡量的困惑度;speed:计算速度,单位是词元每秒
        ppl, speed = train_epoch(net, train_iter, loss, updater, device, use_random_iter) # 单次epoch训练

        # 在训练过程中每隔一定周期作图查看效果
        if (epoch + 1) % 10 == 0:
            print(predict('lose your'))
            animator.add(epoch + 1, [ppl])

    print(f'困惑度 {ppl:.1f}, {speed:.1f} 词元/秒 {str(device)}')
    print(predict('lose your'))
    show()

 主函数测试:

if __name__=='__main__':
    batch_size, num_steps = 3, 10

    train_iter, vocab = loader.loadData(batch_size, num_steps, 'loseyourself.txt')

    num_hiddens = 512
    net = RNNModelScratch(len(vocab), num_hiddens, torch.device('cuda'), get_params, init_rnn_state, rnn)

    # 训练轮次,学习率
    num_epochs, lr = 20, 1

    # 对网络进行训练并作图显示结果
    trainRNN(net, train_iter, vocab, lr, num_epochs, torch.device('cuda'))

参考链接:

《动手学深度学习》 — 动手学深度学习 2.0.0 documentationicon-default.png?t=O83Ahttps://zh-v2.d2l.ai/

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值