PyTorch系列之循环神经网络基础

循环神经网络基础

Posted by Young on February 12, 2020

Task02:循环神经网络基础

Torch 复杂函数理解

  • scatter_(input, dim, index, src) 将src中数据根据index中的索引按照dim的方向填进input中

    • scatter_(input, dim, index, src) 等价于 input.scatter_(dim, index, src)

      • 填充方向 dim:0 代表按列填充

      • 索引 indextorch.LongTensor类型,与src的列数相同,与待填充的input的行数相同

      • 思想:初始化与input维度相同的全0矩阵,将src中数据按index指示的位置填充到input,未被填充的位置仍然为0

      • 应用:将字符字典中对应key(字符)的value(索引)映射成one-hot向量(1027维)

        如{“我”:0;”你”:2}中”我”的one-hot向量为[1,0,0,0,…,0],”你”的one-hot向量为[0,0,1,0,…,0]

循环神经网络

  • 下图展示了如何基于循环神经网络实现语言模型。我们的目的是基于当前的输入与过去的输入序列,预测序列的下一个字符。循环神经网络引入一个隐藏变量$H$,用$H_{t}$表示$H$在时间步$t$的值。$H_{t}$的计算基于$X_{t}$和$H_{t-1}$,可以认为$H_{t}$记录了到当前字符为止的序列信息,利用$H_{t}$对序列的下一个字符进行预测

    • 隐层:$H_{t}=\phi\left(X_{t} W_{x h}+H_{t-1} W_{h h}+b_{h}\right)$

      • $X_{t} \in \mathbb{R}^{n \times d}$:时间步 $t$ 的小批量输入(批量大小, 时间步数)
      • $H_{t} \in \mathbb{R}^{n \times h}$:该时间步的隐藏变量
      • $H_{t-1} \in \mathbb{R}^{n \times h}$:上一个时间步的隐藏变量
      • $W_{x h} \in \mathbb{R}^{d \times h}$:输入序列与隐层 权重矩阵
      • $W_{h h} \in \mathbb{R}^{h \times h}$:隐层与隐层 权重矩阵
      • $b_{h} \in \mathbb{R}^{1 \times h}$:隐层偏置
      • $\phi$:激活函数
    • 输出层:$o_{t}=H_{t} W_{h q}+b_{q}$

      • $W_{h q} \in \mathbb{R}^{h \times q}, \quad b_{q} \in \mathbb{R}^{1 \times q}$
    • def to_onehot(X, n_class):

      • 将 $X_{t}$ 转为 num_steps 个形状为 $(batch_size, vocab_size)$ 的矩阵
      def to_onehot(X, n_class):
          return [one_hot(X[:, i], n_class) for i in range(X.shape[1])]
      
    • 定义模型

      • inputs和outputs皆为num_steps个形状为(batch_size, vocab_size)的矩阵
      • inputs:输入序列
      • state:隐层状态
      def rnn(inputs, state, params):
          W_xh, W_hh, b_h, W_hq, b_q = params
          H, = state
          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,)
      
    • 裁剪梯度 $\min \left(\frac{\theta}{|g|_2}, 1\right) g$

      • 循环神经网络中较容易出现梯度衰减或梯度爆炸,这会导致网络几乎无法训练。裁剪梯度(clip gradient)是一种应对梯度爆炸的方法。假设我们把所有模型参数的梯度拼接成一个向量 $g$,并设裁剪的阈值是 $\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)
        
    • 定义预测函数

      • 优先使用下一步的输入作为当前的最佳预测字符,若没有下一步的输入,则使用隐状态的预测值
      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):
              
              # 将上一时间步的输出作为当前时间步的输入
              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())
                      
          return ''.join([idx_to_char[i] for i in output])
      
    • .detach_() 用于切断反向传播

      • 注意⚠️:如果使用相邻采样,则需要使用.detach_() 切断反向传播
        • 当我们再训练网络的时候可能希望保持一部分的网络参数不变,只对其中一部分的参数进行调整;或者只训练部分分支网络,并不让其梯度对主网络的梯度造成影响,这时候我们就需要使用detach()函数来切断一些分支的反向传播
        • 相邻采样:采用相邻采样仅在每个训练周期开始的时候初始化隐藏状态是因为相邻的两个批量在原始数据上是连续的
        • 随机采样:随机采样中每个样本只包含局部的时间序列信息,因为样本不完整所以每个批量需要重新初始化隐藏状态。
      def train_and_predict_rnn(rnn, get_params, init_rnn_state, num_hiddens,
                                vocab_size, device, corpus_indices, idx_to_char,
                                char_to_idx, is_random_iter, num_epochs, num_steps,
                                lr, clipping_theta, batch_size, pred_period,
                                pred_len, prefixes):
          if is_random_iter:
              data_iter_fn = d2l.data_iter_random
          else:
              data_iter_fn = d2l.data_iter_consecutive
          params = get_params()
          loss = nn.CrossEntropyLoss()
          
          for epoch in range(num_epochs):
              if not is_random_iter:  
              		# 如使用相邻采样,在epoch开始时初始化隐藏状态
                  state = init_rnn_state(batch_size, num_hiddens, device)
              l_sum, n, start = 0.0, 0, time.time()
              data_iter = data_iter_fn(corpus_indices, batch_size, num_steps, device)
              for X, Y in data_iter:
                  if is_random_iter:  
                  		# 如使用随机采样,在每个小批量更新前初始化隐藏状态
                      state = init_rnn_state(batch_size, num_hiddens, device)
                  else:  
                  		# 否则需要使用detach函数从计算图分离隐藏状态
                      for s in state:
                          s.detach_()
                              
                  # inputs是num_steps个形状为(batch_size, vocab_size)的矩阵
                  inputs = to_onehot(X, vocab_size)
                  # outputs有num_steps个形状为(batch_size, vocab_size)的矩阵
                  (outputs, state) = rnn(inputs, state, params)
                  # 拼接之后形状为(num_steps * batch_size, vocab_size)
                  outputs = torch.cat(outputs, dim=0)
                  # Y的形状是(batch_size, num_steps),转置后再变成形状为
                  # (num_steps * batch_size,)的向量,这样跟输出的行一一对应
                  y = torch.flatten(Y.T)
                  # 使用交叉熵损失计算平均分类误差
                  l = loss(outputs, y.long())
                      
                  # 梯度清0
                  if params[0].grad is not None:
                      for param in params:
                          param.grad.data.zero_()
                  l.backward()
                  grad_clipping(params, clipping_theta, device)  # 裁剪梯度
                  d2l.sgd(params, lr, 1)  # 因为误差已经取过均值,梯度不用再做平均
                  l_sum += l.item() * y.shape[0]
                  n += y.shape[0]
          
              if (epoch + 1) % pred_period == 0:
                  print('epoch %d, perplexity %f, time %.2f sec' % (
                      epoch + 1, math.exp(l_sum / n), time.time() - start))
                  for prefix in prefixes:
                      print(' -', predict_rnn(prefix, pred_len, rnn, params, init_rnn_state, num_hiddens, vocab_size, device, idx_to_char, char_to_idx))
      
  • 循环神经网络从零开始实现

  • 循环神经网络使用pytorch的简洁实现