动手学深度学习第六课:循环神经网络基础

ぃ、小莉子 提交于 2020-02-15 08:49:24

循环神经网络

以循环神经网络实现语言模型为例。
循环神经网络实现语言模型
下面分析构造。假设XtRn×dX_t\in\mathbb{R}^{n\times d}是时间步tt的小批量输入,HtRn×hH_t\in\mathbb{R}^{n\times h}是该时间步的隐藏变量,则
Ht=Φ(XtWxh+Ht1Whh+bh).H_t = \Phi(X_tW_{xh}+H_{t-1}W_{hh}+b_h).

对于每一个字符和每一个隐藏变量都用一个向量来表示,这里的ddhh分别表示两个向量的长度。Wxhd×h,WhhRh×h,bR1×hW_{xh}\in\mathbb{d\times h}, W_{hh}\in\mathbb{R}^{h\times h},b\in\mathbb{R}^{1\times h},由此知XtWxhRn×h,Ht1WhhRn×hX_tW_{xh}\in\mathbb{R}^{n\times h}, H_{t-1}W_{hh}\in\mathbb{R}^{n\times h},根据加法的广播机制,三项相加结果为n×hn\times h的矩阵。Φ\Phi是非线性激活函数。
在时间步tt,输出层的输出为:
Ot=HtWhq+bq.O_t=H_tW_{hq}+b_q.

one-hot向量

我们需要将字符表示成向量,这里采用one-hot向量。假设词典大小是NN,每次字符对应一个从0到N1N−1的唯一的索引,则该字符的向量是一个长度为 NN 的向量,若字符的索引是 $i ,则该向量的第 i $个位置为 1 ,其他位置为 0 。下面分别展示了索引为0和2的one-hot向量,向量长度等于词典大小。

def one_hot(x, n_class, dtype=torch.float32):
    # x是一个一维的向量,每个元素都是一个字符的索引
    # n_class是字典的大小,vocab_size
    # dtype为返回向量的数值类型,size为(n,n_class)
    result = torch.zeros(x.shape[0], n_class, dtype=dtype, device=x.device)  # shape: (n, n_class)
    result.scatter_(1, x.long().view(-1, 1), 1)  # result[i, x[i, 0]] = 1
    return result
  1. scatter_()函数
    我们每次采样的小批量的形状是(批量大小, 时间步数)。下面的函数将这样的小批量变换成数个形状为(批量大小, 词典大小)的矩阵,矩阵个数等于时间步数。也就是说,时间步tt的输入为 XtRn×dX_t\in\mathbb{R}^{n×d },其中 nn 为批量大小, dd 为词向量大小,即one-hot向量长度(词典大小)。
def to_onehot(X, n_class):
    # X: 一个小批量
    # 输出为(批量大小, 词典大小)
    return [one_hot(X[:, i], n_class) for i in range(X.shape[1])]

定义模型

函数rnn用循环的方式依次完成循环神经网络每个时间步的计算。

def rnn(inputs, state, params):
    # inputs是长度为num_steps的列表,每个元素是(batch_size, vocab_size)的矩阵
    # state提供状态的初始值,是一个元组,在rnn中就是隐藏状态
    # inputs和outputs皆为num_steps个形状为(batch_size, vocab_size)的矩阵
    W_xh, W_hh, b_h, W_hq, b_q = params
    H, = state # rnn中只有一个状态要维护,就是隐藏状态
    outputs = []
    for X in inputs:
        H = torch.tanh(torch.matmul(X, W_xh) + torch.matmul(H, W_hh) + b_h)
        Y = torch.matmul(H, W_hq) + b_q
        outputs.append(Y)
    return outputs, (H,)
    # 返回h是因为后面要用相邻采样进行训练,对于相邻采样而言,当前这个batch的最后状态就作为下一个状态的初始值
  1. X.to(device)

裁剪梯度

循环神经网络中较容易出现梯度衰减或梯度爆炸,这是因为梯度的传播方式是通过时间反向传播,梯度是幂的形式,指数就是num_steps,随着时间步数增加,就容易出现这些问题。裁剪梯度(clip gradient)是一种应对梯度爆炸的方法。假设我们把所有模型参数的梯度拼接成一个向量 g ,并设裁剪的阈值是 θ 。裁剪后的梯度
min(θg,1)g\min(\frac{\theta}{||g||},1)g

