CHAPTER 14 -Recurrent Neural Networks

本篇文章是個人翻譯的,如有商業用途,請通知本人謝謝.

Recurrent Neurons  

到目前爲止,我們主要關注的是前饋神經網絡,其中激活僅從輸入層到輸出層的一個方向流動(附錄E中的幾個網絡除外)。 循環神經網絡看起來非常像一個前饋神經網絡,除了它也有連接指向後方。 讓我們看一下最簡單的RNN,它由一個神經元接收輸入,產生一個輸出,並將輸出發送回自己,如圖14-1(左)所示。 在每個時間步t(也稱爲一個幀),這個循環神經元接收輸入x(t)以及它自己的前一時間步長y(t-1)的輸出。 我們可以用時間軸來表示這個微小的網絡,如圖14-1(右)所示。 這被稱爲隨着時間的推移展開網絡。


您可以輕鬆創建一個復發神經元層。 在每個時間步t,每個神經元都接收輸入向量x(t)和前一個時間步y(t-1)的輸出向量,如圖14-2所示。 請注意,輸入和輸出都是矢量(當只有一個神經元時,輸出是一個標量)。


每個循環神經元有兩組權重:一組用於輸入x(t),另一組用於前一時間步長y(t-1)的輸出。 我們稱這些權重向量爲wx和wy。如方程14-1所示(b是偏差項,φ(·)是激活函數,例如ReLU),可以計算單個循環神經元的輸出。


就像前饋神經網絡一樣,我們可以使用上一個方程的向量化形式,對整個小批量計算整個層的輸出(見公式14-2)。



  • 是一個m×n(神經元)矩陣,包含在最小批次中每個實例在時間步t處的層輸出(m是最小批次中的實例數,n(神經元)是神經元)。
  • 是包含所有實例的輸入的m×n(輸入)矩陣(n(輸入)是輸入特徵的數量)。
  • 是包含當前時間步的輸入的連接權重的n(輸入)×n(神經元)矩陣。
  • 是包含當前時間步的輸出的連接權重的n(輸入)×n(神經元)矩陣。
  • 權重矩陣Wx和Wy通常連接成形狀(n(輸入)+ n(神經元))×n(神經元)的單個權重矩陣W(見等式14-2的第二行)
  • b是包含每個神經元的偏置項的大小爲n(神經元)的向量。

注意,Y(t)是X(t)和Y(t - 1)的函數,它是X(t - 1)和Y(t - 2)的函數,它是X(t - 2)和Y(t - 3)的函數,等等。 這使得Y(t)是從時間t = 0(即X(0),X(1),...,X(t))開始的所有輸入的函數。 在第一個時間步,t = 0,沒有以前的輸出,所以它們通常被假定爲全零。


Memory Cells 

由於時間t的循環神經元的輸出是由所有先前時間步驟計算來的的函數,你可以說它有一種記憶形式。一個神經網絡的一部分,跨越時間步長保留一些狀態,稱爲存儲單元(或簡稱爲單元)。單個循環神經元或循環神經元層是非常基本的單元,但本章後面我們將介紹一些更爲複雜和強大的單元類型。

一般情況下,時間步t處的單元狀態,記爲h(t)(“h”代表“隱藏”),是該時間步的某些輸入和前一時間步的狀態的函數:h(t) = f(h(t-1),x(t))。 其在時間步t處的輸出,表示爲y(t),也和前一狀態和當前輸入的函數有關。 在我們已經討論過的基本單元的情況下,輸出等於單元狀態,但是在更復雜的單元中並不總是如此,如圖14-3所示。




Input and Output Sequences (輸入和輸出序列)

RNN可以同時進行一系列輸入併產生一系列輸出(見圖14-4,左上角的網絡)。 例如,這種類型的網絡對於預測時間序列(如股票價格)非常有用:您在過去的N天內給出價格,並且它必須輸出向未來一天移動的價格(即從N - 1天 前到明天)。


