Recurrent Neural Networks(RNN) 迴圈神經網路初探

Andrew.Hann發表於2017-04-27

1. 針對機器學習/深度神經網路“記憶能力”的討論

0x1:資料規律的本質是能代表此類資料的通用模式 - 資料探勘的本質是在進行模式提取

資料的本質是儲存資訊的介質,而模式(pattern)是資訊的一種表現形式。在一個資料集中,模式有很多不同的表現形式,不管是在傳統的機器學習訓練的過程,還是是深度學習的訓練過程,本質上都是在進行模式提取。

而從資訊理論的角度來看,模式提取也可以理解為一種資訊壓縮過程,通過將資訊從一種形式壓縮為另一種形式。壓縮的過程不可避免會造成資訊丟失。

筆者這裡列舉幾種典型的體現模式提取思想的演算法。

1. 向量圖表示法

1)畫素圖表示法 - 最原始的資訊記錄方法

畫素這個概念我們都非常熟悉,畫素是表示每個影象的基本單位。

傳統的bmp點陣圖亦稱為點陣影象繪製影象,是由稱作畫素(圖片元素)的單個點組成的。這些點可以進行不同的排列和染色以構成圖樣。

當放大點陣圖時,可以看見賴以構成整個影象的無數單個方塊。擴大點陣圖尺寸的效果是擴大單個畫素,從而使線條和形狀顯得參差不齊。

同時,縮小點陣圖尺寸也會使原圖變形,因為此舉是通過減少畫素來使整個影象變小的。

同樣,由於點陣圖影象是以排列的畫素集合體形式建立的,所以不能單獨操作(如移動)區域性點陣圖。可以想象一下,點陣圖的移動類似陣列的平移,成本非常高。

2)向量圖表示法 - 記錄資訊不如記錄生成原理

向量圖,也稱為物件導向的影象繪圖影象,在數學上定義為一系列由線連線的點。

向量檔案中的圖形元素稱為物件。每個物件都是一個自成一體的實體,它具有顏色、形狀、輪廓、大小和螢幕位置等屬性。

向量圖使用直線和曲線來描述圖形,這些圖形的元素是一些點、線、矩形、多邊形、圓和弧線等等,它們都是通過數學公式計算獲得的。

例如一幅花的向量圖形實際上是由線段形成外框輪廓,由外框的顏色以及外框所封閉的顏色決定花顯示出的顏色。

向量圖最大的好處是“儲存成本小”,因為向量圖並不需要儲存原始檔案的所有畫素資訊,而只要儲存有限的用於生成原始畫素影象的生成演算法即可(頗有機器學習模型引數的感覺),因此,向量圖可以無限放大而不會增加額外的儲存成本。

2. K-mean聚類

1)聚類前的資料集(原始資訊)是怎麼樣的?

2)聚類後得到的壓縮後資訊是怎麼樣的?

原始資料集經過K-mean之後,得到的壓縮後資訊為 N 個聚類中心,N 由演算法操作者決定。

3)Kmeans壓縮後資訊如何代表原始的資訊?

聚類得到的 N 個聚類中心就是 K-means模型的模型引數。

從某種程度上來說,這個 N 個聚類中心就可以代表原始的樣本資訊。

訓練得到的Kmeans模型可以用於新樣本的標註(預測),當輸入待檢測樣本的時候,Kmeans根據一定的搜尋演算法,搜尋已知的聚類中心,將待檢測樣本打標為最靠近的那個類別。

這樣,Kmeans通過 N 個聚類中心,實現了原始資訊的模式提取,或者說核心資訊壓縮。

3. 線性迴歸模型學習

一個典型的例子

在一元線性迴歸的場景中,線性迴歸模型通過訓練得到:Y = ax + b。

通過2個引數【a,b】,對原本龐大的樣本點集進行了描述。

從上面3個例子中,可以看到一個共通點,即“資訊壓縮”,它們的本質都是抽象出了一種形式化表達,而這種表達就代表了一種模式,這種模式可以代表原本的海量樣本集中的某種形式的規律。

至於如何提取這種模式規律,以及提之後的模型規律如何被運用進行後續的新的預測,就是不同機器學習演算法的變化所在了。

0x2:神經網路是如何記憶和儲存資料中的模式規律的?

機器學習的神經是參考人腦神經網路的構造而創造的,那人腦神經網路又是如何識別、儲存、記憶每天看到的我們所謂的有用的知識的呢?

這塊內容筆者只是查詢了網上的一些討論資料,完全跨專業了,也只是看科普看了一些大概,目前業內似乎並沒有一個準確的定義,很多的討論似乎是在實驗觀測和理論假設猜測之上。

但是我看下來,有幾個觀點很多業內學者提到:

1. 記憶並不是一個直接通過bit方式儲存在大腦內,而是通過一些的神經細胞的結構來儲存,即結構及記憶知識,換言之,大腦並不是直接儲存知識本身,而是儲存知識的概念結構,也就是所謂的模式;
2. 腦內反映某外界客觀物體,是由被該外界刺激啟用的所有皮層細胞組成的,這些同時被啟用的神經元稱作“細胞集合”,假如這些細胞相互連線,細胞集合內的連線持續啟用,對外界客觀物體的內部反應就能作為短時程記憶始終儲存,如果細胞集合能持續啟用很長一段時間,那麼細胞間相互連線更有效的神經元就會連線在一起,更緊密的連線就會使細胞集合再次興奮,記憶的鞏固就可能發生。換句話說,要記憶某種資訊模式,就必須生成相應的神經網結構,並且通過不斷的刺激使之固定下來;
3. 僅僅集團內的一部分細胞的破壞並不能消除記憶,記憶的痕跡廣泛分佈於細胞集合的細胞連線內。 

Relevant Link: 

https://arxiv.org/pdf/1706.05394.pdf
http://www.360doc.com/content/17/0622/19/16619343_665595088.shtml 
https://www.zhihu.com/question/20264424 

 

2. 迴圈神經網路RNN(Recurrent Neural Network)介紹

迴圈神經網路(recurrent neural network)或 RNN 是一類用於處理了序列資料的神經網路。我們這個章節來針對RNN的一些基本概念展開討論。

0x1:共享引數思想

我們先從引數共享機制說起,這是RNN迴圈神經網路的一個核心特點,也是RNN能夠擁有某些強大效能的原因之一。

引數共享機制使得神經網路對序列資料中的模式具備了一定的平移不變泛化能力,以及模式記憶能力。

1. 從傳統全連線前饋網路的特徵表徵說起 - 不包含引數共享機制

不單是對DNN,傳統的機器學習模型,也全都要求輸入的特徵向量是一個定長的vector。
對這種定長的 feature vector,不同的特徵維度之間是正交獨立的,即打亂順序是不會影響最後的檢測結果。所以開發者在進行特徵工程的時候,並不需要考慮特徵之間的序列關係,只要把特徵“堆”到一起即可。
傳統的全連線前饋網路會給每個輸入特徵分配一個單獨的引數,不同特徵對應的引數是單獨調整的。

需要注意的是,在NLP場景中,傳統機器學習演算法經常和詞袋編碼結合使用。詞袋編碼雖然不具備共享引數能力,但是因為詞袋編碼本身就就丟棄原始文字中的時序資訊,即詞袋特徵對原始文字中的序列順序變化並不敏感,因此從某種程度上來說,詞袋編碼具備一定的對時序文字中特徵平移的泛化能力。

2. 卷積網路的共享引數 - 感知域平移引數共享

一種捕獲文字中相同單詞在不同位置的特徵的方法是,1 維時間序列上使用卷積

這種卷積方法是時延神經網路的基礎 (Lang and Hinton, 1988; Waibel et al., 1989; Lang et al., 1990)

卷積操作允許網路跨時間共享引數,但是淺層的。卷積的輸出是一個序列,其中輸出中的每一項是相鄰幾項輸入的函式。

引數共享的概念體現在每個時間步中使用的相同卷積核。

3. RNN中使用引數共享機制實現特徵位置平移不變性

從多層網路到迴圈網路,迴圈網路吸收了20世紀80年代機器學習和統計模型早期思想的優點:在模型的不同部分共享引數。引數共享使得RNN模型能夠擴充套件到不同長度的序列樣本並進行泛化。

