如前文所述,《梯度下降法》是《深度學習神經網路》背後的學習算法,但是再輸入變數很多時,純粹靠《多次前向計算》的《梯度下降法》速度會過慢,因此我們需要使用《反傳遞算法》更有效率的計算《梯度》。
反傳遞算法是用來快速計算神經網路中梯度的方法,方法是把每個變數都變成一個具有(值 v + 梯度 g) 的節點 (node),其結構如下:
class Node { constructor(v = 0, g = 0) { this.v = v // 輸出值 (f(x)) this.g = g // 梯度值 (偏微分) } }
神經網路中的每個運算,都被表示成一種閘 (Gate),這些閘可以進行正向 forward 計算,也可以反向 backward 傳遞梯度,以下是 Gate 的類別定義。
class Gate { constructor(o, x, y, f, gfx, gfy) { this.p = {o:o, x:x, y:y, f:f, gfx:gfx, gfy:gfy||gfx} // 當 gfy 沒設定時,就代表 gfy = gfx } forward() { let p = this.p p.o.v = p.f(p.x.v, p.y.v) } backward() { let p = this.p p.x.g = p.gfx(p.o.v) p.y.g = p.gfy(p.o.v) } }
對於每個 gate ,函數 f 是用來進行《正向傳遞》forward() 計算的。
舉例而言,我們可以使用以下指令定義一個乘法閘 gmul
// f gfx gfy let gmul = new Gate(o, x, y, (x,y)=>x*y, (x,y)=>y, (x,y)=>x))
其中的 f(x,y)=>x*y 是 gmul 的正向計算函數,會計算出 o.v = x.v * y.v 的結果,放在節點 o 中。
而 gfx(x,y)=>y 與 gfy(x,y)=>x 是 gmul 的反向計算函數,會根據 o 節點的《值與梯度》 (o.v, o.g) 反向計算出 x, y 的梯度 x.g 與 y.g 。
前向計算函數 f 是個乘法閘 f(x,y)=x*y,應該很容易理解。但是反向計算函數 gfx 與 gfy 為何是 gfx(x,y)=>y 與 gfx(x,y)=>x 呢?
梯度的反向傳遞原理在 Andrej Karpathy 的 Hacker's guide to Neural Networks 一文中有詳盡的說明,讓我們在此簡述其精要。
對於 f(x,y) = xy 而言,假如 f 的梯度為 f.g ,那麼由於:
因此 x 的梯度應該是 x.g = y * f.g。
同理、 y 的梯度 y.g = x * f.g 。
所以只需要知道 f.g 與 f(x,y)=xy,就可以逆向推出 x.g 與 y.g。
這就是所謂的《梯度反傳遞》。
而這也是為何上述程式中 gfx(x,y)=>y 與 gfy(x,y)=>x 的原因。
讓我們考慮一個更複雜的兩層式網路如下圖,該網路是計算 f = (x+y) * z 這個算式。
其中的 q = x+y, 而 f = q*z。
根據偏微分的鏈鎖規則,我們可以用以下數學式描述 f, q, x 之間的梯度關係。
由於 f 為輸出,我們可以先將 f 的梯度 f.g 設定為 1,那麼就可以推斷其他變數的梯度值。
(註:其實輸出 f.g 可以設為一的非零值,代表梯度下降的移動長度,對於有《樣本與標準答案》的學習,可以將 f.g 設為輸出與標準答案的差距)
因此當 f.g
已知的時候,我們可以算出 q.g
,由於 f = q*z
,因此 q 的梯度 q.g = z*f.g
(當 f.g = 1 時,則 q.g = z)。
接著我們可以透過 q=x+y
來計算 x 的梯度,得到 x.g=q.g*f.g
來計算 x 的梯度 x.g
,於是同樣得到 x.g=z*f.g
(因為 )。
經由這種反傳遞的方式,我們就能從輸出端一路到推回中間層、然後再推回輸入端,計算出每個節點的梯度。
透過這些 Node + Gate 的組合,我們可以設計出能計算任何函數的神經網路 Net。
舉例而言,以下網路 net 會計算 ,其輸出結果會放在節點 o 當中。
const net = new nn.Net() let x = net.variable(2) let y = net.variable(1) let x2 = net.mul(x, x) let y2 = net.mul(y, y) let o = net.add(x2, y2)
我們只要呼叫網路的正向傳遞函數 net.forward() 就能從輸入值開始,經過每個閘的前向傳遞 gate.forward() 計算出結果 。
class Net { constructor () { this.gates = [] } variable (v, g) { return new Node(v, g) } op (x, y, f, gfx, gfy) { let o = new Node() let g = new Gate(o, x, y, f, gfx, gfy) this.gates.push(g) this.o = o return o } add (x, y) { return this.op(x, y, (x,y)=>x+y, (x,y)=>1) } mul (x, y) { return this.op(x, y, (x,y)=>x*y, (x,y)=>y, (x,y)=>x) } forward() { // 正向傳遞計算結果 for (let gate of this.gates) { gate.forward() } return this.o } backward() { // 反向傳遞計算梯度 this.o.g = 1 // 設定輸出節點 o 的梯度為 1 for (let i=this.gates.length-1; i>=0; i--) { // 反向傳遞計算每個節點 Node 的梯度 g let gate = this.gates[i] gate.backward() } } watch (nodes) { this.nodes = nodes } dump() { return this.nodes } }
當我們想知道該網路的輸入 (x,y) 應該調整多少,才能朝梯度方向前進時,我們只要呼叫 net.backward() ,就能透過反向傳遞計算出每個節點的梯度。
有了上述概念後,讓我們來看個範例好了,以下這個網路是計算 的函數,我們希望能透過《梯度下降+反傳遞算法》找出其最低點。
檔案: f.js
const nn = require('../../nn') const net = new nn.Net() let x = net.variable(2) let y = net.variable(1) let x2 = net.mul(x, x) let y2 = net.mul(y, y) let o = net.add(x2, y2) net.watch({x,y,x2,y2,o}) module.exports = new nn.FNet(net, {x:x, y:y})
該程式的主程式很簡單,只是對該網路呼叫梯度下降法進行優化而已。
檔案: optimizeEx.js
const nn = require('../../nn') const f = require('./f') nn.optimize(f, {x:1, y:1})
執行的結果如下:
$ cd example/02-backprop $ node .\gradientEx p= {x:2.0000, y:1.0000} f(p)= 5 gp= { x: 4, y: 1 } p= {x:1.9600, y:0.9900} f(p)= 4.8217 gp= { x: 3.8415999999999997, y: 0.9801 } p= {x:1.9216, y:0.9802} f(p)= 4.653275148657 gp= { x: 3.692485069056, y: 0.960790079601 } p= {x:1.8847, y:0.9706} f(p)= 4.493987190929792 gp= { x: 3.5519401090757823, y: 0.9420470818540095 } // ... 中間省略 .... p= {x:0.0479, y:0.0467} f(p)= 0.004475389263812065 gp= { x: 0.00228988872537307, y: 0.0021855005384389947 } p= {x:0.0478, y:0.0467} f(p)= 0.004471155300865204 gp= { x: 0.002287697698821976, y: 0.0021834576020432284 } p= {x:0.0478, y:0.0467} f(p)= 0.004466927345179781 gp= { x: 0.002285509815916863, y: 0.002181417529262918 }
上述程式最後找到了 p={x:0.0478, y:0.0467} 這個非常接近 (0,0) 的點,正確地找到了函數 的最低點 f(0,0)=0。
現在、您應該已經可以理解《梯度下降法與反傳遞算法》的運作原理了。
雖然《反傳遞算法》早在 1963 年就由 Vapnik 提出了,但是卻直到 1986 年時才由於 Hinton 等人在語音辨識上的實作成果而引發注意。
接著《神經網路》又沉寂了十幾年,到了 2011 年 Hinton 的學生用 CNN 捲積神經網路在影像辨識大賽中用 GPU 加速而大勝,再次導致神經網路進化為深度學習而爆紅。
而這一切背後運作的《反傳遞算法》,經歷了整整一甲子,並沒有多大的改變,只要您理解了這個算法的原理,就能輕鬆踏進《深度學習的神經網路領域》了!