Theano:LSTM源碼解析


最難讀的Theano代碼

這份LSTM代碼的作者,感覺和前面Tutorial代碼作者不是同一個人。對於Theano、Python的手法使用得非常嫻熟。

尤其是在兩重並行設計上:

①LSTM各個門之間並行

②Mini-batch讓多個句子並行

同時,在訓練、預處理上使用了諸多技巧,相比之前的Tutorial,更接近一個完整的框架,所以導致代碼閱讀十分困難。

本文旨在梳理這份LSTM代碼的脈絡。

數據集:IMDB Large Movie Review Dataset

來源

該數據集是來自Stanford的一個爬蟲數據集。

對IMDB每部電影的評論頁面的每條評論進行爬蟲,分為正面/負面兩類情感標簽。

相比於朴素貝葉斯用於垃圾郵件分類,顯然,分析一段文字的情感難度比較大。

因為語義在各個詞之間連鎖着,有些喜歡玩梗的負面諷刺語義需要一個強力的Represention Extractor。

該數據集同時也在CS224D:Deep Learning for NLP    [Leture4]中演示,用於體現Pre-Training過后的詞向量威力。

數據讀取

原始數據集被Bengio組封裝過,鏈接 http://www.iro.umontreal.ca/~lisa/deep/data/imdb.pkl

cPickle封裝的格式如下:

train_set[0]  ---->  一個包含所有句子的二重列表,列表的每個元素也為一個列表,內容為:

[詞索引1,詞索引2,.....,詞索引n],構成一個句子。熟悉文本數據的應該很清楚。

詞索引之前建立了一個詞庫,實際使用的時候如果要對照索引,獲取真實的詞,則需要詞庫:~Link~

train_set[0][n]指的是第n個句子。

————————————————————————————————————————————

train_set[1]  ---->  一個一重列表,每個元素為每個句子的情感標簽,0/1。

test_set格式相同。

————————————————————————————————————————————

本Tutorial使用的一些簡單處理包括:maxLen句子詞數剪枝、詞庫越界詞剪枝、句子詞數排序(不知道啥作用)

數據變形與預處理

這份代碼的經典之處在於,讓多個句子並行訓練,構成一個mini-batch。

在RNN章節中,每次訓練只是一個句子,所以輸入就是一個向量,但mini-batch之后就是一個矩陣。

這個矩陣最大不同在於,xy軸是倒置的,文字方向在豎式伸展。

這么做的原因是由於theano.tensor.scan函數的工作機制。

scan函數的一旦sequence不為空,就進入序列循環模式,設sequence=[x],則

①Step1:取x[0]作為循環函數第一參數

②Step2:取x[1]作為循環函數第一參數

........

③StepN: 取x[N]作為循環函數第一參數

對應語言模型的序列學習算法,每個Step就相當於取一個句子的一個詞。

豎式伸展,每次scan取的一排詞,稱為examples,數量等於batch_size。

橫向batch並行,縱向序列時序伸展,這是mini-batch和序列學習的共同作用結果。

——————————————————————————————————————————

每個句子長度是不同的,為了便於並行矩陣計算,必須選定最長句子,最大化矩陣。句子詞較少的,用0填充。

而在實際LSTM計算中,這個0填充則成了麻煩,因為你不能讓標記為0的Padding參與遞歸網絡計算,需要剔除。

這時候就需要Mask矩陣。來看LSTM.py中的實際代碼:

$c = m\_[:, None] * c + (1. - m\_)[:, None] * c\_$

由於mask矩陣也在sequence中,所以m_被降成了1D,通過Numpy的第二維None擴充,可以和c([batch_size,dim_proj])

進行點乘(不是矩陣乘法)。

Mask矩陣的作用就是對於Padding,通過遞推,直接滾到到上一個非0的狀態,而不引入Padding。

源碼解析

def get_minibatches_idx(n, minibatch_size, shuffle=False)

這部分設計對數據(句子)Shuffle隨機排列。首先獲取數據集所有句子數n,

對所有句子,按照batch_size,划分出 (n//batch_size)+1個列表。

拼在一起,構成一個二重列表。

返回一個zip(batch索引,batch內容),用於運行。

每份batch是一個列表,包含句子的索引。后續會根據句子的索引,拼出一個小量的x,

進過prepare_data處理過之后,方能使用。

def dropout_layer(state_before, use_noise, trng)

50%的Dropout,開了之后千萬不要加Weght_Decay,兩種一疊加,懲罰太重了。

Dropout的實現,利用了Tensor.switch(Bool,Ture Operation,False Operation)來動態實現。

傳入的use_noise,在訓練時置為1,這時候Dropout為動態概率屏蔽。

反之在測試時,置為0,這時候Dropout為平均網絡。

具體原理見Hinton關於Dropout的論文:

Dropout: A Simple Way to Prevent Neural Networks from Overfitting

def init_params(options)

這部分主要是初始化全部參數。分為兩個部分:

①初始化Embedding參數、Softmax輸出層參數

②初始化LSTM參數,在下面的 def param_init_lstm()函數中。

①中參數莫名其妙使用了[0,0.01]的隨機值初始化,不知道為什么不用負值。

像Word2Vec的初始化,可能更好些:

$ Init \, \sim \, Rand(\frac{-0.5}{Emb\_Dim},\frac{0.5}{Emb\_Dim}) $

Emb大小為$[VocabSize,Emb\_Dim]$,Softmax參數大小為$[Emb\_Dim,2]$

def param_init_lstm(options, params, prefix='lstm')

這部分用於初始化LSTM參數,W陣、U陣。

LSTM的初始化值很特殊,先用[0,1]隨機數生成矩陣,然后對隨機矩陣進行SVD奇異值分解。

取正交基矩陣來初始化,即 ortho_weight,原理不明,沒有找到相關文獻。

————————————————————————————————————————

值得注意的就是numpy.concatenate函數的使用,它鎖定了AXIS=1軸(橫向,列,第二維)

將Input、Forget、Output三態門與Cell的RNN核的相同操作$Wx+Uh^{'}$合並,並行計算。

如果$x$是$[1000,100]$,即$dim\_proj=100$, 那么$W$大小是$[100,100*4]$。

一次計算,有$Wx$的大小是$[1000,400]$,四個部分在矩陣中被並行計算了。

矩陣計算和FOR循環在串行算法下速度是差不多的,但是在並行算法下,矩陣同時計算比先后串行計算快不少。

def init_tparams(params)

這個函數意義就是一鍵將Numpy標准的params全部轉為Theano.shared標准。

替代大量的Theano.shared(......)

啟用tparams作為Model的正式params,而原來的params廢棄。

def lstm_layer(tparams, state_below, options, prefix='lstm', mask=None)

LSTM的計算核心。

首先得注意參數state_below,這是個3D矩陣,$[n\_Step,BatchSize,Emb\_Dim]$

在scan函數的Sequence里,每步循環,都會降解第一維n_Step,得到一個Emb矩陣,作為輸入$X\_$

計算過程:

①用 $[state\_below] \cdot [lstm\_W]$。

這步很奇妙,這是一個3D矩陣與2D矩陣的乘法,由於每個Step,都需要做$Wx$

所以無須每一個Step做一次$Wx$,而是把所有Step的$Wx$預計算好了,並行量很大。

②進入scan函數過程,每一個Step:

  I、將前一時序$h'$與4倍化$U$陣並行計算,並加上4倍化$Wx$的預計算

  II、分離計算,按照LSTM結構定義,分別計算$Input Gate$、$Forget Gate$、$Outout Gate$

      $\tilde{Cell}$、$Cell$、$h$

③返回rval[0],即h矩陣。注意,scan函數的輸出結果會增加1D。

每個Step里,h結果是一個2D矩陣,$[BatchSize,Emb\_Dim]$

而rval[0]是一個3D矩陣,$[n\_Step,BatchSize,Emb\_Dim]$。

后續會對第一維$n\_Step$進行Mean Pooling,之后才能降解成用於Softmax的2D輸入。

def sgd|adadelta|rmsprop(lr, tparams, grads, x, mask, y, cost)

這是三個可選梯度更新算法。由於作者想要保持格式一致,所以AdaDelta和RMSProp寫的有點啰嗦。

AdaDelta詳見我的 自適應學習率調整:AdaDelta

至於Hinton在2014 Winter的公開課提出的RMSProp,沒有找到具體的數學推導,不好解釋。

應該是由AdaDelta改良過來的,代碼很費解。由可視化結果來看,RMSProp在某些特殊情況下,比AdaDelta要穩定。

官方代碼里默認用的是AdaDelta,畢竟有Matthew D. Zeiler的論文詳細推導與說明。

AdaDelta和RMSProp都是模擬二階梯度更新,所以可以和Learning Rate這個惡心的超參說Bye~Bye了。值得把玩。

def build_model(tparams, options)

該部分是聯合LSTM和Softmax,構成完整Theano.function的重要部分。

①首先是定義幾個Tensor量,$x$、$y$、$mask$。

②接着,從tparams['Wemb']中取出Words*Sentences數量的詞向量,並且變形為3D矩陣。

x.flatten()的使用非常巧妙,它將詞矩陣拆成1D列表,然后按順序取出詞向量,然后再按順序變形成3D形態。

體現了Python和Numpy的強大之處。

③得到3D的輸入state_below之后,配合mask,經過LSTM,得到一個3D的h矩陣proj。

④對3D的h矩陣,各個時序進行Mean Pooling,得到2D矩陣,有點像Dropout的平均網絡。

⑤Dropout處理

⑥Softmax、構建prob、pred、cost,都是老面孔了。

特別的是,這里有一個offset,防止prob爆0,造成log溢出。碰到這種情況可能不大。

def pred_probs|pred_error()

用於Cross-Validation計算Print信息。也就是Debug Mode...

def train_lstm()

訓練過程。Theano代碼塊中最冗長、最臃腫的部分。

①options=locals().copy()是python中的一個小trick。

它能將函數中所有參數按爬蟲下來,保存為一個詞典,方便訪問。

②load_data。定義在imdb.py中,獲取train、vaild、test三個數據集

讓人費解的是,既然這里要對test做shuffle,又何必在load_data里把test排序呢?

③初始化params,並且build_model,返回theano.function的pred、prob、cost。

④WeightDecay。有Dropout之后,必要性不大。

⑤利用cost,得到grad。利用tparams、grad,得到theano.function的update。

這里代碼很啰嗦,如cost完全沒有必要通過theano.function轉化出來,保持Tensor狀態是可以帶入T.grad

而前面就沒有這么寫。代碼風格和前面的章節截然不同。

至此,准備工作完畢,進入mini-batch執行階段。

——————————————————————————————————————————————

①首先獲取vaild和test的zip化minibatch。

每輪zip返回一個二元組(batch_idx,batch_content_list),idx實際並不用。

list是指當前batch中所有句子的idx。然后,對這些離散的idx,拼出實際的1D$x$。

經過imdb.py下的prepare_data,得到2D的$x$,作為Word Embedding的預備輸入。

②進入max_epochs循環階段:

包括early stopping優化,這部分與之前章節大致相同。

——————————————————————————————————————————————

至此,這份源碼算是解析完了。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM