1. 程式人生 > 其它 >RNN入門與實踐

RNN入門與實踐

作者:葉虎

編輯:黃俊嘉

引言

遞迴神經網路(Recurrent Neural Network, RNN)是神經網路家族的重要成員,而且也是深度學習領域中的得力干將,因為深度學習廣泛應用的領域如語音識別,機器翻譯等都有RNN的身影。與經典的神經網路不同,RNN主要解決的是樣本資料為序列的建模問題,如語音序列,語言序列。因為對於序列資料來說,大部分情況下序列的每個元素並不是相互獨立,其存在依賴關係,而RNN特別適合這類建模問題。本文會介紹RNN的原理及應用,並動手實現一個RNN預測模型。

RNN原理

RNN處理的是序列建模問題。給定一個長度為T輸入序列{x0,x1...,xt,....,xT},這裡 表示的是序列在t時刻的輸入特徵向量,這裡的t時刻並不一定真的指的是時間,只是用來表明這是一個序列輸入問題。現在要得到每個時刻的隱含特徵{h0,h1...,ht,....,hT} ,這些隱含特徵用於後面層的特徵輸入。如果採用傳統的神經網路模型,只需要計算:

其中f為非線性啟用函式。但是這樣明顯忽略了這是一個序列輸入問題,即丟失了序列中各個元素的依賴關係。對於RNN模型來說,其在計算t時刻的特徵時,不僅考慮當前時刻的輸入特徵xT ,而且引入前一個時刻的隱含特徵ht-1 ,其計算過程如下:

顯然這樣可以捕捉到序列中依賴關係,可以認為是一個ht-1記憶特徵,其提取了前面t-1個時刻的輸入特徵,有時候又稱ht-1為舊狀態,而ht為新狀態。因此,RNN模型特別適合序列問題。從結構上看,RNN可以看成有環的神經網路模型,如圖1所示。不過可以將其展開成普通的神經網路模型,準確地說展開成T個普通的神經網路模型。但是這T個神經網路不是割立的,其所使用引數是一樣的,即權重共享。這樣每一個時刻,RNN執行的是相同的計算過程,只不過其輸入不一樣而已。所以本質上,RNN也只不過多個普通的神經網路通過權值共享連線而成。

圖1 RNN模型及展開簡圖

