PyTorch系列之循环神经网络进阶

循环神经网络进阶

Posted by Young on February 13, 2020

Task03:循环神经网络进阶

模型选择、过拟合和欠拟合

  • 验证数据集

    • 从严格意义上讲,测试集只能在所有超参数和模型参数选定后使用一次不可以使用测试数据选择模型,如调参。由于无法从训练误差估计泛化误差,因此也不应只依赖训练数据选择模型。鉴于此,我们可以预留一部分在训练数据集和测试数据集以外的数据来进行模型选择。这部分数据被称为验证数据集,简称验证集(validation set)。例如,我们可以从给定的训练集中随机选取一小部分作为验证集,而将剩余部分作为真正的训练集。
    • 由于验证数据集不参与模型训练,当训练数据不够用时,预留大量的验证数据显得太奢侈。一种改善的方法是K折交叉验证(K-fold cross-validation)。在K折交叉验证中,我们把原始训练数据集分割成K个不重合的子数据集,然后我们做K次模型训练和验证。每一次,我们使用一个子数据集验证模型,并使用其他K-1个子数据集来训练模型。在这K次训练和验证中,每次用来验证模型的子数据集都不同。最后,我们对这K次训练误差和验证误差分别求平均。
  • 正则化

  • 通过为模型损失函数添加惩罚项使学出的模型参数值较小,是应对过拟合的常用手段。

  • 丢弃法 dropout

    • 对该隐藏层使用丢弃法时,该层的隐藏单元将有一定概率被丢弃掉。

    • 对之前多层感知机的神经网络中的隐藏层使用丢弃法,一种可能的结果如图所示,其中h2和h5被清零。这时输出值的计算不再依赖h2和h5,在反向传播时,与这两个隐藏单元相关的权重的梯度均为0。由于在训练中隐藏层神经元的丢弃是随机的,即h1,…,h5都有可能被清零,输出层的计算无法过度依赖h1,…,h5中的任一个,从而在训练模型时起到正则化的作用,并可以用来应对过拟合

      def dropout(X, drop_prob):
          X = X.float()
          assert 0 <= drop_prob <= 1
          keep_prob = 1 - drop_prob
          # 这种情况下把全部元素都丢弃
          if keep_prob == 0:
              return torch.zeros_like(X)
          mask = (torch.rand(X.shape) < keep_prob).float()
              
          return mask * X / keep_prob
          
          
      net = nn.Sequential(
              d2l.FlattenLayer(),
              nn.Linear(num_inputs, num_hiddens1),
              nn.ReLU(),
              nn.Dropout(drop_prob1),
              nn.Linear(num_hiddens1, num_hiddens2), 
              nn.ReLU(),
              nn.Dropout(drop_prob2),
              nn.Linear(num_hiddens2, 10)
              )
      

梯度消失和梯度爆炸

  • 当神经网络的层数较多时,模型的数值稳定性容易变差。
  • 随机初始化模型参数的原因

    • 假设输出层只保留一个输出单元o1(删去o2和o3以及指向它们的箭头),且隐藏层使用相同的激活函数如果将每个隐藏单元的参数都初始化为相等的值,那么在正向传播时每个隐藏单元将根据相同的输入计算出相同的值,并传递至输出层。在反向传播中,每个隐藏单元的参数梯度值相等。因此,这些参数在使用基于梯度的优化算法迭代后值依然相等。之后的迭代也是如此。在这种情况下,无论隐藏单元有多少,隐藏层本质上只有1个隐藏单元在发挥作用。因此,正如在前面的实验中所做的那样,我们通常将神经网络的模型参数,特别是权重参数,进行随机初始化。
  • Xavier随机初始化:$U\left(-\sqrt{\frac{6}{a+b}}, \sqrt{\frac{6}{a+b}}\right)$
    • 它的设计主要考虑到,模型参数初始化后,每层输出的方差不该受该层输入个数 a 影响,且每层梯度的方差也不该受该层输出个数 b 影响。
  • 协变量偏移
    • 输入的分布可能随时间而改变,但是标记函数,即条件分布 $P(y \mid x)$ 不会改变
    • 比如:训练集 $x_1$ 由猫的照片组成,而测试集 $x_2$ 只包含猫的卡通图案,虽然对应的标签 $y$ 都是猫,即条件分布 $P(y \mid x)$ 不变 。但在一个看起来与测试集有着本质不同的数据集上进行训练,而不考虑如何适应新的情况,这是不是一个好主意(即用 $x_1$ 训练去预测 $x_2$ )。不幸的是,这是一个非常常见的陷阱。
      • 一个在冬季部署的物品推荐系统在夏季的物品推荐列表中出现了圣诞礼物,我们可以推断该系统没有考虑到:协变量偏移。可以理解为在夏季的物品推荐系统与冬季相比,时间或者说季节发生了变化,导致了夏季推荐圣诞礼物的不合理的现象,这个现象是由于协变量时间发生了变化造成的。
  • 标签偏移可以简单理解为测试时出现了训练时没有的标签

