上一次我們使用了輸出節點和輸入節點直接相連的網路。網路裡只有兩個可變引數。這種網路只能表示一條直線,不能適應複雜的曲線。我們將把它改造為一個多層網路。一個輸入節點,然後是兩個隱藏層,每個隱藏層有3個節點,每個隱藏節點後面都跟一個非線性的Sigmoid函式。如圖所示,這次我們使用的網路是有2個隱藏層,每層有3個節點的多層神經網路。

那麼這樣的結構用程式碼如何表示呢?我們來直接在上一次的程式碼上修改。
這個網路結構是一層一層的疊加起來的,nn庫裡有一個型別叫做Sequential序列,正好適合我們。這個Sequential是一個容器類,我們可以在它裡面新增一些基本的模組。
model = nn.Sequential()
第一個我們要新增的是輸入節點和第一個隱藏層的連線,它是一個Linear線性的型別,它的輸入是1個節點,輸出是3個節點。
model:add(nn.Linear(1,3))
然後我們在他後面新增一個Sigmoid層,它的節點個數會自動和前一層的輸出個數保持一致。
model:add(nn.Sigmoid())
接下來我們新增第一和第二隱藏層中間的線性連線,輸入是3,輸出也是3。
model:add(nn.Linear(3,3))
再新增一個Sigmoid層。
model:add(nn.Sigmoid())
最後是第二隱藏層和輸出節點之間的線性連線,輸入是3,輸出是1。
model:add(nn.Linear(3,1))
所以完整的建立模型的程式碼看起來是這樣的
model = nn.Sequential()
model:add(nn.Linear(1,3))
model:add(nn.Sigmoid())
model:add(nn.Linear(3,3))
model:add(nn.Sigmoid())
model:add(nn.Linear(3,1))
好,理論上講我們已經改造完了網路,可以開始訓練了。我們執行一下,看一下結果。我們會很意外的發現這個結果還不如我們上一次的結果。
其實這裡面存在兩個問題:
一個是我們的訓練資料,輸入的月份取值範圍從1到10,輸出的價格取值範圍是幾萬。這樣開始訓練的時候後面幾層的梯度會受到輸出值的影響,變得非常大,迅速的把前面幾層的引數推到一個很大的數值。而Sigmoid函式在遠離零點的位置幾乎梯度為零,所以就一直固定在一個位置不動了。
解決的方法是把輸入和輸出的取值範圍調整到合適的區間,我這裡把輸入除以10,輸出除以50000。預測時再把50000乘回去。在程式碼裡面體現,就是在開頭和結尾加兩個輔助層,nn.MulConstant,這種型別的模組是對網路中的每個元素乘上一個常數。在輸入進入之前先乘以0.1,在輸入之後乘以50000。
這樣一來,建立模型的程式碼就變成了這樣:
model = nn.Sequential()
model:add(nn.MulConstant(0.1)) --在輸入進入之前先乘以0.1
model:add(nn.Linear(1,3))
model:add(nn.Sigmoid())
model:add(nn.Linear(3,3))
model:add(nn.Sigmoid())
model:add(nn.Linear(3,1))
model:add(nn.MulConstant(50000)) --在輸入之後乘以50000
資料預處理問題現在解決了,還有一個問題是訓練的速度很慢。因為我們現在的優化方法用的是最原始梯度下降法。
其實Torch已經給我們提供了各種先進的優化演算法,都放在optim這個庫裡。我們在檔案的頭部新增包含optim庫:
require 'optim'
另外,還需要把model裡面的引數找出來方便隨時呼叫。
w, dl_dw = model:getParameters()
w是model裡面所有可調引數的集合,dl_dw是每個引數對loss的偏導數。需要注意的是這裡的w和dl_dw都相當於C++裡面的“引用”,一旦你對他們進行了操作,模型裡的引數也會跟著改變。

優化函式的呼叫方法有一點特殊,需要你先提供一個目標函式,這個函式相當於C++裡的回撥函式,他的輸入是一組網路權重引數w,輸出有兩個,第一個是網路使用引數w時,其輸出結果與實際結果之間的差別,也可以叫loss損失,另一個是w中每個引數對於loss的偏導數。
feval = function(w_new)
   if w ~= w_new then w:copy(w_new) end
   dl_dw:zero()

   price_predict = model:forward(month_train)
   loss = criterion:forward(price_predict, price_train)
   model:backward(month_train, criterion:backward(price_predict, price_train))
   return loss, dl_dw
end
這個回撥函式可以參照這個例子來寫,同樣是“例行公事”,呼叫一下反向傳播的演算法。
有了這個目標函式,優化迭代的過程就簡單多了。只需要一句optim.rprop(feval, w, params)。 rprop是一種改進的梯度下降法,它只看梯度的方向,不管大小,只要方向不變,它會無限的增大步長,所以他速度非常快。迭代的程式碼如下:
params = {
   learningRate = 1e-2
}

for i=1,3000 do
   optim.rprop(feval, w, params)

   if i%10==0 then
      gnuplot.plot({month, price}, {month_train:reshape(10), price_predict:reshape(10)})
   end
end
其中每10次迭代會把結果用gnuplot畫出來。
我們來執行一下。
在命令列鍵入
th mlp.lua

看一下結果,這次的結果看起來就好多了。綠線(預測值)幾乎和藍線(實際值)重合在一起了。
下一節,我們將介紹如何用卷積神經網路識別MNIST手寫數字影象。
本節的完整程式碼:
require 'torch'
require 'nn'
require 'optim'
require 'gnuplot'

month = torch.range(1,10)
price = torch.Tensor{28993,29110,29436,30791,33384,36762,39900,39972,40230,40146}

model = nn.Sequential()
model:add(nn.MulConstant(0.1))
model:add(nn.Linear(1,3))
model:add(nn.Sigmoid())
model:add(nn.Linear(3,3))
model:add(nn.Sigmoid())
model:add(nn.Linear(3,1))
model:add(nn.MulConstant(50000))
criterion = nn.MSECriterion()

month_train = month:reshape(10,1)
price_train = price:reshape(10,1)

gnuplot.figure()

w, dl_dw = model:getParameters()

feval = function(w_new)
   if w ~= w_new then w:copy(w_new) end
    dl_dw:zero()

    price_predict = model:forward(month_train)
    loss = criterion:forward(price_predict, price_train)
    model:backward(month_train, criterion:backward(price_predict, price_train))
    return loss, dl_dw
end
    
params = {
   learningRate = 1e-2
}

for i=1,3000 do
   optim.rprop(feval, w, params)

   if i%10==0 then
      gnuplot.plot({month, price}, {month_train:reshape(10), price_predict:reshape(10)})
   end
end

month_predict = torch.range(1,12)
local price_predict = model:forward(month_predict:reshape(12,1))
print(price_predict)

gnuplot.pngfigure('plot.png')
gnuplot.plot({month, price}, {month_predict, price_predict})
gnuplot.plotflush()