循环神经网络
循环神经网络
RNN神经网络
序列模型
通常在自然语言,音频,视频以及其他序列数据的模型
类型
语音识别:输入一段文字输出对应的文字

情感分类:输入一段表示用户情感的文字,输出情感类别或者评分

机器翻译:两种语言互译

架构类型
一对一:一个输入(单一标签)对应一个输出(单一标签)

一对多:一个输入对应多个输出;多用于图片的对象识别,比如输入一张图片,输出一段文本序列

多对一:多个输入对应一个输出,多用于文本分类或视频分类,即输入一段文本或视频片段,输出类别

多对多(1):常用于机器翻译

多对多(2):广泛用于序列标注

基本结构
与全连接神经网络和卷积神经网络不同的是:
- RNN神经网络输入特征是有时序的,而前面我们所学习的神经网络输入特征都是同时输入的

$$
右图是左图的展开形式\
A为计算单元,类似于隐藏层\
X_t表示t时刻的输入特征向量,X_t=(X_{t1},x_{t2},\dots,X_{t,k})\
h_t表示X_t对应的隐藏层输出\
$$
怎么理解图中展示的过程?
X0经过计算单元得到隐藏层输出h0,h0与X1一起作为输入,经计算单元得到h1,如此循环;最终的输出ht会包含前面所有输出h0-(t-1)的有用信息
是一个串行而不是并行的过程

$$
h(t)=activate(X_tw+h_{t-1}v)\
v:h_{t-1}对应的权重\
w:X_t对应的权重
$$
正因为循环神经网络的输入包含前面单元的输出信息,所以它能够学习到时间顺序信息
与全连接神经网络的区别

$$
X_t:t时刻的输入特征向量\
h_t:t时刻时隐藏层输出向量\
O_t:最终的输出层输出向量\
U,V,W:权重参数
$$
$$
RNN:h_t=activate(X_tU+h_{t-1}W)\
全连接神经网络:h_t = activate(X_tU)
$$
对于全连接神经网络结构应该这样去展开:

对于RNN神经网络结构应该这样去展开:

全连接神经网络并不把前一时刻的输出当作隐藏层的输入,因此它难以学习到时间序列信息,换句话说,就是全连接神经网络不具有记忆能力
数学模型以及权重共享

$$
X_t:t时刻的输入特征向量\
h_t:t时刻时隐藏层及其输出向量\
O_t:最终的输出层及其输出向量\
f():隐藏层激活函数\
g():输出层激活函数\
U,V,W:权重参数\
h_t=f(U\cdot X_t+W\cdot h_{t-1})\
O_t = g(V\cdot h_t )\
$$
为什么所有的权重参数都不带有时间下标t呢?
和卷积神经网络一样,RNN神经网络也使用了权重共享
为什么
如果每个时刻都训练一套权重,那么权重就太多了
- 权重多,模型复杂,就很容易过拟合
- 权重多也会带来计算量大的问题
词的表示
通常对于整个序列,给定一个开始标志s和结束标志e
比如对于句子:我昨天上学迟到了
处理成: s 我 昨天 上学 迟到 了 e
输入到网络中就是一个个分词结果

而为了能够让整个网络能够理解我们的输入(各种语言),我们需要将词用向量表示
建立一个包含所有N个序列词的词典包含(开始和结束的两个特殊标志词,以及没有出现过的词等),每个词都有一个唯一索引
那么对于每个词,就可以用一个长度为N的向量,使用one-hot编码进行表示

