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

循环神经网络

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

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

one-hot向量

我们需要将字符表示成向量,这里采用one-hot向量。假设词典大小是 N N N,每次字符对应一个从0到 N − 1 N−1 N1的唯一的索引,则该字符的向量是一个长度为 N N N 的向量,若字符的索引是 $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_()函数
    我们每次采样的小批量的形状是(批量大小, 时间步数)。下面的函数将这样的小批量变换成数个形状为(批量大小, 词典大小)的矩阵,矩阵个数等于时间步数。也就是说,时间步 t t t的输入为 X t ∈ R n × d X_t\in\mathbb{R}^{n×d } XtRn×d,其中 n n n 为批量大小, d d d 为词向量大小,即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 min(gθ,1)g

L 2 L_2 L2范数不超过 θ \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
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值