动手学深度学习笔记(五)下——循环神经网络

动手学深度学习笔记(五)下——循环神经网络

5.6 通过时间反向传播

​ 本节将介绍循环神经网络中梯度的计算和存储方法,即通过时间反向传播(back-propagation through time)。

​ 正向传播在循环神经网络中比较直观,而通过时间反向传播其实是反向传播在循环神经网络中的具体应用。我们需要将循环神经网络按时间步展开,从而得到模型变量和参数之间的依赖关系,并依据链式法则应用反向传播计算并存储梯度。

5.6.1 定义模型

​ 简单起见,我们考虑一个无偏差项的循环神经网络,且激活函数为恒等映射( $\phi(x)=x$ )。设时间步t的输入为单样本 $x_t\in R^d$ ,标签为 $y_t$ ,那么隐藏状态 $h_t \in R^h$​ 的计算表达式为

$h_t=W_{hx}x_t+W_{hh}h_{t-1}$​

其中 $W_{hx}\in R^{hd}$和$W_{hh}\in R^{hh}$ ​是隐藏层权重参数.设输出层权重参数 $W_{qh}\in R^{q*h}$ ,时间步t的输出层变量 $o_t\in R^q$ 计算为

$o_t=W_{qh}h_t$

设时间步t的损失为 $l(o_t,y_t)$ 。时间步数为t的损失函数L定义为

$L=\frac{1}{T}\sum^T_{t=1}l(o_t,y_t)$

我们将L称为有关给定时间步的数据样本的目标函数,并在本节后续讨论中简称为目标函数。

5.6.2 模型计算图

​ 为了可视化循环神经网络中模型变量和参数在计算中的依赖关系,我们可以绘制模型计算图,如图6.3所示。例如,时间步3的隐藏状态 $h_3$ 的计算依赖模型参数 $W_{hx}$$W_{hh}$ 、上一时间步隐藏状态 $h_2$ 以及当前时间步输入 $x_3$​



5.6.3 方法

​ 推理过程自行百度,这里给出各参数的推理结果:











5.7 门控循环单元(GRU)

​ 当时间步数较大或者时间步较小时,循环神经网络的梯度较容易出现衰减或爆炸。虽然裁剪梯度可以应对梯度爆炸,但无法解决梯度衰减的问题。通常由于这个原因,循环神经网络在实际中较难捕捉时间序列中时间步距离较大的依赖关系。

​ 门控循环神经网络(gated recurrent neural network)的提出,正是为了更好地捕捉时间序列中时间步距离较大的依赖关系。它通过可以学习的门来控制信息的流动。其中,门控循环单元(gated recurrent unit,GRU)是一种常用的门控循环神经网络 [1, 2]。另一种常用的门控循环神经网络则将在下一节中介绍。

5.7.1 门控循环单元

​ 它包含了重置门(reset gate)和更新门(update gate)的概念,从而修改了循环神经网络中隐藏状态的计算方式。

  • 重置门和更新门

如图6.4所示,门控循环单元中的重置门和更新门的输入均为当前时间步输入 $X_t$ 与上一时间步隐藏状态 $H_{t-1}$ ​,输出由激活函数为sigmoid函数的全连接层计算得到。



具体来说,假设隐藏单元个数为h,给定时间步t的小批量量输入 $X_t\in R^{nd}$​ (样本数为n,输入个数为d )和上一时间步隐藏状态 $H_{t-1}\in R^{nh}$ ​。重置门 $R_{t}\in R^{nh}$​和更新门$Z_{t}\in R^{nh}$​​ 的计算如下:

$R_t=\sigma(X_tW_{xr}+H_{t-1}W_{hr}+b_r)$

$Z_t=\sigma(X_tW_{xz}+H_{t-1}W_{hz}+b_z)$

其中 $W_{xr},W_{xz}\in R^{dh}$ $W_{hr},W_{hz}\in R ^{hh}$ 是权重参数, $b_r,b_z\in R^{1*h}$ 是偏差参数。3.8节(多层感知机)节中介绍过,sigmoid函数可以将元素的值变换到0和1之间。因此,重置门 $R_t$ 和更新门 $Z_t$ 中每个元素的值域都是[0,1]。

  • 候选隐藏状态

接下来,门控循环单元将计算候选隐藏状态来辅助稍后的隐藏状态计算。如图6.5所示,我们将当前时间步重置门的输出与上一时间步隐藏状态做按元素乘法。如果重置门中元素值接近0,那么意味着重置对应隐藏状态元素为0,即丢弃上一时间步的隐藏状态。如果元素值接近1,那么表示保留上一时间步的隐藏状态。然后,将按元素乘法的结果与当前时间步的输入连结,再通过含激活函数tanh的全连接层计算出候选隐藏状态,其所有元素的值域为[-1,1]。