我们就得到了一个高维(维度为N),稀疏(一个1,N-1个0)的向量
输出表示
使用SoftMax;每个时刻的输出是所有词的概率组成的向量
向量化运算
假设输入序列长度为m,神经元个数为n(也可以说是输出维度 )
$$
h_t = Tanh(UX_t+Wh_{t-1})\
O_t = SoftMax(Vh_t)\
$$
对于1式
$$
h_t = Tanh(\begin{bmatrix}
h_1^t\
h_2^t\
\vdots\
h_n^t\
\end{bmatrix}{n\times 1}=\begin{bmatrix}
u{11},u_{12},\cdots,u_{1m}\
u_{21},u_{22},\cdots,u_{2m}\
\vdots\
u_{n1},u_{n2},\cdots,u_{nm}
\end{bmatrix}{n \times m}\begin{bmatrix}
x_1^t\
x_2^t\
\vdots\
x_m^t\
\end{bmatrix}{m\times 1}+\begin{bmatrix}
w_{11},w_{12},\cdots,w_{1n}\
w_{21},w_{22},\cdots,w_{2n}\
\vdots\
w_{n1},w_{n2},\cdots,w_{nn}
\end{bmatrix}{n \times n}\begin{bmatrix}
h_1^{t-1}\
h_2^{t-1}\
\vdots\
h_n^{t-1}\
\end{bmatrix}{n\times 1}
)\
(n,1) = (n,m)\cdot(m,1)+(n,n)\cdot(n,1)
$$
$$
可以简化为[U,W][\frac{X_t}{h_{t-1}}]=(n, n+m)(n+m,1) = (n,1)
$$
对于2式
$$
O_t = SoftMax(\begin{bmatrix}
v_{11},v_{12},\cdots,v_{1n}\
v_{21},v_{22},\cdots,v_{2n}\
\vdots\
v_{m1},u_{m2},\cdots,v_{mn}
\end{bmatrix}{m \times n}\begin{bmatrix}
h_1^t\
h_2^t\
\vdots\
h_n^t\
\end{bmatrix}{n\times 1})
$$
Ot是所有m个词的概率向量
前向传播
RNN的前向传播过程事实上就是前面提到的隐藏层计算公式和输出层计算公式
$$
d:输入维度\
h:隐藏层神经元数
$$
$$
X_t \in R^{1\times d}\
U \in R^{h\times d}\
W \in R^{h \times h}\
下面是向量化形式的公式\
h_t=f(U\cdot X_t+W\cdot h_{t-1})\
O_t = g(V\cdot h_t )\
$$
用一个案例来演示

