1 引言¶
在此前的多篇博客中,我花了很大的精力研究卷积神经网络相关内容,见证了卷积网络从最初的LeNet一步步发展到ResNet。在深度学习领域,卷积网络占据了半壁江山,例如在图像分类、目标检测等应用中, 卷积神经网络所取得的成就远不是其他算法模型可以比拟的。然而,不得不承认,卷积神经网络说擅长的领域数据大多是静态的,输入数据间相互独立,例如图像这种二维的数据,而对于语义识别,时间序列分析等应用,卷积神经网络就稍显不足。这是因为语义识别、时间序列等等应用中,数据呈序列状,前后输入相互关联,卷积神经网络在这些应用中难以兼顾数据间的关联关系,导致性能变差。
幸运的是,在神经网络算法家族中,也有一类算法专门用于处理此类序列数据,那就是本篇的主角——循环神经网络。循环神经网络(Recurrent Neural Network,RNN)是一种别具一格的网络模型,其循环元节点不仅可以接上来自上层的输入数据,也可以接收自身上一次迭代的输出,基于这种特殊的结构,循环神经网络拥有了短期记忆能力,通过“记忆”保存了数据间的关联关系,所以尤为适合处理语言、文本、视频等时序相关的数据。接下来,我们来具体剖析循环神经网络的特殊结构。
2 循环神经网络¶
2.1 为什么需要RNN¶
在前面引言中对RNN诞生的意义说的不够浅显,这里举个例子来说。语义识别,文本分类是循环神经网络的重点应用领域,当然,并不是说其他的神经网络算法在这些领域就毫无作用,只是效果较差而已。加入有下面一句话:
我是中国人,我会说 _
对于这句话,我想无论是一般的升级网络模型还是循环神经网络模型,都一颗预测出,横线上的内容应该是“汉语”。
继续,我们把这句话说完整:
我是中国人,我会说汉语,但是我成年后移民到美国,所以我也会说_
在一般的神经网络模型中,会孤立地分析每一个词,因为句子中同时出现了中国和美国,所以预测得到的词是“汉语”和“英语”概率相仿,但是在循环神经网络中,模型具有一定的短期记忆功能,能够根据上下文进行语义的预测,所以在较的位置上的“美国”一词有着更大的影响,最终预测结果更有可能是“英语”。这就是为什么循环神经网络在序列相关应用中表现优异的原因。
2.2 单向循环神经网络结构¶
与前馈神经网络结构相比,循环神经网络最大的不同在于其循环神经网络输出不仅能够往下一层传播,也能够传递给同层下一时刻,也就是说,对于循环网络中某一神经元节点,其内部运算数据不仅包含上一层的输出,同时也包含同层上一时刻的输出。如下图所示:
上图是一个简化的循环网络结构,只包含了一个隐藏层,图中,$X$我们可以理解为一个完整的时间序列(例如一句语义完整的文本),$x_t$是指的$t$个时刻的测度(文本中的第$t$个词的向量表示),$h_t$是第$t$个时刻的输出,也就是$x_t$和上一时刻的输出$h_t$在隐藏层中合并运算后的输出。同时可以看到,输出的$h_t$流向输出层的同时,还有流向了延迟期,留待第$t+1$次与$x_{t+1}$共同参与隐藏层运算。对于上图,按时间顺序(在时间维度)上展开,如下图所示:
注意,上图是在时间维度上进行展开,也就是上图三个部分其实是同一网络(而不是三个部分共同组成一个网络),而是在同一个网络在不同时间顺序上的展开,我刚接触循环神经网络看到这个图时,以为多个隐藏层是隐藏层的多个节点,$h$的值不过是在同层节点间进行传递,这是错误的。
图中,$U$和$W$分别是$x$和$h$的权值,所以,在隐藏层节点内的运算可以如下表示:
$$h_t=f(U \times x_t + w \times h_{t-1} + b) \tag{1} $$
式中,$f$是激活函数,$b$是偏置。可见,在计算第$t$时刻输出$h_t$时,上一时刻的输出状态$h_{t-1}$也参与了运算,对$h_{t-1}$也有:
$$h_{t-1}=f(U \times x_{t-1} + w \times h_{t-2} + b) \tag{2} $$ 将(1)式和(2)式结合,也就有: $$h_t=f(U \times x_t + w \times f(U \times x_{t-1} + w \times h_{t-2} + b) + b) \tag{3} $$ 通过这种方式,第$t$时刻的输出与之前时刻的输出关联起来,也就有了之前的“记忆”。
在我理解看来,循环神经网络与卷积等前馈神经网络最大的不同就在于多了一个时间维度,这就要求循环神经网络中的输入是 有序的(顺序对最终结果有影响)。以对一个句子进行情感分类为例,假设这个句子有10个词,每个词表示为长度为100向量(假设为$X=[x_1,…,x_{10}]$),如果是在前馈神经网络,就回将[10 * 100]的向量$X=[x_1,…,x_{10}]$一次性传递给网络,而在循环网络中,第一次是将第一个词的对应的长度为100的向量传递给网络,经过第一个循环时,神经元节点接收到两个输入,一个是词向量$x_1$,另一个是初始状态$h_0$(一般初始状态为全为0的向量),运算后输出为$h_1$,$h_1$输出到下一层的同时,也就等待下一个$x_2$的输入然后一同进行运算产生$h_2$……
有一点必须注意,在RNN循环层中,同一层的参数$U$、$W$和$b$,甚至包括激活函数$f$,是在不同时刻是完全共享的,这种特性大大减少了循环神经网络的参数量。其实在我理解看来,与其说参数共享,倒不如说不同时刻的数据使用的就是相同的神经元。
2.3 双向循环网络结构¶
在单向循环网络中,输出取决于当前的输入和之前时刻的“记忆”,但在有些应用场景中,最终的决策不仅受之前的“记忆”的影响,还需要综合考虑之后事件的发展,这就类似于做英文阅读理解,最终的答案需要我们找准对应的句子(当前时刻的输入$x_t$)还要综合前文($h_{t-1}$)和下文($h_{t+1}$)进行作答。在循环神经网络家族有,有一种双向循环网络结构(Bidirectional Recurrent Neural Network,Bi-RNN)就专用于此类场景。
双向循环神经网络每一层循环运算可以拆分为两层,这两层网络都输入序列$X$,但是信息传递方向相反。如下图所示:
图中循环运算公式如下:
第一行是顺序运算,第二行是逆序运算,第三行是将两种顺序的输出状态进行组合。
3 TensorFlow2实现循环神经网络¶
3.1 单层节点¶
import tensorflow as tf
假设有4个句子,每个句子有80个词汇,每个词汇表示为一个100维向量(有4个时间序列数据,每个序列有80个时间测度,每个测度有100个属性值),我们随机初始化来模拟这种数据结构:
x = tf.random.normal([4, 80, 100])
创建一个RNN神经元:
cell = tf.keras.layers.SimpleRNNCell(64) # 64的意思是经过神经元后,维度转化为64维,[b, 100] -> [b, 64],b是句子数量
初始化最初时刻的状态,因为有4个句子(4个batch),且输出为64维,所以最初的状态初始化为[4, 64],值全为零的矩阵。
ht0 = tf.zeros([4, 64]) # 因为
xt0 = x[:, 0, :]
out, ht1 = cell(xt0, [h0])
out是传递给下一层的值,ht1经过一次神经元后的转台。我们看看这两者的状态:
out.shape, ht1[0].shape
(TensorShape([4, 64]), TensorShape([4, 64]))
输出为[4, 64]的矩阵,确实如我们上面所指定的一样。继续:
id(out), id(ht1[0])
(140525368605520, 140525368605520)
可以看到,id是一样的,也就是说,这两者其实就是同一个对象。正如前文所说,RNN神经元有两个相同的输出,一个传递到下一层节点,也就是out,一个传递给下一时间节点循环,也就是ht1[0]。
3.2 多层节点¶
同样,我们先模拟构造一些数据
x = tf.random.normal([4, 80, 100])
xt0 = x[:, 0, :]
创建多层节点:
# 第一层节点
cell = tf.keras.layers.SimpleRNNCell(64)
# 第二层节点
cell2 = tf.keras.layers.SimpleRNNCell(64)
因为有多个层了,每个层都必须有一个初始状态,所以这里也必须创建多个初始状态矩阵:
state0 = [tf.zeros([4, 64])]
state1 = [tf.zeros([4, 64])]
下面就可以进行数据在神经元间的传递了:
out0, state0 = cell(xt0, state0)
out1, state1 = cell2(out, state1)
在上述运算中,因为只有一次值传递运算,所以对于“循环”表现的不明显,在完整RNN网络中,上述运算将会放在一个循环中,如下所示:
for word in tf.unstack(x, axis=1):
out0, state0 = cell(xt0, state0)
out1, state1 = cell2(out, state1)
第一层的cell每次运算都会对state0进行更新,然后通过state0记录这一次状态在下一次循环,同时,通过out0将输出值传递给下一层的cell2。
3.3 SimpleRNNCell实现完整RNN实现文本分类¶
import os
import tensorflow as tf
import numpy as np
from tensorflow import keras
from tensorflow.keras import layers
tf.random.set_seed(22)
np.random.seed(22)
os.environ['TF_CPP_MIN_LOG_LEVEL'] = '2'
加载数据,电影评价数据:
total_words = 10000 # 常见词汇数量
batchsz = 128
embedding_len = 100
(x_train, y_train), (x_test, y_test) = keras.datasets.imdb.load_data(num_words=total_words)
max_review_len = 80 # 每个句子最大长度
x_train = keras.preprocessing.sequence.pad_sequences(x_train, maxlen=max_review_len)
x_test = keras.preprocessing.sequence.pad_sequences(x_test, maxlen=max_review_len)
db_train = tf.data.Dataset.from_tensor_slices((x_train, y_train))
db_train = db_train.shuffle(1000).batch(batchsz, drop_remainder=True) # drop_remainder是指最后一个batch不足128个则丢弃
db_test = tf.data.Dataset.from_tensor_slices((x_test, y_test))
db_test = db_test.batch(batchsz, drop_remainder=True)
print('x_train shape:', x_train.shape, tf.reduce_max(y_train), tf.reduce_min(y_train))
print('x_test shape:', x_test.shape)
x_train shape: (25000, 80) tf.Tensor(1, shape=(), dtype=int64) tf.Tensor(0, shape=(), dtype=int64)
x_test shape: (25000, 80)
接下来创建网络结构:
class RNN(keras.Model):
def __init__(self, units):
super(RNN, self).__init__()
# 创建初始状态矩阵
self.state0 = [tf.zeros([batchsz, units])]
self.state1 = [tf.zeros([batchsz, units])]
# 对文本数据进行转换为矩阵
# 每句80个单词,每个单词100维矩阵表示 [b, 80] --> [b, 80, 100]
self.embedding = layers.Embedding(total_words, embedding_len, input_length=max_review_len)
# 循环网络层, 语义提取
# [b, 80, 100]--> [b, 64]
self.rnn_cell0 = layers.SimpleRNNCell(units, dropout=0.2) # 第一层rnn
self.rnn_cell1 = layers.SimpleRNNCell(units, dropout=0.2) # 第二层rnn
# 全连接层
# [b, 64] --> [b, 1]
self.outlayer = layers.Dense(1)
def call(self, inputs, training=None):
# [b, 80]
x = inputs
# embedding: [b, 80] -->[b, 80, 100]
x = self.embedding(x)
# rnn cell: [b, 80, 100] --> [b, 64]
state0 = self.state0
state1 = self.state1
for word in tf.unstack(x, axis=1): # 在第二个维度展开,遍历句子的每一个单词
#
out0, state0 = self.rnn_cell0(word, state0, training)
out1, state1 = self.rnn_cell1(out0, state1, training)
# out: [b, 64]
x = self.outlayer(out1)
prob = tf.sigmoid(x)
return prob
units = 64
epochs = 4
model = RNN(units)
model.compile(optimizer=keras.optimizers.Adam(0.001),
loss=tf.losses.BinaryCrossentropy(),
metrics=['accuracy'],
experimental_run_tf_function = False
)
model.fit(db_train, epochs=epochs, validation_data=db_test)
Epoch 1/4
193/195 [============================>.] - ETA: 0s - loss: 0.5973 - accuracy: 0.5506Epoch 1/4
195/195 [==============================] - 3s 15ms/step - loss: 0.4002 - accuracy: 0.8220
195/195 [==============================] - 13s 65ms/step - loss: 0.5958 - accuracy: 0.5520 - val_loss: 0.4002 - val_accuracy: 0.8220
Epoch 2/4
193/195 [============================>.] - ETA: 0s - loss: 0.3326 - accuracy: 0.8490Epoch 1/4
195/195 [==============================] - 1s 7ms/step - loss: 0.4065 - accuracy: 0.8224
195/195 [==============================] - 5s 28ms/step - loss: 0.3315 - accuracy: 0.8492 - val_loss: 0.4065 - val_accuracy: 0.8224
Epoch 3/4
193/195 [============================>.] - ETA: 0s - loss: 0.1839 - accuracy: 0.9180Epoch 1/4
195/195 [==============================] - 1s 7ms/step - loss: 0.5103 - accuracy: 0.8193
195/195 [==============================] - 6s 28ms/step - loss: 0.1841 - accuracy: 0.9182 - val_loss: 0.5103 - val_accuracy: 0.8193
Epoch 4/4
193/195 [============================>.] - ETA: 0s - loss: 0.0960 - accuracy: 0.9574Epoch 1/4
195/195 [==============================] - 1s 7ms/step - loss: 0.6747 - accuracy: 0.8083
195/195 [==============================] - 5s 28ms/step - loss: 0.0958 - accuracy: 0.9575 - val_loss: 0.6747 - val_accuracy: 0.8083
3.4 高层API实现RNN¶
数据加载和预处理还是使用上一章节的代码。这里主要是使用TensorFlow中高层API
model = keras.Sequential([
layers.Embedding(total_words, embedding_len, input_length=max_review_len),
layers.SimpleRNN(units, dropout=0.2, return_sequences=True, unroll=True),
layers.SimpleRNN(units, dropout=0.2, unroll=True),
layers.Dense(1, activation='sigmoid')
])
model.compile(loss='binary_crossentropy', optimizer='adam',
metrics=['accuracy'])
history1 = model.fit(db_train, epochs=epochs, validation_data=db_test)
loss, acc = model.evaluate(db_test)
print('准确率:', acc) # 0.81039
Train for 195 steps, validate for 195 steps
Epoch 1/4
195/195 [==============================] - 11s 54ms/step - loss: 0.6243 - accuracy: 0.6187 - val_loss: 0.4674 - val_accuracy: 0.7830
Epoch 2/4
195/195 [==============================] - 6s 29ms/step - loss: 0.3770 - accuracy: 0.8352 - val_loss: 0.4061 - val_accuracy: 0.8230
Epoch 3/4
195/195 [==============================] - 6s 29ms/step - loss: 0.2527 - accuracy: 0.9007 - val_loss: 0.4337 - val_accuracy: 0.8201
Epoch 4/4
195/195 [==============================] - 6s 29ms/step - loss: 0.1383 - accuracy: 0.9493 - val_loss: 0.5847 - val_accuracy: 0.8076
195/195 [==============================] - 1s 7ms/step - loss: 0.5847 - accuracy: 0.8076
准确率: 0.8075721