1. 程式人生 > 實用技巧 >迴圈神經網路(RNN)

迴圈神經網路(RNN)

1 引言

在此前的多篇部落格中,我花了很大的精力研究卷積神經網路相關內容,見證了卷積網路從最初的LeNet一步步發展到ResNet。在深度學習領域,卷積網路佔據了半壁江山,例如在影象分類、目標檢測等應用中, 卷積神經網路所取得的成就遠不是其他演算法模型可以比擬的。然而,不得不承認,卷積神經網路說擅長的領域資料大多是靜態的,輸入資料間相互獨立,例如影象這種二維的資料,而對於語義識別,時間序列分析等應用,卷積神經網路就稍顯不足。這是因為語義識別、時間序列等等應用中,資料呈序列狀,前後輸入相互關聯,卷積神經網路在這些應用中難以兼顧資料間的關聯關係,導致效能變差。
幸運的是,在神經網路演算法家族中,也有一類演算法專門用於處理此類序列資料,那就是本篇的主角——迴圈神經網路。迴圈神經網路(Recurrent Neural Network,RNN)是一種別具一格的網路模型,其迴圈元節點不僅可以接上來自上層的輸入資料,也可以接收自身上一次迭代的輸出,基於這種特殊的結構,迴圈神經網路擁有了短期記憶能力,通過“記憶”儲存了資料間的關聯關係,所以尤為適合處理語言、文字、視訊等時序相關的資料。接下來,我們來具體剖析迴圈神經網路的特殊結構。

2 迴圈神經網路

2.1 為什麼需要RNN

在前面引言中對RNN誕生的意義說的不夠淺顯,這裡舉個例子來說。語義識別,文字分類是迴圈神經網路的重點應用領域,當然,並不是說其他的神經網路演算法在這些領域就毫無作用,只是效果較差而已。加入有下面一句話:
我是中國人,我會說 _

對於這句話,我想無論是一般的升級網路模型還是迴圈神經網路模型,都一顆預測出,橫線上的內容應該是“漢語”。

繼續,我們把這句話說完整:
我是中國人,我會說漢語,但是我成年後移民到美國,所以我也會說_

在一般的神經網路模型中,會孤立地分析每一個詞,因為句子中同時出現了中國和美國,所以預測得到的詞是“漢語”和“英語”概率相仿,但是在迴圈神經網路中,模型具有一定的短期記憶功能,能夠根據上下文進行語義的預測,所以在較的位置上的“美國”一詞有著更大的影響,最終預測結果更有可能是“英語”。這就是為什麼迴圈神經網路在序列相關應用中表現優異的原因。

2.2 單向迴圈神經網路結構

與前饋神經網路結構相比,迴圈神經網路最大的不同在於其迴圈神經網路輸出不僅能夠往下一層傳播,也能夠傳遞給同層下一時刻,也就是說,對於迴圈網路中某一神經元節點,其內部運算資料不僅包含上一層的輸出,同時也包含同層上一時刻的輸出。如下圖所示:

上圖是一個簡化的迴圈網路結構,只包含了一個隱藏層,圖中,$X$我們可以理解為一個完整的時間序列(例如一句語義完整的文字),$x_t$是指的$t$個時刻的測度(文字中的第$t$個詞的向量表示),$h_t$是第$t$個時刻的輸出,也就是$x_t$和上一時刻的輸出$h_t$在隱藏層中合併運算後的輸出。同時可以看到,輸出的$h_t$流向輸出層的同時,還有流向了延遲期,留待第$t+1$次與$x_{t+1}$共同參與隱藏層運算。對於上圖,按時間順序(在時間維度)上展開,如下圖所示:

注意,上圖是在時間維度上進行展開,也就是上圖三個部分其實是同一網路(而不是三個部分共同組成一個網路),而是在同一個網路在不同時間順序上的展開,我剛接觸迴圈神經網路看到這個圖時,以為多個隱藏層是隱藏層的多個節點,$h$的值不過是在同層節點間進行傳遞,這是錯誤的。
圖中,$U$和$W$分別是$x$和$h$的權值,所以,在隱藏層節點內的運算可以如下表示:
$$h_t=f(U \times x_t + w \times h_{t-1} + b) \tag{1} $$

