6.2 循环神经网络

不含隐藏状态的神经网络

考虑一个含单隐藏层的多层感知机。给定样本数为\(n\)、输入个数(特征数或特征向量维度)为\(d\)的小批量数据样本\(\boldsymbol{X} \in \mathbb{R}^{n \times d}\)。设隐藏层的激活函数为\(\phi\),那么隐藏层的输出\(\boldsymbol{H} \in \mathbb{R}^{n \times h}\)计算为

\[ \boldsymbol{H} = \phi(\boldsymbol{X} \boldsymbol{W}_{xh} + \boldsymbol{b}_h), \]

其中隐藏层权重参数\(\boldsymbol{W}_{xh} \in \mathbb{R}^{d \times h}\),隐藏层偏差参数 \(\boldsymbol{b}_h \in \mathbb{R}^{1 \times h}\)\(h\)为隐藏单元个数。把隐藏变量\(\boldsymbol{H}\)作为输出层的输入,且设输出个数为\(q\)(如分类问题中的类别数),输出层的输出为

\[ \boldsymbol{O} = \boldsymbol{H} \boldsymbol{W}_{hq} + \boldsymbol{b}_q, \]

其中输出变量\(\boldsymbol{O} \in \mathbb{R}^{n \times q}\), 输出层权重参数\(\boldsymbol{W}_{hq} \in \mathbb{R}^{h \times q}\), 输出层偏差参数\(\boldsymbol{b}_q \in \mathbb{R}^{1 \times q}\)。如果是分类问题,可以使用\(\text{softmax}(\boldsymbol{O})\)来计算输出类别的概率分布。

含隐藏状态的循环神经网络

现在考虑输入数据存在时间相关性的情况。假设\(\boldsymbol{X}_t \in \mathbb{R}^{n \times d}\)是序列中时间步\(t\)的小批量输入,\(\boldsymbol{H}_t \in \mathbb{R}^{n \times h}\)是该时间步的隐藏变量。与多层感知机不同的是,建立一个隐藏变量\(\boldsymbol{H}_{t-1}\),用来保存上一时间步,并引入一个新的权重参数\(\boldsymbol{W}_{hh} \in \mathbb{R}^{h \times h}\),即隐藏变量的权重。具体来说,时间步\(t\)的隐藏变量的计算由当前时间步的输入和上一时间步的隐藏变量共同决定:

\[ \boldsymbol{H}_t = \phi(\boldsymbol{X}_t \boldsymbol{W}_{xh} + \boldsymbol{H}_{t-1} \boldsymbol{W}_{hh} + \boldsymbol{b}_h). \]

与多层感知机相比,在这里添加了\(\boldsymbol{H}_{t-1} \boldsymbol{W}_{hh}\)一项。由上式中相邻时间步的隐藏变量\(\boldsymbol{H}_t\)\(\boldsymbol{H}_{t-1}\)之间的关系可知,这里的隐藏变量能够捕捉截至当前时间步的序列的历史信息,就像是神经网络当前时间步的状态或记忆一样。因此,该隐藏变量也称为隐藏状态。由于隐藏状态在当前时间步的定义使用了上一时间步的隐藏状态,上式的计算是循环的。使用循环计算的网络即循环神经网络(recurrent neural network)。

在时间步\(t\),输出层的输出和多层感知机中的计算类似:

\[ \boldsymbol{O}_t = \boldsymbol{H}_t \boldsymbol{W}_{hq} + \boldsymbol{b}_q. \]

循环神经网络的参数包括隐藏层的权重\(\boldsymbol{W}_{xh} \in \mathbb{R}^{d \times h}\)\(\boldsymbol{W}_{hh} \in \mathbb{R}^{h \times h}\)和偏差 \(\boldsymbol{b}_h \in \mathbb{R}^{1 \times h}\),以及输出层的权重\(\boldsymbol{W}_{hq} \in \mathbb{R}^{h \times q}\)和偏差\(\boldsymbol{b}_q \in \mathbb{R}^{1 \times q}\)。值得一提的是,即便在不同时间步,循环神经网络也始终使用这些模型参数。因此,循环神经网络模型参数的数量不随时间步的增加而增长。