L2L_2范数不超过θ\theta
代码展示:

def grad_clipping(params, theta, device):
    norm = torch.tensor([0.0], device=device)
    for param in params:
        norm += (param.grad.data ** 2).sum()
    norm = norm.sqrt().item()
    if norm > theta:
        for param in params:
            param.grad.data *= (theta / norm)

定义预测函数

以下函数基于前缀prefix(含有数个字符的字符串)来预测接下来的num_chars个字符。
代码展示:

def predict_rnn(prefix, num_chars, rnn, params, init_rnn_state,
                num_hiddens, vocab_size, device, idx_to_char, char_to_idx):
    state = init_rnn_state(1, num_hiddens, device)
    output = [char_to_idx[prefix[0]]]   # output记录prefix加上预测的num_chars个字符
    for t in range(num_chars + len(prefix) - 1):
        # 将上一时间步的输出作为当前时间步的输入
        # 这里的batch_size为1
        X = to_onehot(torch.tensor([[output[-1]]], device=device), vocab_size)
        # 计算输出和更新隐藏状态
        (Y, state) = rnn(X, state, params)
        # 下一个时间步的输入是prefix里的字符或者当前的最佳预测字符
        if t < len(prefix) - 1:
            output.append(char_to_idx[prefix[t + 1]])
        else:
            output.append(Y[0].argmax(dim=1).item()) # 这里用Y[0]是因为Y输出的是一个列表,取出Y中第一个元素
    return ''.join([idx_to_char[i] for i in output])

torch.flatten()函数(转自GhostintheCode,这个博主介绍的很详细,可进去看看例子):
#展平一个连续范围的维度,输出类型为Tensor
torch.flatten(input, start_dim=0, end_dim=-1) → Tensor
#Parameters:input (Tensor) – 输入为Tensor
#start_dim (int) – 展平的开始维度
#end_dim (int) – 展平的最后维度

#一个3x2x2的三维张量
>>> t = torch.tensor([[[1, 2],
                       [3, 4]],
                      [[5, 6],
                       [7, 8]],
                  [[9, 10],
                       [11, 12]]])
#当开始维度为0,最后维度为-1,展开为一维
>>> torch.flatten(t)
tensor([ 1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11, 12])
#当开始维度为0,最后维度为-1,展开为3x4,也就是说第一维度不变,后面的压缩
>>> torch.flatten(t, start_dim=1)
tensor([[ 1,  2,  3,  4],
        [ 5,  6,  7,  8],
        [ 9, 10, 11, 12]])
————————————————
版权声明:本文为CSDN博主「GhostintheCode」的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/GhostintheCode/article/details/102530451

循环神经网络的简洁实现

我们使用Pytorch中的nn.RNN来构造循环神经网络。在本节中,我们主要关注nn.RNN的以下几个构造函数参数:

  • input_size - The number of expected features in the input x (整型,int)
  • hidden_size – The number of features in the hidden state h(整型,int)
  • nonlinearity – The non-linearity to use. Can be either ‘tanh’ or ‘relu’. Default: ‘tanh’
  • batch_first – If True, then the input and output tensors are provided as (batch_size, num_steps, input_size). Default: False
    这里的batch_first决定了输入的形状,我们使用默认的参数False,对应的输入形状是 (num_steps, batch_size, input_size)。

一个完整的基于循环神经网络的语言模型

class RNNModel(nn.Module):
    def __init__(self, rnn_layer, vocab_size):
        super(RNNModel, self).__init__()
        self.rnn = rnn_layer
        # 这里取1
        self.hidden_size = rnn_layer.hidden_size * (2 if rnn_layer.bidirectional else 1) 
        self.vocab_size = vocab_size
        self.dense = nn.Linear(self.hidden_size, vocab_size)

    def forward(self, inputs, state):
        # inputs.shape: (batch_size, num_steps)
        X = to_onehot(inputs, vocab_size)
        X = torch.stack(X)  # X.shape: (num_steps, batch_size, vocab_size)
        hiddens, state = self.rnn(X, state)
        hiddens = hiddens.view(-1, hiddens.shape[-1])  # hiddens.shape: (num_steps * batch_size, hidden_size)
        output = self.dense(hiddens)
        return output, state
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!