和一般的神经网络相比,RNN 引入了记忆功能。具体地,RNN 通过隐藏层存储之前时间步的信息,并通过 “按时间反向传播” 进行参数学习。
语言模型
假设序列 $w_1, w_2, ..., w_T$
的每个词是依次生成的,则
$$ P(w_1, ..., w_T) = \textrm{prod}_{t=1}^T P(w_t | w_1, ..., w_{t-1}). $$
基于 $n-1$
阶马尔可夫链,语言模型可改写为
$$ P(w_1, ..., w_T) \approx \textrm{prod}_{t=1}^T P(w_t | w_{t-(n-1)}, ..., w_{t-1}), $$
即当前词的出现仅和前面的 $n-1$
个词有关,这就是 $n$
元语法。
RNN 的基本结构
循环神经网络并非刚性地记忆所有固定长度的序列,而是通过隐藏状态来存储之前时间步的信息。
在 MLP 中,不妨设输入的小批量数据样本为 $X \in \mathbb{R}^{n \times d}$
,则隐藏层的输出为 $H = \phi(XW_{xh} + \vec{b}_h) \in \mathbb{R}^{n \times h}$
,
输出层的输出为 $O = HW_{hq} + \vec{b}_q \in \mathbb{R}^{n \times q}$
,最后通过 $\textrm{softmax}(O)$
得到输出类别的概率分布。
在 MLP 的基础上,将上一时间步隐藏层的输出作为这一时间步隐藏层计算的输入,即
$$ H_t = \phi \left(X_t W_{xh} + H_{t-1}W_{hh} + \vec{b}_h \right), $$
通过引入新的权重参数将上一轮隐藏层的输出作为本轮隐藏层计算的依据之一。输出层的计算和 MLP 一致。采用这种方式构建的循环神经网络的参数包含 $W_{xh} \in \mathbb{R}^{d \times h}, W_{hh} \in \mathbb{R}^{h \times h}, \vec{b}_h \in \mathbb{R}^{1 \times h}, W_{hq} \in \mathbb{R}^{h \times q}, \vec{b}_q \in \mathbb{R}^{1 \times q}$
。
在时间步 $t$
,隐藏状态的计算可以看成是将输入 $X_t$
和前一时间步隐藏状态 $H_{t-1}$
连结后输入一个激活函数为 $\phi$
的全连接层。
该全连接层的输出就是当前时间步的隐藏状态 $H_t$
且模型参数为 $W_{xh}$
和 $W_{hh}$
的连结,偏差为 $\vec{b}_h$
。
# 定义 RNN 模型
def rnn(inputs, hidden_state, params):
W_xh, W_hh, b_h, W_hq, b_q = params
H, = hidden_state
outputs = []
for X in inputs: # iteration over num_steps
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,)
基于字符级循环神经网络来创建语言模型:输入是一个字符,神经网络基于当前和过去的字符来预测下一个字符。在训练时, 我们对每个时间步的输出层输出使用 softmax 运算,然后使用交叉熵损失函数来计算它与标签的误差。
时序数据的采样
-
随机采样:每次从数据里随机采样一个小批量。其中批量大小
batch_size
指每个小批量的样本数,num_steps
为每个样本所包含的时间步数。 在随机采样中,每个样本是原始序列上任意截取的一段序列。相邻的两个随机小批量在原始序列上的位置不一定相毗邻。因此, 我们无法用一个小批量最终时间步的隐藏状态来初始化下一个小批量的隐藏状态。在训练模型时,每次随机采样前都需要重新初始化隐藏状态。 -
相邻采样:相邻的两个随机小批量在原始序列上的位置相毗邻。这时候,我们就可以用一个小批量最终时间步的隐藏状态来初始化下一个小批量的隐藏状态, 从而使下一个小批量的输出也取决于当前小批量的输入,并如此循环下去。这对实现循环神经网络造成了两方面影响:一方面, 在训练模型时,我们只需在每一个迭代周期开始时初始化隐藏状态;另一方面,当多个相邻小批量通过传递隐藏状态串联起来时,模型参数的梯度计算将依赖所有串联起来的小批量序列。同一迭代周期中,随着迭代次数的增加,梯度的计算开销会越来越大。 为了使模型参数的梯度计算只依赖一次迭代读取的小批量序列, 我们可以在每次读取小批量前将隐藏状态从计算图中分离出来。
裁剪梯度
循环神经网络中较容易出现梯度衰减或梯度爆炸。为了应对梯度爆炸,我们可以裁剪梯度(clip gradient),裁剪后的梯度的 $\Vert \cdot \Vert_2$
不超过 $\theta$
:
$$ \min \bigg(\frac{\theta}{\| \vec{g} \|}, 1\bigg) \cdot \vec{g}. $$
# 裁剪梯度
def grad_clipping(params, theta, device):
norm = torch.tensor([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
困惑度
使用困惑度(perplexity)评价语言模型的好坏。困惑度是对交叉熵损失函数做指数运算后得到的值。
- 最佳情况下,模型总是把标签类别的概率预测为 1,此时困惑度为 1;
- 最坏情况下,模型总是把标签类别的概率预测为 0,此时困惑度为正无穷;
- 基线情况下,模型总是预测所有类别的概率都相同,此时困惑度为类别个数。
一个有效地模型的困惑度应在 1 和 vocab_size
之间。
RNN 的实现
首先按照如下方式实现 rnn_layer
:
rnn_layer = nn.RNN(input_size=vocab_size, hidden_size=hidden_size)
作为 nn.RNN
的实例,rnn_layer
在前向计算后会分别返回输出和隐藏状态。其中 “输出” 指的是隐藏层在各个时间步上计算并输出的隐藏状态,
它们通常作为后续输出层的输入,形状为 (num_steps, batch_size, hidden_size)
。需要强调的是,该输出本身并不涉及输出层计算。
“隐藏状态”指的是隐藏层在 “最后时间步” 的隐藏状态(图 3 中的 $H_T^{(1)},...,H_T^{(L)}$
)。当隐藏层有多层时,每一层的隐藏状态都会记录在该变量中。
基于 rnn_layer
,实现 RNN 模型:
class RNNModel(nn.Module):
def __init__(self, rnn_layer, vocab_size):
super(RNNModel, self).__init__()
self.rnn = rnn_layer
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)
self.state = None
def forward(self, inputs, state):
# input: (batch_size, num_steps)
# 经过独热编码之后,得到 (num_steps, batch_size, vocab_size)
X = to_onehot(inputs, self.vocab_size)
Y, self.state = self.rnn(torch.stack(X), state)
# change Y's size into (num_steps * batch_size, num_hiddens)
output = self.dense(Y.view(-1, Y.shape[-1]))
return output, self.state
通过时间反向传播(BPTT)
如果不裁剪梯度,RNN 模型将无法正常训练。为了深刻理解这一现象,本节将介绍循环神经网络中梯度的计算和存储方法, 即通过时间反向传播(back-propagation through time)。需要将循环神经网络按时间步展开,从而得到模型变量和参数之间的依赖关系, 并依据链式法则应用反向传播计算并存储梯度。
含有单隐藏层的 RNN
考虑一个无偏差项的循环神经网络,且激活函数为恒等映射 $\phi(\vec{x}) = \vec{x}$
。设时间步 $t$
的输入为单个样本 $\vec{x}_t \in \mathbb{R}^d$
,标签为 $y_t$
,
则隐藏状态 $\vec{h}_t \in \mathbb{R}^h$
的计算表达式为
$$ \vec{h}_t = W_{hx} \vec{x}_t + W_{hh} \vec{h}_{t-1}, $$
其中 $W_{hx} \in \mathbb{R}^{h \times d}$
和 $W_{hh} \in \mathbb{R}^{h \times h}$
是隐藏层权重参数。
设输出层权重参数为 $X_{qh} \in \mathbb{R}^{q \times h}$
,则时间步 $t$
的输出层变量 $\vec{o}_t \in \mathbb{R}^q$
的计算表达式为
$$ \vec{o}_t = W_{qh} \vec{h}_t. $$
设时间步 $t$
的损失为 $l(\vec{o}_t, y_t)$
,则时间步数为 $T$
的损失函数定义为
$$ L \triangleq \frac{1}{T} \sum_{t=1}^T l(\vec{o}_t, y_t). $$
模型计算图
图 4 给出了时间步数为 3 的循环神经网络模型计算中的依赖关系。方框代表变量(无阴影)或参数(有阴影),圆圈代表运算符。
通过时间反向传播
计算 $L$
关于各时间步输出层变量 $\vec{o}_t$
的梯度:
$$ \forall t \in \{1, ..., T\}: \frac{\partial L}{\partial \vec{o}_t} = \frac{\partial l(\vec{o}_t, y_t)}{T \cdot \partial \vec{o}_t}. $$
计算 $L$
关于输出层权重参数 $W_{qh}$
的梯度:
$L$
通过 $\vec{o}_1, ..., \vec{o}_T$
依赖 $W_{qh}$
。所以
$$ \frac{\partial L}{\partial W_{qh}} = \sum_{t=1}^T \textrm{prod} \Big( \frac{\partial L}{\partial \vec{o}_t}, \frac{\partial \vec{o}_t}{\partial W_{qh}} \Big) = \sum_{t=1}^T \frac{\partial L}{\partial \vec{o}_t} \vec{h}_t^\textrm{T}. $$
计算 $L$
关于各时间步 $t$
隐藏层变量 $\vec{h}_t$
的梯度:
对于 $t = T$
和 $t=1, .., T-1$
而言,$L$对
$\vec{h}_t$的依赖不同。 对于
$t = T$,$L$
只通过 $\vec{o}_T$
依赖隐藏状态 $\vec{h}_T$
。因此,梯度计算表达式为
$$ \frac{\partial L}{\partial \vec{h}_T} = \textrm{prod} \Big( \frac{\partial L}{\partial \vec{o}_T}, \frac{\partial \vec{o}_T}{\partial \vec{h}_T} \Big) = W_{qh}^\textrm{T} \frac{\partial L}{\partial \vec{o}_T}. $$
对于 $t = 1,...,T-1$
,$L$
通过 $\vec{o}_t$
和 $\vec{h}_{t+1}$
依赖隐藏状态 $\vec{h}_t$
。因此,梯度计算表达式为
$$ \frac{\partial L}{\partial \vec{h}_t} = \textrm{prod} \Big( \frac{\partial L}{\partial \vec{h}_{t+1}}, \frac{\partial \vec{h}_{t+1}}{\partial \vec{h}_t} \Big) + \textrm{prod} \Big( \frac{\partial L}{\partial \vec{o}_t}, \frac{\partial \vec{o}_t}{\partial \vec{h}_t} \Big) = W_{hh}^\textrm{T} \frac{\partial L}{\partial \vec{h}_{t+1}} + W_{qh}^\textrm{T} \frac{\partial L}{\partial \vec{o}_t}. $$
将上面的递归公式展开,对任意时间步 $1 \leq t \leq T$
,我们可以得到目标函数有关隐藏状态梯度的通项公式:
$$ \begin{aligned} \frac{\partial L}{\partial \vec{h}_t} &= \Big(W_{hh}^\textrm{T} \Big)^2 \frac{\partial L}{\partial \vec{o}_{t+2}} + W_{hh}^\textrm{T} W_{qh}^\textrm{T} \frac{\partial L}{\partial{\vec{o}_{t+1}}} \\ &= \Big(W_{hh}^\textrm{T} \Big)^3 \frac{\partial L}{\partial \vec{o}_{t+3}} + \Big( W_{hh}^\textrm{T} \Big)^2 W_{qh}^\textrm{T} \frac{\partial L}{\partial{\vec{o}_{t+2}}} + W_{hh}^\textrm{T} W_{qh}^\textrm{T} \frac{\partial L}{\partial{\vec{o}_{t+1}}}\\ &= \cdots\\ &= \sum_{i=t}^T \Big( W_{hh}^\textrm{T} \Big)^{T-i} W_{qh}^\textrm{T} \frac{\partial L}{\partial \vec{o}_{T-i+t}}. \end{aligned} $$
计算 $L$
关于隐藏层权重参数 $W_{qh}$
的梯度:
$$ \frac{\partial L}{\partial W_{hx}} = \sum_{t=1}^T \textrm{prod} \Big( \frac{\partial T}{\partial \vec{h}_t}, \frac{\vec{h}_t}{\partial W_{hx}} \Big) = \sum_{t=1}^T \frac{\partial L}{\partial \vec{h}_t} \vec{x}_t^\textrm{T} $$
$$ \frac{\partial L}{\partial W_{hh}} = \sum_{t=1}^T \textrm{prod} \Big( \frac{\partial T}{\partial \vec{h}_t}, \frac{\vec{h}_t}{\partial W_{hh}} \Big) = \sum_{t=1}^T \frac{\partial L}{\partial \vec{h}_t} \vec{h}_{t-1}^\textrm{T}. $$
可以发现,$\frac{\partial L}{\partial \vec{h}_t}$
用到了权重 $W_{hh}$
的指数运算。如果 $W_{hh}$
很大,就会发生梯度爆炸的现象。
因此裁剪梯度在 RNN 的训练中是十分有必要的。
门控逻辑单元(GRU)
当时间步数较大或者时间步较小时,循环神经网络的梯度较容易出现衰减或爆炸。虽然裁剪梯度可以应对梯度爆炸,但无法解决梯度衰减的问题。 通常由于这个原因,循环神经网络在实际中较难捕捉时间序列中时间步距离较大的依赖关系。
门控循环神经网络(gated recurrent neural network)的提出,正是为了更好地 捕捉时间序列中时间步距离较大的依赖关系。 它通过可以学习的门来控制信息的流动。其中,门控循环单元(gated recurrent unit,GRU)是一种常用的门控循环神经网络。
GRU 引入了重置门(reset gate)和更新门(update gate)的概念,从而修改了循环神经网络中隐藏状态的计算方式。
重置门和更新门的输入均为当前时间步的小批量输入 $X_t \in \mathbb{R}^{n \times d}$
和上一时间步的隐藏状态 $H_{t-1} \in \mathbb{R}^{n \times h}$
,
输出由激活函数为 sigmoid 函数的全连接层得到:
$$ R_t = \sigma \Big(X_t W_{xr} + H_{t-1} W_{hr} + \vec{b}_r \Big) \in \mathbb{R}^{n \times h} $$
$$ Z_t = \sigma \Big(X_t W_{xz} + H_{t-1} W_{hz} + \vec{b}_z \Big) \in \mathbb{R}^{n \times h}, $$
其中 $W_{xr},W_{xz} \in \mathbb{R}^{d \times h}$
和 $W_{hr},W_{hz} \in \mathbb{R}^{h \times h}$
为权重参数,
$\vec{b}_r,\vec{b}_z \in \mathbb{R}^{1 \times h}$
为偏置。选择 sigmoid 作为激活函数是为了将这两个逻辑门的输出限定在 0 到 1 之间。
随后,将当前时间步重置门的输出与上一时间步隐藏状态做按元素乘法。如果重置门中元素值接近 0,那么意味着重置对应隐藏状态元素为 0,即丢弃上一时间步的隐藏状态。如果元素值接近 1,那么表示保留上一时间步的隐藏状态。然后,将按元素乘法的结果与当前时间步的输入连结,再通过含激活函数 tanh 的全连接层计算出候选隐藏状态,其所有元素的值域为 $[−1,1]$
。因此,当前时间步候选隐藏状态的计算表达式为
$$ \tilde{H}_t = \textrm{tanh} \Big(X_t W_{xh} + \big( H_{t-1} \odot R_t \big) W_{hh} + \vec{b}_h \Big) \in \mathbb{R}^{n \times h}, $$
其中 $W_{xh} \in \mathbb{R}^{d \times h},W_{hh} \in \mathbb{R}^{h \times h}$
为权重参数,$\vec{b}_h \in \mathbb{R}^{1 \times h}$
为偏置。重置门控制了上一时间步的隐藏状态如何流入当前时间步的候选隐藏状态,从而更好地捕捉时间序列里短期的依赖关系。
而上一时间步的隐藏状态可能包含了时间序列截至上一时间步的全部历史信息。因此,重置门可以用来丢弃与预测无关的历史信息。
然后,计算当前时间步的隐藏状态 $H_t \in \mathbb{R}^{n \times h}$
:
$$ H_t = Z_t \odot H_{t-1} + (1 - Z_t) \odot \tilde{H}_t. $$
更新门可以控制隐藏状态应该如何被包含当前时间步信息的候选隐藏状态所更新。假设更新门在时间步 $t'$
到 $t$
($t' < t$
)之间一直近似 1,
那么在时间步 $t'$
到 $t$
之间的输入信息几乎没有流入时间步 $t$
的隐藏状态 $H_t$
。这种现象可以理解为:
较早时刻的隐藏状态 $H_{t'-1}$
一直通过时间保存并传递至当前的时间步 $t$
。这个设计可以应对循环神经网络中的梯度衰减问题,
并更好地捕捉时间序列中时间步距离较大的依赖关系。
最后,时间步 $t$
的输出的计算方式不变,仍为
$$ O_t = HW_{hq} + \vec{b}_q \in \mathbb{R}^{n \times q}, $$
其中 $W_{hq} \in \mathbb{R}^{h \times q}$
为权重参数,$\vec{b}_q \in \mathbb{R}^{1 \times q}$
为偏置。
# 定义 GRU 模型
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_t = torch.tanh(torch.matmul(X, W_xh) + torch.matmul(R * H, W_hh) + b_h)
H = Z * H + (1 - Z) * H_t
Y = torch.matmul(H, W_hq) + b_q
outputs.append(Y)
return outputs, (H,)
长短时记忆(LSTM)
长短时记忆(long short-term memory,LSTM)中引入了 3 个门,即输入门(input gate)、遗忘门(forget gate)和输出门(output gate), 以及与隐藏状态形状相同的记忆细胞(某些文献把记忆细胞当成一种特殊的隐藏状态),从而记录额外的信息。
与 GRU 一样,LSTM 的遗忘门、输入门和输出门的输入均为当前时间步的输入 $X_t$
和上一时间步的隐藏状态 $H_{t-1}$
,输出由激活函数为 sigmoid 函数的全连接层计算得到。
三个门的输出均在 0 到 1 之间。计算表达式如下:
$$ I_t = \sigma \Big(X_t W_{xi} + H_{t-1} W_{hi} + \vec{b}_i\Big) \in \mathbb{R}^{n \times h} $$
$$ F_t = \sigma \Big(X_t W_{xf} + H_{t-1} W_{hf} + \vec{b}_f\Big) \in \mathbb{R}^{n \times h} $$
$$ O_t = \sigma \Big(X_t W_{xo} + H_{t-1} W_{ho} + \vec{b}_o\Big) \in \mathbb{R}^{n \times h}, $$
其中 $W_{xi},W_{xf},W_{xo} \in \mathbb{R}^{d \times h}$
和 $W_{hi},W_{hf},W_{ho} \in \mathbb{R}^{h \times h}$
为权重参数,$\vec{b}_i, \vec{b}_f, \vec{b}_o \in \mathbb{R}^{1 \times h}$
为偏置。
计算候选记忆细胞 $\tilde{C}_t \in \mathbb{R}^{n \times h}$
:采用 tanh 作为激活函数,有
$$ \tilde{C}_t = \textrm{tanh} \Big(X_t W_{xc} + H_{t-1}W_{hc} + \vec{b}_c \Big) \in \mathbb{R}^{n \times h}, $$
其中 $W_{xc} \in \mathbb{R}^{d \times h}$
和 $W_{hc} \in \mathbb{R}^{h \times h}$
为权重参数, $\vec{b}_c \in \mathbb{R}^{1 \times h}$
为偏置。
计算记忆细胞 $C_t \in \mathbb{R}^{n \times h}$
:
我们可以通过元素值域在 $[0,1]$
的输入门、遗忘门和输出门来控制隐藏状态中信息的流动,这一般也是通过使用按元素乘法来实现的。
当前时间步记忆细胞 $C_t$
的计算组合了上一时间步记忆细胞和当前时间步候选记忆细胞的信息,并通过遗忘门和输入门来控制信息的流动:
$$ C_t = F_t \odot C_{t-1} + I_t \odot \tilde{C}_t. $$
遗忘门控制上一时间步的记忆细胞 $C_{t-1}$
中的信息是否传递到当前时间步,而输入门则通过候选记忆细胞 $\tilde{C}_t$
控制当前时间步的输入 $X_t$
如何流入当前时间步的记忆细胞。
如果遗忘门一直近似 1 且输入门一直近似 0,过去的记忆细胞将一直通过时间保存并传递至当前时间步。这个设计可以应对循环神经网络中的梯度衰减问题,
并更好地捕捉时间序列中时间步距离较大的依赖关系。
计算隐藏状态 $H_t \in \mathbb{R}^{n \times h}$
:输出门来控制从记忆细胞到隐藏状态 $H_t$
的信息的流动:
$$ H_t = O_t \odot \textrm{tanh} (C_t). $$
这里的 tanh 函数确保隐藏状态元素值在 - 1 到 1 之间。需要注意的是,当输出门近似 1 时,记忆细胞信息将传递到隐藏状态供输出层使用;当输出门近似 0 时,记忆细胞信息只自己保留。
最后,输出变量的计算表达式和 GRU 一致,不再赘述。
# 定义 LSTM 模型
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_t = torch.tanh(torch.matmul(X, W_xc) + torch.matmul(H, W_hc) + b_c)
C = F * C + I * C_t
H = O * C.tanh()
Y = torch.matmul(H, W_hq) + b_q
outputs.append(Y)
return outputs, (H, C)
深度循环神经网络
本章到目前为止介绍的循环神经网络只有一个单向的隐藏层,在深度学习应用里,我们通常会用到含有多个隐藏层的循环神经网络,
也称作深度循环神经网络。图 11(同图 3)演示了一个有 $L$
个隐藏层的深度循环神经网络,
每个隐藏状态不断传递至当前层的下一时间步和当前时间步的下一层。
在时间步 $t$
,设小批量输入为 $X_t \in \mathbb{R}^{n \times d}$
,第 $l$
个隐藏层($l=1, ..., L$
)的隐藏状态为 $H_t^{(l)} \in \mathbb{R}^{n \times h}$
,
输出变量为 $O_t \in \mathbb{R}^{n \times q}$
,且隐藏层的激活函数为 $\phi$
。
则第 1 个隐藏层的隐藏状态的计算和之前一样:
$$ H_t^{(1)} = \phi (X_t W_{xh}^{(1)} + H_{t-1}^{(1)}{W_{hh}^{(1)}} + \vec{b}_h^{(1)}). $$
对于后续隐藏层,
$$ H_t^{(l)} = \phi (H_t^{(l-1)} W_{xh}^{(l)} + H_{t-1}^{(l)}{W_{hh}^{(l)}} + \vec{b}_h^{(l)}). $$
注意 $W_{xh}^{(1)} \in \mathbb{R}^{d \times h}$
,$\forall l = 2, ..., L: W_{xh}^{(l)} \in \mathbb{R}^{h \times h}$
。
输出层变量仅依赖于最后一层隐藏状态:
$$ O_t = H_t^{(L)} W_{hq} + \vec{b}_q. $$
模型最后会返回输出层变量和每一个隐藏层的隐藏变量。
双向循环神经网络
之前介绍的循环神经网络模型都是假设当前时间步是由前面的较早时间步的序列决定的,因此它们都将信息通过隐藏状态从前往后传递。 有时候,当前时间步也可能由后面时间步决定。例如,当我们写下一个句子时,可能会根据句子后面的词来修改句子前面的用词。 双向循环神经网络通过增加从后往前传递信息的隐藏层来更灵活地处理这类信息。图 12 演示了一个含单隐藏层的双向循环神经网络的架构。
给定时间步 $t$
的小批量输入 $X_t \in \mathbb{R}^{n \times d}$
和隐藏层激活函数 $\phi$
。设时间步正向隐藏状态为 $\overrightarrow{H}_t \in \mathbb{R}^{n \times h}$
,
反向隐藏状态为 $\overleftarrow{H}_t \in \mathbb{R}^{n \times h}$
,分别按如下方式计算:
$$ \overrightarrow{H}_t = \phi \Big(X_t W_{xh}^{(f)} + \overrightarrow{H}_{t-1} W_{hh}^{(f)} + \vec{b}_h^{(f)}\Big) $$
$$ \overleftarrow{H}_t = \phi \Big(X_t W_{xh}^{(b)} + \overleftarrow{H}_{t+1} W_{hh}^{(b)} + \vec{b}_h^{(b)}\Big). $$
两个方向上的隐藏单元个数可以不同。
连接两个方向的隐藏状态 $\overrightarrow{H}_t$
和 $\overleftarrow{H}_t$
得到 $H_t \in \mathbb{R}^{n \times 2h}$
,传递给输出层:
$$ O_t = H_t W_{hq} + \vec{b}_q \in \mathbb{R}^{n \times q}. $$
最后
本文所使用的图片来自《动手学深度学习》(PyTorch 版)章节 6。
本文提及的各类 RNN 的 PyTorch 实现见本链接。
转载申请
本作品采用 知识共享署名 4.0 国际许可协议 进行许可,转载时请注明原文链接。您必须给出适当的署名,并标明是否对本文作了修改。