RNN 迴圈神經網路系列 5: 自定義單元

lsvih發表於2017-11-03

本系列文章彙總

  1. RNN 迴圈神經網路系列 1:基本 RNN 與 CHAR-RNN
  2. RNN 迴圈神經網路系列 2:文字分類
  3. RNN 迴圈神經網路系列 3:編碼、解碼器
  4. RNN 迴圈神經網路系列 4:注意力機制
  5. RNN 迴圈神經網路系列 5:自定義單元

RNN 迴圈神經網路系列 5: 自定義單元

在本文中,我們將探索並嘗試建立我們自己定義的 RNN 單元。不過在此之前,我們需要先仔細研究簡單的 RNN,再逐步深入較為複雜的單元(如 LSTM 與 GRU)。我們會分析這些單元在 tensorflow 中的實現程式碼,最終參照這些程式碼來建立我們的自定義單元。本文將援引由 Chris Olah 所著,在 RNN、LSTM 方面非常棒的一篇文章中的圖片。在此我強烈推薦你閱讀這篇文章,本文中會重申其中許多相關內容,不過由於我們主要還是關注 tf 程式碼,所以這些內容將會較快地略過。將來當我要對 RNN 結構進行層規範化時,我還會引用本文中的程式碼。之後的文章可以在這兒檢視。

基本 RNN:

對於傳統的 RNN 來說,最大的問題就在於每個單元的重複輸入都是靜態的,因此我們無法充分學習到長期的依賴情況。你回想一下基本 RNN 單元,就會發現所有操作都是單一的 tanh 運算。

screen-shot-2016-10-04-at-5-54-13-am
screen-shot-2016-10-04-at-5-54-13-am

對於解決短期依賴情況的問題來說,這種結構已經夠用了;但如果我們希望通過有效的長期記憶來預測目標,則需要使用更穩定強大的 RNN 單元 —— LSTM。

長短期記憶網路(LSTM):

LSTM 的結構可以讓我們在更多的操作中進行長期的資訊控制。傳統的 RNN 僅有一個輸出,其既作為隱藏狀態表示也作為此單元的輸出端。

Screen Shot 2016-11-16 at 6.25.04 PM.png
Screen Shot 2016-11-16 at 6.25.04 PM.png

這種結構缺乏對資訊的控制,無法存住對許多步之後有用的資訊。而 LSTM 有兩種不同的輸出。其中一種仍與前面的傳統結構一樣,既作為隱藏狀態表示也作為單元輸出;但 LSTM 單元還有另一種輸出 - 單元狀態 C。這也是 LSTM 精髓所在,讓我們仔細研究它。

Screen Shot 2016-11-16 at 6.28.06 PM.png
Screen Shot 2016-11-16 at 6.28.06 PM.png

遺忘門:

第一個要介紹的門就是遺忘門。這個門可以讓我們選擇性地傳遞資訊以決定單元的狀態。我將公式羅列在下,後面介紹其它的門時也會如此。

Screen Shot 2016-11-16 at 6.30.38 PM.png
Screen Shot 2016-11-16 at 6.30.38 PM.png

Screen Shot 2016-11-16 at 6.39.17 PM.png
Screen Shot 2016-11-16 at 6.39.17 PM.png

你可以參考類似 tf 的 _linear 函式來實現它。不過遺忘門的主要要點是對輸入與隱藏狀態前應用了 sigmoid。那麼這個 sigmoid 的作用是什麼?請回想一下,sigmoid 會輸出在 [0, 1] 範圍的值,在此我們將其應用於 [N X H] 的矩陣,因此會得到 NXH 個 sigmoid 算出的值。如果 sigmoid 得到 0 值,那麼其對應的隱藏值就會失效;如果 sigmoid 得到 1 值,那麼此隱藏值將會被應用在計算中。而處於 0 和 1 之間的值將會允許一部分的資訊繼續傳遞。這樣就能很好地通過阻塞與選擇性地傳遞輸入單元的資料,以達到控制資訊的目的。

這就是遺忘門。它是我們的單元得到最終結果前的第一個步驟。下面介紹另一個操作:輸入門。

輸入門:

輸入門將獲取我們的輸入值 X 以及在前面的隱藏狀態,並對它們進行兩次運算。首先會通過 sigmoid 門來選擇性地允許部分資料輸入,接著將其與輸入值的 tanh 值相乘。

Screen Shot 2016-11-16 at 6.48.07 PM.png
Screen Shot 2016-11-16 at 6.48.07 PM.png

這兒的 tanh 與前面的 sigmoid 操作不同。請回憶一下,tanh 會將輸入值改變為 [-1, 1] 範圍內的值。它本質上通過非線性的方式改變了輸入的表示。這一步與我們在基本 RNN 單元中進行的操作一致,不過在此我們將兩值的乘積加上遺忘門得到的值得到本單元的狀態值。

