RNN基础

Recurrent Neural Networks 循环神经网络

应用领域

语音识别,情感分类,机器翻译

Notation

符号

Recurrent Neural Network Model

  • 不同样本的输入序列长度或输出序列长度不同,即$T_x^{(i)}\neq T_x^{(j)}$,$T_y^{(i)}\neq T_y^{(j)}$,造成模型难以统一。 解决办法之一是设定一个最大序列长度,对每个输入和输出序列补零并统一到最大长度。但是这种做法实际效果并不理想。
  • 这种标准神经网络结构无法共享序列不同\(x^{<t>}\)之间的特征。例如,如果某个\(x^{<t>}\)即“Harry”是人名成分,那么句子其它位置出现了“Harry”, 也很可能也是人名。这是共享特征的结果,如同CNN网络特点一样。但是,上图所示的网络不具备共享特征的能力。 值得一提的是,共享特征还有助于减少神经网络中的参数数量,一定程度上减小了模型的计算复杂度

序列模型从左到右,依次传递,此例中,$T_x=T_y$。x<t>  y^<t>a<t>会传入到第t+1个元素中,作为输入。其中,a<0>一般为零向量。

RNN的正向传播(Forward Propagation)过程为:

\[a^{<t>}=g(W_{aa}\cdot a^{<t-1>}+W_{ax}\cdot x^{<t>}+ba)\] \[\hat y^{<t>}=g(W_{ya}\cdot a^{<t>}+b_y)\]

其中,$g(\cdot)$表示激活函数,不同的问题需要使用不同的激活函数。

简化为:

\[a^{<t>}=g(W_a[a^{<t-1>},x^{<t>}]+b_a)\] \[\hat y^{<t>}=g(W_{y}\cdot a^{<t>}+b_y)\]

Backpropagation

针对上面识别人名的例子,经过RNN正向传播,单个元素的Loss function为:

\[L^{<t>}(\hat y^{<t>},y^{<t>})=-y^{<t>}log\ \hat y^{<t>}-(1-y^{<t>})log\ (1-\hat y^{<t>})\]

该样本所有元素的Loss function为:

\[L(\hat y,y)=\sum_{t=1}^{T_y}L^{<t>}(\hat y^{<t>},y^{<t>})\]

然后,反向传播(Backpropagation)过程就是从右到左分别计算$L(\hat y,y)$对参数$W_{a},W_{y},b_a,b_y$

Different types of RNNs

RNN模型包含以下几个类型:

  • Many to many: $T_x=T_y$ seq to seq
  • Many to many: $T_x\neq T_y$
  • Many to one: $T_x>1,T_y=1$
  • One to many: $T_x=1,T_y>1$
  • One to one: $T_x=1,T_y=1$
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import numpy as np
import tensorflow as tf

n_inputs = 3
n_neurons = 5
X0 = tf.placeholder(tf.float32, [None, n_inputs])
X1 = tf.placeholder(tf.float32, [None, n_inputs])
Wx = tf.Variable(tf.random_normal(shape=[n_inputs, n_neurons], dtype=tf.float32))
Wy = tf.Variable(tf.random_normal(shape=[n_neurons, n_neurons], dtype=tf.float32))
b = tf.Variable(tf.zeros([1, n_neurons], dtype=tf.float32))
Y0 = tf.tanh(tf.matmul(X0, Wx) + b)
Y1 = tf.tanh(tf.matmul(Y0, Wy) + tf.matmul(X1, Wx) + b)
init = tf.global_variables_initializer()

# Mini-batch: instance 0,instance 1,instance 2,instance 3
X0_batch = np.array([[0, 1, 2], [3, 4, 5], [6, 7, 8], [9, 0, 1]])  # t = 0
X1_batch = np.array([[9, 8, 7], [0, 0, 0], [6, 5, 4], [3, 2, 1]])  # t = 1
with tf.Session() as sess:
 init.run()
 Y0_val, Y1_val = sess.run([Y0, Y1], feed_dict={X0: X0_batch, X1: X1_batch})

