在深度学习中,RNN是处理序列数据的有效方法之一,也是深度的一种很好的体现,本文将简单介绍RNN的工作方式,以及针对IMDB数据集的简单实践
RNN简介
RNN(Recurrent Neural Network),在基本的全连接层上迭代一层或多层带有历史信息(h)的RNN神经单元(RNN cell),使神经网络能够处理具有上下文关联的序列数据,能够有效减少隐层的参数量,提升训练效率和准确率
为了更好的说明RNN的工作原理,我们带入一个具体的目标,就是评价情感分析,如图所示:
我们所要做的就是通过下方由单词组成的评论来确定其情感是积极还是消极。我们把语句定义为x,输出定义为y,输出的结果即:P(y|x)
这里的embedding操作可以简单理解为一个线性和,即
Oi=x@weighti+biasi
但这样简单的线性传递操作之后,只能通过每一个单词的含义来判定情感,无法关联到上下文,为了保存并处理上下文的语义,我们给线性操作附加一个历史信息h。如果这样处理,那我们完全可以省略掉针对每一个单词不同的weight,而使用一个公共的weight用于单词提取,称为weightx,同理偏置称为biasx,此时引入历史信息h,初始化h0为全零,则公式修改为:
Oi=x@weightx+hi@weighth+biasx=hi+1
每一次计算的输出和传递给下一层的历史信息其实是相同的,这里分开来写是为了下一篇LSTM留坑;而所谓的传递给下一层,实际上可以由同一个RNNcell迭代完成,这也是RNN名字的由来
说完了公式,我们回到神经网络的根基,也就是梯度的求解
额外的参数定义:
t表示第t个句子,或者t时刻
激活函数——tan()
则:
ht=tan(x*weightx+ht-1@weighth)
yt=weighto*ht
这里我们忽略偏置
则损失函数的梯度由链式法则可以写为:
第一个导数,由于损失函数和t时刻输出yt是直接关联的,因此第一个导数就是我们定义的损失函数对yt的直接求导,已知
第二个导数,当前时刻输出yt对当前时刻历史信息ht的导数在公式中可直接看出为weighto,已知
第三个导数,
令f=tanh(x),由ht公式可知
推导过程请自行演算
第四个导数,对tan激活函数求导后再对weighth 求导即可,已知
综上可知,RNN梯度的复杂度需要对时间轴进行展开,复杂程度很高,因此需要用到TensorFlow等框架进行计算
IMDB数据集和RNN网络的简单实践
对于数据集的加载可以直接使用TensorFlow2下的Keras中Dataset直接导入,如果下载速度很慢可能是因为……你懂得
total_words = 10000
(x_train, y_train), (x_test, y_test) = keras.datasets.imdb.load_data(num_words=total_words)
接下来做数据预处理
max_review_len = 80
# x_train:[b, 80]
# x_test: [b, 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)
db_test = tf.data.Dataset.from_tensor_slices((x_test, y_test))
db_test = db_test.batch(batchsz, drop_remainder=True)
简单起见,我们只设计一层的RNN网络,自定义一个RNN网络用于训练
class MyRNN(keras.Model):
def __init__(self, units):
super(MyRNN, self).__init__()
# [b, 64]
self.state = [tf.zeros([batchsz, units])]
# self.state1 = [tf.zeros([batchsz, units])]
# transform text to embedding representation
# [b, 80] => [b, 80, 100]
self.embedding = layers.Embedding(total_words, embedding_len,
input_length=max_review_len)
# [b, 80, 100] , h_dim: 64
# RNN: cell1 ,cell2, cell3
# SimpleRNN
self.rnn_cell = layers.SimpleRNNCell(units, dropout=0.2)
# self.rnn_cell1 = layers.SimpleRNNCell(units, dropout=0.5)
# fc, [b, 80, 100] => [b, 64] => [b, 1]
self.fc= 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 compute
# [b, 80, 100] => [b, 64]
state = self.state
# state1 = self.state1
for word in tf.unstack(x, axis=1): # word: [b, 100]
# h1 = x*wxh+h0*whh
# out: [b, 64]
out, state = self.rnn_cell(word, state, training)
# out: [b, 64] => [b, 1]
x = self.fc(out)
# p(y is pos|x)
prob = tf.sigmoid(x)
return prob
然后使用TensorFlow2中的compile and fit功能即可实现训练和测试,给出笔者的运行结果
整体来看运行的正确率达到82%,没有达到很高的原因在于层数太少,仅仅简单实现了一层的RNN网络,同时可以发现笔者使用了随机种子,这样的随机RNN如果更换成更加贴合数据的因子就能够有所提升