如果我們在每個時間點都有一個單獨的引數,不但不能泛化到訓練時沒有見過的序列長度,也不能在時間上共享不同序列長度和不同位置的統計強度。當資訊的特定部分會在序列內多個位置出現時,這樣的共享尤為重要。

例如,考慮這兩句話:“I went to Nepal in 2009’’ “In 2009, I went to Nepal.”。

如果我們讓一個機器學習模型讀取這兩個句子,並提取敘述者去Nepal的年份,無論 “2009’’ 是作為句子的第六個單詞還是第二個單詞出現,我們都希望模型能認出 “2009’’ 作為相關資料片段。

相比傳統DNN網路,迴圈神經網路在幾個時間步內共享相同的權重,不需要分別學習句子每個位置的所有語言規則。

相比於CNN卷積網路,迴圈神經網路以不同的方式共享引數。輸出的每一項是前一項的函式。輸出的每一項對先前的輸出應用相同的更新規則(引數共享)而產生。這種迴圈方式導致引數通過很深的計算圖共享。

此外, 需要注意的是,所謂的時間序列不必是字面上現實世界中流逝的時間。有時,它僅表示序列中的位置。RNN 也可以應用於跨越兩個維度的空間資料(如影象)

當應用於涉及時間的資料, 並且將整個序列提供給網路之前就能觀察到整個序列時,該網路可具有關於時間向後的連線。

筆者思考:RNN和CNN這種神經網路具備引數共享機制,可以對特徵的位置平移實現泛化能力,那是不是就意味著RNN全面優於傳統機器學習演算法呢?筆者認為答案是否定的!

因為並不是所有的數學建模場景中,特徵都呈現時序關係的。總體上來說,特徵工程得到的特徵分為兩大類:1)正交獨立的特徵集,例如說描述一個桌子的一組物理引數;2)時序特徵,例如某人發出的一段聲音的聲紋訊號。

對於正交獨立的特徵集,時序模型就不一定能發揮其本身的作用,甚至還會起反效果。正交獨立的特徵集更適合使用傳統機器學習模型進行概率分佈/模型引數的訓練評估。

在具體的AI專案中,我們會遇到各種各樣的具體問題,首先要思考的是,從哪些角度入手進行特徵工程,如果是正交獨立特徵集,選擇哪些特徵?如果是時序特徵,選取什麼資料抽取時序特徵?

4. 引數共享的假設前提

在迴圈網路中使用的引數共享的前提是相同引數可用於不同時間步的假設。也 就是說,假設給定時刻 t 的變數後,時刻 t + 1 變數的條件概率分佈是 平穩的 (stationary),這意味著之前的時間步與下個時間步之間的關係並不依賴於 t

0x2:RNN網路具備的時序記憶能力

在很多專案場景中,針對當前樣本的判斷不單單僅限於當前樣本,而需要結合歷史的樣本進行時序依賴判斷。

以突顯識別舉例來說(實際上RNN並不侷限於影象時序資料):

如果我們看到一個沙灘的場景,我們應該在接下來的幀數中增強沙灘活動:如果影象中的人在海水中,那麼這個影象可能會被標記為“游泳”;如果影象中的人閉著眼睛躺在沙灘上,那麼這個影象可能會被標記為“日光浴”。

如果如果我們能夠記得Bob剛剛抵達一家超市的話,那麼即使沒有任何特別的超市特徵,Bob手拿一塊培根的影象都可能會被標記為“購物”而不是“烹飪”。

因此,我們希望讓我們的模型能夠跟蹤事物的各種狀態:

  • 在檢測完每個影象後,模型會輸出一個標籤,這個標籤對應該影象的識別結果(即RNN每個時間步輸出的 y 值)。同時模型對世界的認識也會有所更新(更新隱狀態)。例如,模型可能會學習自主地去發現並跟蹤相關的資訊,如位置資訊(場景發生的地點是在家中還是在沙灘上?)、時間(如果場景中包含月亮的影象,模型應該記住該場景發生在晚上)和電影進度(這個影象是第一幀還是第100幀?)

  • 在向模型輸入新的影象時,模型應該結合它收集到的歷史資訊,對當前的輸入圖片進行更合理的綜合判斷。

迴圈神經網路(RNN),它不僅能夠完成簡單地影象輸入和事件輸出行為,還能保持對世界的記憶(給不同資訊分配的權重),以幫助改進自己的分類功能。

0x3: RNN的圖靈完備性

"迴圈"兩個字,表達了RNN的核心特徵, 即系統的輸出會保留在網路裡和系統下一刻的輸入一起共同決定下一刻的輸出。這就把動力學的本質體現了出來, 迴圈正對應動力學系統的反饋概念,可以刻畫複雜的歷史依賴。

另一個角度看也符合著名的圖靈機原理。 即此刻的狀態包含上一刻的歷史,又是下一刻變化的依據。

這其實包含了可程式設計神經網路的核心概念,即當你有一個未知的過程,但你可以測量到輸入和輸出, 你假設當這個過程通過RNN的時候,它是可以自己學會這樣的輸入輸出規律的, 而且因此具有預測能力。 在這點上說, RNN是圖靈完備的。

下面列舉了一些可能的圖靈操作規則:

1. 圖1即CNN的架構
2. 圖2是把單一輸入轉化為序列輸出,例如把影象轉化成一行文字
3. 圖三是把序列輸入轉化為單個輸出,比如情感測試,測量一段話正面或負面的情緒
4. 圖四是把序列轉化為序列,最典型的是機器翻譯 
5. 圖5是無時差(注意輸入和輸出的"時差")的序列到序列轉化, 比如給一個錄影中的每一幀貼標籤(每一箇中間狀態都輸出一個output)

0x4:長期依賴的挑戰

學習迴圈網路長期依賴的數學挑戰在於“梯度消失”和“梯度爆炸”。根本問題是,經過許多階段傳播後的梯度傾向於消失(大部分情況)或爆炸(很少,但對優化過程影響很大)。

即使我們假設迴圈網路是引數穩定的(可儲存記憶,且梯度不爆炸),但長期依賴的困難來自比短期相互作用指數小的權重(涉及許多 Jacobian 相乘)。

迴圈網路涉及相同函式的多次組合,每個時間步一次。這些組合可以導致極端非線性行為,如下圖所示:

當組合許多非線性函式(如這裡所示的線性 tanh 層)時,得到的結果是高度非線性的。
在大多數情況下,導數不是過大,就是過小,以及在增加和減小之間的多次交替。
此處,我們繪製從 100 維隱藏狀態降到單個維度的線性投影,繪製於 y 軸上。x 軸是 100 維空間中沿著隨機方向的初始狀態的座標。因此,我們可以將該圖視為高維函式的線性截面。曲線顯示每個時間步之後的函式,或者等價地,轉換函式被組合一定次數之後。

特別地,迴圈神經網路所使用的函式組合有點像矩陣乘法。我們可以認為,迴圈聯絡:

是一個非常簡單的、缺少非線性啟用函式和輸入 x 的迴圈神經網路。這種遞推關係本質上描述了冪法。它可以被簡化為:

而當 W 符合下列形式的特徵分:

其中 Q 正交,迴圈性可進一步簡化為

特徵值提升到 t 次後,導致幅值不到一的特徵值衰減到零,而幅值大於一的特徵值就會激增。任何不與最大特徵向量對齊的 h(0) 的部分將最終被丟棄。

RNN 梯度消失和爆炸問題是由不同研究人員獨立發現 (Hochreiter, 1991a; Bengio et al., 1993, 1994b)。有人可能會希望通過簡單地停留在梯度不消失或爆炸的引數空間來避免這個問題。不幸的是,為了儲存記憶並對小擾動具有魯棒性,RNN 必須進入引數空間中的梯度消失區域。
具體來說,每當模型能夠表示長期依賴時,長期相互作用的梯度幅值就會變得指數小,這將直接導致長期依賴的權重調整速度極度放緩,這也是RNN很難學到長序列特徵的原因。
同時,由於長期依賴關係的訊號很容易被短期相關性產生的最小波動隱藏,因而學習長期依賴可能需要很長的時間。
實踐中實驗表明,當我們增加了需要捕獲的依賴關係的跨度, 基於梯度的優化變得越來越困難,SGD 在長度僅為 10 或 20 的序列上成功訓練傳統 RNN 的概率迅速變為 0。
我們這裡討論幾種緩解Long Term依賴問題的方法。