(來源:http://colah.github.io/posts/2015-08-Understanding-LSTMs/)

還有一點,RNN可以提取一組特徵{h0,h1...,ht,....,hT},但是並不是所有的特徵都會送入後面的層,如果你只是需要根據輸入序列進行分類,可能你僅需要最後時刻的特徵hT。這和具體的應用場景相關。

RNN訓練

RNN模型像其他神經網路模型一樣也是採用梯度下降法訓練,相應的也需要計算梯度。計算梯度也是採用BP演算法,但是由於RNN的特殊性,其對應的BP演算法又稱為BPTT(Backpropagation Through Time)。BPTT的背後含義是梯度還要在時間層進行反向傳播,這很好理解,比如ht的梯度ht-1,....,h0還要對做貢獻。這從數學公式上可以看出來的,本質上還是鏈式規則。但是你可能知道梯度消失的問題,在RNN模型中其同樣存在。梯度消失的問題在RNN上表現為ht的梯度傳播距離可能有限,這帶來的一個直接後果是:RNN對長依賴序列問題(long-term dependencies)無效。這使得經典的RNN模型的應用很受限,所以才會出現RNN的變種如LSTM,它們可以很好地解決這類問題。

RNN應用

RNN主要應用在輸入為序列資料的業務場景。其中一個很重要的領域是自然語言處理,如語言模型及機器翻譯等。RNN還可以對具有周期性特徵的資料建立預測模型。對於序列問題,可以用下圖來說明RNN的應用:

圖2 RNN處理序列問題(來源:cs231n)

其中one to one是典型的神經網路的應用,給定一個輸入,預測一個輸出。而其他的情形都需要應用RNN模型。one to many的一個例子是影象標註(Image Captioning),輸入一個圖片,得到對圖片的語言描述,這是一個序列輸出。對於many to one,其應用例項如情感分類(Sentiment Classification),給定一句話判斷其情感,其中輸入是序列。第一種Many to many的典型應用場景是機器翻譯,比如一句英文,輸出一句中文,這時輸入與輸出都是序列。第二種many to many可以應用在視訊分類問題(Video classification on frame level),輸入一段視訊,對每一幀圖片分類。因此,可見RNN模型廣泛應用在各種業務場景中。

RNN實踐

最後我們使用RNN模型實現一個簡單的二進位制加法器。任何一個整數都可以用一個二進位制串來表示,給定兩個二進位制串,我們希望生成表示其和的二進位制串。一個二進位制串可以看成一個序列,這可以用RNN來搭建模型。先上圖來說明:

圖3 二進位制加法器(來源:angelfire.com)

二進位制器加法器從左向右開始計算,通過兩個運算數對應位上二進位制數來得到新的二進位制數。但是你要考慮運算溢位的問題,圖上彩色方框中的1表示的是運算溢位後的“攜帶位”,你需要將其傳遞給下一位的運算。好吧,這是序列依賴關係,到了RNN發揮作用了。你就想象著上一個時刻的隱含特徵儲存這個“攜帶位”資訊就可以了,這樣當前時刻的運算就可以捕獲到前面運算溢位得到的“攜帶位”。不過,這裡的時刻指的是位置。

那麼,現在開始設計這個RNN模型,首先肯定的這是many to many的例子。假定二進位制串長度為L,那麼時間步長為L,而且每個時刻的輸入特徵的維度是2。利用RNN模型,我們可以得到每個時刻的隱含特徵,這個特徵維度大小可以自定義,這裡我們取16。將隱含特徵送入輸出層,得到預測結果,我們希望預測輸出的維度是1,並且值限制在0和1這兩個數。此時可以使用sigmoid啟用函式,將值限制在[0,1]範圍內,這個值大於0.5取1,反之取0。Python實現的程式碼如下:

import numpy as np
# sigmoid
def sigmoid(x):
    return 1.0 / (1.0 + np.exp(-x))
# sigmoid導數
def sigmoid_derivative(output):
    return output * (1.0 - output)
# 生成整數與二進位制數轉化字典
int2binary = {}
binary_dim = 8
largest_number = pow(2, binary_dim)
binary = np.unpackbits(np.array([range(largest_number)], dtype=np.uint8).T,
                       axis=1)
for i in range(largest_number):
    int2binary[i] = binary[i]

# 模型引數
input_dim = 2
hidden_dim = 16
output_dim = 1
learing_rate = 1e-1

# 初始化模型引數
# 模型: h(t) = sigmoid(Ux + Vh(t-1)) -> output(t) = sigmoid(Wh(t))
U = np.random.randn(input_dim, hidden_dim)
V = np.random.randn(hidden_dim, hidden_dim)
W = np.random.randn(hidden_dim, output_dim)

# 初始化引數梯度
dU = np.zeros_like(U)
dV = np.zeros_like(V)
dW = np.zeros_like(W)

iterations = 20000
# 訓練過程:不使用batch
for i in range(iterations):
    # 生成一個簡單的加法問題 (a+b = c), a, b 除以2防止c溢位
    a_int = np.random.randint(largest_number / 2)
    a = int2binary[a_int]
    b_int = np.random.randint(largest_number / 2)
    b = int2binary[b_int]

    c_int = a_int + b_int
    c = int2binary[c_int]

    d = np.zeros_like(c)
    # 訓練樣本
    X = np.array([a, b]).T
    y = np.array([c]).T

    loss = 0  # 損失函式

    hs = []  # 儲存每個時間步長下的隱含特徵
    hs.append(np.zeros((1, hidden_dim)))  # 初始化0時刻特徵為0
    os = []  # 儲存每個時間步長的預測值

    # forward過程
    for t in range(binary_dim):
        # 當前時刻特徵
        xt = X[binary_dim - t - 1]
        # 隱含層
        ht = sigmoid(xt.dot(U) + hs[-1].dot(V))
        # 輸出層
        ot = sigmoid(ht.dot(W))
        # 儲存結果
        hs.append(ht)
        os.append(ot)
        # 計算loss,採用L1
        loss += np.abs(ot - y[binary_dim - t - 1])[0][0]
        # 預測值
        d[binary_dim - t - 1] = np.round(ot)[0][0]

    # backward過程
    future_d_ht = np.zeros((1, hidden_dim))  # 從上一個時刻傳遞的梯度
    for t in reversed(range(binary_dim)):
        xt = X[binary_dim - t - 1].reshape(1, -1)
        ht = hs[t+1]
        ht_prev = hs[t]
        ot = os[t]
        # d_loss/d_ot
        d_ot = ot - y[binary_dim - t - 1]
        d_ot_output = sigmoid_derivative(ot) * d_ot
        dW += ht.T.dot(d_ot_output)
        d_ht = d_ot_output.dot(W.T) + future_d_ht  # 別忘來了上一時刻傳入的梯度
        d_ht_output = sigmoid_derivative(ht) * d_ht
        dU += xt.T.dot(d_ht_output)
        dV += ht_prev.T.dot(d_ht_output)

        # 更新future_d_ht
        future_d_ht = d_ht_output.dot(V.T)

    # SGD更新引數
    U -= learing_rate * dU
    V -= learing_rate * dV
    W -= learing_rate * dW

    # 重置梯度
    dU *= 0
    dV *= 0
    dW *= 0

    # 輸出loss和預測結果
    if (i % 1000 == 0):
        print("loss:" + str(loss))
        print("Pred:" + str(d))
        print("True:" + str(c))
        out = 0
        for index, x in enumerate(reversed(d)):
            out += x * pow(2, index)
        print(str(a_int) + " + " + str(b_int) + " = " + str(out))
        print("------------")

最後經過一定訓練步長之後,得到的二進位制加法器效果還是非常好的:

總結

本文簡單介紹了RNN的原理以及應用場景,並給出了一個RNN的純Python例項,後序大家可以學習更復雜的應用例項,也可以深入瞭解RNN的變種如LSTM等模型。

參考資料

1. Recurrent Neural Networks Tutorial, Part 1 – Introduction to RNNs: http://www.wildml.com/2015/09/recurrent-neural-networks-tutorial-part-1-introduction-to-rnns/.

2. Understanding LSTM Networks: http://colah.github.io/posts/2015-08-Understanding-LSTMs/.

3. Anyone Can Learn To Code an LSTM-RNN in Python (Part 1: RNN): http://iamtrask.github.io/2015/11/15/anyone-can-code-lstm/.