RNN简单介绍
计算机视觉中,识别图像时每张图片是孤立的,前一张图片识别的结果并不会对后一张图片识别的结果有影响。但现实生活中,许多数据带有明显的顺序,如NLP领域中,顺序是语言的基本特征,如“我吃苹果”与“苹果吃我”就是两个完全不同的意义,也可以从语言结构中得到信息,比如主语“我”后面接一个动词“吃”,“吃”后面往往接一个名词,这种隐藏在语言当中的序列关系如何提取与表示呢,人们找到了RNN(Recurrent Neural Network),一个高度重视序列信息的网络。
RNN基本思想
RNN的基础结构仍然是神经网络,只不过多了个“小盒子”,用来记录数据输入时的状态,如输入的数据为多个单词时,在每一个单词输入到输出的过程中会考虑前一个单词输入的状态,当前状态加上前一个的状态再训练输出。随着数据的一次次输入,“小盒子”中保存的信息会不断更新,盒子中的信息称为隐状态。
大致流程如下图:
左边部分表示单个词输入到输出的过程,其中黑色方框就是“小盒子”,表示上一次输入数据的状态,在数据x输入时会加入。RNN输入到隐藏的连接由权重矩阵 U 参数化,隐藏到隐藏的循环连接由权重矩阵 W 参数化以及隐藏到输出的连接由权重矩阵 V 参数化。
我们可以将左图中一次输入输出的过程用一下数学形式表示:
a
(
t
)
=
b
+
W
h
(
t
−
1
)
+
U
x
(
t
)
(1)
a^{(t)}=b+Wh^{(t-1)}+Ux^{(t)}\tag{1}
a(t)=b+Wh(t−1)+Ux(t)(1)
h
(
t
)
=
t
a
n
h
(
a
(
t
)
)
(2)
h^{(t)}=tanh(a^{(t)})\tag{2}
h(t)=tanh(a(t))(2)
o
(
t
)
=
c
+
V
h
(
t
)
(3)
o^{(t)}=c+Vh^{(t)}\tag{3}
o(t)=c+Vh(t)(3)
y
^
(
t
)
=
s
o
f
t
m
a
x
(
o
(
t
)
)
(4)
\hat{y}^{(t)}=softmax(o^{(t)})\tag{4}
y^(t)=softmax(o(t))(4)
其中(1)表示原始数据进入隐藏层时加入隐状态,偏置项为b,权重从W、V给出;(2)表示将结合隐状态的数据激活,activation为tanh,并将激活后的数据作为新的隐状态与下一次输入的数据结合;(3)表示用权重矩阵V对激活后
a
(
t
)
a^{(t)}
a(t)做线性变换,加入偏置项c;(4)为最后的输出,对
o
(
t
)
o^{(t)}
o(t)做softmax,在单词序列中,最后softmax的结果输出的是下一个某某单词出现的概率,这样我们可以根据训练数据确定loss,训练参数。
注意:RNN的参数一旦训练出来就不会变,每次数据的输入输出用的参数是一样的,如W、U、b、c。
举例
输入一词x1的向量维度为3,表示为
(
x
11
x
12
x
13
)
′
(x_{11} x_{12} x_{13})^{'}
(x11 x12 x13)′,隐藏层有4个神经元,设为
(
h
11
h
12
h
13
h
14
)
′
(h_{11} h_{12} h_{13} h_{14})^{'}
(h11 h12 h13 h14)′,输出类别数为2,设为
(
y
11
y
12
)
′
(y_{11} y_{12})^{'}
(y11 y12)′。基础架构表示如下:
由于x1为第一个输入的数据,没有之前数据的隐状态,这里定义一个初始隐转态为
h
0
h_0
h0,为4维的列向量。权重矩阵U为4x3的矩阵,权重矩阵W为4x4的矩阵(因为隐状态是通过隐藏层后得到的,形状由隐含层的维数决定),偏置项b为4维的列向量。
t=1(第一次输入)时:
由以上定义,得到隐含层的输出
h
1
h_1
h1:
h
1
=
t
a
n
h
(
b
+
W
h
0
+
U
x
1
)
h_1=tanh(b+Wh_0+Ux_1)
h1=tanh(b+Wh0+Ux1)
由于输出的类别数为2,权重矩阵的形状为2x4,偏置项c为2维的列向量,得到第一次输入数据后的输出结果为:
y
1
=
s
o
f
t
m
a
x
(
c
+
V
h
1
)
y_1=softmax(c+Vh_1)
y1=softmax(c+Vh1)
softmax后输出的是概率值。
t=2(第二次输入)时,参数是一模一样的:
由以上定义,得到隐含层的输出
h
1
h_1
h1:
h
2
=
t
a
n
h
(
b
+
W
h
1
+
U
x
2
)
h_2=tanh(b+Wh_1+Ux_2)
h2=tanh(b+Wh1+Ux2)
第一次输入数据后的输出结果为:
y
2
=
s
o
f
t
m
a
x
(
c
+
V
h
2
)
y_2=softmax(c+Vh_2)
y2=softmax(c+Vh2)
类似的,可以算出t=3、4、5…
不断数据输入的情况可由下图表示:
隐状态在不断更新,但网络参数始终不会改变。
损失函数可以有以下定义,B为总的词的长度:
L
τ
=
1
∣
B
∣
l
(
y
t
+
τ
,
f
(
x
t
+
τ
)
)
L_\tau=\frac{1}{|B|}l(y_{t+\tau},f(x_{t+\tau}))
Lτ=∣B∣1l(yt+τ,f(xt+τ))
其中的
l
l
l可以为交叉熵或者其他的损失函数,随后可用梯度下降求解参数。
简单RNN的Numpy代码实现
import numpy as np
timesteps = 100 # 输入序列的时间步数,自然语言处理中可以看做词的个数
input_features = 32 # 输入特征空间的维度
out_features = 64 # 输出特征空间的维度(注意对应的是隐藏层的输出)
inputs = np.random.random((timesteps, input_features)) # 随机生成输入数据
state_t = np.zeros((output_features,)) # 初始状态,为全0向量
#创建随机的权重矩阵
U = np.random.random((output_features, input_features)) # 为输入样本的权重矩阵
W = np.random.random((output_features, output_features)) # 为隐状态的权重矩阵
b = np.random.random((output_features, )) # 偏置项
successive_outputs = [] # 保存输出的list
for input_t in inputs: # input_t的形状为(input_features, )的一维张量
output_t = np.tanh(np.dot(U, input_t)+np.dot(W, state_t)+b) # 由输入和当前状态(前一个输出)计算得到当前输出
successive_outputs.append(output_t) # 保存输出
state_t = output_t # 更新隐状态,用于下一次输入
final_output_sequence = np.stack(successive_outputs, axis=0) # 最终输出(隐藏层的输出)的结果是一个形状为(timesteps, output_features)的二维张量
RNN缺陷与改进
RNN虽然是重视序列信息的网络,但只有一定的记忆能力,只能保留短期记忆,如果一段话中的一个词与后面很长的一个词有关联的话,此时RNN通常捕捉不到这个信息。所以在实际任务中RNN表现并不好。此时人们改造了RNN,引入LSTM(长短时记忆网络)以及和面的变种GRU(门控循环单元),后面会写一下。