1. 多時間尺度的策略

處理長期依賴的一種方法是設計工作在多個時間尺度的模型,使模型的某些部分在細粒度時間尺度上操作並能處理小細節;而其他部分在粗時間尺度上操作,並能把遙遠過去的資訊更有效地傳遞過來。

這種迴圈網路的依賴鏈是在時間單元間“跳躍的”,類似於我們常常會將不同感知域的CNN Filter進行Stacking以獲得綜合的效果,多時間尺度的目的也是一樣,希望同時兼顧短程依賴和長程依賴的序列特徵模式。

細粒度時間尺度不用特殊設計,就是原始的RNN結構。粗時間尺度需要特殊設計,目前存在多種同時構建粗細時間尺度的策略。

1)在時間軸增加跳躍連線

增加從遙遠過去的變數到目前變數的直接連線是得到粗時間尺度的一種方法。使用這樣跳躍連線的想法可以追溯到Lin et al. (1996),緊接是向前饋網路引入延遲的想法 (Lang and Hinton, 1988)。但其實增加跳躍的本質就是引入延時,本該被傳入鄰接時間步的輸出被延遲傳入了之後 N 步時間步中。

在普通的迴圈網路中,迴圈從時刻 t 的單元連線 到時刻 t + 1 單元。構造較長的延遲迴圈網路是可能的。

對於梯度爆炸和梯度消失問題,引入了 d 延時的迴圈連線可以減輕這個問題。因為引入 d 延時後,導數指數減小的速度變為 τ/d相關,而不是 τ。

既然同時存在延遲連線單步連線,梯度仍可能成 t 指數爆炸,只是問題會有所緩解。

2)滲漏單元

獲得導數乘積接近 1 的另一方式是設定線性自連線單元,並且這些連線的權重接近 1。
我們對某些 v 值應用更新累積一個滑動平均值 μ(t), 其中 α 是一個從 μ(t−1) 到 μ(t) 線性自連線的例子。

當 α 接近 1 時,滑動平均值能記住過去很長一段時間的資訊,而當 α 接近 0,關於過去的資訊被迅速丟棄。

線性自連線的隱藏單元可以模擬滑動平均的行為。這種隱藏單元稱為滲漏單元(leaky unit)

d 時間步的跳躍連線可以確保單元總能被 d 個時間步前的那個值影響。使用權重接近 1 的線性自連線是確保該單元可以訪問過去值的不同方式。

線性自連線通過調節實值 α 更平滑靈活地調整這種效果,比整數的跳躍長度更“柔順”。

我們可以通過兩種基本策略設定滲漏單元使用的時間常數。

1. 一種策略是手動將其固定為常數,例如在初始化時從某些分佈取樣它們的值;
2. 另一種策略是使時間常數成為自由變數,並學習出來;

3)刪除連線

處理長期依賴另一種方法是在多個時間尺度組織 RNN 狀態的想法。

一個根本的理論是:資訊在較慢的時間尺度上更容易長距離流動。這很容易理解,如果時間尺度很小,資訊在每個時間步要迅速的被決策是保留還是捨棄,以及保留和捨棄的比例。時間尺度變慢,意味著決策的次數減少,資訊保留的概率就增大了。

這個想法與之前討論的時間維度上的跳躍連線不同:

1. 該方法涉及主動刪除長度為 1 的連線並用更長的連線替換它們。以這種方式修改的單元被迫在長時間尺度上運作;
2. 而通過時間跳躍連線是新增邊,收到這種新連線的單元,可以學習在長時間尺度上運作,但也可以選擇專注於自己其他的短期連線;

0x5:長短期記憶和門控RNN

像滲漏單元一樣,門控 RNN 想法也是基於生成通過時間的路徑,其中導數既不消失也不發生爆炸。

滲漏單元通過手動選擇常量的連線權重或引數化的連線權重來達到這一目的。門控 RNN 將其推廣為在每個時間步都可能改變的連線權重。

滲漏單元允許網路在較長持續時間內積累資訊(諸如用於特定特徵或類的線索)。然而,一旦該資訊被使用了,讓神經網路遺忘舊的狀態可能是有幫助的。

例如,如果一個序列是由子序列組成,我們希望滲漏單元能在各子序列內積累線索,但是當進入新的子序列前可以忘記舊的子序列的線索資訊,我們需要一種忘記舊狀態的機制。

我們希望神經網路學會決定何時清除狀態,而不是手動決定。這就是門控 RNN 要做的事。

1. LSTM

引入自迴圈的巧妙構思,以產生梯度長時間持續流動的路徑是初始長短期記憶 (long short-term memory, LSTM)模型的核心貢獻。

其中一個關鍵擴充套件是使自迴圈的權重視上下文而定,而不是固定的

我們前面說過,RNN的核心思想就是引數共享,LSTM也同樣遵守這個核心思想,所不同的是,LSTM並不是從頭到尾引數一直共享,而是在某個“時間區間”內進行共享,在整個時間步鏈路上權重會動態調整。

在這種情況下,即使是具有固定引數的 LSTM,累積的時間尺度也可以因輸入序列而改變,因為時間常數是模型本身的輸出。

2. LSTM的核心思想

1. 允許網路動態地控制時間尺度,即資訊在時間步鏈條上的存活時間是動態控制的;
2. 允許網路動態控制不同單元的遺忘行為;

Relevant Link:

https://blog.csdn.net/cf2suds8x8f0v/article/details/79244587
https://blog.csdn.net/qq_36279445/article/details/72724649

 

3. 迴圈神經網路計算圖(Computational Graph)

本章我們將計算圖的思想擴充套件到包括迴圈。

這些週期代表了變數自身的值在未來某一時間步會對自身值的影響。這樣的計算圖允許我們定義迴圈神經網路。

0x1:計算圖定義

我們使用圖中的每一個節點來表示一個變數。

變數可以是標量、向量、矩陣、張量、或者甚至是另一型別的變數。

為了形式化我們的圖形,我們還需引入操作(operation)這一概念。操作是指一個或多個變數的簡單函式。

我們的圖形語言伴隨著一組被允許的操作。我們可以通過將多個操作複合在一起來描述更為複雜的函式。

如果變數 y 是變數 x 通過一個操作計算得到的,那麼我們畫一條從 x 到 y 的有向邊。我們有時用操作的名稱來註釋輸出的節點,當上下文很明確時,有時也會省略這個標註。

1. 不同操作對應的計算圖舉例

使用 × 操作計算 z = xy 的圖

用於邏輯迴歸預測 yˆ = σ(x⊤w + b) 的圖。一些中間表示式在代數表示式中沒有名稱,但在圖形中卻需要。我們簡單地將 第 i 個這樣的變數命名為 u(i)

表示式 H = max{0, XW + b} 的計算圖,在給定包含小批量輸入資料的設計矩陣 X 時,它計算整流線性單元啟用的設計矩陣 H。

對變數實施多個操作也是可能的。該計算圖對線性迴歸模型的權重 w 實施多個操作。這個權重不僅用於預測 yˆ,也用於權重衰減罰項 λ∑ w2。這就是所謂的結構化風險評估。

0x2:展開RNN計算圖

計算圖是形式化一組計算結構的方式,如那些涉及將輸入和引數對映到輸出和損失的計算。

我們對展開(unfolding)遞迴或迴圈計算得到的重複結構進行解釋,這些重複結構通常對應於一個事件鏈展開(unfolding)這個計算圖將更好地視覺化深度網路結構中的引數共享。

1. 動態系統的經典形式計算圖

例如,考慮下式:

,其中,稱為系統的狀態。

s 在時刻 t 的定義需要參考時刻 t-1 時同樣的定義,因此上式是迴圈的

對有限時間步 τ, τ - 1 次應用這個定義可以展開這個圖。例如 τ = 3,我們對上式進行展開,可以得到:

以這種方式重複應用定義,展開等式,就能得到不涉及迴圈的表示式。

現在我們可以使用傳統的有向無環圖(和HMM一樣都是有向無環圖概率圖模型)呈現上式的表達。

每個節點表示在某個時刻 t 的狀態,並且函式 f t 處的狀態對映到 t + 1 處的狀態。所有時間步都使用相同的引數(用於引數化 f 的相同 θ 值)