具体来说,时间步t的候选隐藏状态$ \widehat{H}_t\in R ^{n*h} $​的计算为

$ \widehat{H}t= tanh(X_tW{xh}+(R_t\cdot H_{t-1})W_{hh}+b_h) $

其中$ W_{xh}\in R ^{dh} $和$ W_{hh}\in R ^{hh} $是权重参数,$ b_{h}\in R ^{1*h} $是偏差参数。从上面这个公式可以看出,重置门控制了上一时间步的隐藏状态如何流入当前时间步的候选隐藏状态。而上一时间步的隐藏状态可能包含了时间序列截至上一时间步的全部历史信息。因此,重置门可以用来丢弃与预测无关的历史信息。

  • 隐藏状态

最后,时间步t的隐藏状态$ H_{t}\in R ^{n*h} $的计算使用当前时间步的更新门$ Z_t $来对上一时间步的隐藏状态$ H_{t-1} $和当前时间步的候选隐藏状态$ \widehat{H}_t $​做组合:

$ H_t=Z_t \cdot H_{t-1} + (1-Z_t)\cdot \widehat{H}_t $



更新门可以控制隐藏状态应该如何被包含当前时间步信息的候选隐藏状态所更新,如图6.6所示。假设更新门在时间步 t’ 到t(t’ < t)之间一直近似1。那么,在时间步t’到t之间的输入信息几乎没有流入时间步t的隐藏状态$H_t$。实际上,这可以看作是较早时刻的隐藏状态$H_{t'-1}$​一直通过时间保存并传递至当前时间步t。这个设计可以应对循环神经网络中的梯度衰减问题,并更好地捕捉时间序列中时间步距离较大的依赖关系。

5.7.2 读取数据集

​ 为了实现并展示门控循环单元,下面依然使用周杰伦歌词数据集来训练模型作词。这里除门控循环单元 以外的实现已介绍过.

import numpy as np
import torch
from torch import nn, optim
import torch.nn.functional as F
import sys
import zipfile
sys.path.append("..") 
import d2lzh_pytorch as d2l
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')

with zipfile.ZipFile('./dataset/jaychou_lyrics.txt.zip') as zin:
    with zin.open('jaychou_lyrics.txt') as f:
        corpus_chars = f.read().decode('utf-8')
chars = corpus_chars[0:20000]
corpus_chars = corpus_chars.replace('\n', ' ').replace('\r', ' ')
idx_to_char = list(set(corpus_chars))
char_to_idx = dict([(char, i) for i, char in enumerate(idx_to_char)])
vocab_size = len(char_to_idx)
corpus_indices = [char_to_idx[char] for char in corpus_chars]

5.7.3 从零开始实现

  • 初始化模型参数

下面的代码对模型参数进行初始化。超参数 num_hiddens 定义了隐藏单元的个数。

num_inputs, num_hiddens, num_outputs = vocab_size, 256, vocab_size
print('will use', device)
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])
  • 定义模型

下面的代码定义隐藏状态初始化函数 init_gru_state 。同前文定义的 init_rnn_state 函数一样,它返回由一个形状为(批量大小, 隐藏单元个数)的值为0的 Tensor组成的元组。

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) + R * torch.matmul(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,)
  • 训练模型并创作歌词

在训练模型时只使用相邻采样。设置好超参数后,我们将训练模型并根据前缀“分开”和“不分开”分别创作长度为50个字符的一段歌词。

num_epochs, num_steps, batch_size, lr, clipping_theta = 200, 35, 32, 1e2, 1e-2
pred_period, pred_len, prefixes = 40, 50, ['分开',  '离开有你的世界']

每过40个迭代周期便根据当前训练的模型创作一段歌词。

d2l.train_and_predict_rnn(gru, get_params, init_gru_state, num_hiddens,vocab_size, device, corpus_indices, idx_to_char,char_to_idx, False, num_epochs, num_steps, lr,clipping_theta, batch_size, pred_period, pred_len,prefixes)



5.7.4 简洁实现

在PyTorch中我们直接调用 nn 模块中的 GRU 类即可。

#简洁实现
lr = 1e-2 # 注意调整学习率
gru_layer = nn.GRU(input_size=vocab_size, hidden_size=num_hiddens)
model = d2l.RNNModel(gru_layer, vocab_size).to(device)
d2l.train_and_predict_rnn_pytorch(model, num_hiddens, vocab_size, device,corpus_indices, idx_to_char, char_to_idx,num_epochs, num_steps, lr, clipping_theta,batch_size, pred_period, pred_len, prefixes)



