第十四章——循環神經網絡(Recurrent Neural Networks)(第一部分)


本章共兩部分,這是第一部分:

第十四章——循環神經網絡(Recurrent Neural Networks)(第一部分)

第十四章——循環神經網絡(Recurrent Neural Networks)(第二部分)

 

 

這幾年提到RNN,一般指Recurrent Neural Networks,至於翻譯成循環神經網絡還是遞歸神經網絡都可以。wiki上面把Recurrent Neural Networks叫做時間遞歸神經網絡,與之對應的還有一個結構遞歸神經網絡(recursive neural network)。本文討論的是前者。

RNN是一種可以預測未來(在某種程度上)的神經網絡,可以用來分析時間序列數據(比如分析股價,預測買入點和賣出點)。在自動駕駛中,可以預測路線來避免事故。更一般的,它可以任意序列長度作為輸入,而不是我們之前模型使用的固定序列長度。例如RNN可以將句子、文檔、語音作為輸入,進行自動翻譯、情感分析、語音轉文字。此外,RNN還用於作曲(谷歌Magenta項目作出的the one)、作文圖片自動生成標題

14.1 周期神經元(Recurrent Neurons)

此前介紹的大部分是前饋神經網絡,激活流只有一個方向,從輸入層流向輸出層。RNN和前饋神經網絡很相似,不過也會向后連接。我們來看一個最簡單的RNN,只有一個神經元接受輸入,只產生一個輸出,然后再將輸出傳遞給自己,如圖14-1(左側)。在每一個time step $t$(也叫做一幀),循環神經元接受輸入$x_{(t)}$和前一步的輸出$y_{(t-1)}$。可以將這一神經元隨時間展開,如圖14-1(右)。

圖14-1 一個循環神經元(左),隨時間展開(右)

創建一層循環神經元也很簡單,只不過在一個time step,輸入和輸出都是向量,如圖14-2。

圖14-2 一層循環神經元(左),隨時間展開(右)

每個神經元都有兩套權重:一個用於本層輸入$x_{(t)}$,一個用於上層輸出$y_{(t-1)}$。我們分別記為$w_x$和$w_y$。

一個循環神經元關於一個實例的輸出:

\begin{align*}
y_{(t)} = \phi(x_{(t)}^T \cdot w_x + y_{(t-1)}^T \cdot w_y + b)
\end{align*}

其中,$b$是偏置項,$\phi(\cdot)$是激活函數,比如ReLU(許多研究者更喜歡使用hyperbolic tangent (tanh)作為RNN的激活函數。例如,可以參考Vu Pham等人的Dropout Improves Recurrent Neural Networks for Handwriting Recognition。不過,基於ReLU的RNN也是可以的,比如Quoc V. Le等人的論文A Simple Way to Initialize Recurrent Networks of Rectified Linear Units。)。

一層循環神經元關於整個mini-batch的輸出:

\begin{align*}
Y_{(t)} &= \phi(X_{(t)}^T \cdot W_x + Y_{(t-1)}^T \cdot W_y + \textbf{b}) \\
&= \phi([X_{(t)} \quad Y_{(t)}] \cdot W + \textbf{b}) \quad \mbox{with} \quad
W = \begin{bmatrix}
W_x \\
W_y
\end{bmatrix}
\end{align*}

  • $Y_{(t)}$是一個$m \times n_{\mbox{neurons}}$矩陣,包含該層在time step $t$關於整個mini-batch實例的輸出($m$是mini-batch的實例數,$n_{\mbox{neurons}}$是神經元數量)。
  • $X_{(t)}$是一個$m \times n_{\mbox{inputs}}$矩陣,包含該time step $t$所有實例的輸入($n_{\mbox{inputs}}$是特征數)。
  • $W_x$是一個$n_{\mbox{inputs}} \times n_{\mbox{neurons}}$矩陣,包含當前time step輸入到輸出的連接權重。
  • $W_y$是一個$n_{\mbox{neurons}} \times n_{\mbox{neurons}}$矩陣,包含上個time step輸出到當前time step輸出的連接權重。
  • $W$的形狀是$(n_{\mbox{inputs}} + n_{\mbox{neurons}}) \times n_{\mbox{neurons}}$
  • $ \textbf{b}$是一個大小為$n_{\mbox{neurons}}$的向量,包含所有神經元的偏置項。