如图所示是RNN中一个时刻t下的单元结构,输入数据含有三个时间步,每个时间步特征向量Xt含有两个元素X1,X2,隐藏层中有2个神经元h1,h2,输出层也有两个神经元O1,O2,最终输出向量含有两个元素y1,y2
$$
X_t = ((1,1),(1,1),(2,2))\
为了方便,我们设定W=V=U=((1,1),(1,1)),所有的激活函数都是不带偏置的线性函数
$$
$$
当t=1时\
X_1=(1,1)\
对于h_{t1},h_{t2}来说,没有前一个隐藏层的输出值作为输入,因此我们设置h_0=(0,0)\
h_{t1} = f(U_1X_1+W_1h_{01}) = 1\times1+1\times1+1\times0+1\times0=2\
h_{t2} = f(U_2X_1+W_2h_{02}) = 1\times1+1\times1+1\times0+1\times0=2\
h_t=(2,2)\
O_{t1} = g(V_1h_t) = 2\times1+2\times1=4\
O_{t2} = g(V_2h_t) = 2\times1+2\times1=4\
O_{t} = (2,2)\
$$
$$
当t=2时\
h_1=(2,2),X_2=(1,1)\
h_{21}=f(U_1X_2+W_1h_{11}) = 1\times1+1\times1+1\times2+1\times2=6\
h_{22}=f(U_2X_2+W_1h_{12}) = 1\times1+1\times1+1\times2+1\times2=6\
h_2 = (6,6)\
O_{21}=g(V_1h_2) = 6 \times 1+6\times 1 = 12\
O_{22}=g(V_2h_2)= 6 \times 1+6\times 1 = 12 \
O_2 = (12,12)
$$
$$
当t=3时\
h_2=(6,6),X_3=(2,2)\
h_{31}=f(U_1X_3+W_1h_{21}) = 1\times2+1\times2+1\times6+1\times6=16\
h_{32}=f(U_2X_3+W_1h_{22}) = 1\times2+1\times2+1\times6+1\times6=16\
h_3=(16,16)\
O_{31}=g(V_1h_{31}) = 1 \times 16+1\times 16 = 32\
O_{32}= g(V_2h_{32})=1 \times 16+1\times 16 = 32 \
O_3 = (32,32)
$$
前面所有时间步的信息对后面时间步会有影响,通过反向传播训练W,U,V来控制前面时间步信息的占比
激活函数
RNN通常使用Tanh(双曲正切函数)作为激活函数
$$
Tanh:
y = \frac{e^z-e^{-z}}{e^z+e^{-z}} \
y’ = 1 - y^2
$$
为什么全连接神经网络,卷积神经网络喜欢使用ReLU作为激活函数,而RNN使用Tanh(?)
对于全连接神经网络和CNN:ReLU的导数值只有0或1,Tanh或sigmoid在两级处的导数值都趋近于0,不利于梯度下降
对于RNN:
- RNN与CNN最大的不同就在于会将前一个时刻的隐藏层输出作为此时刻隐藏层的输入;而ReLU的值域在[0,+∞),会导致输出值太大,传递过程中难以控制,出现爆炸;Tanh的值域为[-1,1],在传输隐藏状态ht时,有助于控制其大小
- Tanh关于y轴对称,有助于信息在多个时间步之间稳定传递
交叉熵损失
总损失定义:一整个序列(一个句子)作为训练实例,总误差就是各个时刻的误差之和
$$
E_t(y_t,\hat{y_t})=-y_tlog(\hat{y_t})\
E(y,\hat{y})=\sum_{t}E_t(y_t,\hat{y_t})=-\sum_{t}y_tlog(\hat{y_t})\
y_t:t时刻的正确的词的one-hot编码值
\\hat{y_t}:预测的词概率
$$
时间反向传播BPTT
RNN神经网络中反向传播算法利用的是时间反向传播算法BPTT;需要求解所有时间步的梯度之后,利用多变量链式求导法则求解梯度
由于RNN的权重共享以及分时间步计算,总的梯度是各个时间步梯度的加和
- 我们的目标是计算损失关于参数U,V,W,偏置bx,by的梯度
前向传播公式:
$$
h_t = Tanh(UX_t+Wh_{t-1}+b_x)\
O_t = SoftMax(Vh_t+b_y)
$$
步骤