5.8 长短期记忆(LSTM)

本节将介绍另一种常用的门控循环神经网络:长短期记忆(long short-term memory,LSTM)

5.8.1 长短期记忆

​ LSTM 中引入了3个门,即输入门(input gate)、遗忘门(forget gate)和输出门(output gate),以及与隐藏状态形状相同的记忆细胞(某些文献把记忆细胞当成一种特殊的隐藏状态),从而记录额外的信息。

  • 输入门, 遗忘门和输出门

与门控循环单元中的重置门和更新门⼀一样,如图6.7所示,长短期记忆的门的输入均为当前时间步输入 $X_t$ 与上一时间步隐藏状态 $H_{t-1}$ ​,输出由激活函数为sigmoid函数的全连接层计算得到。如此一来,这3个门元素的值域均为[0,1]。



具体来说,假设隐藏单元个数为h,给定时间步t的小批量输入 $X_t\in R^{nd}$ (样本数为n,输入个数为d)和上一时间步隐藏状态 $H_{t-1}\in R ^{nh}$ 。 时间步t的输入门 $I_t\in R^{nh}$ 、遗忘门 $F_t\in R^{nh}$ 和输出门 $O_t\in R^{n*h}$ 分别计算如下:

$I_t=\sigma(X_tW_{xi}+H_{t-1}W_{hi}+b_i)$

$F_t=\sigma(X_tW_{xf}+H_{t-1}W_{hf}+b_f)$​

$O_t=\sigma(X_tW_{xo}+H_{t-1}W_{ho}+b_o)$​

其中的 $W_{xi},W_{xf},W_{xo}\in R^{dh}$ $W_{hi},W_{hf},W_{ho}\in R^{hh}$ 是权重参数, $b_{i},b_{f},b_{o}\in R^{1*h}$ 是偏差参数。

  • 候选记忆细胞

接下来,长短期记忆需要计算候选记忆细胞 $\widehat{C}_t$​ 。它的计算与上面介绍的3个门类似,但使用了值域在[-1,1]的tanh函数作为激活函数,如图6.8所示。



具体来说,时间步t的候选记忆细胞$ \widehat{C}_t\in R^{n*h} $​的计算为

​ $ \widehat{C}t = tanh(X_tW{xc}+H_{t-1}W_{hc}+b_c) $

其中$ W_{xc}\in R^{dh}$​​和$W_{hc}\in R^{hh} $​​是权重参数,$ b_c\in R^{1*h} $​​是偏差参数。

  • 记忆细胞

我们可以通过元素值域在[0,1]的输入门、遗忘门和输出门来控制隐藏状态中信息的流动,这一般也是通过使用按元素乘法来实现的。当前时间步记忆细胞$C_t\in R^{n*h}$​的计算组合了上一时间步记忆细胞和当前时间步候选记忆细胞的信息,并通过遗忘门和输入门来控制信息的流动:

$ C_t=F_t\cdot C_{t-1}+I_t \cdot \widehat{C}_t $

如图6.9所示,遗忘门控制上一时间步的记忆细胞$ C_{t-1} $中的信息是否传递到当前时间步,而输入门则控制当前时间步的输入$X_t X_t $过候选记忆细胞$ \widehat{C}_t $​如何流入当前时间步的记忆细胞。如果遗忘门一直近似1且输入门一直近似0,过去的记忆细胞将一直通过时间保存并传递至当前时间步。这个设计可以应对循环神经网络中的梯度衰减问题,并更好地捕捉时间序列列中时间步距离较大的依赖关系。



  • 隐藏状态

有了记忆细胞以后,接下来我们还可以通过输出门来控制从记忆细胞到隐藏状态$ H_t\in R^{n*h} $的信息的流动:

$ H_t=O_t \cdot tanh(C_t) $

这里的tanh函数确保隐藏状态元素值在-1到1之间。需要注意的是,当输出门近似1时,记忆细胞信息将传递到隐藏状态供输出层使用;当输出门近似0时,记忆细胞信息只自己保留。图6.10展示了长短期记忆中隐藏状态的计算。


5.8.2 读取数据集

如上文,这里依然使用周杰伦歌词数据集来训练模型作词。

import numpy as np
import torch
from torch import nn, optim
import torch.nn.functional as F
import sys
import zipfile
sys.path.append("..") 
import d2lzh_pytorch as d2l
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')

with zipfile.ZipFile('./dataset/jaychou_lyrics.txt.zip') as zin:
    with zin.open('jaychou_lyrics.txt') as f:
        corpus_chars = f.read().decode('utf-8')
