2317 字
12 分钟
循环神经网络 - RNN

前言#

这里的目标是 循环神经网络(RNN)

总结一下目前的学习情况:

不使用任何的深度学习框架,仅使用 Numpy、Math 等数学计算工具

学习内容状态
类似 Pytorch 的 DataLoader已完成
深度神经网络(DNN)- 交通量预测 与 鸢尾花分类已完成
卷积神经网络(CNN)- MNIST分类已完成
Sigmoid、Tanh、Relu、Leaky-Relu已完成
L1Loss、L2Loss、SmoothL1Loss已完成
多分类网络,Softmax + 交叉熵损失已完成
One-Hot 编码已完成
循环神经网络(RNN)已完成
长短期记忆网络(LSTM)未完成
K均值聚类(K-means)
主成分分析(PCA)
Hog 特征提取
SVM 分类器
Region with CNN Feature(R-CNN)
You Only Look Once(YOLO)

目前为止已经完成了 DNN、CNN、RNN 三种基础神经网络的数学推理和算法实现,至于后面的 LSTM、R-CNN 等,都基本是在这三种基础神经网络上,通过不断改善方法、组合多种机制实现的,例如:LSTM 中的门结构、细胞状态,R-CNN 中的候选框、Hog特征提取等。后面的就基本学不到头了,只能有针对性地对这些经典神经网络的中特色部分进行学习。

概述#

循环神经网络(RNN) 是一类以 序列数据 为输入,在序列的演进方向进行递归且所以 循环单元 按链式连接的递归神经网络。1

之前完成的 深度神经网络(DNN)卷积神经网络(CNN),网络的输入节点或输出节点的数量是固定的,因此无法处理 序列较长且长度不一 的数据,例如:一段语音、一段文字等。对这些问题就是 循环神经网络 所擅长的。23

网络结构#

最主流的 RNN 模型可以参考下图:

整个 RNN 模型可以看作是多个循环结构的组合,这里可以从 单个模型组合模型 两个角度来解析模型。

对于 单个模型 而言,它需要包含以下内容(tt 为模型的序列索引号):

  • xtx_t 代表在序列索引号为 tt 的模型的输入。
    • xtx_t 是某个固定长度的数据序列
    • xtx_txt+1x_{t+1} 之间不需要存在某种连续的关系,例如:时间、序列等
  • ztz_t 代表在序列索引号为 tt 的模型的净输入。
    • ztz_txtx_tht1h_{t-1} 共同决定
  • hth_t 代表在序列索引号为 tt 的模型的激活后输出(隐藏状态)。
  • oto_t 代表在序列索引号为 tt 的模型的输出。
    • hth_toto_t 间的关系相当于全连接层
  • EtE_t 代表在序列索引号为 tt 的模型的损失。
  • yty_t 代表在序列索引号为 tt 的模型的真实输出。
  • UUWWVV 分别是计算 ztz_txtx_tht1h_{t-1} 的权重,以及计算 oto_thth_t 的权重。
    • 它们在 RNN 的同一层模型中是共享的,体现了RNN的模型的“循环反馈”的思想

对于 组合模型 而言:

  • xtx_t 作为单个模型的输入是固定长度的,组合模型的输入是不定长的,将不定长输入分割为多个 xtx_t 长度的输入,然后依次输入单个模型中,组合模型的头尾是相连接的。
  • 隐藏状态 hth_t 并不像上图,只有1层,可能有多层,但循环只发生在每层的隐藏状态之间。

前向传播#

对于任意一个序列索引号 tt,隐藏状态 hth_txtx_tht1h_{t-1} 得到:

zt=Uxt+Wht1+bz_t = U \cdot x_t + W \cdot h_{t-1} + b

xtx_tht1h_{t-1} 分别构成两个全连接,并将结果进行叠加。

其中 b 是线性关系的偏置,同时由于 RNN 是首尾相连进行循环,当 t=0t=0 时,h1h_{-1} 是指最后一个模型的隐藏状态

序列索引号为 tt 的模型的激活后输出 hth_t 的表达式:

ht=σ(ht)h_t = \sigma(h_t)

其中 σ\sigma 是模型的激活函数,一般为 Tanh