或者,您可以向網絡輸入一系列輸入,並忽略除最後一個之外的所有輸出(請參閱右上角的網絡)。 換句話說,這是一個向量網絡的序列。 例如,您可以向網絡提供與電影評論相對應的單詞序列,並且網絡將輸出情感評分(例如,從-1 [仇恨]到+1 [愛情])。


相反,您可以在第一個時間步中爲網絡提供一個輸入(而在其他所有時間步中爲零),然後讓它輸出一個序列(請參閱左下角的網絡)。 這是一個矢量到序列的網絡。 例如,輸入可以是圖像,輸出可以是該圖像的標題。


最後,你可以有一個序列到矢量網絡,稱爲編碼器,後面跟着一個稱爲解碼器的矢量到序列網絡(參見右下角的網絡)。 例如,這可以用於將句子從一種語言翻譯成另一種語言。 你會用一種語言給網絡喂一個句子,編碼器會把這個句子轉換成單一的矢量表示,然後解碼器將這個矢量解碼成另一種語言的句子。 這種稱爲編碼器 - 解碼器的兩步模型,比用單個序列到序列的RNN(如左上方所示的那個)快速地進行翻譯要好得多,因爲句子的最後一個單詞可以 影響翻譯的第一句話,所以你需要等到聽完整個句子才能翻譯。


Basic RNNs in TensorFlow
首先,我們來實現一個非常簡單的RNN模型,而不使用任何TensorFlow的RNN操作,以更好地理解發生了什麼。 我們將使用tanh激活函數創建一個由5個復發神經元層組成的RNN(如圖14-2所示的RNN)。 我們將假設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()


這個網絡看起來很像一個雙層前饋神經網絡,有一些改動:首先,兩個層共享相同的權重和偏差項,其次,我們在每一層都有輸入,並從每個層獲得輸出。 爲了運行模型,我們需要在兩個時間步中都有輸入,如下所示:

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

這個小批量包含四個實例,每個實例都有一個由兩個輸入組成的輸入序列。 最後,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


這並不難,但是當然如果你想能夠運行100多個時間步驟的RNN,這個圖形將會非常大。 現在讓我們看看如何使用TensorFlow的RNN操作創建相同的模型。

完整代碼

import numpy as np
import tensorflow as tf


if __name__ == '__main__':
    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 Unrolling Through Time 

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,您可以將其視爲一個工廠,創建單元的副本以構建展開的RNN(每個時間步一個)。 然後我們調用static_rnn(),給它的單元工廠和輸入張量,並告訴它輸入的數據類型(這是用來創建初始狀態矩陣,默認情況下是滿零)。 static_rnn()函數爲每個輸入調用單元工廠的__call __()函數,創建單元格的兩個拷貝(每個單元包含一個5個循環神經元層),並具有共享的權重和偏置項,並且像前面一樣。 static_rnn()函數返回兩個對象。 第一個是包含每個時間步的輸出張量的Python列表。 第二個是包含網絡最終狀態的張量。 當你使用基本的單元格時,最後的狀態就等於最後的輸出。

如果有50個時間步長,則不得不定義50個輸入佔位符和50個輸出張量。而且,在執行時,您將不得不爲50個佔位符中的每個佔位符輸入數據並且還要操縱50個輸出。我們來簡化一下。下面的代碼再次構建相同的RNN,但是這次它需要一個形狀爲[None,n_steps,n_inputs]的單個輸入佔位符,其中第一個維度是最小批量大小。然後提取每個時間步的輸入序列列表。 X_seqs是形狀n_steps張量的Python列表[None,n_inputs],其中第一個維度再次是最小批量大小。爲此,我們首先使用轉置()函數交換前兩個維度,以便時間步驟現在是第一維度。然後,我們使 unstack()函數沿第一維(即每個時間步的一個張量)提取張量的Python列表。接下來的兩行和以前一樣。最後,我們使用stack()函數將所有輸出張量合併成一個張量,然後我們交換前兩個維度得到形狀的最終輸出張量[None, n_steps,n_neurons](第一個維度是mini-批量大小)。

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