chars = corpus_chars[0:20000]
corpus_chars = corpus_chars.replace('\n', ' ').replace('\r', ' ')
idx_to_char = list(set(corpus_chars))
char_to_idx = dict([(char, i) for i, char in enumerate(idx_to_char)])
vocab_size = len(char_to_idx)
corpus_indices = [char_to_idx[char] for char in corpus_chars]

5.8.3 从零开始实现

下面的代码对模型参数进行初始化。超参数 num_hiddens 定义了隐藏单元的个数。

num_inputs, num_hiddens, num_outputs = vocab_size, 256, vocab_size
print('will use', device)
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])

5.8.4 定义模型

在初始化函数中,长短期记忆的隐藏状态需要返回额外的形状为(批量大小, 隐藏单元个数)的值为0的记忆细胞。

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)

同上一节一样,我们在训练模型时只使用相邻采样。设置好超参数后,我们将训练模型并根据前缀“分开”和“不分开”分别创作长度为50个字符的一段歌词。

num_epochs, num_steps, batch_size, lr, clipping_theta = 160, 35, 32, 1e2, 1e-2
pred_period, pred_len, prefixes = 40, 50, ['分开', '我一路向北']

#训练
d2l.train_and_predict_rnn(lstm, get_params, init_lstm_state, num_hiddens,vocab_size, device, corpus_indices, idx_to_char,char_to_idx, False, num_epochs, num_steps, lr,clipping_theta, batch_size, pred_period, pred_len,prefixes)



5.8.5 简洁实现

在pytorch中我们可以直接调用 nn 模块中的 LSTM 类。

lr = 1e-2 # 注意调整学习率
lstm_layer = nn.LSTM(input_size=vocab_size, hidden_size=num_hiddens)
model = d2l.RNNModel(lstm_layer, vocab_size)
d2l.train_and_predict_rnn_pytorch(model, num_hiddens, vocab_size, device,corpus_indices, idx_to_char, char_to_idx,num_epochs, num_steps, lr, clipping_theta,batch_size, pred_period, pred_len, prefixes)



5.9 深度循环神经网络

​ 本章到目前为止介绍的循环神经网络只有一个单向的隐藏层,在深度学习应用里,我们通常会用到含有多个隐藏层的循环神经网络,也称作深度循环神经网络。图6.11演示了一个有L个隐藏层的深度循环神经网络,每个隐藏状态不断传递至当前层的下一时间步和当前时间步的下一层。



具体来说,在时间步t里,设小批量输入$ X_t\in R^{nd} $(样本数为n,输入个数为d),第$l$隐藏层($l=1,...,L$​)的隐藏状态为 $H_t^{(l)}\in R^{nh}$ (隐藏单元个数为h),输出层变量为$ O_t\in R^{n*q} $(输出个数为q),且隐藏层的激活函数为$ \phi $​。第1隐藏层的隐藏状态和之前的计算一样:

$ H_t^{(1)}=\phi (X_tW_{xh}^{(1)}+H_{t-1}^{(1)}+b_h^{(1)}) $

其中权重 $W_{xh}^{(1)}\in R^{dh}、W_{hh}^{(1)}\in R^{hh}和偏差b_h^{(1)}\in R ^{1*h}$ 分别为第1隐藏层的模型参数。

当$1<l \leq L$时,第$l$隐藏层的隐藏状态的表达式为

$H_t^{(l)} = \phi (H_t^{(t-1)}W_{xh}^{(l)}+H_{t-1}^{(l)}W_{hh}^{(1)}+b_h^{(1)})$​

其中权重$W_{xh}^{(l)}\in R^{hh}、W_{hh}^{(l)}\in R^{hh}和偏差b_h^{(1)}\in R ^{1*h}$​ 分别为第$l$​隐藏层的模型参数。

最终,输出层的输出只需基于第L隐藏层的隐藏状态:

$O_t = H_t^{(L)}W_{hq}+b_q$

其中权重 $W_{hq}\in R^ {hq}$和偏差$b_q\in R{1q}$ ​为输出层的模型参数。

同多层感知机一样,隐藏层个数L和隐藏单元个数h都是超参数。此外,如果将隐藏状态的计算换成门控循环单元或者长短期记忆的计算,我们可以得到深度门控循环神经网络。

5.10 双向循环神经网络

​ 之前介绍的循环神经网络模型都是假设当前时间步是由前面的较早时间步的序列决定的,因此它们都将信息通过隐藏状态从前往后传递。有时候,当前时间步也可能由后面时间步决定。例如,当我们写下一个句子时,可能会根据句子后面的词来修改句子前面的用词。双向循环神经网络通过增加从后往前传递信息的隐藏层来更更灵活地处理这类信息。图6.12演示了一个含单隐藏层的双向循环神经网络的架构。






发布于 2021-10-15 15:59