遺忘門與輸入門的操作可以看做同時儲存了舊狀態(C_{t-1})的一部分與新變換(tanh)單元狀態(C~_t)的一部分。這些權重將會通過我們資料的訓練學到需要儲存多少資料以及如何進行正確的變換。

輸出門:

最後一個門是輸出門,它利用輸入值、前面的隱藏狀態值以及新單元狀態值來共同決定新隱藏狀態的表示。

Screen Shot 2016-11-16 at 6.54.29 PM.png
Screen Shot 2016-11-16 at 6.54.29 PM.png

該步驟依舊涉及到了 sigmoid,將它的值與單元狀態的 tanh 值相乘以決定資訊的去留。需要注意這一步的 tanh 計算與輸入門的 tanh 計算不同,此步不再是神經網路的計算,而僅僅是單純、不帶任何權重地計算單元狀態值的 tanh 值。這樣我們就能強制單元狀態矩陣 [NXH] 的值處於 [-1, 1] 的範圍內。

變體

RNN 單元有許多種變體,在此再次建議去閱讀 Chris Olah 的這篇博文學習更多相關知識。不過他在文中討論的是 peehole 模型(在計算 C_{t-1} 或 C_t 時允許所有門都能觀察到單元狀態值)以及單元狀態的 couple(更新與遺忘同時進行)。不過目前 LSTM 的競爭對手是正在被廣泛使用的 GRU(Gated Recurrent Unit)。

GRU(Gated Recurrent Unit):

GRU 的主要原理是將遺忘門與輸入門結合成一個更新門。

Screen Shot 2016-11-16 at 7.01.15 PM.png
Screen Shot 2016-11-16 at 7.01.15 PM.png

在實際使用中,GRU 的效能與 LSTM 相當,但其計算量更小,因此它現在日益流行。

原生 Tensorflow 實現:

我們先觀察一下 Tensorflow 官方對於 GRU 單元的實現程式碼,主要關注其函式呼叫方式、輸入以及輸出。然後我們會複製它的結構用於建立我們自己的單元。如果你對其它的單元有興趣,可以在這兒找到它們的實現。本文將主要關注 GRU,因為它在大多數情況下效能與 LSTM 相當且複雜度更低。

class GRUCell(RNNCell):
  """Gated Recurrent Unit cell (cf. http://arxiv.org/abs/1406.1078)."""

  def __init__(self, num_units, input_size=None, activation=tanh):
    if input_size is not None:
      logging.warn("%s: The input_size parameter is deprecated.", self)
    self._num_units = num_units
    self._activation = activation

  @property
  def state_size(self):
    return self._num_units

  @property
  def output_size(self):
    return self._num_units

  def __call__(self, inputs, state, scope=None):
    """Gated recurrent unit (GRU) with nunits cells."""
    with vs.variable_scope(scope or type(self).__name__):  # "GRUCell"
      with vs.variable_scope("Gates"):  # Reset gate and update gate.
        # We start with bias of 1.0 to not reset and not update.
        r, u = array_ops.split(1, 2, _linear([inputs, state],
                                             2 * self._num_units, True, 1.0))
        r, u = sigmoid(r), sigmoid(u)
      with vs.variable_scope("Candidate"):
        c = self._activation(_linear([inputs, r * state],
                                     self._num_units, True))
      new_h = u * state + (1 - u) * c
    return new_h, new_h複製程式碼

GRUCell 類由 init 函式開始執行。在 init 函式中定義了單元的數量與其使用的啟用函式。其啟用函式一般是 tanh,不過也可以使用 sigmoid 來使得值固定在 [0, 1] 範圍內方便我們控制資訊流。另外,它還有兩個在呼叫時會返回 self._num_units 的屬性。最後定義了 call 函式,它將處理輸入值並得出新的隱藏值。回憶一下,GRU 沒有類似 LSTM 的單元狀態值。

call 中,我們首先計算 r 和 u(u 是前面圖中的 z)。在這步中,我們沒有單獨去計算它們,而是以乘以 2 倍 num_units 的形式合併了權重,再將結果分割成兩份得到它們(split(dim, num_splits, value))。然後對得到的值應用 sigmoid 啟用函式,以選擇性地控制資訊流。接著計算 c 的值,用它計算新隱藏狀態表示值。你可能發現它計算 new_h 的順序和之前顛倒了,不過由於權重會同時進行訓練,因此程式碼仍能正常執行。

其它的單元程式碼都與此程式碼類似,你弄明白了上面的程式碼就能輕鬆解釋其它單元的程式碼。


掘金翻譯計劃 是一個翻譯優質網際網路技術文章的社群,文章來源為 掘金 上的英文分享文章。內容覆蓋 AndroidiOSReact前端後端產品設計 等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章