对于最后一个ht:计算交叉熵对于ht的梯度,记忆交叉熵对ht,V,by的梯度
$$
\frac{\partial J}{\partial h^t} = dh^t \
J:交叉熵损失
$$对于前面的ht:
第一步:求出当前层交叉熵损失对于当前隐藏状态输出值ht的梯度+前一层相对于ht的梯度
$$
\frac{\partial J}{\partial h^{t-1}} =\frac{\partial J}{\partial h^t}\frac{\partial h^t}{\partial x}\frac{\partial x}{\partial h^{t-1}}=dh^{t}(1-Tanh(UX^t+Wh^{t-1}+b_x))W^T
$$
对于前一时刻的cell来说:
$$
\frac{\partial J}{\partial h^t} = dh^t+dh^{t+1}(1-h^{(t+1)2})W^T
$$
为什么是这个形式(?)- 在 RNN 的反向传播中,由于前向传播中 h^(t−1) 会影响 ht,所以损失函数 J 对 ht 的梯度会通过链式法则反向传播,影响 h^(t−1) 的梯度
第二步:计算tanh激活函数的梯度
$$
\frac{\partial J}{\partial x} = \frac{\partial J}{\partial h^t}\frac{\partial h^t}{\partial x} \
这里的x就是Tanh(x)中的x\
h^t = Tanh(x),\frac{\partial h^t}{\partial x} = 1-Tanh(x)^2=1-(h^t)^2 \
\frac{\partial J}{\partial x} = \frac{\partial J}{\partial h^t}\frac{\partial h^t}{\partial x}=dh^t(1-(h^t)^2) \
$$计算UXt+Wht-1+bx的对于不同参数的梯度
$$
\frac{\partial J}{\partial U} = \frac{\partial J}{\partial h^t}\frac{\partial h^t}{\partial x}\frac{\partial x}{\partial U}=dh^t(1-Tanh(UX^t+Wh_{t-1}+b_x)^2)\frac{\partial UX^t}{\partial U}=dh^t(1-Tanh(UX^t+Wh_{t-1}+b_x)^2)X_t^T=dh^t(1-h^{t2})X^{tT}\
\frac{\partial J}{\partial W}=\frac{\partial J}{\partial h^t}\frac{\partial h^t}{\partial x}\frac{\partial x}{\partial W}=dh^t(1-Tanh(UX_t+Wh^{t-1}+b_x)^2)\frac{\partial Wh^{t-1}}{\partial W}=dh^t(1-Tanh(UX_t+Wh^{t-1}+b_x)^2)h^{(t-1)T}=dh^t(1-h^{t2})h^{(t-1)T}\
\frac{\partial J}{\partial b_x}=\frac{\partial J}{\partial h^t}\frac{\partial h^t}{\partial x}\frac{\partial x}{\partial b_x}=\sum dh^t(1-Tanh(UX_t+Wh^{t-1}+b_x)^2)\
$$
为什么bx的梯度是显式求和的(?)- bx是向量而不是矩阵,U,V,W矩阵运算中已经蕴含了求和的运算
梯度消失和梯度爆炸
$$
以损失函数对W的梯度为例,如果将整个式子展开:\
\frac{\partial J}{\partial W}=\frac{\partial J}{\partial O^t}\frac{\partial O^t}{\partial h^t}\frac{\partial h^t}{\partial h^{t-1}}\frac{\partial h^{t-1}}{\partial h^{t-2}}\cdots\frac{\partial h^1}{\partial W}=\frac{\partial J}{\partial O^t}VW^{t-1}h^0\
出现了W^{t-1}这样的高次项
$$
由于矩阵的高次幂运算:
- 如果矩阵中值很小,那么相乘t-1次后,梯度将趋近于0,导致梯度消失
- 如果矩阵中值大于1,相乘t-1次后,梯度将变得非常非常大(指数增长),造成梯度爆炸
代码实现
单个cell的前向传播
$$
h_t = Tanh(UX_t+Wh_{t-1}+b_x)\
O_t = SoftMax(Vh_t+b_y)
$$
1 | def softMax(z): |
测试代码
1 | if __name__ == '__main__': |
输出
1 | h_next=[0.99999661 0.9999691 0.99999336 0.99999988 0.99991662] |
所有cell的前向传播
要对单个cell前向传播的函数进行一点点修改
1 | def single_cell_forward(X_t, h_prev, params): |
1 | def all_cell_forward(X, h_0, params): |
测试代码
1 | if __name__ == '__main__': |
输出
1 |
|
单个cell的反向传播