可以看到,$Y_{(t)}$是關於$X_{(t)}$和$Y_{(t-1)}$的函數,$Y_{(t-1)}$又是關於$X_{(t-1)}$和$Y_{(t-2)}$的函數,等等。這使得$Y_{(t)}$其實是關於$X_{(0)},X_{(1)},\cdots ,X_{(t)}$的函數。

14.1.1 Memory Cells

由於神經網絡在第$t$個time step的輸出是一個關於前$t$個time step所有輸入的函數,這可以理解為一種形式的記憶(memory)。神經網絡中保存前面時刻狀態的部分稱為memory cell(或者簡單稱為cell)。一個單獨的周期神經元,或者一層周期神經元,就是一個很基礎的cell。隨后我們會看到更加復雜和強大的cell。

一般一個cell在時刻$t$(姑且把time step稱作時刻把,不然太麻煩)的狀態,記做$\textbf{h}_{(t)}$(“h”代表“hidden”),這是一個關於當前時刻輸入和前一時刻狀態的函數:$\textbf{h}_{(t)} = f(\textbf{h}_{(t-1)}, \textbf{x}_t)$。在$t$時刻的輸出,記做$\textbf{y}_{(t)}$,這也是一個關於當前時刻輸入和前一時刻狀態的函數。在前面討論的基本cell中,狀態和輸出是一致的,但在復雜的模型中這是不一致的,如圖14-3。

圖14-3 一個cell的隱狀態可能與它的輸出不一致

14.1.2 輸入和輸出序列

RNN可以一個序列作為輸入,再同時輸出一個序列(如圖14-4左上)。該模型可用於股價預測,輸入前$N$天的股價,輸出每一天的股價,知道第$N+1$天。每增加一天的輸入,就預測下一天的輸出。

此外,還可以序列作為輸入,忽略除了最后一個之外所有的輸出(如圖右上)。例如用於情感分析,可以將電影評論作為輸入,輸出情感分值。

相反的,也可以輸入單一的樣本,輸出一個序列(如圖左下)。例如,輸入可以是一幅圖像,輸出是該圖像的標題。

最后,右下角的神經網絡就是一個翻譯系統了。這是序列到向量神經網絡(稱為encoder)和向量到序列神經網絡(稱為decoder)的組合。比如,輸入可以是一種語言的一句話,encoder將這句話轉換為向量表示,decoder再把這個向量表示轉換成另一種語言的一句話。這是一個two-step模型,稱為Encoder–Decoder,執行翻譯任務時,效果比一個序列序列的神經網絡好得多。因為原文的最后一個詞可能會影響譯文的第一個詞,所以需要讀完全句后再進行翻譯。

 

圖14-4 序列到序列(左上),序列到向量(右上),向量到序列(左下),延時的序列到序列(右下)

14.2 基本RNN的TensorFlow實現

首先,我們來實現一個很簡單的RNN模型,不使用TensorFlow的任何運算,以便了解底層原理。我們會創建一層有5個訓練神經元的RNN(如圖14-2),使用tanh激活函數。假設這一RNN有兩個時刻,每一時刻的輸入是大小為3的向量。下面的代碼創建這一RNN,並隨時間展開:

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()

輸入訓練數據並運行:

import numpy as np

# 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})

這一mini-batch有4個實例,每個實例都是包含兩個輸入的序列。最后Y0_val和Y1_val包含神經網絡在兩個時刻關於所有實例的輸出:

>>> print(Y0_val) # output at t = 0
[[-0.2964572 0.82874775 -0.34216955 -0.75720584 0.19011548] # instance 0
[-0.12842922 0.99981797 0.84704727 -0.99570125 0.38665548] # instance 1
[ 0.04731077 0.99999976 0.99330056 -0.999933 0.55339795] # instance 2
[ 0.70323634 0.99309105 0.99909431 -0.85363263 0.7472108 ]] # instance 3
>>> print(Y1_val) # output at t = 1
[[ 0.51955646 1. 0.99999022 -0.99984968 -0.24616946] # instance 0
[-0.70553327 -0.11918639 0.48885304 0.08917919 -0.26579669] # instance 1
[-0.32477224 0.99996376 0.99933046 -0.99711186 0.10981458] # instance 2
[-0.43738723 0.91517633 0.97817528 -0.91763324 0.11047263]] # instance 3