式中,$f$是啟用函式,$b$是偏置。可見,在計算第$t$時刻輸出$h_t$時,上一時刻的輸出狀態$h_{t-1}$也參與了運算,對$h_{t-1}$也有:
$$h_{t-1}=f(U \times x_{t-1} + w \times h_{t-2} + b) \tag{2} $$ 將(1)式和(2)式結合,也就有: $$h_t=f(U \times x_t + w \times f(U \times x_{t-1} + w \times h_{t-2} + b) + b) \tag{3} $$ 通過這種方式,第$t$時刻的輸出與之前時刻的輸出關聯起來,也就有了之前的“記憶”。
在我理解看來,迴圈神經網路與卷積等前饋神經網路最大的不同就在於多了一個時間維度,這就要求迴圈神經網路中的輸入是 有序的(順序對最終結果有影響)。以對一個句子進行情感分類為例,假設這個句子有10個詞,每個詞表示為長度為100向量(假設為$X=[x_1,…,x_{10}]$),如果是在前饋神經網路,就回將[10 * 100]的向量$X=[x_1,…,x_{10}]$一次性傳遞給網路,而在迴圈網路中,第一次是將第一個詞的對應的長度為100的向量傳遞給網路,經過第一個迴圈時,神經元節點接收到兩個輸入,一個是詞向量$x_1$,另一個是初始狀態$h_0$(一般初始狀態為全為0的向量),運算後輸出為$h_1$,$h_1$輸出到下一層的同時,也就等待下一個$x_2$的輸入然後一同進行運算產生$h_2$……

有一點必須注意,在RNN迴圈層中,同一層的引數$U$、$W$和$b$,甚至包括啟用函式$f$,是在不同時刻是完全共享的,這種特性大大減少了迴圈神經網路的引數量。其實在我理解看來,與其說引數共享,倒不如說不同時刻的資料使用的就是相同的神經元。

2.3 雙向迴圈網路結構

在單向迴圈網路中,輸出取決於當前的輸入和之前時刻的“記憶”,但在有些應用場景中,最終的決策不僅受之前的“記憶”的影響,還需要綜合考慮之後事件的發展,這就類似於做英文閱讀理解,最終的答案需要我們找準對應的句子(當前時刻的輸入$x_t$)還要綜合前文($h_{t-1}$)和下文($h_{t+1}$)進行作答。在迴圈神經網路家族有,有一種雙向迴圈網路結構(Bidirectional Recurrent Neural Network,Bi-RNN)就專用於此類場景。

雙向迴圈神經網路每一層迴圈運算可以拆分為兩層,這兩層網路都輸入序列$X$,但是資訊傳遞方向相反。如下圖所示:

圖中迴圈運算公式如下:

第一行是順序運算,第二行是逆序運算,第三行是將兩種順序的輸出狀態進行組合。

3 TensorFlow2實現迴圈神經網路

3.1 單層節點

In[1]:
import tensorflow as tf

假設有4個句子,每個句子有80個詞彙,每個詞彙表示為一個100維向量(有4個時間序列資料,每個序列有80個時間測度,每個測度有100個屬性值),我們隨機初始化來模擬這種資料結構:

In[2]:
x = tf.random.normal([4, 80, 100])

建立一個RNN神經元:

In[9]:
cell = tf.keras.layers.SimpleRNNCell(64)  # 64的意思是經過神經元后,維度轉化為64維,[b, 100] -> [b, 64],b是句子數量

初始化最初時刻的狀態,因為有4個句子(4個batch),且輸出為64維,所以最初的狀態初始化為[4, 64],值全為零的矩陣。

In[10]:
ht0 = tf.zeros([4, 64])  # 因為
In[6]:
xt0 = x[:, 0, :]
In[18]:
out, ht1 = cell(xt0, [h0])

out是傳遞給下一層的值,ht1經過一次神經元后的轉檯。我們看看這兩者的狀態:

In[22]:
out.shape, ht1[0].shape
Out[22]:
(TensorShape([4, 64]), TensorShape([4, 64]))