由图中确定的需要计算的梯度变量
- dh_next:当前cell的损失对输出h^t的导数
- dtanh:当前cell的损失对激活函数tanh(x)的导数
- dx_t:当前cell的损失对输入x_t的导数
- dU:表示当前cell的损失对U的导数
- dh_prev:当前cell的损失对上一个cell的隐藏状态输出的梯度
- dW:当前cell的损失对W的导数
- dbx:当前cell的损失对bx的导数
1 | def single_cell_bp(dh_next, cache): |
所有cell的反向传播
- 最后一个cell和其他cell,ht的梯度的组成不一样
- 不同时刻对于参数U,V,W,b的梯度需要相加
1 | def rnn_backpagation(dh, caches): |
测试代码
1 | if __name__ == '__main__': |
输出结果
1 | {'dU': array([[6.49540232e-06, 3.72579122e-06, 3.23351186e-06], |
GRU(门控循环单元)
什么是GRU

仍然是两个输入:
- t时刻特征xt
- 上一时刻隐藏状态输出h_t-1
2个输出:
- 当前时刻隐藏状态输出ht
- 输出层预测输出Ot
但是内部结构发生了变化,新增了两个门,重置门(Reset gate)与更新门(Update gate)
重置门决定了如何将新的输入信息与前面的记忆相结合
$$
r_t =\sigma(W_t\cdot[h_{t-1},x_t])\
\sigma:sigmoid(x)
$$更新门定义了前面记忆保存到当前时间步的量
$$
z_t = \sigma(W_t\cdot[h_{t-1},x_t])
$$节点状态
$$
\tilde{h_t} = Tanh(W\cdot[r_t*h_{t-1},x_t])\
将重置门设为1,更新门设为0:\
\tilde{h_t}= Tanh(W\cdot[h_{t-1},x_t])\
等于标准RNN的h_t
$$隐藏状态输出
$$
h_t = (1-z_t)h_{t-1}+z_t\tilde{h_t}
$$输出
$$
y_t = softMax(W_oh_t)
$$
直观理解

GRU会记住cat这个位置是1,直到was的位置,选择was而不是were
本质解决问题
为了解决短期记忆问题,每个能够自适应捕捉不同尺度的依赖关系
解决梯度消失的问题,在隐层输出的地方ht,ht-1的关系用加法而不是RNN中乘法+激活函数
$$
使用:h_t = (1-z_t)h_{t-1}+z_t\tilde{h_t}\
而不是h_t = tanh(W\cdot[h_{t-1},X_t]) \
避免了出现梯度消失和梯度爆炸
$$
LSTM(长短记忆网络)

$$
f^t = \sigma(U^fx^t+W^fh^{t-1}+b^f)(遗忘门)\
i^t =\sigma(U^ix^t+W^ih^{t-1}+b^i)(输入门)\
\tilde{c}^t = tanh(U^cx^t+W^ch^{t-1}+b^c)\
c^t = f^tc^{t-1}+i^t\tilde{c}^t\
o^t = \sigma(U^ox^t+W^oh^{t-1}+b^o)(输出门)\
h^t = o^t*tanh(c^t)\
\
$$
- ht为该cell单元的输出
- ct为隐藏状态
- 三个门:遗忘门f,输入门i,输出门o
- 遗忘门(forget gate):决定有多少旧信息被保留。
- 输入门/更新门(input gate):决定有多少新信息被写入记忆单元。
- 输出门(output gate):决定有多少记忆单元的信息被输出
作用
便于记忆更长距离的时间状态
RNN案例
前置知识
set(text):将文本转换为一个集合,去除重复字符
eg:
1 | str = "aaaa" |
list():转换为列表
1 | str = "aaaa" |
enumerate(text):参数转换为字典,索引+元素的形式
1 | str = "Hello" |
1 | i=0,c=H |
np.eye():将普通向量进行one-hot编码
1 | x = np.array([1, 2, 3, 4]) |
1 | [[0. 1. 0. 0. 0.] |
用法:可以构建字符的唯一整数索引,同时也可以构造整数索引映射回原始字符
1 | text = "Heello World" |
1 | char_to_idx={' ': 0, 'H': 1, 'W': 2, 'd': 3, 'e': 4, 'l': 5, 'o': 6, 'r': 7} |
torch.tensor.unsqueeze():指定张量增加的维度
1 | import torch |
1 | list_tensor.shape=torch.Size([3, 2]) |
torch.tensor.squeeze():去掉张量中长度为1的维度
1 | import torch |
1 | list_tensor.shape=torch.Size([3, 2]) |
预测文本输入
依赖
1 | from shlex import join |
文本初步处理
1 | # 文本 |
1 | char_to_idx = {' ': 0, '!': 1, ':': 2, 'H': 3, 'W': 4, 'd': 5, 'e': 6, 'l': 7, 'o': 8, 'r': 9} |
处理输入数据与真实目标
1 | # 输入与目标 |
RNN模型
1 | class RNN(nn.Module): |
训练模型
1 | # 参数 |
验证代码
1 | hidden = None |
预测结果
1 | Epoch [100/100], Loss: 0.0041 |