print(Y0_val,'\n')
print(Y1_val)

相同模型

static_rnn()函数为每个输入调用单元工厂的__call __()函数,创建单元的两个副本(每个单元包含 5 个循环神经元的循环层), 并具有共享的权重和偏置项,像前面一样。

static_rnn()函数返回两个对象。 第一个是包含每个时间步的输出张量的 Python 列表。 第二个是包含网络最终状态的张量。 当你使用基本的单元时,最后的状态就等于最后的输出。

1
2
3
4
5
6
7
X0 = tf.placeholder(tf.float32, [None, n_inputs])
X1 = tf.placeholder(tf.float32, [None, n_inputs])

basic_cell = tf.contrib.rnn.BasicRNNCell(num_units=n_neurons)
output_seqs, states = tf.contrib.rnn.static_rnn(basic_cell, [X0, X1],
                                                dtype=tf.float32)
Y0, Y1 = output_seqs

缺点 这种方法仍然会建立一个每个时间步包含一个单元的图。如果使用具有200个时间步长的输入进行调用, 则创建一个具有200个RNN步长的静态图形。首先,图形创建很慢。其次,您无法传递比您最初指定的更长的序列(> 200)。

tf.nn.dynamic_rnn解决了这个问题。它使用tf.While循环在执行时动态构造图形。这意味着图表创建速度更快,您可以提供可变大小的批量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import numpy as np
import tensorflow as tf
import pandas as pd

if __name__ == '__main__':
    n_steps = 2
    n_inputs = 3
    n_neurons = 5
    """
    input shape [batch_size, n_steps, n_inputs]
    """
    X = tf.placeholder(tf.float32, [None, n_steps, n_inputs])

    basic_cell = tf.contrib.rnn.BasicRNNCell(num_units=n_neurons)
    outputs, states = tf.nn.dynamic_rnn(basic_cell, X, dtype=tf.float32)

    init = tf.global_variables_initializer()

    X_batch = np.array([
        [[0, 1, 2], [9, 8, 7]],  # instance 1
        [[3, 4, 5], [0, 0, 0]],  # instance 2
        [[6, 7, 8], [6, 5, 4]],  # instance 3
        [[9, 0, 1], [3, 2, 1]],  # instance 4
    ])

    with tf.Session() as sess:
        init.run()
        outputs_val = outputs.eval(feed_dict={X: X_batch})

    print(outputs_val)

变长输入序列

你应该在调用dynamic_rnn()(或static_rnn())函数时设置sequence_length参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
n_steps = 2
n_inputs = 3
n_neurons = 5

X = tf.placeholder(tf.float32, [None, n_steps, n_inputs])
basic_cell = tf.contrib.rnn.BasicRNNCell(num_units=n_neurons)
seq_length = tf.placeholder(tf.int32, [None])
outputs, states = tf.nn.dynamic_rnn(basic_cell, X, dtype=tf.float32,
                                    sequence_length=seq_length)

"""
例如,假设第二个输入序列只包含一个输入而不是两个输入。 为了适应输入张量X,必须填充零向量(因为输入张量的第二维是最长序列的大小,即 2)
"""
X_batch = np.array([
        # step 0     step 1
        [[0, 1, 2], [9, 8, 7]], # instance 1
        [[3, 4, 5], [0, 0, 0]], # instance 2 (padded with zero vectors)
        [[6, 7, 8], [6, 5, 4]], # instance 3
        [[9, 0, 1], [3, 2, 1]], # instance 4
    ])
seq_length_batch = np.array([2, 1, 2, 2])


with tf.Session() as sess:
    init.run()
    outputs_val, states_val = sess.run(
        [outputs, states], feed_dict={X: X_batch, seq_length: seq_length_batch})

action

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
n_steps = 28
n_inputs = 28
n_neurons = 150
n_outputs = 10

learning_rate = 0.001

X = tf.placeholder(tf.float32, [None, n_steps, n_inputs])
y = tf.placeholder(tf.int32, [None])