下面我們看一下,如何使用TensorFlow的RNN運算來實現相同的模型。

14.2.1 隨時間靜態展開

static_rnn()函數可以創建一個展開的RNN。以下代碼可創建與先前相同的模型:

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

首先我們創建了輸入占位符,然后是BasicRNNCell,可以將其看作cell工廠。static_rnn()函數調用cell工廠的__call__()函數,為每一時刻創建一個cell,並共享權重和偏置項。static_rnn()返回兩個對象,第一個是包含每一時刻輸出張量的Python list,另一個是整個網絡最終的狀態。由於我們使用了最基本的cell,最終的狀態其實與第二時刻的輸出是一致的。

如果有50個時刻,操作50個輸入占位符和50個輸出張量實在太繁瑣了,需要簡化這一過程。下面的代碼創建同樣的模型,但輸出占位符的形狀是[None, n_steps, n_inputs],第一個維度是mini-batch的尺寸。X_seqs是一個大小為n_steps的Python list,該list每個元素都是形狀為[None, n_inputs]的張量,第一維同樣是mini-batch尺寸。為了得到X_seqs,我們首先使用transpose()轉置函數交換前兩個維度,轉置之后時刻就位於第一維度了。然后使用unstack()關於第一維度提取張量list。隨后的兩行與之前一樣。最后再將輸出轉換成一個形狀為[None, n_steps, n_neurons]的張量。

X = tf.placeholder(tf.float32, [None, n_steps, n_inputs])
X_seqs = tf.unstack(tf.transpose(X, perm=[1, 0, 2]))

basic_cell = tf.contrib.rnn.BasicRNNCell(num_units=n_neurons)
output_seqs, states = tf.contrib.rnn.static_rnn(basic_cell, X_seqs, dtype=tf.float32)

outputs = tf.transpose(tf.stack(output_seqs), perm=[1, 0, 2])

然后輸入訓練數據,運行這一網絡:

X_batch = np.array([
    # t = 0      t = 1
    [[0, 1, 2], [9, 8, 7]], # instance 0
    [[3, 4, 5], [0, 0, 0]], # instance 1
    [[6, 7, 8], [6, 5, 4]], # instance 2
    [[9, 0, 1], [3, 2, 1]], # instance 3
])
with tf.Session() as sess:
    init.run()
    outputs_val = outputs.eval(feed_dict={X: X_batch})

最終的outputs_val是一個包含所有實例、任一時刻、所有神經元的輸出的張量。

然而,這一過程所創建的圖仍然是每一時刻包含一個cell。如果有50個時刻,這個圖看起來就很丑陋。這就像是寫程序而不使用循環(比如Y0=f(0, X0); Y1=f(Y0, X1); Y2=f(Y1, X2); ...;Y50=f(Y49, X50))。圖這么大,在反向傳播時也很容易造成內存溢出(尤其是運行與GPU內存時),因為需要記錄前向傳播時每層的所有輸出,以便反向傳播時計算梯度。

幸運的是,還有更好的解決方案,那就是dynamic_rnn()函數。

14.2.2 隨時間動態展開

dynamic_rnn()函數通過while_loop()對cell運算適當地次數。還可以設置swap_memory=True,在反向傳播時交換GPU內存和CPU內存來防止OOM錯誤。更方便的是,它可以接受形狀為[None, n_steps, n_inputs])的張量,這就不需要stack,unstack,以及transpose。下面簡潔的代碼實現了相同的模型:

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)

14.2.3 處理變長輸入序列

前面我們使用的輸入序列都是定長的(都是兩個時刻),如果輸入序列是變長的呢(比如句子)?這樣的話,在調用dynamic_rnn()(或者static_rnn())時,就要使用sequence_length參數了。這是一個1D張量,指明了每個實例的序列長度。例如:

seq_length = tf.placeholder(tf.int32, [None])

[...]
outputs, states = tf.nn.dynamic_rnn(basic_cell, X, dtype=tf.float32, sequence_length=seq_length)

假設我們的第二個實例只有一個時刻的輸入,表示該實例張量的第二維需要補零,如下所示:

X_batch = np.array([
    # step 0 step 1
    [[0, 1, 2], [9, 8, 7]], # instance 0
    [[3, 4, 5], [0, 0, 0]], # instance 1 (padded with a zero vector)
    [[6, 7, 8], [6, 5, 4]], # instance 2
    [[9, 0, 1], [3, 2, 1]], # instance 3
])
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})

14.2.4 處理變長輸出

如果輸出序列是變長的怎么辦呢?如果你預先知道輸出序列的長度(比如輸出序列與輸入序列等長),那就可以像先前那樣定義一個類似的sequence_length參數。不幸的是,一般無法預測輸出的序列長度。這種情況下,最常見的做法是定義一個end-of-sequence token (EOS token)的特殊輸出(這將后面自然語言處理的小節進行討論)。

14.3 訓練模型

訓練RNN,技巧就是隨時間展開,然后應用常規的反向傳播(如圖14-5)。這一策略稱作隨時間反向傳播(backpropagation through time,BPTT)。

圖14-5 隨時間反向傳播

 

和常規的反向傳播類似,首先展開神經網絡前向傳播(如上圖虛箭頭所示),然后使用損失函數$C(Y_{(t_{min})},Y_{(t_{min} + 1)},\cdots,Y_{(t_{max})})$(其中,$t_{min}$、$t_{max}$是第一個和最后一個輸出,並且不計算被忽略的輸出)對輸出進行評估。最后使用梯度更新參數。上圖中,損失函數用到了$Y_{(2)}$、$Y_{(3)}$、$Y_{(4)}$三個輸出,並沒有使用$Y_{(0)}$、$Y_{(1)}$。

14.3.1 訓練一個序列分類器 

我們來訓練一個RNN對MNIST圖片進行分類。雖然CNN更適合做圖像分類,這里只是使用這個例子來熟悉RNN。可以將MNIST中的每一個圖像都看作是28行的序列,每一行又有28個像素點。我們使用150個循環神經元,加上一個全連接層,與輸出層連接。最后是softmax層。如圖14-6:

圖14-6 序列分類器

構建過程是很直接的,並且和第十章的MNIST分類器很相似,只是用RNN的展開替換掉了之前的隱層。與輸出層進行全連接的是states張量,只包含最后一個時刻的輸出。$y$是目標類別的占位符。

from tensorflow.contrib.layers import fully_connected

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 = fully_connected(states, n_outputs, activation_fn=None)
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

接着是模型的訓練,這與第十章是類似的,只不過要改下訓練數據的形狀:

n_epochs = 100
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)

14.3.2 訓練時序數據

 這次我們處理時序數據,比如股價、氣溫、腦電波等等。每一個訓練實例都是從時序中隨機選出20個時刻(如圖14-7左側)。目標序列與訓練序列是相同的,只不是目標序列始終比訓練序列晚一個時刻(如圖14-7右側,最下角的實心籃圈,是$t_0$時刻的輸入。緊接着的空心籃圈,是$t_1$時刻的輸入同時也是$t_0$時刻的目標值。依此類推)。

圖14-7 時序數據(左),從時序中選出的一個實例(右)

首先,我們來創建RNN。它包含100個循環神經元,並展開為20個時刻。每個時刻的輸入只有一個特征。目標值也是同樣的20個時刻。代碼如下:

n_steps = 20
n_inputs = 1
n_neurons = 100
n_outputs = 1

X = tf.placeholder(tf.float32, [None, n_steps, n_inputs])
y = tf.placeholder(tf.float32, [None, n_steps, n_outputs])
cell = tf.contrib.rnn.BasicRNNCell(num_units=n_neurons, activation=tf.nn.relu)
outputs, states = tf.nn.dynamic_rnn(cell, X, dtype=tf.float32)

一般情況下,每一時刻的輸入可能不止一個特征。比如,進行股價預測時,可能還會使用專家評級等信息,來提高預測准確性。我們這里是對模型進行了簡化。

