本文從 RNN 的侷限性開始,透過簡單的概念與詳細的運算過程描述 LSTM 的基本原理,隨後再透過文字生成案例加強對這種 RNN 變體的理解。LSTM 是目前應用非常廣泛的模型,我們使用 TensorFlow 或 PyTorch 等深度學習庫呼叫它甚至都不需要了解它的運算過程,希望本文能為各位讀者進行預習或複習 LSTM 提供一定的幫助。
序列預測問題已經存在很長時間了。它被認為是資料科學領域裡最難解決的問題之一。其中包括多種問題:從預測股價波動到理解人說話的方式,從語言翻譯到預測你在 iPhone 鍵盤上打出的下一個單詞。
近年來隨著資料科學的技術突破,人們逐漸發現幾乎所有的序列問題的最佳解決方案都是長短期記憶網路(即 LSTM),它被認為是最有效的方法。
LSTM 在許多方面比傳統的前饋神經網路和 RNN 具有優勢,這是因為它會在長時間尺度上有選擇地記憶部分特徵。本文將詳細解釋 LSTM 的原理,以讓你能夠對它進行更好的運用。
Note:為了理解本文內容,你需要一些迴圈神經網路和 Keras(一種流行深度學習庫)的基礎知識。
LSTM、GRU 與神經圖靈機:詳解深度學習最熱門的迴圈神經網路
目錄
1. 迴圈神經網路(RNN)簡介
2. RNN 的限制
3. 提升 RNN 的效能:長短期記憶網路(LSTM)
4.LSTM 架構
4.1 遺忘門
4.2 輸入門
4.3 輸出門
4.4 LSTM 整體過程
5. 使用 LSTM 生成文字
1. 迴圈神經網路(RNN)簡介
以股票市場中某支股票價格這樣的連續資料為例。一個簡單的機器學習模型(或稱人工神經網路)可以透過學習股價歷史的某些資訊來預測未來價格:股票數量、股票開盤價等等。股票價格取決於股票的這些特徵,同時也與過去幾天的股票價格相關性很高。實際上,對於一個交易者來說,過去幾天的價格(或趨勢)是對於未來股價預測的決定性因素之一。
在傳統的前饋神經網路中,所有的示例都被認為是獨立的。這意味著當模型被用於預測某一天時不會考慮之前幾天的股價。
這種時間關聯性是由迴圈神經網路實現的。一個典型的 RNN 就像這樣:
如果將其展開,它會變成這樣:
在預測今天的股價之前,我們現在更容易展示這些網路如何預測股票價格的趨勢。這裡,時間 t (h_t) 處的每個預測都依賴於先前所有的預測以及從中獲知的資訊。
RNN 可以在很大程度上實現我們處理序列的目的,但不是完全。我們想要計算機足夠擅長寫作莎士比亞十四行詩。當下 RNN 在短期語境上表現很好,但是為了能夠創作一個故事並記住它,我們需要模型理解並記住序列之後的語境,就像人類一樣。透過簡單 RNN 這不可能實現。
為什麼?讓我們來探究一下。
2. RNN 的限制
當我們處理短期依賴性時,迴圈神經網路工作得很好。當應用於像這樣的問題:
RNN 被證明相當有效。這是因為該問題與陳述的語境無關。RNN 不需要記住之前的資訊,或者其含義,它只需知道大多數情況下天空是藍的。因此預測將是:
然而,一般 RNN 無法理解輸入蘊含的語境。當做出當前預測時,一些過去的資訊無法被回憶。讓我們透過一個例項理解它:
這裡我們之所以能理解是因為作者在西班牙工作了 20 年,他很可能掌握了西班牙語。但是為了做出適當的預測,RNN 需要記住這個語境。相關資訊可能會被大量不相關資料從需要的地方分離出來。這正是 RNN 失敗的地方!
這背後的原因是梯度消失的問題。為了理解這一點,你需要了解前饋神經網路學習的一些知識。我們知道,對於傳統的前饋神經網路,在特定層上應用的權重更新是學習率、來自前一層的誤差項以及該層輸入的倍數。因此,特定層的誤差項可能是先前所有層的誤差的結果。當處理像 sigmoid 那樣的啟用函式時,隨著我們移向起始層,其小的導數值(出現在誤差函式中)會倍增。結果,隨著移向起始層,梯度幾乎消失,這些層也變的難以訓練。
一個類似情況出現在了 RNN 中。RNN 只有短期記憶,也就是說,如果我們在一小段時間之後需要這些資訊是可行的,但是一旦大量的單詞被輸入,資訊就會在某處丟失。這個問題可以透過應用稍加調整的 RNN——長短期記憶網路——來解決。
3. 提升 RNN 的效能:長短期記憶網路(LSTM)
當安排日程時,我們首先會考慮是否有會議預訂。但是如果需要為更重要的事情騰出時間,我們可能會取消某些次要的會議。
但是 RNN 並不能做到這樣,為了新增一個新資訊,它需要透過一個函式完全地轉換當前的資訊。因此資訊是以整體為單位進行修改的,模型並沒有考慮重要的和不重要的資訊。
另一方面,LSTM 會透過乘法和加法等運算對資訊進行區域性的修改。因此透過 LSTM,資訊流會選擇性地透過單元狀態,也就是說 LSTM 會選擇性地記憶或遺忘某些特徵。此外,特定單元狀態下的資訊共有三種不同的依賴性。
我們將用一些例子理解這一點,若我們特定股票的股價為例,那麼當今股價取決於:
前幾天的股票走勢,如上升或下降等,即前面時間步的單元狀態或記憶的資訊;
前一天的收盤價,因為它與當天的開盤價有很大的關係,即前一時間步的隱藏單元狀態或記憶的資訊;
當天可能影響股票的因素,即當前 LSTM 單元的輸入值或輸入的新資訊。
LSTM 另一個比較重要的特徵是它的序列處理方式,LSTM 利用這種方式收集更多的資訊與語境關係。下圖展示了 LSTM 的這種序列式的處理方式:
雖然上圖並沒有表明詳細和真實的 LSTM 架構,但它能給我們一個直觀的理解。此外,正因為 LSTM 這種屬性,它不會對整個資訊進行統一的運算,而是稍微修改一些區域性的資訊。因此 LSTM 可以選擇性地記住或遺忘一些事情,它也就有了「較長的短期記憶」。
4.LSTM 架構
透過了解新聞報導謀殺案的過程,我們可以類似地理解與視覺化 LSTM 的運算過程。現在假設一條新聞是圍繞許多事實、證據和證人所構建的,無論發生任何謀殺事件,我們都可以透過這三方面進行報導。
例如,若最初假設謀殺是透過給被害人下毒完成的,但屍檢報告表明死亡原因是「對頭部的影響」。那麼作為新聞團隊的一部分,我們很快就會「遺忘」前面的原因,後主要關注後面的原因而展開報導。
如果一個全新的嫌疑人進入了我們的視角,而該嫌疑人曾怨恨被害者,那麼是否他有可能就是兇手?因此我們需要把他「輸入」到我們的新聞中作進一步分析。
但是現在所有這些碎片資訊都不夠在主流媒體上進行報導,因此在一段時間後,我們需要總結這些資訊並「輸出」對應的結果給我們的讀者。也許這個輸出就表明並分析了到底誰才是機率最大的兇手。
下面,我們將詳細介紹 LSTM 網路的架構:
這個架構和我們之間瞭解的簡化版完全不同,但是本文將詳細解釋它。一個典型的 LSTM 網路由不同的單元或記憶塊組成,即上圖中我們看到的黃色矩形塊。LSTM 單元一般會輸出兩種狀態到下一個單元,即單元狀態和隱藏狀態。記憶塊負責記憶各個隱藏狀態或前面時間步的事件,這種記憶方式一般是透過三種門控機制實現,即輸入門、遺忘門和輸出門。
4.1 遺忘門
我們下面將採用以下語句作為文字預測問題的案例,首先假設該語句已經饋送到 LSTM 網路中。
當模型遇到了「person」後面的第一個句號,遺忘門可能就會意識到下一個語句的語境可能會發生變化。因此語句的主語可能就需要遺忘,主語所處的位置也就空了出來。而當我們討論到「Dan」時,前面空出來的主語位置就應該分配給「Dan」。遺忘前一語句的主語「Bob」的過程就由遺忘門控制。
遺忘門負責從單元狀態中移除資訊,LSTM 不需要這些資訊來理解事物,這些不太重要的資訊將透過濾波器運算而得到移除。這是最佳化 LSTM 效能所必須考慮的方面。
該遺忘門採取兩個輸入,即 h_t-1 和 x_t。h_t-1 為前一個單元的隱藏狀態或輸出狀態,x_t 為特定時間步的輸入,即輸入序列 x 的第 t 的元素。給定的輸入向量與權重矩陣的乘積,再新增偏置項以輸入 Sigmoid 函式。Sigmoid 函式將會輸出一個向量,取值的範圍為 0 到 1,其對應於單元狀態中的每個數值。基本上,Sigmoid 函式決定保留哪些值和忘記哪些值。若單元狀態取零這個特定值,那麼遺忘門就要求單元狀態完全忘記該資訊。這個輸出的 Sigmoid 函式向量最後會乘以單元狀態。
4.2 輸入門
下面我們使用另一個案例展示 LSTM 如何分析語句:
現在我們知道比較重要的資訊是「Bob」知道游泳,且他在海軍服役了四年。這可以新增到單元狀態,因此這種新增新資訊的過程就可以透過輸入門完成。
輸入門負責將資訊新增到單元狀態,這一新增資訊的過程主要可以分為三個步驟:
- 透過 Sigmoid 函式來調節需要新增到單元狀態的值,這與遺忘門非常相似,它起到的作用就是作為一個濾波器過濾來自 h_t-1 和 x_t 的資訊。
- 建立一個包含所有可能值的向量,它可以被新增到單元狀態中。該過程透過使用 tanh 函式實現,輸出值為-1 到 1.
- 將調節濾波器的值(Sigmoid 門控)乘以建立的向量(tanh 函式),然後將這些有用的資訊新增到單元狀態中。
在完成這三個步驟後,我們基本上確保了新增到單元狀態的資訊都是重要的,且不是冗餘的。
4.3 輸出門
並非所有在單元狀態執行的資訊都適合在特定時間輸出。我們將用一個例項進行展示:
在這一語句中,空格處可以有大量選擇。但是我們知道空格之前的輸入「brave」是一個修飾名詞的形容詞。因此,不管怎樣,空格處存在一個很強的名詞傾向。因此,Bob 可能是一個正確的輸出。
從當前單元狀態中選擇有用資訊並將其顯示為輸出的工作是透過輸出門完成的。其結構如下:
輸出門的功能可再次分為三個步驟:
1. 把 tanh 函式應用到單元狀態之後建立一個向量,從而將值縮放在-1 到+1 之間。
2. 使用 h_t-1 和 x_t 的值生成一個過濾器,以便它可以調節需要從上述建立的向量中輸出的值。這個過濾器再次使用一個 sigmoid 函式。
3. 將此調節過濾器的值乘以在步驟 1 中建立的向量,並將其作為輸出傳送出去,併傳送到下個單元的隱藏態。
上述例項中的過濾器將確保它減少除了「Bob」之外所有其他的值,因此過濾器需要建立在輸入和隱藏態值上,並應用在單元狀態向量上。
4.4 LSTM 整體過程
以上我們具體瞭解了 LSTM 的各個部分,但讀者可能對 LSTM 的整體過程仍然不是太瞭解,下面我們簡要地向讀者介紹 LSTM 單元選擇記憶或遺忘的具體處理流程。
以下是 LSTM 單元的詳細結構,其中 Z 為輸入部分,Z_i、Z_o 和 Z_f 分別為控制三個門的值,即它們會透過啟用函式 f 對輸入資訊進行篩選。一般啟用函式可以選擇為 Sigmoid 函式,因為它的輸出值為 0 到 1,即表示這三個門被開啟的程度。
圖片來源於李弘毅機器學習講義。
若我們輸入 Z,那麼該輸入向量透過啟用函式得到的 g(Z) 和輸入門 f(Z_i ) 的乘積 g(Z) f(Z_i ) 就表示輸入資料經篩選後所保留的資訊。Z_f 控制的遺忘門將控制以前記憶的資訊到底需要保留多少,保留的記憶可以用方程 c*f(z_f)表示。以前保留的資訊加上當前輸入有意義的資訊將會保留至下一個 LSTM 單元,即我們可以用 c' = g(Z)f(Z_i) + cf(z_f) 表示更新的記憶,更新的記憶 c' 也表示前面與當前所保留的全部有用資訊。我們再取這一更新記憶的啟用值 h(c') 作為可能的輸出,一般可以選擇 tanh 啟用函式。最後剩下的就是由 Z_o 所控制的輸出門,它決定當前記憶所啟用的輸出到底哪些是有用的。因此最終 LSTM 的輸出就可以表示為 a = h(c')f(Z_o)。
5. 使用 LSTM 生成文字
我們已經對 LSTM 的理論概念和功能有了足夠了解。現在我們嘗試建立一個模型,預測 Macbeth 原始文字之後的「n」的字元數量。絕大多數經典文字不再受版權保護,你可以在這裡找到(https://www.gutenberg.org/),更新的 TXT 版本可以在這裡找到(https://s3-ap-south-1.amazonaws.com/av-blog-media/wp-content/uploads/2017/12/10165151/macbeth.txt)。
我們使用 Keras,它是一個用於神經網路的高階 API,並在 TensorFlow 或 Theano 之上工作。因此在進入程式碼之前,請確保你已安裝執行正常的 Keras。好的,我們開始生成文字!
匯入依賴項
# Importing dependencies numpy and keras
import numpy
from keras.models import Sequential
from keras.layers import Dense
from keras.layers import Dropout
from keras.layers import LSTM
from keras.utils import np_utils
我們匯入所有必需的依賴項,並且這不證自明。
載入文字檔案並建立字元到整數對映
# load text
filename = "/macbeth.txt"
text = (open(filename).read()).lower()
# mapping characters with integers
unique_chars = sorted(list(set(text)))
char_to_int = {}
int_to_char = {}
for i, c in enumerate (unique_chars):
char_to_int.update({c: i})
int_to_char.update({i: c})
文字檔案已開啟,所有字元都轉換為小寫字母。為了方便操作以下步驟,我們把每個字元對映到相應數字。這樣做是為了使 LSTM 的計算部分更容易。
準備資料集
# preparing input and output dataset
X = []
Y = []
for i in range(0, len(text) - 50, 1):
sequence = text[i:i + 50]
label =text[i + 50]
X.append([char_to_int[char] for char in sequence])
Y.append(char_to_int[label])
資料需以這種格式準備如果我們想要 LSTM 預測「HELLO」中的「O」,我們需要輸入 [H, E , L , L ],並且 [O] 作為預期輸出。相似地,這裡我們確定了想要的序列長度(在該例項中設定為 50),接著在 X 中儲存前 49 個字元的編碼和預期輸出,即 Y 中的第 50 個字元。
重塑 X
# reshaping, normalizing and one hot encoding
X_modified = numpy.reshape(X, (len(X), 50, 1))
X_modified = X_modified / float(len(unique_chars))
Y_modified = np_utils.to_categorical(Y)
LSTM 網路希望輸入形式是 [樣本,時間步,特徵],其中樣本是我們擁有的資料點的數量,時間步是單個資料點中存在的與時間相關的步數,特徵是我們對於 Y 中相應的真值的變數數目。我們接著把 X_modified 中的值在 0 到 1 之間進行縮放,並且在 Y_modified 中對真值進行獨熱編碼(one hot encode)。
定義 LSTM 模型
# defining the LSTM model
model = Sequential()
model.add(LSTM(300, input_shape=(X_modified.shape[1], X_modified.shape[2]), return_sequences=True))
model.add(Dropout(0.2))
model.add(LSTM(300))
model.add(Dropout(0.2))
model.add(Dense(Y_modified.shape[1], activation='softmax'))
model.compile(loss='categorical_crossentropy', optimizer='adam')
使用一個含有線性堆疊層的序列模型。首層是一個帶有 300 個記憶單元的 LSTM 層,並且它返回序列。如此做是為了確保下一 LSTM 層接收到序列,而不僅僅是隨機分散的資料。每個 LSTM 層之後應用一個 dropout 層,以避免模型過擬合。最後,我們得到一個作為全連線層的最後一層,它帶有一個 softmax 啟用函式和與唯一字元數量相同的神經元,因為我們需要輸出獨熱編碼結果(one hot encoded result)。
擬合模型並生成字元
# fitting the model
model.fit(X_modified, Y_modified, epochs=1, batch_size=30)
# picking a random seed
start_index = numpy.random.randint(0, len(X)-1)
new_string = X[start_index]
# generating characters
for i in range(50):
x = numpy.reshape(new_string, (1, len(new_string), 1))
x = x / float(len(unique_chars))
#predicting
pred_index = numpy.argmax(model.predict(x, verbose=0))
char_out = int_to_char[pred_index]
seq_in = [int_to_char[value] for value in new_string]
print(char_out)
new_string.append(pred_index)
new_string = new_string[1:len(new_string)]
該模型擬合超過 100 個 epoch,每個批大小為 30。接著我們修復一個隨機種子(為了便於復現),並開始生成字元。模型預測給出了已預測字元的字元編碼,接著它被解碼為字元值並附加到該模式。
下圖展示了該網路的輸出方式:
最終在訓練足夠的 epoch 之後,它會隨著時間獲得越來越好的結果。這正是你使用 LSTM 解決序列預測問題的方式。
結語
LSTM 是序列和時序相關問題方面的一個很有前途的解決方案,同時也有著難以訓練的缺點。我們甚至需要大量時間和系統資源用來訓練一個簡單的模型。但這僅是一個硬體方面的限制。本文希望幫助你準確理解這些網路的基本知識,如有任何相關問題,歡迎留言。
原文連結:https://www.analyticsvidhya.com/blog/2017/12/fundamentals-of-deep-learning-introduction-to-lstm/