Kaggle 房价预测实战

  • 预处理数据

    • 对连续数值的特征做标准化(standardization):设该特征在整个数据集上的均值为μ,标准差为σ。

      all_features[numeric_features] = all_features[numeric_features].apply(
          lambda x: (x - x.mean()) / (x.std()))
      # 标准化后,每个数值特征的均值变为0,所以可以直接用0来替换缺失值
      all_features[numeric_features] = all_features[numeric_features].fillna(0)
      
    • 将离散数值转成指示特征,会增加特征数量

      • 设特征MSZoning里面有两个不同的离散值RL和RM,那么这一步转换将去掉MSZoning特征,并新加两个特征MSZoning_RL和MSZoning_RM,其值为0或1。如果一个样本原来在MSZoning里的值为RL,那么有MSZoning_RL=1且MSZoning_RM=0。
      # dummy_na=True将缺失值也当作合法的特征值并为其创建指示特征
      all_features = pd.get_dummies(all_features, dummy_na=True)
      all_features.shape
      

循环神经网络进阶

  • ⻔控循环神经⽹络:捕捉时间序列中时间步距离较⼤的依赖关系

    • RNN存在的问题:梯度较容易出现衰减或爆炸(BPTT)

      \[H_{t} = ϕ(X_{t}W_{xh} + H_{t-1}W_{hh} + b_{h})\]

    • GRU及其PyTorch实现

      $$ R_{t} = σ(X_tW_{xr} + H_{t−1}W_{hr} + b_r)\\ Z_{t} = σ(X_tW_{xz} + H_{t−1}W_{hz} + b_z)\\ \widetilde{H}_t = tanh(X_tW_{xh} + (R_t ⊙H_{t−1})W_{hh} + b_h)\\ H_t = Z_t⊙H_{t−1} + (1−Z_t)⊙\widetilde{H}_t $$

      • 重置⻔有助于捕捉时间序列⾥短期的依赖关系;

      • 更新⻔有助于捕捉时间序列⾥⻓期的依赖关系。

      • 初始化参数、隐层、模型定义

        def get_params():
            def _one(shape):
                ts = torch.tensor(np.random.normal(0, 0.01, size=shape), device=device, dtype=torch.float32)
                return torch.nn.Parameter(ts, requires_grad=True)
            def _three():
                return (_one((num_inputs, num_hiddens)),
                        _one((num_hiddens, num_hiddens)),
                      torch.nn.Parameter(torch.zeros(num_hiddens, device=device, dtype=torch.float32), requires_grad=True))
                  
            W_xz, W_hz, b_z = _three()  # 更新门参数
            W_xr, W_hr, b_r = _three()  # 重置门参数
          W_xh, W_hh, b_h = _three()  # 候选隐藏状态参数
                  
            # 输出层参数
            W_hq = _one((num_hiddens, num_outputs))
            b_q = torch.nn.Parameter(torch.zeros(num_outputs, device=device, dtype=torch.float32), requires_grad=True)
            return nn.ParameterList([W_xz, W_hz, b_z, W_xr, W_hr, b_r, W_xh, W_hh, b_h, W_hq, b_q])
                  
              
        def init_gru_state(batch_size, num_hiddens, device):
              return (torch.zeros((batch_size, num_hiddens), device=device), )
                    
                    
        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(torch.matmul(X, W_xz) + torch.matmul(H, W_hz) + b_z)
                R = torch.sigmoid(torch.matmul(X, W_xr) + torch.matmul(H, W_hr) + b_r)
                H_tilda = torch.tanh(torch.matmul(X, W_xh) + torch.matmul(R * H, W_hh) + b_h)
                H = Z * H + (1 - Z) * H_tilda
                Y = torch.matmul(H, W_hq) + b_q
                outputs.append(Y)
            return outputs, (H,)
        
  • LSTM及其PyTorch实现

    $$ I_t = σ(X_tW_{xi} + H_{t−1}W_{hi} + b_i) \\ F_t = σ(X_tW_{xf} + H_{t−1}W_{hf} + b_f)\\ O_t = σ(X_tW_{xo} + H_{t−1}W_{ho} + b_o)\\ \widetilde{C}_t = tanh(X_tW_{xc} + H_{t−1}W_{hc} + b_c)\\ C_t = F_t ⊙C_{t−1} + I_t ⊙\widetilde{C}_t\\ H_t = O_t⊙tanh(C_t) $$

    • 遗忘门:控制上一时间步的记忆细胞

      • 输入门:控制当前时间步的输入

      • 输出门:控制从记忆细胞到隐藏状态

      • 记忆细胞:⼀种特殊的隐藏状态的信息的流动

      • 初始化参数、隐层、模型定义

        def get_params():
            def _one(shape):
                ts = torch.tensor(np.random.normal(0, 0.01, size=shape), device=device, dtype=torch.float32)
                return torch.nn.Parameter(ts, requires_grad=True)
            def _three():
                return (_one((num_inputs, num_hiddens)),
                        _one((num_hiddens, num_hiddens)),
                        torch.nn.Parameter(torch.zeros(num_hiddens, device=device, dtype=torch.float32), requires_grad=True))
                  
            W_xi, W_hi, b_i = _three()  # 输入门参数
            W_xf, W_hf, b_f = _three()  # 遗忘门参数
            W_xo, W_ho, b_o = _three()  # 输出门参数
            W_xc, W_hc, b_c = _three()  # 候选记忆细胞参数
                  
            # 输出层参数
            W_hq = _one((num_hiddens, num_outputs))
            b_q = torch.nn.Parameter(torch.zeros(num_outputs, device=device, dtype=torch.float32), requires_grad=True)
            return nn.ParameterList([W_xi, W_hi, b_i, W_xf, W_hf, b_f, W_xo, W_ho, b_o, W_xc, W_hc, b_c, W_hq, b_q])
                  
                  
        def init_lstm_state(batch_size, num_hiddens, device):
            return (torch.zeros((batch_size, num_hiddens), device=device), 
                    torch.zeros((batch_size, num_hiddens), device=device))
                          
              
        def lstm(inputs, state, params):
            [W_xi, W_hi, b_i, W_xf, W_hf, b_f, W_xo, W_ho, b_o, W_xc, W_hc, b_c, W_hq, b_q] = params
            (H, C) = state
            outputs = []
            for X in inputs:
                I = torch.sigmoid(torch.matmul(X, W_xi) + torch.matmul(H, W_hi) + b_i)
                F = torch.sigmoid(torch.matmul(X, W_xf) + torch.matmul(H, W_hf) + b_f)
                O = torch.sigmoid(torch.matmul(X, W_xo) + torch.matmul(H, W_ho) + b_o)
                C_tilda = torch.tanh(torch.matmul(X, W_xc) + torch.matmul(H, W_hc) + b_c)
                C = F * C + I * C_tilda
                H = O * C.tanh()
                Y = torch.matmul(H, W_hq) + b_q
                outputs.append(Y)
            return outputs, (H, C)
        
    • Deep Recurrent Neural Networks

    • Bidirectional Model