序列索引号为 tt 的模型的输出 oto_t 的表达式:

ot=Vht+co_t = V \cdot h_t + c

最总序列索引号为 tt 的模型的预测输出为:

yt^=σ(ot)\hat{y_t} = \sigma(o_t)

一般矩阵乘积 = ABA \cdot B 哈达马积 = ABA * B 克罗内克积 = ABA \otimes B

反向传播#

对于 RNN,我们在所有的输出节点都有损失,所有损失的表达式为:

Eτ=t=1τLoss(yt^,yt)E_\tau = \sum^\tau_{t=1} Loss(\hat{y_t}, y_t)

EτE_\tau 是序列索引号为 τ\tau 模型的输出损失(梯度) 输出损失的计算需要按数据输入的顺序进行

对于 VVcc 梯度计算的表达式:

EV=t=1τEtot×otV=t=1τEt(ht)TEc=t=1τEt\begin{aligned} \frac{\partial E}{\partial V} & = \sum^\tau_{t=1} \frac{\partial E_t}{\partial o_t} \times \frac{\partial o_t}{\partial V} = \sum^\tau_{t=1} E_t \cdot (h_t)^T \\\\ \frac{\partial E}{\partial c} & = \sum^\tau_{t=1} E_t \end{aligned}

由于输出层 oto_t 只是一层全连接层,只是由于输入和隐藏状态的实现,所有输出层的输出损失需要进行累加,其表达式也较为简单,比较复杂的是隐藏层梯度的计算。

由于 ztz_t 是由 xtx^tht1h^{t-1} 决定的,所以隐藏状态的梯度 δht\delta h_t 需要从 t+1t+1tt 方向进行推导(前向传播的倒序):