basic_cell = tf.contrib.rnn.BasicRNNCell(num_units=n_neurons)
outputs, states = tf.nn.dynamic_rnn(basic_cell, X, dtype=tf.float32)

logits = tf.layers.dense(states, n_outputs)
xentropy = tf.nn.sparse_softmax_cross_entropy_with_logits(labels=y,
                                                          logits=logits)
loss = tf.reduce_mean(xentropy)
optimizer = tf.train.AdamOptimizer(learning_rate=learning_rate)
training_op = optimizer.minimize(loss)
correct = tf.nn.in_top_k(logits, y, 1)
accuracy = tf.reduce_mean(tf.cast(correct, tf.float32))

init = tf.global_variables_initializer()

"""
现在让我们加载 MNIST 数据,并按照网络的预期方式将测试数据重塑为[batch_size, n_steps, n_inputs]。 我们之后会关注训练数据的重塑。
"""
from tensorflow.examples.tutorials.mnist import input_data
mnist = input_data.read_data_sets("/tmp/data/")
X_test = mnist.test.images.reshape((-1, n_steps, n_inputs))
y_test = mnist.test.labels

batch_size = 150

with tf.Session() as sess:
    init.run()
    for epoch in range(n_epochs):
        for iteration in range(mnist.train.num_examples // batch_size):
            X_batch, y_batch = mnist.train.next_batch(batch_size)
            X_batch = X_batch.reshape((-1, n_steps, n_inputs))
            sess.run(training_op, feed_dict={X: X_batch, y: y_batch})
        acc_train = accuracy.eval(feed_dict={X: X_batch, y: y_batch})
        acc_test = accuracy.eval(feed_dict={X: X_test, y: y_test})
        print(epoch, "Train accuracy:", acc_train, "Test accuracy:", acc_test)


NLP

如何使用RNN构建语言模型

大量的单词语句语料库(corpus)构成足够大的训练集,对corpus的每句话进行切分词(tokenize),建立vocabulary,对每个单词进行one-hot编码。 对语料库的每条语句进行RNN模型训练,最终得到的模型可以根据给出语句的前几个单词预测其余部分,将语句补充完整。

循环神经网络的梯度消失(Vanishing gradients with RNNs)

某个word可能与它距离较远的某个word具有强依赖关系。 一般的RNN模型每个元素受其周围附近的影响较大,难以建立跨度较大的依赖性。上面两句话的这种依赖关系, 由于跨度很大,普通的RNN网络容易出现梯度消失,捕捉不到它们之间的依赖,造成语法错误。

RNN也可能出现梯度爆炸的问题,即gradient过大。常用的解决办法是设定一个阈值, 一旦梯度最大值达到这个阈值,就对整个梯度向量进行尺度缩小。这种做法被称为gradient clipping。

GRU单元(Gated Recurrent Unit(GRU))

更好地捕捉深层连接,并改善了梯度消失问题 为了解决梯度消失问题,对上述单元进行修改,添加了记忆单元,构建GRU(门控循环单元)

GRU单元将会有个新的变量称为c,代表细胞(cell),记忆细胞的作用是提供了记忆的能力。 \(我们将用一个候选值重写记忆细胞,即{\tilde{c}}^{<t>}的值,所以它就是个候选值,替代了c^{<t>}的值。\) \(GRU中真正重要的思想是我们有一个门,我先把这个门叫做\Gamma_u,这是个下标为u的大写希腊字母\Gamma,u代表更新门,这是一个0到1之间的值。\)

\(如果这个更新值\Gamma_{u} =1,也就是说把这个新值,即c^{<t>}设为候选值,c^{<t>} = {\tilde{c}}^{<t>}。\) \(如果\Gamma_{u}= 0,意思就是说不更新它,就用旧的值。c^{<t>} =c^{<t-1>}。\)

这就是GRU单元或者说是一个简化过的GRU单元,它的优点就是通过门决定,当你从左到右扫描一个句子的时候, 这个时机是要更新某个记忆细胞,还是不更新,不更新直到你到你真的需要使用记忆细胞的时候,这可能在句子之前就决定了。 因为sigmoid的值,现在因为门很容易取到0值,只要这个值是一个很大的负数,再由于数值上的四舍五入,上面这些门大体上就是0,或者说非常非常非常接近0。 所以在这样的情况下,这非常有利于维持细胞的值。因为$\Gamma_{u}$很接近0,可能是0.000001或者更小,这就不会有梯度消失的问题了。 因为$\Gamma_{u}$很接近0,这就是说$c^t$几乎就等于$c^{t-1}$,而且$c^t$的值也很好地被维持了, 即使经过很多很多的时间步。这就是缓解梯度消失问题的关键,因此允许神经网络运行在非常庞大的依赖词上,比如说catwas单词即使被中间的很多单词分割开。

上面介绍的是简化的GRU模型,完整的GRU添加了另外一个gate,即$\Gamma_r$,表达式如下: \(r代表相关性(**relevance**)。这个\Gamma_{r}门告诉你计算出的下一个c^{<t>}的候选值{\tilde{c}}^{<t>}跟c^{<t-1>}有多大的相关性。\)

\[\tilde c^{<t>}=tanh(W_c[\Gamma_r*c^{<t-1>},x^{<t>}]+b_c)\] \[\Gamma_u=\sigma(W_u[c^{<t-1>},x^{<t>}]+b_u)\] \[\Gamma_r=\sigma(W_r[c^{<t-1>},x^{<t>}]+b_r)\] \[c^{<t>}=\Gamma_u*\tilde c^{<t>}+(1-\Gamma_u)*c^{<t-1>}\] \[a^{<t>}=c^{<t>}\]

LSTM

长短期记忆 long short term memory LSTM包含三个gates:Γu,Γf,Γo,分别对应update gate,forget gate和output gate。

\(peephole connection(窥视孔连接)其实意思就是门值不仅取决于a^{<t-1>}和x^{<t>}, 也取决于上一个记忆细胞的值(c^{<t-1>}),\) \(然后peephole connection就可以结合这三个门(\Gamma_{u}、\Gamma_{f}、\Gamma_{o})来计算了。\)

1
lstm_cell = tf.contrib.rnn.BasicLSTMCell(num_units=n_neurons)

Bidirectional RNN

  • 在序列的某点处不仅可以获取之前的信息,还可以获取未来的信息。
  • 深层的RNN 双向循环神经网络,并且这些基本单元不仅仅是标准RNN单元,也可以是GRU单元或者LSTM单元。 事实上,很多的NLP问题,对于大量有自然语言处理问题的文本,有LSTM单元的双向RNN模型是用的最多的。 所以如果有NLP问题,并且文本句子都是完整的,首先需要标定这些句子,一个有LSTM单元的双向RNN模型,有前向和反向过程是一个不错的首选。

缺点:

  • 需要完整的数据的序列,才能预测任意位置。

Deep RNN

深层循环神经网络

RNN的多个层堆叠在一起构建更深的模型 与DNN一样,用上标[l]表示层数。Deep RNNs中\(a^{[l]<t>}\)的表达式为:

\[a^{[l]<t>}=g(W_a^{[l]}[a^{[l]<t-1>},a^{[l-1]<t>}]+b_a^{[l]})\]

我们知道DNN层数可达100多,而Deep RNNs一般没有那么多层,3层RNNs已经较复杂了。

1
2
3
4
5
6
n_neurons = 100
n_layers = 3

basic_cell = tf.contrib.rnn.BasicRNNCell(num_units=n_neurons)
multi_layer_cell = tf.contrib.rnn.MultiRNNCell([basic_cell] * n_layers)
outputs, states = tf.nn.dynamic_rnn(multi_layer_cell, X, dtype=tf.float32)

states 变量包含了每层的一个张量,这个张量就代表了该层神经单元的最终状态(维度为[batch_size, n_neurons])