2. 存在外部驅動訊號的動態系統的計算圖

作為另一個例子,讓我們考慮由外部訊號驅動的動態系統:

從公式上看,當前狀態包含了整個過去序列的資訊。但是這個歷史資訊是有損的

當訓練迴圈網路根據過去預測未來時,網路通常要學會使用 h(t) 作為過去序列的有損摘要。

此摘要一般而言一定是有損的,因為其對映任意長度的序列到一固定長度的向量 h(t)。

根據不同的訓練準則,摘要可能選擇性地精確保留過去序列的某些方面

例如,如果在統計語言建模中使用RNN,通常給定前一個詞預測下一個詞,可能沒有必要儲存時刻 t 前輸入序列中的所有資訊,而僅僅儲存足夠預測句子其餘部分的資訊(類似HMM)。

最苛刻的情況是我們要求 h(t) 足夠豐富,並能大致恢復輸入序列,如自編碼器框架。

上面公式可以用兩種不同的方式繪製,如下圖:

1)迴路圖表示法

一種方法是為可能在模型的物理實現中存在的部分賦予一個節點,如生物神經網路。在這個觀點下,網路定義了實時操作的迴路,如上圖左側,其當前狀態可以影響其未來的狀態。

我們使用 迴路圖的黑色方塊表明在時刻 t 的狀態到時刻 t + 1 的狀態單個時刻延遲中的相互作用。

2)展開表示法

另一個繪製 RNN 的方法是展開的計算圖,其中每一個元件由許多不同的變數表示,每個時間步一個變數,表示在該時間點元件的狀態。每個時間步的每個變數繪製為計算圖的一個獨立節點,如上圖右側。

我們所說的展開是將左圖中的迴路對映為右圖中包含重複元件的計算圖的操作。目前,展開圖的大小取決於序列長度。

3. 展開計算圖的優點

我們可以用一個函式 g(t) 代表經 t 步展開後的迴圈:

函式 g(t) 將全部的過去序列作為輸入來生成當前狀態,但是展開的迴圈架構允許我們將 g(t) 分解為函式 f 的重複應用。因此,展開過程引入兩個主要優點:

1. 無論序列的長度,學成的模型始終具有相同的輸入大小,因為它指定的是從一種狀態到另一種狀態的轉移,而不是在可變長度的歷史狀態上操作
2. 我們可以在每個時間步使用相同引數的相同轉移函式 f

這兩個因素使得學習在所有時間步所有序列長度上操作單一的模型 f 是可能的,而不需要在所有可能時間步學習獨立的模型 g(t)。

學習單一的共享模型允許泛化到訓練集中未出現的序列長度,並且估計模型所需的訓練樣本遠遠少於不帶引數共享的模型。

迴圈神經網路可以通過許多不同的方式建立。就像幾乎所有函式都可以被認為是前饋網路,本質上任何涉及迴圈的函式都可以被認為是一個迴圈神經網路

Relevant Link:

《深度學習》花書

 

4. 迴圈神經網路邏輯圖結構

基於之前討論的圖展開引數共享的思想,可以設計各種迴圈神經網路。

讀者朋友需要注意的是,迴圈神經網路不是特指一定具體的演算法實現,迴圈神經網路是特指一整類具備某些特性的神經網路結構,注意要和TensorFlow/theano中的RNN實現類區分開來。

迴圈神經網路從大的分類來說可以分為以下幾種:

1. 從序列到序列的神經網路:即每個時間步都有輸出;
3. 從序列到向量的神經網路:即讀取整個序列後產生單個輸出,即整個迴圈網路可以壓縮為一個擁有唯一輸出的迴圈遞迴函式;
3. 從向量到序列的神經網路:輸入單個向量,在每個時間步都有輸出;

在遵循以上設計模型的原則之下,RNN可以進行各種結構上的變種,使之具備相應新的能力和效能。

我們學習RNN,就是要重點理解不同結構之間的區別和原理,理解不同的網路拓樸結構是如何影響資訊流的傳遞和依賴。至於具體網路內部的啟用函式是用tang還是relu,其實倒還不是那麼重要了。

0x1:經典RNN結構 - 時間步之間存在“隱藏神經元”迴圈連線

1. 邏輯流程圖 - 將輸入序列對映到等長的輸出序列

下圖是該迴圈神經網路的邏輯流程圖:

計算迴圈網路(將 x 值的輸入序列對映到輸出值 o 的對應序列) 訓練損失的計算圖;
RNN輸入到隱藏的連線由權重矩陣 U 引數化;
隱藏到隱藏的迴圈連線由權重矩陣 W 引數化以及隱藏到輸出的連線由權重矩陣 V 引數化;
其中每個節點現在與一個特定的時間例項相關聯

任何圖靈可計算的函式都可以通過這樣一個有限維的迴圈網路計算,在這個意義上公式代表的迴圈神經網路是萬能的。

RNN 經過若干時間步後讀取輸出,這與由圖靈機所用的時間步是漸近線性的,與輸入長度也是漸近線性的。

RNN 作為圖靈機使用時,需要一個二進位制序列作為輸入,其輸出必須離散化以提供二進位制輸出。利用單個有限大小的特定 RNN 計算所有函式是可能的。

RNN 可以通過啟用和權重(由無限精度的有理數表示)來模擬無限堆疊。 

2. 一個具體的網路結構 - 指定特定的啟用函式

再次強調,迴圈神經網路的結構和具體的啟用函式和損失函式是不存在強關聯的,網路可以選擇任何啟用函式

為了能夠更好地公式化地描述上圖網路結構,我們指定特定的模型引數。

啟用函式:雙曲正切啟用函式;
輸出形式:假定輸出是離散的,如用於預測詞或字元的RNN。表示離散變數的常規方式是把輸出 o 作為每個離散變數可能值的非標準化對數概率;
損失函式。應用 softmax 函式後續處理後,獲得標準化後概率的輸出向量 yˆ;

RNN從特定的初始狀態 h(0) 開始前向傳播。從 t = 1 到 t = τ 的每個時間步,我們應用以下更新方程:

這個迴圈網路將一個輸入序列對映到相同長度的輸出序列。

與 x 序列配對的 y 的總損失就是所有時間步的損失之和。例如,L(t) 為給定的 x(1),...,x(t) 後y(t) 的負對數似然,則

其中,需要讀取模型輸出向量 yˆ(t) 中對應於 y(t) 的項。

0x2:導師驅動過程迴圈網路 - 時間步之間存在”目標值單元“和”隱藏神經元”連線的迴圈連線

1. 邏輯流程圖 - 將輸入序列對映到等長的輸出序列

導師驅動過程迴圈神經網路,它僅在一個時間步的目標單元值下一個時間步的隱藏單元間存在迴圈連線。邏輯流程如下:

從本質上理解,這種導師驅動的迴圈神經網路,就是將多個單神經元感知機按照序列的方式串聯起來,相鄰神經元感知機之間存在2階的依賴關係。

2. 導師驅動迴圈網路的優缺點

1)缺點

因為缺乏隱藏到隱藏的迴圈連線,所以它不能模擬通用圖靈機。本質原因在於,隱藏神經元中儲存和傳遞的是高階維度特徵,隱藏神經元之間迴圈連線使得這種高階維度特徵得以傳播和記憶。

而目標值單元和隱藏神經元相連的網路結構,它要求目標值單元捕捉用於預測未來的關於過去的所有資訊。

但是因為目標值單元(輸出單元)明確地訓練成匹配訓練集的目標,它們不太能捕獲關於過去輸入歷史的必要資訊,除非使用者知道如何描述系統的全部狀態,並將它作為訓練目標的一部分。

2)優點

反過來說,消除隱藏到隱藏迴圈的優點在於,任何基於比較時刻 t 的預測和時刻 t 的訓練目標的損失函式中的所有時間步都解耦了。因此訓練可以並行化,即在各時刻 t 分別計算梯度。因為訓練集提供輸出的理想值,所以沒有必要先計算前一時刻的輸出。

3. 導師驅動過程訓練(teacher forcing)

由輸出反饋到模型而產生迴圈連線的模型可用導師驅動過程(teacher forcing) 進行訓練。
訓練模型時,導師驅動過程在時刻 t + 1 接收真實值 y(t) 作為輸入。我們可以通過檢查兩個時間步的序列得知這一點:

在這個例子中,同時給定迄今為止的 x 序列和來自訓練集的前一 y 值,我們可 以看到在時刻 t = 2 時,模型被訓練為最大化 y(2) 的條件概率。

因此最大似然在訓練時指定正確反饋,而不是將自己的輸出反饋到模型。

我們使用導師驅動過程的最初動機是為了在缺乏隱藏神經元到隱藏神經元連線的模型中避免通過時間反向傳播

0x3:時間步之間存在“隱藏神經元”迴圈連線,且網路只有唯一的單向量輸出

1. 邏輯流程圖 - 將輸入序列對映為固定大小的向量

關於時間展開的迴圈神經網路,在序列結束時具有單個輸出。

這樣的網路可以用於概括序列產生用於進一步處理的固定大小的表示。在結束處可能存在目標(如此處所示),或者通過更下游模組的反向傳播來獲得輸出 o(t) 上的梯度。

0x4:基於上下文的RNN建模 - 隱藏神經元之間存在迴圈連線,且”目標值單元“和”隱藏神經元“之間存在連線

一般情況下,RNN 允許將圖模型的觀點擴充套件到不僅代表 y 變數的聯合分佈也能表示給定 x 後 y 條件分佈。

需要重點理解的一點是,輸入序列 x 的方式的不同,會造成RNN的效果和效能的很大差別。某種程度上甚至可以說,RNN網路中結構的一個調整,可能就是一個完全不同的新演算法了,這也是深度神經網路強大而複雜的一面了。

1. 在每個時間步將完整的 x序列 輸入網路中 - 輸入序列 x 和輸出序列 y 不一定要等長 - 將固定大小的向量對映成一個序列

網路結構如下圖所示:

輸入 x 和每個隱藏單元向量 h(t) 之間 的相互作用是通過新引入的權重矩陣 R 引數化的。乘積 x⊤R 在每個時間步作為隱藏單元的一個額外輸入。

我們可以認為 x 的選擇(確定 x⊤R 的值),是有效地用於每個隱藏單元的一個新偏置引數。權重與輸入保持獨立。

將固定長度的向量 x 對映到序列 Y 上每個時間步的 RNN上。這類 RNN 適用於很多工如影象標註, 其中單個影象作為模型的輸入,然後產生描述影象的詞序列。觀察到的輸出序列的每個元素 y(t) 同時用作輸入(對於當前時間步)和訓練期間的目標(對於前一時間步)

筆者思考:這種結構能用於圖注任務的原理非常的直觀,圖注的註解序列的每一個詞都應該和整張圖片有關,所以需要在每個時間步都輸入完整的 x 序列,同時,圖注註解序列的單詞之間也存在依賴推導關係,因為 y(t) 需要傳入下一個時間步

2. 將 x序列 依次輸每個時間步中 - 輸入序列 x 和輸出序列 y 等長 - 將輸入序列對映為等長的輸出序列

RNN 可以接收向量序列 x(t) 作為輸入,而不是僅接收單個向量 x 作為輸入。

接受向量序列的RNN的條件概率分佈公式為:

結構圖如下:
將可變長度的 x 值序列對映到相同長度的 y 值序列上分佈的條件迴圈神經網路。此 RNN 包含從前一個輸出到當前狀態的連線。這些連線允許此RNN對給定 x 的序列後 相同長度的 y 序列上的任意分佈建模

0x5:雙向RNN

傳統的前饋RNN網路都有一個 ‘‘因果’’ 結構,意味著在時刻 t 的狀態只能從過去的序列x(1),...,x(t−1) 以及當前的輸入x(t) 捕獲資訊。

然而,在許多應用中,我們要輸出的 y(t) 的預測可能依賴於整個輸入序列。

例如,在語音識別中,由於協同發音,當前聲音作為音素的正確解釋可能取決於未來幾個音素,甚至潛在的可能取決於未來的幾個詞,因為詞與附近的詞之間的存在語義依賴,如果當前的詞有兩種聲學上合理的解釋,我們可能要在更遠的未來(和過去)尋找資訊區分它們。這在手寫識別和許多其他序列到序列學習的任務中也是如此。

雙向迴圈神經網路(或雙向 RNN)為滿足這種需要而被髮明。他們在需要雙向資訊的應用中非常成功,如手寫識別,,語音識別以及生物資訊學。
顧名思義,雙向RNN結合時間上從序列起點開始移動的RNN和另一個時間上從序列末尾開始移動的RNN。下圖展示了典型的雙向 RNN

1. 邏輯流程圖 - 將輸入序列對映到等長的輸出序列

其中 h(t) 代表通過時間向前移動的子 RNN 的狀態,g(t) 代表通過時間向後移動的子 RNN 的狀態。因此在每個點 t,輸出單元 o(t) 可以受益於輸入 h(t) 中關於過去的相關概要以及輸入 g(t) 中關於未來的相關概要

這允許輸出單元 o(t) 能夠計算同時依賴於過去和未來且對時刻 t 的輸入值最敏感的表示,而不必指定 t 周圍固定大小的視窗。

0x6:基於編碼 - 解碼的序列到序列結構

這一小節,我們將討論RNN如何將一個輸入序列對映到不等長的輸出序列。這在許多場景中都有應用,如語音識別、機器翻譯或問答,其中訓練集的輸入和輸出序列的長度通常不相同。

我們經常將RNN的輸入稱為“上下文”。我們希望產生此上下文的表示C。這個上下文C可能是一個概括輸入序列 X = (x(1) , . . . , x(nx ) ) 的向量或者向量序列,即神經網路的隱層高維度向量。

實際上,輸入長度和輸出長度不一致的神經網路並不罕見,DNN和CNN中這種情況都非常常見(例如將影象輸入得到手寫數字輸出),實現這一能力的核心思想就是增加 1 個及以上的隱層,對於RNN也是一樣的,隱層起到資訊壓縮和解壓縮的承上啟下作用。

這種架構稱為編碼-解碼序列到序列架構。如下圖所示:

這個結構實際上是由兩個RNN結構拼接組成的。

(1) 編碼器(encoder)或讀取器(reader)或輸入 (input) RNN 處理輸入序列。編碼器輸出上下文C(通常是最終隱藏狀態的簡單函式),C表示輸入序列的語義概要;

(2) 解碼器(decoder)或寫入器 (writer)或輸出 (output) RNN 則以固定長度的向量(即上下文C)為條件產生輸出序列 Y = (y(1),...,y(ny))。

在序列到序列的架構中,兩個 RNN 共同訓練以最大化 logP(y(1),...,y(ny) | x(1),...,x(nx))(訓練集中所有 x 和 y 對的損失)。

編碼器 RNN 的最後一個狀態 hnx 通常被當作輸入的表示 C 並作為解碼器 RNN 的輸入。 

0x7:LSTM

LSTM邏輯圖如下所示:

LSTM 迴圈網路除了外部的 RNN 迴圈外,還具有內部的 “LSTM 細胞’’ 迴圈(自環),因此 LSTM 不是簡單地向輸入和迴圈單元的仿射變換之後施加一個逐元素的非線性。

與普通的迴圈網路類似,每個單元有相同的輸入和輸出,但也有更多的引數和控制資訊流動的門控單元系統

最重要的組成部分是狀態單元 s(t),與之前討論的滲漏單元有類似的線性自環。然而,此處自環的權重(或相關聯的時間常數)由遺忘門 (forget gate) f(t) 控制。而滲漏單元需要設計者實現手工決定。一個是資料驅動,一個是經驗驅動。

遺忘門函式 f(t) 由 sigmoid 單元將權重設定為 0 和 1 之間的值:

其中 x(t) 是當前輸入向量,ht 是當前隱藏層向量,ht 包含所有 LSTM 細胞的輸出。 bf , Uf , Wf 分別是偏置、輸入權重和遺忘門的迴圈權重。

LSTM 細胞內部狀態以如下方式更新:

其中 b, U, W 分別是 LSTM 細胞中的偏置、輸入權重和遺忘門的迴圈權重。外部輸入門 (external input gate) 單元 g(t) 以類似遺忘門(使用sigmoid獲得一個 0 和 1 之
間的值)的方式更新,但有自身的引數:

LSTM 細胞的輸出 h(t) 也可以由輸出門 (output gate) q(t) 關閉(使用sigmoid單元作為門控):

