循环神经网络
一、循环神经网络
循环神经网络(recurrent neural network,RNN)是一类用于处理序列数据的神经网络。
1.1 CNN与RNN简单对比
CNN: 借助卷积核(kernel)提取特征后,送入后续网络(如全连接网络 Dense)
进行分类、目标检测等操作。CNN 借助卷积核从空间维度提取信息,卷积核参数空间共享。
RNN: 借助循环核(cell)提取特征后,送入后续网络(如全连接网络 Dense)
进行预测等操作。RNN 借助循环核从时间维度提取信息,循环核参数时间共享。
1.2 循环核
循环核具有记忆力,通过不同时刻的参数共享,实现了对时间序列的信息提
取。每个循环核有多个记忆体,对应下图中的多个小圆柱。
记忆体内存储着每个时刻的状态信息
h
𝑡
ℎ_𝑡
ht,
h
𝑡
=
t
a
n
h
(
𝑥
𝑡
𝑤
𝑥
h
+
h
𝑡
−
1
w
h
h
+
𝑏
h
)
h𝑡 = tanh( 𝑥_𝑡𝑤_{𝑥ℎ}+ℎ_{𝑡−1}w_{ℎℎ} + 𝑏ℎ)
ht=tanh(xtwxh+ht−1whh+bh)其中,
𝑤
𝑥
h
、
w
h
h
𝑤_{𝑥ℎ}、w_{ℎℎ}
wxh、whh为权重矩阵,
𝑏
h
𝑏ℎ
bh为偏置,
𝑥
𝑡
𝑥_𝑡
xt为当前时刻的输入特征,
h
𝑡
−
1
ℎ_{𝑡−1}
ht−1为记忆体上一时刻存储的状态信息,
t
a
n
h
tanh
tanh 为激活函数
当前时刻循环核的输出特征
𝑦
𝑡
=
s
o
f
t
m
a
x
(
h
𝑡
𝑤
h
𝑦
+
𝑏
y
)
𝑦_𝑡=softmax(ℎ_𝑡𝑤_{ℎ𝑦}+𝑏y )
yt=softmax(htwhy+by)其中
w
w
y
w_{wy}
wwy为权重矩阵、
𝑏
y
𝑏y
by 为偏置、
s
o
f
t
m
a
x
softmax
softmax为激活函数,其实就相当于一层全连接层。我们可以设定记忆体的个数从而改变记忆容量,当记忆体个数被指定、输入
𝑥
𝑡
𝑥_𝑡
xt输出
y
t
y_t
yt维度被指定,周围这些待训练参数的维度也就被限定了。
在前向传播时,记忆体内存储的状态信息h𝑡𝑡在每个时刻都被刷新,而三个参数矩阵
w
x
h
w_{xh}
wxh、
w
h
h
w_{hh}
whh、
w
h
y
w_{hy}
why和两个偏置项
b
h
bh
bh、
b
y
by
by 自始至终都是固定不变的。在反向传播时,三个参数矩阵和两个偏置项由梯度下降法更新。
1.3 循环核按时间步展开
将循环核按时间步展开,就是把循环核按照时间轴方向展开,可以得到如下图的形式。每个时刻记忆体状态信息
h
t
h_t
ht被刷新,记忆体周围的参数矩阵和两个偏置项是固定不变的,我们训练优化的就是这些参数矩阵。训练完成后,使用效果最好的参数矩阵执行前向传播,然后输出预测结果。
其实这和我们人类的预测是一致的:我们脑中的记忆体每个时刻都根据当前的输入而更新;当前的预测推理是根据我们以往的知识积累用固化下来的“参数矩阵”进行的推理判断。
可以看出,循环神经网络就是借助循环核实现时间特征提取后把提取到的信
息送入全连接网络,从而实现连续数据的预测。
1.4 循环计算层:向输出方向生长
在RNN中,每个循环核构成一层循环计算层,循环计算层的层数是向输出方向增长的。如下图所示,左图的网络有一个循环核,构成了一层循环计算层;中图的网络有两个循环核,构成了两层循环计算层;右图的网络有三个循环核,构成了三层循环计算层。其中,三个网络中每个循环核中记忆体的个数可以根据我们的需求任意指定。
1.5 RNN 训练
得到 RNN 的前向传播结果之后,和其他神经网络类似,我们会定义损失函数,使用反向传播梯度下降算法训练模型。
RNN 唯一的区别在于:由于它每个时刻的节点都可能有一个输出,所以 RNN 的总损失为所有时刻(或部分时刻)上的损失和。
二、Tensorflow描述循环计算层
tf.keras.layers.SimpleRNN(神经元个数,activation=‘激活函数’,return_sequences=是否每个时刻输出ℎ𝑡到下一层)
(1)神经元个数:即循环核中记忆体的个数
(2) return_sequences:在输出序列中,返回最后时间步的输出值ℎ𝑡还是返回全部时间步的输出。False返回最后时刻,True返回全部时刻。当下一层依然是RNN层,通常为True,反之如果后面是Dense层,通常为Fasle。
(3)输入维度:三维张量(输入样本数, 循环核时间展开步数, 每个时间步输入特征个数)。
如上图所示,左图一共要送入RNN层两组数据,每组数据经过一个时间步就会得到输出结果,每个时间步送入三个数值,则输入循环层的数据维度就是 图1.2.6 RNN层输入维度[2, 1, 3]
右图输入只有一组数据,分四个时间步送入循环层,每个时间步送入两个数值,则输入循环层的数据维度就是 [1,4,2]。
(4)输出维度:
- return_sequenc=True,三维张量(输入样本数, 循环核时间展开步数,本层的神经元个数)
- return_sequenc=False,二维张量(输入样本数,本层的神经元个数)
(5) activation:‘激活函数’(不写默认使用tanh)
三、循环计算过程实例
RNN最典型的应用就是利用历史数据预测下一时刻将发生什么,即根据以前见过的历史规律做预测。举一个简单的字母预测例子体会一下循环网络的计算过程:输入一个字母预测下一个字母——输入a预测出b、输入b预测出c、输入c预测出d、输入d预测出e、输入e预测出a。计算机不认识字母,只能处理数字。所以需要我们对字母进行编码。这里假设使用独热编码(实际中可使用其他编码方式),编码结果下图所示。
独热编码 | 字母 |
---|---|
10000 | a |
01000 | b |
00100 | c |
00010 | d |
00001 | e |
假设使用一层RNN网络,记忆体的个数选取3,则字母预测的网络如下图所示。
假设输入字母b,即输入
𝑥
𝑡
𝑥_𝑡
xt为
[
0
,
1
,
0
,
0
,
0
]
[0,1,0,0,0]
[0,1,0,0,0],这时上一时刻的记忆体状态信息
h
𝑡
ℎ_𝑡
ht为0。由上文理论知识不难得到:
h
𝑡
=
t
a
n
h
(
𝑥
𝑡
𝑤
𝑥
h
+
h
t
−
1
w
h
h
+
𝑏
)
=
t
a
n
h
(
[
−
2.3
,
0.8
,
1.1
]
+
0
+
[
0.5
,
0.3
,
−
0.2
]
)
=
t
a
n
h
[
−
1.8
,
1.1
,
0.9
]
=
[
−
0.9
,
0.8
,
0.7
]
h_𝑡=tanh(𝑥_𝑡𝑤_{𝑥ℎ}+ℎ_{t-1}w_{ℎℎ}+𝑏)\\=tanh([−2.3,0.8,1.1 ]+ 0 + [ 0.5,0.3,−0.2])\\= tanh[−1.8,1.1,0.9 ] = [−0.9 ,0.8,0.7]
ht=tanh(xtwxh+ht−1whh+b)=tanh([−2.3,0.8,1.1]+0+[0.5,0.3,−0.2])=tanh[−1.8,1.1,0.9]=[−0.9,0.8,0.7] 这个过程可以理解为脑中的记忆因为当前输入的事物而更新了。
输出
y
𝑡
y_𝑡
yt是把提取到的时间信息通过全连接进行识别预测的过程,是整个网络的输出层。
y
𝑡
=
s
o
f
t
m
a
x
(
h
𝑡
𝑤
h
𝑦
+
𝑏
y
)
=
s
o
f
t
m
a
x
(
[
−
0.7
,
−
0.6
,
2.9
,
0.7
,
−
0.8
]
+
[
0.0
,
0.1
,
0.4
,
−
0.7
,
0.1
]
)
=
s
o
f
t
m
a
x
(
[
−
0.7
−
0.53.30.0
−
0.7
]
)
=
[
0.02
,
0.02
,
0.91
,
0.03
,
0.02
]
y_𝑡=softmax(ℎ_𝑡𝑤_{ℎ𝑦}+𝑏y )= softmax([−0.7,−0.6,2.9,0.7,−0.8] + [ 0.0,0.1,0.4,−0.7,0.1])\\= softmax([−0.7 −0.5 3.3 0.0 −0.7])= [0.02,0.02,0.91, 0.03,0.02 ]
yt=softmax(htwhy+by)=softmax([−0.7,−0.6,2.9,0.7,−0.8]+[0.0,0.1,0.4,−0.7,0.1])=softmax([−0.7−0.53.30.0−0.7])=[0.02,0.02,0.91,0.03,0.02]可见模型认为有91%的可能性输出字母c ,所以循环网络输出了预测结果 c。
import numpy as np
import tensorflow as tf
from tensorflow.keras.layers import Dense, SimpleRNN
import matplotlib.pyplot as plt
import os
input_word = "abcde"
w_to_id = {'a': 0, 'b': 1, 'c': 2, 'd': 3, 'e': 4} # 单词映射到数值id的词典
id_to_onehot = {0: [1., 0., 0., 0., 0.], 1: [0., 1., 0., 0., 0.], 2: [0., 0., 1., 0., 0.], 3: [0., 0., 0., 1., 0.],
4: [0., 0., 0., 0., 1.]} # id编码为one-hot
x_train = [id_to_onehot[w_to_id['a']], id_to_onehot[w_to_id['b']], id_to_onehot[w_to_id['c']],
id_to_onehot[w_to_id['d']], id_to_onehot[w_to_id['e']]]
y_train = [w_to_id['b'], w_to_id['c'], w_to_id['d'], w_to_id['e'], w_to_id['a']]
np.random.seed(7)
np.random.shuffle(x_train)
np.random.seed(7)
np.random.shuffle(y_train)
tf.random.set_seed(7)
# 使x_train符合SimpleRNN输入要求:[送入样本数, 循环核时间展开步数, 每个时间步输入特征个数]。
# 此处整个数据集送入,送入样本数为len(x_train);输入1个字母出结果,循环核时间展开步数为1; 表示为独热码有5个输入特征,每个时间步输入特征个数为5
x_train = np.reshape(x_train, (len(x_train), 1, 5))
y_train = np.array(y_train)
model = tf.keras.Sequential([
SimpleRNN(3),
Dense(5, activation='softmax')
])
model.compile(optimizer=tf.keras.optimizers.Adam(0.01),
loss=tf.keras.losses.SparseCategoricalCrossentropy(from_logits=False),
metrics=['sparse_categorical_accuracy'])
checkpoint_save_path = "./checkpoint/rnn_onehot_1pre1.ckpt"
if os.path.exists(checkpoint_save_path + '.index'):
print('-------------load the model-----------------')
model.load_weights(checkpoint_save_path)
cp_callback = tf.keras.callbacks.ModelCheckpoint(filepath=checkpoint_save_path,
save_weights_only=True,
save_best_only=True,
monitor='loss') # 由于fit没有给出测试集,不计算测试集准确率,根据loss,保存最优模型
history = model.fit(x_train, y_train, batch_size=32, epochs=100, callbacks=[cp_callback])
model.summary()
# print(model.trainable_variables)
file = open('./weights.txt', 'w') # 参数提取
for v in model.trainable_variables:
file.write(str(v.name) + '\n')
file.write(str(v.shape) + '\n')
file.write(str(v.numpy()) + '\n')
file.close()
############################################### show ###############################################
# 显示训练集和验证集的acc和loss曲线
acc = history.history['sparse_categorical_accuracy']
loss = history.history['loss']
plt.subplot(1, 2, 1)
plt.plot(acc, label='Training Accuracy')
plt.title('Training Accuracy')
plt.legend()
plt.subplot(1, 2, 2)
plt.plot(loss, label='Training Loss')
plt.title('Training Loss')
plt.legend()
plt.show()
############### predict #############
preNum = int(input("input the number of test alphabet:"))
for i in range(preNum):
alphabet1 = input("input test alphabet:")
alphabet = [id_to_onehot[w_to_id[alphabet1]]]
# 使alphabet符合SimpleRNN输入要求:[送入样本数, 循环核时间展开步数, 每个时间步输入特征个数]。此处验证效果送入了1个样本,送入样本数为1;输入1个字母出结果,所以循环核时间展开步数为1; 表示为独热码有5个输入特征,每个时间步输入特征个数为5
alphabet = np.reshape(alphabet, (1, 1, 5))
result = model.predict([alphabet])
pred = tf.argmax(result, axis=1)
pred = int(pred)
tf.print(alphabet1 + '->' + input_word[pred])
运行结果如下:
Epoch 100/100
5/5 [==============================] - 0s 34ms/sample - loss: 0.0400 - sparse_categorical_accuracy: 1.0000
Model: "sequential"
_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
simple_rnn (SimpleRNN) multiple 27
_________________________________________________________________
dense (Dense) multiple 20
=================================================================
Total params: 47
Trainable params: 47
Non-trainable params: 0
_________________________________________________________________
input the number of test alphabet:>? 5
input test alphabet:>? a
a->b