下图展示了循环神经网络在3个相邻时间步的计算逻辑。在时间步\(t\),隐藏状态的计算可以看成是将输入\(\boldsymbol{X}_t\)和前一时间步隐藏状态\(\boldsymbol{H}_{t-1}\)连结后输入一个激活函数为\(\phi\)的全连接层。该全连接层的输出就是当前时间步的隐藏状态\(\boldsymbol{H}_t\),且模型参数为\(\boldsymbol{W}_{xh}\)\(\boldsymbol{W}_{hh}\)的连结,偏差为\(\boldsymbol{b}_h\)。当前时间步\(t\)的隐藏状态\(\boldsymbol{H}_t\)将参与下一个时间步\(t+1\)的隐藏状态\(\boldsymbol{H}_{t+1}\)的计算,并输入到当前时间步的全连接输出层。

隐藏状态中\(\boldsymbol{X}_t \boldsymbol{W}_{xh} + \boldsymbol{H}_{t-1} \boldsymbol{W}_{hh}\)的计算等价于\(\boldsymbol{X}_t\)\(\boldsymbol{H}_{t-1}\)连结后的矩阵乘以\(\boldsymbol{W}_{xh}\)\(\boldsymbol{W}_{hh}\)连结后的矩阵。首先,构造矩阵XW_xhHW_hh,它们的形状分别为(3, 1)、(1, 4)、(3, 4)和(4, 4)。将XW_xhHW_hh分别相乘,再把两个乘法运算的结果相加,得到形状为(3, 4)的矩阵。

1
2
3
4
5
import torch

X, W_xh = torch.randn(3, 1), torch.randn(1, 4)
H, W_hh = torch.randn(3, 4), torch.randn(4, 4)
print(torch.matmul(X, W_xh) + torch.matmul(H, W_hh))

输出:

1
2
3
tensor([[ 5.2633, -3.2288,  0.6037, -1.3321],
[ 9.4012, -6.7830, 1.0630, -0.1809],
[ 7.0355, -2.2361, 0.7469, -3.4667]])

将矩阵XH按列(维度1)连结,连结后的矩阵形状为(3, 5)。可见,连结后矩阵在维度1的长度为矩阵XH在维度1的长度之和(\(1+4\))。然后,将矩阵W_xhW_hh按行(维度0)连结,连结后的矩阵形状为(5, 4)。最后将两个连结后的矩阵相乘,得到与上面代码输出相同的形状为(3, 4)的矩阵。

1
print(torch.matmul(torch.cat((X, H), dim=1), torch.cat((W_xh, W_hh), dim=0)))

输出:

1
2
3
tensor([[ 5.2633, -3.2288,  0.6037, -1.3321],
[ 9.4012, -6.7830, 1.0630, -0.1809],
[ 7.0355, -2.2361, 0.7469, -3.4667]])

应用:基于字符级循环神经网络的语言模型

设小批量中样本数为1,文本序列为“想”“要”“有”“直”“升”“机”。下图演示了如何使用循环神经网络基于当前和过去的字符来预测下一个字符。在训练时,对每个时间步的输出层输出使用softmax运算,然后使用交叉熵损失函数来计算它与标签的误差。在图中,由于隐藏层中隐藏状态的循环计算,时间步3的输出\(\boldsymbol{O}_3\)取决于文本序列“想”“要”“有”。 由于训练数据中该序列的下一个词为“直”,时间步3的损失将取决于该时间步基于序列“想”“要”“有”生成下一个词的概率分布与该时间步的标签“直”。

因为每个输入词是一个字符,因此这个模型被称为字符级循环神经网络(character-level recurrent neural network)。因为不同字符的个数远小于不同词的个数(对于英文尤其如此),所以字符级循环神经网络的计算通常更加简单。