1. 程式人生 > >深度學習筆記(五)用Torch實現RNN來製作一個神經網路計時器

深度學習筆記(五)用Torch實現RNN來製作一個神經網路計時器

本節程式碼地址

現在終於到了激動人心的時刻了。我最初選用Torch的目的就是為了學習RNN。RNN全稱Recurrent Neural Network(遞迴神經網路),是通過在網路中增加回路而使其具有記憶功能。對自然語言處理,影象識別等方面都有深遠影響。

這次我們要用RNN實現一個神經網路計時器,給定一個時間長度,它會等待直到時間結束,然後切換自己的狀態。

如果用C語言實現一個計時器是一件非常簡單的事。我們大概要這樣寫:

void timer(int delay_time)
{
    for(int i=0; i<delay_time; i++)
    {
        delay(1);
    }
    return;
}

但是用神經網路如何來實現呢?我們可以把RNN網路想想成一個黑盒,它有一個輸入訊號和一個輸出訊號。我想讓輸入和輸出符合這樣的關係:

藍色是輸入,綠色是輸出。當輸入訊號產生一個脈衝時,計時器開始工作,計時的長度由脈衝的高度決定。計時器工作時,輸出為1,停止工作時,輸出為0。

要實現這個功能,模組內部必須有儲存單元可以記錄自身的狀態。最起碼需要一個相當於臨時變數的機制來記錄已經經過的時間長度。這正是RNN需要完成的使命。

我們如果要訓練RNN神經網路,首先要有足夠多的資料。好在我們的資料是可以無限生成的。我使用data_gen.lua來生成大量的資料,每段資料的長度為100,儲存在同一目錄下data.t7檔案中。

data_gen.lua需要單獨執行

th data_gen.lua

有了大量的資料,現在該設計我們的遞迴神經網路了。


這次的網路也很簡單,左邊有一個輸入節點,中間是擁有20個節點的隱藏層,右邊是一個輸出節點。和多層感知器不同的是,RNN中間隱藏層裡面的20個節點,每個都會連線至本層的所有節點(包括其本身)。所以這些反饋回自身的連線的個數總共有20*20=400條。

為了要在Torch裡實現這樣一個網路,我們需要用到rnn的庫。如果之前沒有安裝過的話,可以在命令列裡輸入:

luarocks install rnn

在程式中,首先仍然是包含必要的庫檔案。

require 'rnn'
require 'gnuplot'

接下來定義一些常量
batchSize = 8 --mini batch的大小
rho = 100 --rnn在訓練時所需考慮的時間最大長度,這也是Back propagation through time所要經歷的次數
hiddenSize = 20 --中間隱藏層的節點個數

接下來定義隱藏層的結構:
r = nn.Recurrent(
   hiddenSize, nn.Linear(1, hiddenSize), 
   nn.Linear(hiddenSize, hiddenSize), nn.Sigmoid(), 
   rho
)

隱藏層r的型別是nn.Recurrent。後面跟的引數依次分別是:

1. 本層中包含的節點個數,為hiddenSize

2. 前一層(也就是輸入層)到本層的連線。這裡是一個輸入為1,輸出為hiddenSize的線性連線。

3. 本層節點到自身的反饋連線。這裡是一個輸入為hiddenSize,輸出也是hiddenSize的線性連線。

4. 本層輸入和反饋連線所用的啟用函式。這裡用的是Sigmoid。

5. Back propagation through time所進行的最大的次數。這裡是rho = 100

接下來定義整個網路的結構:

rnn = nn.Sequential()
rnn:add(nn.Sequencer(r))
rnn:add(nn.Sequencer(nn.Linear(hiddenSize, 1)))
rnn:add(nn.Sequencer(nn.Sigmoid()))

首先定義一個容器,然後新增剛才定義好的隱藏層r。隨後新增隱藏層到輸出層的連線,在這裡用的是輸入為20,輸出為1的線性連線。最後接上一層Sigmoid函式。

這裡在定義網路的時候,每個具體的模組都是用nn.Sequencer的括號給括起來的。nn.Sequencer是一個修飾模組。所有經過nn.Sequencer包裝過的模組都變得可以接受序列的輸入。

舉個例子來說,假設有一個模組本來能夠接受一個2維的Tensor作為輸入,並輸出另一個2維的Tensor。如果我們想把一系列的2維Tensor依次輸入給這個模組,需要寫一個for迴圈來實現。有了nn.Sequencer的修飾就不用這麼麻煩了。只需要把這一系列的2維Tensor統一放到一個大的table裡,然後一次性的丟給nn.Sequencer就行了。nn.Sequencer會把table中的Tensor依次放入網路,並將網路輸出的Tensor也依次放入一個大的table中返回給你。

定義好了網路,接下來是定義評判標準:

criterion = nn.SequencerCriterion(nn.MSECriterion())

接下來的事情又是例行公事了。向前傳播,向後傳播,更新引數......
batchLoader = require 'MinibatchLoader'
loader = batchLoader.create(batchSize)

lr = 0.01
i = 1
for n=1,6000 do
   -- prepare inputs and targets
   local inputs, targets = loader:next_batch()

   local outputs = rnn:forward(inputs)
   local err = criterion:forward(outputs, targets)
   print(i, err/rho)
   i = i + 1
   local gradOutputs = criterion:backward(outputs, targets)
   rnn:backward(inputs, gradOutputs)
   rnn:updateParameters(lr)
   rnn:zeroGradParameters()
end

需要重點說明的是輸入和輸出資料的格式。我使用了MinibatchLoader(同目錄下的MinibatchLoader.lua檔案)來從data.t7中讀取資料,每次讀取8個序列,每個序列的時間長度是100。那麼程式碼中inputs的型別是table,這個table中有100個元素,每個元素是一個2維8列1行的Tensor。在訓練的時候,mini batch中8個序列中的每一個的第一個資料一起進入網路,接下來是8個排在第二的資料一起輸入,如此迭代。

當訓練完成之後,用其中的組輸入放進網路觀察其輸出:

inputs, targets = loader:next_batch()
outputs = rnn:forward(inputs)

x={}
y={}
for i=1,100 do
   table.insert(x,inputs[i][{1,1}])
   table.insert(y,outputs[i][{1,1}])
end

x = torch.Tensor(x)
y = torch.Tensor(y)
	
gnuplot.pngfigure('timer.png')
gnuplot.plot({x},{y})
gnuplot.plotflush()
結果顯示如下:


雖然時間的把握上還有一些偏差,但這個RNN計時器可以算是基本學到了如何計時。