其中 bo, Uo, Wo 分別是偏置、輸入權重和遺忘門的迴圈權重。

LSTM 網路比簡單的迴圈架構更易於學習長期依賴

筆者思考:LSTM從數學公式上並沒有什麼特別的地方,這是相比於原始的RNN公式加入了一些額外的函式,使得聯合優化過程更復雜了,當然也帶來的額外的好處。LSTM的核心思想就是在隱狀態的迴圈傳遞中插入了一個“門控函式”,該門控函式具備“放行”和“阻斷”這兩種能力,而具體是否要放行以及放行多少由輸入和輸出進行BP聯合訓練。
進一步擴充套件,我們甚至可以將門控函式改為一個“訊號放大函式”,使其成為一個具備新能力的RNN網路,所有的功能背後都是數學公式以及該公式具備的線性和非線效能力

0x8:GRU(門控迴圈單元)

GRU 與 LSTM 的主要區別是,單個門控單元同時控制遺忘因子和更新狀態單元的決定。更新公式如下:

其中 u 代表 ”更新門”,r 表示 “復位門“。它們的值定義如下:

復位和更新門能獨立地 ‘‘忽略’’ 狀態向量的一部分。

1. 更新門像條件滲漏累積器一樣,可以線性門控任意維度,從而選擇將它複製(在 sigmoid 的一個極端)或完全由新的 ‘‘目標狀態’’ 值(朝向滲漏累積器的收斂方向)替換並完全忽略它(在另一個極端)。
2. 復位門控制當前狀態中哪些部分用於計算下一個目標狀態,在過去狀態和未來狀態之間引入了附加的非線性效應。

圍繞這一主題可以設計更多的變種。例如復位門(或遺忘門)的輸出可以在多個隱藏單元間共享。或者,全域性門的乘積(覆蓋一整組的單元,例如整一層)和一個區域性門(每單元)可用於結合全域性控制和區域性控制。但不管怎樣,讀者朋友需要明白的是,這些本質上都是數學公式上的增加和變化,在核心架構上,GRU和我們之前討論的RNN單元公式是類似的。

 

5. 迴圈神經網路概率圖結構

從概率模型的角度來看,我們可以將深度神經網路的輸出解釋為一個概率分佈,並且我們通常使用與分佈相關聯的交叉熵來定義損失。

需要注意的是,是否將上一時間步的某種形式輸出作為當前時間步的輸入(即是否存在序列依賴),以及取多少步的歷史時間步輸出作為當前時間步的輸入,會對網路模型的效能和效果造成非常大的變化,我們這個小節來嘗試討論下這個話題。

0x1:序列時間步之間輸入輸出獨立

將整個序列 y 的聯合分佈分解為一系列單步的概率預測是捕獲關於整個序列完整聯合分佈的一種方法。

當我們不把過去時間步的 y 值反饋給下一步作為預測的條件時,那麼該有向圖模型模型不包含任何從過去 y(i) 到當前 y(t) 的邊。在這種情況下,輸出 y 與給定的 x 序列是條件獨立的。

樸素貝葉斯NB演算法中的樸素貝葉斯假設本質上就屬於這種情況。

0x2:序列時間步之間有限步(階)輸入輸出依賴

許多概率圖模型的目標是省略不存在強相互作用的邊以實現統計和計算的效率

例如經典的Markov假設, 即圖模型應該只包含從 {y(t−k), . . . , y(t−1)} 到 y(t) 的邊(k階馬爾科夫),而不是包含整個過去歷史的邊。

然而,在一些情況下,我們認為整個過去的輸入會對序列的下一個元素有一定影響。當我們認為 y(t) 的分佈可能取決於遙遠過去 (在某種程度) 的 y(i) 的值,且 無法通過 y(t−1) 捕獲 y(i) 的影響時,RNN 將會很有用。

筆者思考:在實際專案中,我們對序列依賴的長度的需求是需要仔細思考的,並不是所有情況下都需要針對超長序列提取模式記憶。例如在webshell檢測場景中,我們往往更關注”短程語法句式模式“,因為惡意程式碼的主題功能往往在5步之內就會完成,我們需要捕獲的也就是這些短程的序列模式。

0x3:歷史時間步長序列輸入輸出依賴

RNN 被訓練為能夠根據之前的歷史輸入估計下一個序列元素 y(t) 的條件分佈,條件概率公式如下:

可以看到,RNN遵循的是一種長序列依賴假設。

舉一個簡單的例子,讓我們考慮對標量隨機變數序列 Y = {y(1),...,y(τ)} 建模的 RNN,也沒有額外的輸入 x(實際大多數情況是存在輸入 x 的)。在時間步 t 的輸入僅僅是時間步 t − 1 的輸出:

這個例子中的 RNN 定義了關於 y 變數的有向圖模型。我們使用鏈式法則引數化這些觀察值的聯合分佈:

其中當 t = 1 時豎槓右側顯然為空。因此,根據這樣一個模型,一組值 {y(1),...,y(τ)} 的負對數似然為:

,其中,

”該RNN中每一時間步都參考了歷史上所有歷史時間步的輸出“,這句話有點抽象不好理解,為了更好地討論這句話的概念,我們將計算圖展開為完全圖:

我們將RNN視為定義一個結構為完全圖的圖模型,且能夠表示任何一對 y 值之間的直接聯絡。即每一個時間步之間都存在某種聯絡。這預示著 RNN 能對觀測的聯合分佈提供非常有效的引數,如下圖:

序列 y(1), y(2), . . . , y(t), . . . 的全連線圖模型。給定先前的值,每個過去的觀察值 y(i) 可 以影響一些 y(t)(t > i) 的條件分佈。

當序列中每個元素的輸入和引數的數目越來越多,根據此圖直接引數化圖模型可能是非常低效的。RNN 可以通過高效的引數化(引數共享機制)獲得相同的全連線。

但是全連線帶來一個嚴重的問題,引數膨脹

假設我們用表格表示法來表示離散值上任意的聯合分佈,即對每個值可能的賦值分配一個單獨條目的陣列,該條目表示發生該賦值的概率。如果 y 可以取 k 個不同的 值,表格表示法將有 O(kτ ) 個引數。

但是 RNN 由於使用引數共享機制,RNN 的引數數目為 O(1) 且是序列長度的函式。我們可以調節 RNN 的引數數量來控制模型容量,但不用被迫與序列長度成比例。

下式展示了所述 RNN 通過迴圈應用相同的函式,以及在每個時間步的相同引數 θ,有效地引數化的變數之間的長期聯絡:

同時在 RNN 圖模型中引入狀態變數,儘管它是輸入的確定性函式,但它有助於我們獲得非常高效的引數化。

序列中的每個階段(對於 h(t) 和 y(t) )使用相同的結構(每個節點具有相同數量的輸入),並且可以與其他階段共享相同的引數。

在圖模型中結合 h(t) 節點可以用作過去和未來之間的中間量,從而將它們解耦。遙遠過去的變數 y(i) 可以通過其對 h 的影響來影響變數 y(t)。

 

6. 迴圈神經網路的梯度計算

迴圈神經網路中,關於各個引數計算這個損失函式的梯度是計算成本很高的操作。

通過將RNN的計算圖展開後可以清楚地看到,梯度計算涉及執行一次前向傳播,接著是由右到左的反向傳播。執行時間是 O(τ),並且不能通過並行化來降低,因為前向傳播圖是固有循序的,每個時間步只能一前一後地計算。前向傳播中的各個狀態必須儲存,直到它們反向傳播中被再次使用,因此記憶體代價也是 O(τ)。

應用於展開圖且代價為 O(τ) 的反向傳播演算法稱為通過時間反向傳播(back-propagation through time, BPTT),隱藏單元之間存在迴圈的網路非常強大但訓練代價也很大。

0x1:舉例說明BPTT計算過程

以文章之前討論的例子為例計算梯度:

計算圖的節點包括引數 U, V, W, b 和 c,以及以 t 為索引的節點序列 x(t), h(t), o(t) 和 L(t)。

對於每一個節點 N,我們需要基於 N 後面的節點的梯度,遞迴地計算梯度 ∇NL。我們從緊接著最終損失的節點開始往回遞迴:

在這個導數中,我們假設輸出 o(t) 作為 softmax 函式的引數,我們可以從 softmax函式可以獲得關於輸出概率的向量 yˆ。我們也假設損失是迄今為止給定了輸入後的真實目標 y(t) 的負對數似然。對於所有 i, t,關於時間步 t 輸出的梯度 ∇o(t) L 如下:

我們從序列的末尾開始,反向進行計算。在最後的時間步 τ, h(τ) 只有 o(τ) 作為後續節點,因此這個梯度很簡單:

然後,我們可以從時刻 t = τ − 1 到 t = 1 反向迭代,通過時間反向傳播梯度,注意h(t)(t < τ) 同時具有 o(t) 和 h(t+1) 兩個後續節點。因此,它的梯度由下式計算:

其中 diag 1−(h(t+1))2 表示包含元素 1−(h(t+1))2 的對角矩陣。這是關於時刻 t+1 與隱藏單元 i 關聯的雙曲正切的Jacobian。

一旦獲得了計算圖內部節點的梯度,我們就可以得到關於引數節點的梯度。

因為引數在許多時間步共享,我們必須在表示這些變數的微積分操作時謹慎對待。我們希望使用 bprop 方法計算計算圖中單一邊對梯度的貢獻。然而微積分中的 ∇Wf 運算元,計算 W 對於 f 的貢獻時將計算圖中的所有邊都考慮進去了。為了消除這種歧義,我們定義只在 t 時刻使用的虛擬變數 W(t) 作為 W 的副本。然後,我們可以使用 ∇W(t) 表示權重在時間步 t 對梯度的貢獻。

使用這個表示,計算節點內部引數的梯度可以由下式給出:

因為計算圖中定義的損失的任何引數都不是訓練資料 x(t) 的父節點,所以我們不需要計算關於它的梯度。

 

7. 迴圈神經網路的具體應用

0x1:基於RNN+LSTM的模型自動編寫古詩

1. 語料資料

一共四萬多首古詩,每行一首詩。

2. 樣本預處理

這裡我們採用one-hot的形式,基於當前的詩句檔案統計出一個字典,這樣詩句中的每個字都能用向量來表示。當然,也可以採用emberding方式進行詞向量嵌入。

# *-* coding:utf-8 *-*


puncs = [']', '[', '', '', '{', '}', '', '', '']


def preprocess_file(Config):
    # 語料文字內容
    files_content = ''
    with open(Config.poetry_file, 'r', encoding='utf-8') as f:
        for line in f:
            # 每行的末尾加上"]"符號代表一首詩結束
            for char in puncs:
                line = line.replace(char, "")
            files_content += line.strip() + "]"  

    # 統計整個預料的詞頻
    words = sorted(list(files_content))
    words.remove(']')
    counted_words = {}
    for word in words:
        if word in counted_words:
            counted_words[word] += 1
        else:
            counted_words[word] = 1

    # 去掉低頻的字
    erase = []
    for key in counted_words:
        if counted_words[key] <= 2:
            erase.append(key)
    for key in erase:
        del counted_words[key]
    del counted_words[']']
    wordPairs = sorted(counted_words.items(), key=lambda x: -x[1])

    words, _ = zip(*wordPairs)
    # word到id的對映
    word2num = dict((c, i + 1) for i, c in enumerate(words))
    num2word = dict((i, c) for i, c in enumerate(words))
    word2numF = lambda x: word2num.get(x, 0)
    return word2numF, num2word, words, files_content

3. 生成序列資料

RNN是序列到序列的有監督模型,因此我們需要定義每個時間步的輸入x,以及每個時間步的輸出目標值y。

我們給模型學習的方法是,給定前六個字,生成第七個字,所以在後面生成訓練資料的時候,會以6的跨度,1的步長擷取文字,生成語料。

比如“我要吃香蕉”,現在以3的跨度生成訓練資料就是("我要吃", “香”),("要吃香", "蕉")。跨度為6的句子中,前後每個字都是有關聯的。如果出現了]符號,說明]符號之前的語句和之後的語句是兩首詩裡面的內容,兩首詩之間是沒有關聯關係的,所以我們後面會捨棄掉包含]符號的訓練資料。

def data_generator(self):
        '''生成器生成資料'''
        i = 0
        while 1:
            x = self.files_content[i: i + self.config.max_len]  # max_len跨度作為x
            y = self.files_content[i + self.config.max_len]     # max_len+1 的那個跟隨詞作為y

            puncs = [']', '[', '', '', '{', '}', '', '', '', ':']
            if len([j for j in puncs if j in x]) != 0:  # x中出現詩句停止符,丟棄該x
                i += 1
                continue
            if len([j for j in puncs if j in y]) != 0:  # y剛好是詩句停止符,丟棄該y
                i += 1
                continue

            y_vec = np.zeros(
                shape=(1, len(self.words)),
                dtype=np.bool
            )
            y_vec[0, self.word2numF(y)] = 1.0  # y是one-hot編碼,對應出現的那個詞為true,其他為false

            x_vec = np.zeros(
                shape=(1, self.config.max_len),
                dtype=np.int32
            )

            for t, char in enumerate(x):
                x_vec[0, t] = self.word2numF(char)
            yield x_vec, y_vec
            i += 1 

x表示輸入,y表示輸出,輸入就是前六個字,輸出即為第七個字。再將文字轉換成向量的形式。

4. 構建模型

def build_model(self):
        '''建立模型'''

        # 輸入的dimension
        input_tensor = Input(shape=(self.config.max_len,))
        embedd = Embedding(len(self.num2word) + 2, 300, input_length=self.config.max_len)(input_tensor)
        lstm = Bidirectional(GRU(128, return_sequences=True))(embedd)
        # dropout = Dropout(0.6)(lstm)
        # lstm = LSTM(256)(dropout)
        # dropout = Dropout(0.6)(lstm)
        flatten = Flatten()(lstm)
        dense = Dense(len(self.words), activation='softmax')(flatten)
        self.model = Model(inputs=input_tensor, outputs=dense)
        optimizer = Adam(lr=self.config.learning_rate)
        self.model.compile(loss='categorical_crossentropy', optimizer=optimizer, metrics=['accuracy'])

雙向lstm之後用flattern和DNN進行壓平和整合,最後softmax得到單個向量的輸出。

5. 訓練模型

 def train(self):
        '''訓練模型'''
        number_of_epoch = len(self.words) // self.config.batch_size
 
        if not self.model:
            self.build_model()
 
        self.model.fit_generator(
            generator=self.data_generator(),
            verbose=True,
            steps_per_epoch=self.config.batch_size,
            epochs=number_of_epoch,
            callbacks=[
                keras.callbacks.ModelCheckpoint(self.config.weight_file, save_weights_only=False),
                LambdaCallback(on_epoch_end=self.generate_sample_result)
            ]
        )

6. 每輪epoch訓練結果,進行一次成果展示

 def generate_sample_result(self, epoch, logs):
        '''訓練過程中,每個epoch列印出當前的學習情況'''
        # if epoch % 5 != 0:
        #     return
        print("\n==================Epoch {}=====================".format(epoch))
        for diversity in [0.5, 1.0, 1.5]:
            print("------------Diversity {}--------------".format(diversity))
            start_index = random.randint(0, len(self.files_content) - self.config.max_len - 1)
            generated = ''
            sentence = self.files_content[start_index: start_index + self.config.max_len]   # 隨機擷取一段6詞序列作為待預測x
            generated += sentence
            for i in range(20):     # 迴圈20次,即生成一句20個詞的詩句
                x_pred = np.zeros((1, self.config.max_len))
                for t, char in enumerate(sentence[-6:]):
                    x_pred[0, t] = self.word2numF(char)

                preds = self.model.predict(x_pred, verbose=0)[0]    # 得到y預測結果
                print "preds: ", preds
                next_index = self.sample(preds, diversity)          # 從y中選擇概率最大的詞編碼
                next_char = self.num2word[next_index]   # 翻譯回可讀漢字

                generated += next_char
                sentence = sentence + next_char
            print(sentence)

下圖展示了訓練初期和訓練一段時間之後,RNN的詩句生成效果

訓練一段時間後:

Relevant Link:

https://www.ioiogoo.cn/2018/02/01/%E7%94%A8keras%E5%AE%9E%E7%8E%B0rnnlstm%E7%9A%84%E6%A8%A1%E5%9E%8B%E8%87%AA%E5%8A%A8%E7%BC%96%E5%86%99%E5%8F%A4%E8%AF%97/
https://github.com/LittleHann/poetry_generator_Keras

0x2:基於LSTM生成城市名稱

RNN具備記憶性,在經過大量訓練後可以學習到時序資料的潛在規律,並且可以使用這種規律隨機生成新的序列。

1. 如何給RNN輸入訓練樣本

RNN可以學習到資料中的時序規律,但是作為模型設計者,我們需要明確地定義:該樣本集中時序規律的形式是什麼

例如筆者在專案中遇到的一些典型場景:

1. 你有一段時序向量資料,並且擁有對這個時序向量資料的一個 0/1 label,即二分類問題,這在安全攻防場景中很常見;
2. 你有一個預料庫,該語料庫中包含了各種句子。你希望讓RNN從中學習到隱藏的”句式、語法模式“。我們知道,語言對話是由詞/句/短語/段落組成的,我麼可以採取”滑動視窗“的方式,逐段地將整個句子分成多個【X(可能長度為7), Y(可能長度為1)】的訓練樣本,通過讓RNN學習 X序列 和緊隨其後的 Y 字元的序列特徵,等效地讓RNN學會語料庫中的”句式、語法模式“;
3. 同理,基於影象生成標註的道理也是類似的(同2);

2. 資料集

Abbeville
Abbotsford
Abbott
Abbottsburg
Abbottstown
Abbyville
Abell
Abercrombie
Aberdeen
Aberfoil
Abernant
Abernathy
Abeytas
Abie
Abilene
Abingdon
Abington
Abiquiu
Abita Springs
Abo
Aboite
Abraham
Abram
Abrams
Absarokee
Absecon
Academy
Accokeek
Accomac
Accord
Ace
Aceitunas
Acequia
Achille
Achilles
Ackerly
Ackerman
Ackley
Ackworth
Acme
Acomita Lake
Acra
Acree
Acton
Acworth
Acy
Ada
Adair
Adair Village
Adairsville
Adairville
Adams
Adams Center
Adams City
Adamstown
Adamsville
Adario
Addicks
Addie
Addieville
Addington
Addis
Addison
Addy
Addyston
Adel
Adelaide
Adelanto
Adelino
Adell
Adelphi
Adelphia
Aden
Adena
Adgateville
Adin
Adjuntas
Admire
Adna
Adona
Adrian
Advance
Adwolf
Ady
Aetna
Affton
Afton
Agar
Agate
Agate Beach
Agawam
Agency
Agnes
Agness
Agnew
Agnos
Agoura
Agra
Agricola
Agua Dulce

3. 訓練程式碼

from __future__ import absolute_import, division

import os
from six import moves
import ssl

import tflearn
from tflearn.data_utils import *

path = "../data/US_Cities.txt"
maxlen = 20

file_lines = open(path, "r").read()
X, Y, char_idx = string_to_semi_redundant_sequences(file_lines, seq_maxlen=maxlen, redun_step=3)
print "X[0]", X[0]
print "len(X[0])", len(X[0])
print "Y[0]", Y[0]
print "char_idx", char_idx


g = tflearn.input_data(shape=[None, maxlen, len(char_idx)])
g = tflearn.lstm(g, 512, return_seq=True)
g = tflearn.dropout(g, 0.5)
g = tflearn.lstm(g, 512)
g = tflearn.dropout(g, 0.5)
g = tflearn.fully_connected(g, len(char_idx), activation='softmax')
g = tflearn.regression(g, optimizer='adam', loss='categorical_crossentropy',
                       learning_rate=0.001)

m = tflearn.SequenceGenerator(g, dictionary=char_idx,
                              seq_maxlen=maxlen,
                              clip_gradients=5.0,
                              checkpoint_path='model_us_cities')


for i in range(40):
    seed = random_sequence_from_string(file_lines, maxlen)
    m.fit(X, Y, validation_set=0.1, batch_size=128,
          n_epoch=1, run_id='us_cities')
    print("-- TESTING...")
    print("-- Test with temperature of 1.2 --")
    print(m.generate(30, temperature=1.2, seq_seed=seed))
    print("-- Test with temperature of 1.0 --")
    print(m.generate(30, temperature=1.0, seq_seed=seed))
    print("-- Test with temperature of 0.5 --")
    print(m.generate(30, temperature=0.5, seq_seed=seed))

4. 實驗結果

0x3:基於LSTM生成JSP WEBSHELL樣本

1. 樣本集

我們收集了131個大小在4096bytes內的JSP webshell檔案,這批樣本作為訓練語料庫。

需要特別注意的一點是,每個檔案之間理論上應該是一個獨立的樣本集,最合理的做法是單獨從每個檔案中以ngram方式提取序列。

我們這裡為了簡單起見,把所有檔案concat到一個整體的字串中,進行向量化,讀者朋友在實際專案中要注意這點。

2. webshell詞法模式提取原理

採集滑動視窗進行詞模式提取,視窗越小,提取到的詞模式特定空間就越大,描述能力就越強,相對的,訓練難度也越大,舉例說明:

<?php
    eval($_POST['op']);
?>
採用step_size = 2的滑動視窗進行詞模式提取:
EOF< -> ?
<? -> p
?p -> h
... 
ev -> a
va -> l
al -> (
...
$_ -> P
..
?> -> EOF

3. 生成(預測)過程

RNN是一種sequence to sequence的神經網路,因此我們需要給模型提供一個種子seed字元,作為啟動字元,選擇這個字元的原則也很簡單,選擇對應程式語言開頭的第一個字母。

這裡我們簡述過程

1. step_1: 輸入 START<,prediect後進行softmax得到"?"
2. step_2: 在上一步的基礎上,輸入"<?",prediect後進行softmax得到"換行"或者"空格"
3. ...
4. 迴圈到直接網路輸出EOF或者達到開發者設定的filesize
5. 最終得到的序列就是一個目標webshell序列

4. 實驗程式碼

from __future__ import absolute_import, division

import os
from six import moves
import ssl

import tflearn
from tflearn.data_utils import *

DataDir = "../data/jsp_hash"
maxlen = 20
shelllen = 4096
step_size = 2

file_lines = ""
rootDir = DataDir
for file in os.listdir(rootDir):
    if file == '.DS_Store':
        continue
    print file
    path = os.path.join(rootDir, file)
    file_content = open(path, "r").read()
    file_lines += file_content

X, Y, char_idx = string_to_semi_redundant_sequences(file_lines, seq_maxlen=maxlen, redun_step=step_size)
print "X[0]", X[0]
print "len(X[0])", len(X[0])
print "Y[0]", Y[0]
print "char_idx", char_idx


g = tflearn.input_data(shape=[None, maxlen, len(char_idx)])
g = tflearn.lstm(g, 512, return_seq=True)
g = tflearn.dropout(g, 0.5)
g = tflearn.lstm(g, 512)
g = tflearn.dropout(g, 0.5)
g = tflearn.fully_connected(g, len(char_idx), activation='softmax')
g = tflearn.regression(g, optimizer='adam', loss='categorical_crossentropy',
                       learning_rate=0.001)

m = tflearn.SequenceGenerator(g, dictionary=char_idx,
                              seq_maxlen=maxlen,
                              clip_gradients=5.0,
                              checkpoint_path='model_us_cities')


for i in range(40):
    seed = random_sequence_from_string(file_lines, maxlen)
    m.fit(X, Y, validation_set=0.2, batch_size=128,
          n_epoch=5, run_id='webshell generate')
    print("-- GENERATING...")
    print("-- Test with temperature of 1.2 --")
    print(m.generate(shelllen, temperature=1.2, seq_seed=seed))
    print("-- Test with temperature of 1.0 --")
    print(m.generate(shelllen, temperature=1.0, seq_seed=seed))
    print("-- Test with temperature of 0.5 --")
    print(m.generate(shelllen, temperature=0.5, seq_seed=seed))

3. 實驗結果

0x4:GENERATING IMAGE DESCRIPTIONS

Together with convolutional Neural Networks, RNNs have been used as part of a model to generate descriptions for unlabeled images. It’s quite amazing how well this seems to work. The combined model even aligns the generated words with features found in the images.

 

相關文章