現在我們可以通過給它提供一個包含所有minibatch序列的張量來運行網絡:

X_batch = np.array([
        # t = 0      t = 1 
        [[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})
我們得到所有實例,所有時間步長和所有神經元的單一outputs_val張量:


但是,這種方法仍然會建立一個每個時間步包含一個單元的圖。 如果有50個時間步,這個圖看起來會非常難看。 這有點像寫一個程序而沒有使用循環(例如,Y0 = f(0,X0); Y1 = f(Y0,X1); Y2 = f(Y1,X2); ...; Y50 = f Y49,X50))。 如果使用大圖,在反向傳播期間(特別是在GPU卡內存有限的情況下),您甚至可能會發生內存不足(OOM)錯誤,因爲它必須在正向傳遞期間存儲所有張量值,因此可以使用它們來計算 梯度在反向通過。

幸運的是,有一個更好的解決方案:dynamic_rnn()函數。


Dynamic Unrolling Through Time 

dynamic_rnn()函數使用while_loop()操作在單元格上運行適當的次數,如果要在反向傳播期間將GPU內存交換到CPU內存,可以設置swap_memory = True,以避免內存不足錯誤。 方便的是,它還可以在每個時間步(形狀[None,n_steps,n_inputs])接受所有輸入的單張量,並且在每個時間步(形狀[None,n_steps,n_neurons])上輸出所有輸出的單張量。 沒有必要堆疊,拆散或轉置。 以下代碼使用dynamic_rnn()函數創建與之前相同的RNN。 這太好了!

完整代碼

import numpy as np
import tensorflow as tf
import pandas as pd

if __name__ == '__main__':
    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)
    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)

在反向傳播期間,while_loop()操作會執行相應的步驟:在正向傳遞期間存儲每次迭代的張量值,以便在反向傳遞期間使用它們來計算梯度。


Handling Variable Length Input Sequences
到目前爲止,我們只使用固定大小的輸入序列(全部正好兩個步長)。 如果輸入序列具有可變長度(例如,像句子)呢? 在這種情況下,您應該在調用dynamic_rnn()(或static_rnn())函數時設置sequence_length參數; 它必須是一維張量,表示每個實例的輸入序列的長度。 例如:

n_steps = 2
n_inputs = 3
n_neurons = 5

reset_graph()

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])
當然,您現在需要爲兩個佔位符X和seq_length提供值:
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})
現在,RNN輸出序列長度的每個時間步都會輸出零矢量(查看第二個時間步的第二個輸出):


此外,狀態張量包含每個單元的最終狀態(不包括零向量):


Handling Variable-Length Output Sequences(處理可變長度輸出序列)

如果輸出序列長度不一樣呢? 如果事先知道每個序列的長度(例如,如果知道長度與輸入序列的長度相同),那麼可以按照上面所述設置sequence_length參數。 不幸的是,通常這是不可能的:例如,翻譯後的句子的長度通常與輸入句子的長度不同。 在這種情況下,最常見的解決方案是定義一個稱爲序列結束標記(EOS標記)的特殊輸出。 任何通過EOS的輸出應該被忽略(我們將在本章稍後討論)。

好,現在你知道如何建立一個RNN網絡(或者更準確地說是一個隨着時間的推移而展開的RNN網絡)。 但是你怎麼訓練呢?


Training RNNs 

爲了訓練一個RNN,訣竅是通過時間展開(就像我們剛剛做的那樣),然後簡單地使用常規反向傳播(見圖14-5)。 這個策略被稱爲反向傳播(BPTT)。