輸出為[4, 64]的矩陣,確實如我們上面所指定的一樣。繼續:

In[23]:
id(out), id(ht1[0])
Out[23]:
(140525368605520, 140525368605520)

可以看到,id是一樣的,也就是說,這兩者其實就是同一個物件。正如前文所說,RNN神經元有兩個相同的輸出,一個傳遞到下一層節點,也就是out,一個傳遞給下一時間節點迴圈,也就是ht1[0]。

3.2 多層節點

同樣,我們先模擬構造一些資料

In[25]:
x = tf.random.normal([4, 80, 100])
xt0 = x[:, 0, :]

建立多層節點:

In[26]:
# 第一層節點
cell = tf.keras.layers.SimpleRNNCell(64)
# 第二層節點
cell2 = tf.keras.layers.SimpleRNNCell(64) 

因為有多個層了,每個層都必須有一個初始狀態,所以這裡也必須建立多個初始狀態矩陣:

In[27]:
state0 = [tf.zeros([4, 64])]
state1 = [tf.zeros([4, 64])]

下面就可以進行資料在神經元間的傳遞了:

In[29]:
out0, state0 = cell(xt0, state0)
out1, state1 = cell2(out, state1)

在上述運算中,因為只有一次值傳遞運算,所以對於“迴圈”表現的不明顯,在完整RNN網路中,上述運算將會放在一個迴圈中,如下所示:

In[]:
for word in tf.unstack(x, axis=1):
    out0, state0 = cell(xt0, state0)
    out1, state1 = cell2(out, state1)

第一層的cell每次運算都會對state0進行更新,然後通過state0記錄這一次狀態在下一次迴圈,同時,通過out0將輸出值傳遞給下一層的cell2。

3.3 SimpleRNNCell實現完整RNN實現文字分類

In[30]:
import os 
import tensorflow as tf
import numpy as np
from tensorflow import keras
from tensorflow.keras import layers
In[33]:
tf.random.set_seed(22)
np.random.seed(22)
os.environ['TF_CPP_MIN_LOG_LEVEL'] = '2'

載入資料,電影評價資料:

In[59]:
total_words = 10000  # 常見詞彙數量
batchsz = 128
embedding_len = 100
(x_train, y_train), (x_test, y_test) = keras.datasets.imdb.load_data(num_words=total_words)
In[60]:
max_review_len = 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)
In[61]:
db_train = tf.data.Dataset.from_tensor_slices((x_train, y_train))
db_train = db_train.shuffle(1000).batch(batchsz, drop_remainder=True)  # drop_remainder是指最後一個batch不足128個則丟棄
db_test = tf.data.Dataset.from_tensor_slices((x_test, y_test))
db_test = db_test.batch(batchsz, drop_remainder=True)
In[62]:
print('x_train shape:', x_train.shape, tf.reduce_max(y_train), tf.reduce_min(y_train))
print('x_test shape:', x_test.shape)
x_train shape: (25000, 80) tf.Tensor(1, shape=(), dtype=int64) tf.Tensor(0, shape=(), dtype=int64)
x_test shape: (25000, 80)

接下來建立網路結構:

In[72]:
class RNN(keras.Model):
    def __init__(self, units):
        super(RNN, self).__init__()
        # 建立初始狀態矩陣
        self.state0 = [tf.zeros([batchsz, units])]
        self.state1 = [tf.zeros([batchsz, units])]
        # 對文字資料進行轉換為矩陣
        # 每句80個單詞,每個單詞100維矩陣表示  [b, 80]  --> [b, 80, 100]
        self.embedding = layers.Embedding(total_words, embedding_len, input_length=max_review_len)
        # 迴圈網路層, 語義提取
        # [b, 80, 100]--> [b, 64]
        self.rnn_cell0 = layers.SimpleRNNCell(units, dropout=0.2)  # 第一層rnn
        self.rnn_cell1 = layers.SimpleRNNCell(units, dropout=0.2)  # 第二層rnn
        # 全連線層
        # [b, 64] --> [b, 1]
        self.outlayer = 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:  [b, 80, 100] --> [b, 64]
        state0 = self.state0
        state1 = self.state1
        for word in tf.unstack(x, axis=1): # 在第二個維度展開,遍歷句子的每一個單詞
            # 
            out0, state0 = self.rnn_cell0(word, state0, training)
            out1, state1 = self.rnn_cell1(out0, state1, training)
        # out: [b, 64]
        x = self.outlayer(out1)
        prob = tf.sigmoid(x)
        return prob
