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) 的部分將最終被丟棄。
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 的圖


表達式 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的條件概率分布公式為:![]()
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.