就像在正常的反向傳播中一樣,展開的網絡(用虛線箭頭表示)有第一個正向傳遞。 那麼使用成本函數評估輸出序列(其中tmin和tmax是第一個和最後一個輸出時間步長,不計算忽略的輸出),並且該損失函數的梯度通過展開的網絡向後傳播(由固體箭頭); 最後使用在BPTT期間計算的梯度來更新模型參數。 請注意,梯度在損失函數所使用的所有輸出中反向流動,而不僅僅通過最終輸出(例如,在圖14-5中,損失函數使用網絡的最後三個輸出Y(2), Y(3)和Y(4),所以梯度流經這三個輸出,但不通過Y(0)和Y(1))。 而且,由於在每個時間步驟使用相同的參數W和b,所以反向傳播將做正確的事情並且總結所有時間步驟。


Training a Sequence Classifer (訓練序列分類器)

我們訓練一個RNN來分類MNIST圖像。 卷積神經網絡將更適合於圖像分類(見第13章),但這是一個你已經熟悉的簡單例子。 我們將把每個圖像視爲28行28像素的序列(因爲每個MNIST圖像是28×28像素)。 我們將使用150個循環神經元的細胞,再加上一個完整的連接圖層,其中包含連接到上一個時間步的輸出的10個神經元(每個類一個),然後是一個softmax層(見圖14-6)。


建模階段非常簡單, 它和我們在第10章中建立的MNIST分類器幾乎是一樣的,只是展開的RNN替換了隱藏的層。 注意,完全連接的層連接到狀態張量,其僅包含RNN的最終狀態(即,第28個輸出)。 另請注意,y是目標類的佔位符。

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
現在我們準備訓練RNN了。 執行階段與第10章中MNIST分類器的執行階段完全相同,不同之處在於我們在將每個訓練的批量喂到網絡之前要重新調整。

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)
輸出應該是這樣的:


我們獲得了超過98%的準確性 - 不錯! 另外,通過調整超參數,使用He初始化初始化RNN權重,更長時間訓練或添加一些正則化(例如,丟失),您肯定會獲得更好的結果。

您可以通過將其構造代碼包裝在一個變量範圍內(例如,使用variable_scope(“rnn”,initializer = variance_scaling_initializer())來使用He初始化)來爲RNN指定初始化器。


Training to Predict Time Series 

現在讓我們來看看如何處理時間序列,如股價,氣溫,腦電波模式等等。 在本節中,我們將訓練一個RNN來預測生成的時間序列中的下一個值。 每個訓練實例是從時間序列中隨機選取的20個連續值的序列,目標序列與輸入序列相同,除了向後移動一個時間步(參見圖14-7)。

首先,我們來創建一個RNN。 它將包含100個經常性的神經元,並且我們將在20個時間步驟中展開它,因爲每個訓練實例將是20個輸入長。 每個輸入將僅包含一個特徵(在該時間的值)。 目標也是20個輸入序列,每個輸入包含一個值。 代碼與之前幾乎相同:


一般來說,你將不只有一個輸入功能。 例如,如果您試圖預測股票價格,則您可能在每個時間步驟都會有許多其他輸入功能,例如競爭股票的價格,分析師的評級或可能幫助系統進行預測的任何其他功能。

在每個時間步,我們現在有一個大小爲100的輸出向量。但是我們實際需要的是每個時間步的單個輸出值。 最簡單的解決方法是將單元格包裝在OutputProjectionWrapper中。 單元格包裝器就像一個普通的單元格,代理每個方法調用一個底層單元格,但是它也增加了一些功能。 Out putProjectionWrapper在每個輸出之上添加一個完全連接的線性神經元層(即沒有任何激活函數)(但不影響單元狀態)。 所有這些完全連接的層共享相同(可訓練)的權重和偏差項。 結果RNN如圖14-8所示。

包裝細胞是相當容易的。 讓我們通過將BasicRNNCell包裝到OutputProjectionWrapper中來調整前面的代碼:


包裝細胞是相當容易的。 讓我們通過將BasicRNNCell包裝到OutputProjectionWrapper中來調整前面的代碼:

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

到現在爲止還挺好。 現在我們需要定義成本函數。 我們將使用均方誤差(MSE),就像我們在之前的迴歸任務中所做的那樣。 接下來,我們將像往常一樣創建一個Adam優化器,訓練操作和變量初始化操作:








發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章