In[73]:
units = 64
epochs = 4
model = RNN(units)
model.compile(optimizer=keras.optimizers.Adam(0.001),
             loss=tf.losses.BinaryCrossentropy(),
              metrics=['accuracy'],
              experimental_run_tf_function = False
             )
model.fit(db_train, epochs=epochs, validation_data=db_test)
Epoch 1/4
193/195 [============================>.] - ETA: 0s - loss: 0.5973 - accuracy: 0.5506Epoch 1/4
195/195 [==============================] - 3s 15ms/step - loss: 0.4002 - accuracy: 0.8220
195/195 [==============================] - 13s 65ms/step - loss: 0.5958 - accuracy: 0.5520 - val_loss: 0.4002 - val_accuracy: 0.8220
Epoch 2/4
193/195 [============================>.] - ETA: 0s - loss: 0.3326 - accuracy: 0.8490Epoch 1/4
195/195 [==============================] - 1s 7ms/step - loss: 0.4065 - accuracy: 0.8224
195/195 [==============================] - 5s 28ms/step - loss: 0.3315 - accuracy: 0.8492 - val_loss: 0.4065 - val_accuracy: 0.8224
Epoch 3/4
193/195 [============================>.] - ETA: 0s - loss: 0.1839 - accuracy: 0.9180Epoch 1/4
195/195 [==============================] - 1s 7ms/step - loss: 0.5103 - accuracy: 0.8193
195/195 [==============================] - 6s 28ms/step - loss: 0.1841 - accuracy: 0.9182 - val_loss: 0.5103 - val_accuracy: 0.8193
Epoch 4/4
193/195 [============================>.] - ETA: 0s - loss: 0.0960 - accuracy: 0.9574Epoch 1/4
195/195 [==============================] - 1s 7ms/step - loss: 0.6747 - accuracy: 0.8083
195/195 [==============================] - 5s 28ms/step - loss: 0.0958 - accuracy: 0.9575 - val_loss: 0.6747 - val_accuracy: 0.8083
Out[73]:
<tensorflow.python.keras.callbacks.History at 0x7fce3bc7d390>

3.4 高層API實現RNN

資料載入和預處理還是使用上一章節的程式碼。這裡主要是使用TensorFlow中高層API

In[76]:
model = keras.Sequential([
    layers.Embedding(total_words, embedding_len, input_length=max_review_len),
    layers.SimpleRNN(units, dropout=0.2, return_sequences=True, unroll=True),
    layers.SimpleRNN(units, dropout=0.2,  unroll=True),
    layers.Dense(1, activation='sigmoid')
])
model.compile(loss='binary_crossentropy', optimizer='adam',
              metrics=['accuracy'])
history1 = model.fit(db_train, epochs=epochs, validation_data=db_test)
loss, acc = model.evaluate(db_test)
print('準確率:', acc) # 0.81039
Train for 195 steps, validate for 195 steps
Epoch 1/4
195/195 [==============================] - 11s 54ms/step - loss: 0.6243 - accuracy: 0.6187 - val_loss: 0.4674 - val_accuracy: 0.7830
Epoch 2/4
195/195 [==============================] - 6s 29ms/step - loss: 0.3770 - accuracy: 0.8352 - val_loss: 0.4061 - val_accuracy: 0.8230
Epoch 3/4
195/195 [==============================] - 6s 29ms/step - loss: 0.2527 - accuracy: 0.9007 - val_loss: 0.4337 - val_accuracy: 0.8201
Epoch 4/4
195/195 [==============================] - 6s 29ms/step - loss: 0.1383 - accuracy: 0.9493 - val_loss: 0.5847 - val_accuracy: 0.8076
195/195 [==============================] - 1s 7ms/step - loss: 0.5847 - accuracy: 0.8076
準確率: 0.8075721