在這個模型中,每一時刻都會輸出一個大小為100的向量。但我們需要在每一時刻的輸出是個標量。最簡單的解決方案是使用OutputProjectionWrapper,將cell封裝起來。OutputProjectionWrapper在每一時刻的輸出之上增加一層全連接的線性神經元(比如不使用激活函數),而且不會影響cell狀態。所有這些全連接層共享同樣的權重的偏置項(可訓練),如圖14-8:

圖14-8 使用輸出投影的RNN cells

封裝cell很容易,簡單改變之前的代碼即可:

cell = tf.contrib.rnn.OutputProjectionWrapper(
    tf.contrib.rnn.BasicRNNCell(num_units=n_neurons, activation=tf.nn.relu),
    output_size=n_outputs)

然后就是定義損失函數,創建Adam優化器,等等:

learning_rate = 0.001

loss = tf.reduce_mean(tf.square(outputs - y))
optimizer = tf.train.AdamOptimizer(learning_rate=learning_rate)
training_op = optimizer.minimize(loss)

init = tf.global_variables_initializer()

接着就可以執行了:

n_iterations = 10000
batch_size = 50

with tf.Session() as sess:
    init.run()
    for iteration in range(n_iterations):
        X_batch, y_batch = [...] # fetch the next training batch
        sess.run(training_op, feed_dict={X: X_batch, y: y_batch})
        if iteration % 100 == 0:
            mse = loss.eval(feed_dict={X: X_batch, y: y_batch})
            print(iteration, "\tMSE:", mse)

程序的輸出如下:

0   MSE: 379.586
100 MSE: 14.58426
200 MSE: 7.14066
300 MSE: 3.98528
400 MSE: 2.00254
[...]

訓練好之后,就可以預測了:

X_new = [...] # New sequences
y_pred = sess.run(outputs, feed_dict={X: X_new})

圖14-9顯示了迭代訓練1000次之后的預測序列:

圖14-9 時序預測

雖然OutputProjectionWrapper是一種解決方案,但還有一種更高效的方案:首先將RNN的輸出從[batch_size, n_steps, n_neurons]轉換成[batch_size * n_steps, n_neurons],然后使用一個全連接層給下恰當的輸出個數(在我們的例子中,輸出1個標量)。 然后再將計算結果從[batch_size * n_steps, n_outputs]轉換回[batch_size, n_steps, n_outputs],如圖14-10所示:

圖14-10 堆疊所有輸出,應用投影,最后再展開

為實現這一方案,我們首先將代碼還原成基本的cell,不再使用OutputProjectionWrapper:

cell = tf.contrib.rnn.BasicRNNCell(num_units=n_neurons, activation=tf.nn.relu)
rnn_outputs, states = tf.nn.dynamic_rnn(cell, X, dtype=tf.float32)

然后就是改變RNN輸出數據的形狀,做映射,再改變回原來的形狀:

stacked_rnn_outputs = tf.reshape(rnn_outputs, [-1, n_neurons])
stacked_outputs = fully_connected(stacked_rnn_outputs, n_outputs, activation_fn=None)
outputs = tf.reshape(stacked_outputs, [-1, n_steps, n_outputs])

剩下的代碼就和之前的一樣了。

14.3.3 創造性的RNN 

既然我們有了一個可以預測未來的模型,當然也可以用它來產生一些創造性的東西,正如本章開頭所提到的。我們只需提供一個包含n_steps個值的種子序列(該種子序列可以全是0), 模型就可以預測下一時刻值。將預測出的值在作為輸入,又能得到一個預測值,如此循環下去。代碼如下:

sequence = [0.] * n_steps
for iteration in range(300):
    X_batch = np.array(sequence[-n_steps:]).reshape(1, n_steps, 1)
    y_pred = sess.run(outputs, feed_dict={X: X_batch})
    sequence.append(y_pred[0, -1, 0])

可以得到一個新的時序,並且與原來的時序有相似之處,如圖14-11

圖14-11 創造性的序列,左側種子是0,右側種子是一個實例

本文較長,剩下的將寫在一篇新的博客中。

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM