shamangary's Blog

A Coding Level Note for Extending CNN to RNN/LSTM Implementation in Torch

| Comments

CNN training

在單張image處理時,我們在torch中CNN model的寫法通常是先建立

<<Network shell>>

model = nn.Sequential()

隨後再插入你想要的layers,注意到這裡我們在training時的tensor input,其維度是

<<Input tensor convention>>

input tensor(4 dim): batchsize x channel x width x height

值得注意的是,在CNN的設計中,batchsize的大小是多少比較無所謂,
是取決於你自己而已,然而後面三項會直接影響到你的CNN會不會發生錯誤,
因為每個layer中都會有嚴格對這三項的限制。

我們先看一下標準的CNN在training會需要哪些東西:

<<General CNN training process>>

Step.1  Preprocessing
---------------------------------------------------------------------------

<model or mlp (multi-layer perceptron) loading>

<criterion setting>

<optimState setting>

local w,dE_dw = model:getParameters()

---------------------------------------------------------------------------



Step.2 Training through epoch
---------------------------------------------------------------------------

for epoch = 1, maxEpoch do

    for t = 1, nBatches-1 do
        
        xlua.progress(t+1, nBatches) -- disp progress

        <inputs per batch setting> 

        local eval_E = function(w)
             
             <forward and backward propagation>

             return loss, dE_dw
        end

        optim.sgd(eval_E, w, optimState)

    end
end

---------------------------------------------------------------------------

可以發現torch設計的非常的精良,只要遵循這個流程基本上CNN的training
不會有太大的困難,大部分人對於CNN的改進設計主要是在

(1) model

(2) loss function

而這兩件事情通常可以在 Step.1 Preprocessing 就處理完大部分的設計,
其他小改動就不會影響到程式的整體架構,所以可以說還是相當清楚的。

我們再把其中propagation的地方寫清楚一點

<<Propagations>>

local eval_E = function(w)

    -- reset gradients 
    dE_dw:zero() 

    -- evaluate function for complete mini batch 
    local outputs = model:forward(inputs) 
    local loss = criterion:forward(outputs,targets) 

    -- estimate dE/dW 
    local dE_dy = criterion:backward(outputs,targets) 

    model:backward(inputs,dE_dy) 

    -- return E and dE/dW 
    return loss,dE_dw 

end

簡單來說就是從前到後:inputs -> outputs -> loss
再從後到前:outputs -> dE_dy -> dE_dw
就醬而已,沒惹 (´・ω・`),技術上來說數學他都幫你處理好了,
連sgd本身你甚至都不需要知道是什麼他就幫你update這樣。
(當然我們作研究還是要自己搞懂基礎喔^.<)

如果想要用GPU加速的話,記得要用

inputTensor:cuda()
model:cuda()

就是把東西移到GPU那裡去,才可以加速喔。

此外,如果為了避免爆炸可能會需要gradient clipping

dE_dw:clamp(-opt.gradClip,opt.gradClip)

這種的細節就是各種實驗中各有不同了。

參考資料:
PN-Net
Activity-Recognition-with-CNN-and-RNN

RNN/LSTM training

由於最近也在弄action recognition的東西,
其中我們實驗是的研究方向有一部份是LSTM的處理和改進,
所以我選擇了torch的RNN library作為實作的媒介,以下是採用的來源。
Github: https://github.com/Element-Research/rnn

在影片處理中,比影像處理多了一個維度:時間,
如果我們加入了時間維度,我們應該將tensor變成5維嗎?
這跟library的設計有關係,而我們想要處理的這個rnn library不是這樣處理的,
他將整個時間序列(time sequence)視為一個lua table,
也就是說我們處理的tensor的維度不變,但是每個時間點的image都被紀錄在同一個table中,
假設我們已經通過了某幾層的CNN,而每張圖都可以用2048x7x7的feature map來表示,
那麼一段影片包含了好幾個幀,也就是好幾張圖片的特徵可以用下面的table來表示。

Input sequence split list.
(batchsize=2, channel=2048, w=7, h=7, numSequence=10)
{
  1 : CudaTensor - size: 2x2048x7x7
  2 : CudaTensor - size: 2x2048x7x7
  3 : CudaTensor - size: 2x2048x7x7
  4 : CudaTensor - size: 2x2048x7x7
  5 : CudaTensor - size: 2x2048x7x7
  6 : CudaTensor - size: 2x2048x7x7
  7 : CudaTensor - size: 2x2048x7x7
  8 : CudaTensor - size: 2x2048x7x7
  9 : CudaTensor - size: 2x2048x7x7
  10 : CudaTensor - size: 2x2048x7x7
}

而RNN或LSTM說穿了也是CNN的一種而已,只不過是比較複雜一點,
所以接下來的問題是,如何使用CNN來直接處理(平行或是有相互關係的)時間序列?
Element-Research給我們的答案是他們創造了一個新的模式

model = nn.Sequencer()

可以想像成,input table中的每個element,也就是每個tensor,
會經過一樣的CNN處理,如果中間有RNN連結時間之間的關係的話,
似乎會連結彼此的時序關係。

其中他們設計的rnn library有幾個比較需要注意的地方,是之前處理CNN沒有遇到的。

model:remember()
model:training()
model:forget()
model:evaluate()

我們看一下他們的定義

training()

In training mode, the network remembers all previous rho (number of time-steps) states. This is necessary for BPTT.

evaluate()

During evaluation, since their is no need to perform BPTT at a later time, only the previous step is remembered. This is very efficient memory-wise, such that evaluation can be performed using potentially infinite-length sequence.

remember([mode])

When mode='neither' (the default behavior of the class), the Sequencer will additionally call forget before each call to forward. When mode='both' (the default when calling this function), the Sequencer will never call forget. In which case, it is up to the user to call forget between independent sequences. This behavior is only applicable to decorated AbstractRecurrent modules. Accepted values for argument mode are as follows :

'eval' only affects evaluation (recommended for RNNs)
'train' only affects training
'neither' affects neither training nor evaluation (default behavior of the class)
'both' affects both training and evaluation (recommended for LSTMs)

forget(offset)

This method brings back all states to the start of the sequence buffers, i.e. it forgets the current sequence. It also resets the step attribute to 1. It is highly recommended to call forget after each parameter update. Otherwise, the previous state will be used to activate the next, which will often lead to instability. This is caused by the previous state being the result of now changed parameters. It is also good practice to call forget at the start of each new sequence.

裡面有一句話:
Note : make sure you call brnn:forget() after each call to updateParameters().
就是每次你update完之後記得要forget()一下。

由於在原本Element-Research他們的github我總覺得看的不是很清楚,
所以我看了一下ConvLSTM他們的實作,
有幾種情況的組合如以下所示:

在建立model後:

model:remember('both')
model:training()

在training進行之前(還沒進入epoch的for loop):

model:training()
model:forget()

在training進行之中(已進入epoch的for loop,每次epoch optim update結束之後):

model:forget()

在evaluation開始之前執行一次:

model:evaluate() 
model:forget()

簡單來說,一旦你執行了forward propagation不管是在training或testing,
你的time step可能就不是初始值了,而這樣再下一次要再propagation時似乎會造成不穩定,
所以都要記得forget()一下這樣。

至於training()和evaluate()就是前者需要記得所有的時間點,
後者只是為了一次的測試而已,不需要記這麼多消耗資源。

而remember('both')是說forget()他內部不會去call,
是完全讓使用者自己決定什麼時候call forget()這樣。
(雖然好像沒有明確寫出remember()和training()之間的差別就是了,
不過他好像兩個都會call一次,以後更懂了再更新。)

除此之外幾乎跟CNN的training是一模一樣的。

Comments

comments powered by Disqus