基于tensorflow的RNN-LSTM(一)实现RNN

时间:2021-08-14 19:06:30

开始入门深度学习,在手推了RNN和LSTM的BP算法后(参考paper:Supervised Sequence Labelling with Recurrent Neural)。开始尝试用tensorflow实现RNN和LSTM以及基于其的一些衍生模型,在实现的过程中有一些思考和未解决的问题,在这里一并记录。

变量解释


本文的实现主要是依据这篇博客, 开篇作者便引入了五个变量:

num_steps = 5
batch_size = 200

num_classes = 2
state_size = 8
learning_rate = 0.1
num_epochs = 1

这里有必要先解释一下这些变量的含义,但是读者们大概率觉得枯燥,可以大致浏览下然后往下看,中途多次回顾即可。第一个变量num_steps,RNN循环的次数,由此可见深入理解这个变量对于理解代码所实现的RNN网络拓扑和RNN网络至关重要。第二个变量batch_size指的是批的大小,这个可以自己指定;num_classes是数字的类别,这里为2也即是0和1;state_size指隐藏层的神经元个数在图中是S内部的神经元数目;下一个是学习率,这个是应用于Adagrad算法的;最后一个是网络执行的次数。

先看原博客中给出的RNN结构图基于tensorflow的RNN-LSTM(一)实现RNN
这个图比较笼统,不易观察出batch_size、state_size等变量的对应关系,因此,我找到了第二张RNN结构图基于tensorflow的RNN-LSTM(一)实现RNN
我对这两张图的理解是:第一张图的P和第二张中的y是输出层,所不同的是第一张把真正的输出y进行了softmax处理得到了P;第一张的S和第二张的h都是隐藏层,其中上图的每一个S对应的下图三个小h(这里的“三个”在代码中用变量state_size表示);上图的X和下图的x都是输入层。
到此为止都比较宏观,读者可以在阅读代码时想一想开篇这一节所说的概念。

数据处理


def gen_data(size=1000000):
X = np.array(np.random.choice(2, size=(size, )))
Y = []
for i in range(size):
threshold = 0.5
if X[i-3] == 1:
threshold += 0.5
if X[i-8] == 1:
threshold -= 0.25
if np.random.rand() > threshold:
Y.append(0)
else:
Y.append(1)
return X, np.array(Y)


def gen_batch(raw_data, batch_size, num_steps):
raw_x, raw_y = raw_data
data_length = len(raw_x)
batch_partition_length = data_length // batch_size
data_x = np.zeros([batch_size, batch_partition_length], dtype = np.int32)
data_y = np.zeros([batch_size, batch_partition_length], dtype = np.int32)
for i in range(batch_size):
data_x[i] = raw_x[batch_partition_length*i : batch_partition_length*(i+1)]
data_y[i] = raw_y[batch_partition_length*i : batch_partition_length*(i+1)]

epoch_size = batch_partition_length // num_steps
for i in range(epoch_size):
x = data_x[:, i*num_steps : (i+1)*num_steps]
y = data_y[:, i*num_steps : (i+1)*num_steps]
yield (x, y)

def gen_epochs(n, num_steps):
for i in range(n):
yield gen_batch(gen_data(), batch_size, num_steps)

一些基本的Python语法这里不再赘述,原始数据的生成规则可以参看原博客,这个部分比较重要的是对原始数据的一个处理:x, y 从一个1000000的向量变成了如下图的格式基于tensorflow的RNN-LSTM(一)实现RNN
在代码的最后使用yield的函数生成一个迭代变量,留待后续使用enumerate函数进行处理。

仅仅处理到这一步还是不够的,在接下来的代码中可以看出

x = tf.placeholder(tf.int32, [batch_size, num_steps], name='input_placeholder')
y = tf.placeholder(tf.int32, [batch_size, num_steps], name='label_placeholder')
init_state = tf.zeros([batch_size, state_size])

x_one_hot = tf.one_hot(x, num_classes)
rnn_inputs = tf.unstack(x_one_hot, axis=1)

这里使用tf的占位符将x, y设定为shape为[batch_size, num_steps]([200, 5]); 并将init_state设置为shape为[200,4],接下来对x的处理,参看下图:基于tensorflow的RNN-LSTM(一)实现RNN
在模型训练之后会给出更完整的数据的shape变化图以及其与RNN内部单元的对应关系。

模型构建


with tf.variable_scope('rnn_cell'):
W = tf.get_variable('W', [num_classes+state_size, state_size])
b = tf.get_variable('b', [state_size], initializer=tf.constant_initializer(0.0))

def rnn_cell(rnn_input, state):
with tf.variable_scope('rnn_cell', reuse=True):
W = tf.get_variable('W', [num_classes+state_size, state_size])
b = tf.get_variable('b', [state_size], initializer=tf.constant_initializer(0.0))
return tf.tanh(tf.matmul(tf.concat([rnn_input, state], 1), W) + b)

state = init_state
rnn_outputs = []

for rnn_input in rnn_inputs:
state = rnn_cell(rnn_input, state)
rnn_outputs.append(state)
final_state = rnn_outputs[-1]

with tf.variable_scope('softmax'):
W = tf.get_variable('W', [state_size, num_classes])
b = tf.get_variable('b', [num_classes], initializer=tf.constant_initializer(0.0))

logits = [tf.matmul(rnn_output, W) + b for rnn_output in rnn_outputs]
predictions = [tf.nn.softmax(logit) for logit in logits]

y_as_list = tf.unstack(y, num=num_steps, axis=1)

losses = [tf.nn.sparse_softmax_cross_entropy_with_logits(labels=label, logits=logit) \
for logit, label in zip(logits, y_as_list)]

total_loss = tf.reduce_mean(losses)
train_step = tf.train.AdagradOptimizer(learning_rate).minimize(total_loss)

这一段定义了RNN网络的参数W和b,并且按照RNN的规则计算了中间层,并且加了一个softmax层,值得注意的是,在代码最后使用的是Adagrad算法进行梯度求解,去比较Adagrad和SGD是另一个宏大的课题,可以留给以后的系列文章去解决。

训练模型


def train_network(num_epochs, num_steps, state_size=state_size, verbose=True):
with tf.Session() as sess:
sess.run(tf.global_variables_initializer())
training_losses = []
for idx, epoch in enumerate(gen_epochs(num_epochs, num_steps)): # gen_epoch 获得1000(batch_size*num_steps)个数据,
# 每个数据均为2维向量,表示为one_hot形式
training_loss = 0
training_state = np.zeros((batch_size, state_size))
if verbose :
print("\nEPOCH", idx)

for step, (X, Y) in enumerate(epoch):
tr_losses, training_loss_, training_state, _ = \
sess.run([losses, total_loss,
final_state, train_step],
feed_dict={x: X, y: Y, init_state: training_state})
training_loss += training_loss_
step += 1
if step % 100 == 0 and step > 0:
if verbose:
print("Average loss at step", step, "for last", step , "steps:", training_loss/100)
training_losses.append(training_loss/100)
training_loss = 0
return training_losses

training_losses = train_network(num_epochs, num_steps)
plt.plot(training_losses)
plt.show()

训练到这里可以给出网络的拓扑图如下:
基于tensorflow的RNN-LSTM(一)实现RNN
在看图之前,先要说明一点是在下文中的shape的表示中,冒号前是抽象形式,冒号后是本例中的具象形式。明白这点之后,先看最底下,最底下就是一个x或X的shape(batch_size * num_steps : 200 * 5),省略号表示一共有batch_size行;接下来,经过tf.one_hot处理,成为了中间的shape,本质上就是把元素0变成了[1,0],把元素1变成了[0,1],变成了x_one_hot;然后再用tf.unstack处理得到了rnn_inputs,这个变量的shape是num_steps(目前是5)个(batch_size * num_classes : 200 * 2)。

花开两朵,各表一枝。现在在来看看本例实现的网络的拓扑结果,也即是图中上部。网络*有num_steps个RNN单元,而为什么是这种形式,原博客有关截断的部分已经讲的很好了,这里不再赘述。具体来看每个RNN单元,首先是输入层x,输入层内有两个神经单元对应的是rnn_inputs中的one_hot表示形式的数据;接下来是中间层S及它的四个神经单元,这个是由state_size决定的;再往上是输出层y,也是两个神经单元,表示输出预测值的one_hot形式;最上面一层可以跟y层放在一起,我这里单独出来只是为了突出softmax。

展望


在本文中还有一些疑问留待以后解决:

  1. 为什么采用one_hot形式?如果直接用原始数据如何呢?如果采用word2ec形式又如何呢?
  2. 如果把num_steps扩大到和输入数据的维度一样大的程度,梯度爆炸会以怎样的形式出现?
  3. 比较Adagrad算法比SGD算法的优劣。