δht=Etht+Et+1ht=Etot×otht+Et+1ht+1×ht+1zt+1×zt+1ht=VTEt+WTdiag(σ(ht+1))δht+1 \begin{aligned} \delta h_t & = \frac{\partial E_t}{\partial h_t} + \frac{\partial E_{t+1}}{\partial h_t} \\\\ & = \frac{\partial E_t}{\partial o_t} \times \frac{\partial o_t}{\partial h_t} + \frac{\partial E_{t+1}}{\partial h_{t+1}} \times \frac{\partial h_{t+1}}{\partial z_{t+1}} \times \frac{\partial z_{t+1}}{\partial h_t} \\\\ & = V^T \cdot E_t + W^T \cdot diag(\sigma{'}(h_{t+1})) \cdot \delta h_{t+1} \end{aligned}

这里在代码实现时有一个坑,生成对角矩阵:np.diag(aray) 4 array是一个1维数组时,结果形成一个以一维数组为对角线元素的矩阵 array是一个n维矩阵时,结果输出矩阵的对角线元素 由于 hth_t 是一个 N×1N\times1 的矩阵,所以无法生成对应的对角矩阵,需要先通过 reshape 将 hth_t 转换成 1×N1\times N

这里还有一个问题就是,隐藏状态的序列索引号为 τ\tau 的输出损失(即最后时刻的输出损失),它后面已经没有其他序列索引了,所以只需要按当前时刻计算:

δhτ=Ehτ=Eτoτ×oτhτ=VTEτ\delta h_\tau = \frac{\partial E}{\partial h_\tau} = \frac{\partial E_\tau}{\partial o_\tau} \times \frac{\partial o_\tau}{\partial h_\tau} = V^T \cdot E_\tau

关于为什么会使用到对角矩阵,由于 ht+1h_{t+1}zt+1z_{t+1} 都是 N×1N \times 1 的向量,向量对向量求导会形成 雅可比矩阵

$$ \frac{\partial h_{t+1}}{\partial z_{t+1}} = \begin{pmatrix} & \frac{\partial h^{t+1}_1}{\partial z^{t+1}_1} & \frac{\partial h^{t+1}_1}{\partial z^{t+1}_2} & \cdots & \frac{\partial h^{t+1}_1}{\partial z^{t+1}_N} \\\\ & \frac{\partial h^{t+1}_2}{\partial z^{t+1}_1} & \frac{\partial h^{t+1}_2}{\partial z^{t+1}_2} & \cdots & \frac{\partial h^{t+1}_2}{\partial z^{t+1}_N} \\\\ & \vdots & \vdots & \ddots & \vdots \\\\ & \frac{\partial h^{t+1}_N}{\partial z^{t+1}_1} & \frac{\partial h^{t+1}_N}{\partial z^{t+1}_2} & \cdots & \frac{\partial h^{t+1}_N}{\partial z^{t+1}_N} \end{pmatrix} $$

但因为激活函数 hnt+1=σ(znt+1)h^{t+1}_n = \sigma(z^{t+1}_n) ,所以上述的雅可比矩阵仅有主对角线是有效的:

$$ \frac{\partial h_{t+1}}{\partial z_{t+1}} = \begin{pmatrix} & \frac{\partial h^{t+1}_1}{\partial z^{t+1}_1} & 0 & \cdots & 0 \\ & 0 & \frac{\partial h^{t+1}_2}{\partial z^{t+1}_2} & \cdots & 0 \\ & \vdots & \vdots & \ddots & \vdots \\ & 0 & 0 & \cdots & \frac{\partial h^{t+1}_N}{\partial z^{t+1}_N} \end{pmatrix} = diag(\sigma{'}(h_{t+1})) $$

在获取隐藏状态 hth_t 的梯度 δht\delta h_t 后,按照激活函数的求导方法,求净输入的梯度(导数) δzt\delta z_t

δzt=δhtσ(ht)\delta z_t = \delta h_t * \sigma{'}(h_t)

有了净输入的梯度 δzt\delta z_t 后,就能得到 WWUUbb 梯度计算的表达式:

EW=t=1τδht(ht1)TEb=t=1τδhtEU=t=1τδht(xt)T\begin{aligned} \frac{\partial E}{\partial W} &= \sum^\tau_{t=1} \delta h_t \cdot (h_{t-1})^T \\\\ \frac{\partial E}{\partial b} &= \sum^\tau_{t=1} \delta h_t \\\\ \frac{\partial E}{\partial U} &= \sum^\tau_{t=1} \delta h_t \cdot (x_t)^T \end{aligned}

训练结果#

没有做一个复杂的网络,借用了一下之前深度神经网络-DNN里的数据,构建了一个以 人数机动车数公路面积 为输入,公路客运量 为输出的网络。

网络包含有3个模型,模型的输入分别是 人数机动车数公路面积,将第3个模型的输出作为网络的输出,即 公路客运量 的预测值。

如果需要更加符合网络设计目的,那么理想情况是将 某一年的人数机动车数公路面积 作为单个模型的输入,然后将连续数年作为不同模型的输入,每一层的输出就是该年的预测值。

RNN 的问题#

计算慢#

RNN 本质上还是 DNN 的改进,由于无法处理 序列数据输入不定长输入 等问题,所以将一个庞大的 DNN 网络拆分未多个小的 DNN 网络,然后通过创建 隐藏状态 实现网络间的关联。

为了保证单个网络的表达能力,所以单个网络的规模不会过小,那么组合后的网络规模相比全连接会更大,计算会更慢,但也只是个可大可小的问题。

梯度消失#

既然 RNN 中使用了 Tanh 指数函数,不可避免的RNN就需要面对 梯度消失 的问题。

RNN 计算网络损失的过程如下,是通过累加进行计算的:

Eτ=t=1τLoss(yt^,yt)E_\tau = \sum^\tau_{t=1} Loss(\hat{y_t}, y_t)

举个例子:在计算 τ\tau 时刻的梯度时,如果 τ3\tau-3 时刻的权重过小,那么在反向传播时,网络本质上已经忽略了 τ3\tau-3 时刻以及该时刻之前的状态,这些被忽略的时刻,没有对 τ\tau 时刻的梯度计算产生贡献。

Footnotes#

  1. 百度百科-循环神经网络

  2. 循环神经网络(RNN)模型与前向反向传播算法

  3. RECURRENT NEURAL NETWORK REGULARIZATION

  4. RNN的反向传播推导与numpy实现

循环神经网络 - RNN
https://fuwari.vercel.app/posts/人工智能/神经网络/循环神经网络-rnn/
作者
Asuwee
发布于
2022-07-05
许可协议
CC BY-NC-SA 4.0