無需反向傳播的深度學習:DeepMind的合成梯度
在這篇博文中,我們將從起點(從零開始)學習 DeepMind 最近提出的一篇論文—使用合成梯度的解耦神經接口。
一、合成梯度概述
通常,神經網絡將其預測與數據集進行比較,以決定如何更新其權重。然后使用反向傳播來確定每個權重應該如何移動,以使預測更加準確。然而,對于合成梯度來說,數據的「***預測」由各層完成,然后基于這個預測更新權重。這個「***預測」被稱為合成梯度。數據僅用于幫助更新每個層的「預測器」或者合成梯度生成器。這使得(大部分情況下)單個層獨立學習,提升了訓練的速度。
上圖(論文中)對于所發(fā)生的事情(從左到右)給出了非常直觀的解釋。圓角的正方形是層,菱形物體是(我稱其為)合成梯度生成器。讓我們來看看一個常見的神經網絡層是如何更新的。
二、使用合成梯度
我們不關注合成梯度(Synthetic Gradients)的創(chuàng)建方式,僅僅是關注其使用方法。最左邊的框顯示了如何更新神經網絡的***層。***層前向傳播到合成梯度生成器(M i+1),然后返回梯度。使用此梯度而不是實際梯度(這將需要一個完整的正向傳播和反向傳播來計算)。然后,權重正常更新,并認為該合成梯度是真實的梯度值。如果你需要了解如何使用梯度更新權重,請查看 A Neural Network in 11 Lines of Python (http://iamtrask.github.io/2015/07/12/basic-python-network/)或者也可以關注在 Gradient Descent 的后續(xù)帖子。
簡而言之,合成梯度(Synthetic Gradients)就像是普通梯度一樣被使用,并且因為某些神奇的原因,它們似乎是準確的(沒有使用數據)!像是魔法嗎?讓我們看看它們是如何構建的。
三、生成合成梯度
好的,這部分真的非常巧妙,坦白說,它的工作原理非常精妙。你如何為一個神經網絡生成合成梯度?當然是使用另一個網絡!合成梯度生成器不過是一個訓練良好的神經網絡,在該網絡中可以得到某一層的輸出并預測可能發(fā)生在該層的梯度。
1. Geoffrey Hinton 相關的工作
實際上,這讓我想起了幾年前 Geoffrey Hinton 做的一些工作。雖然我找不到參考文獻了,但是他的確做了一些工作,證明你可以通過隨機生成的矩陣反向傳播,并且仍然完成學習。此外,他表明其有一種正則化效應。這的確是一些有趣的工作。
好的,回到合成梯度。所以,現在我們知道合成梯度是由另一個神經網絡訓練的,該神經網絡基于某一層的輸出給出相應的梯度預測。論文還顯示,任何其他相關的信息均可以被用作合成梯度生成器網絡的輸入,但在論文中,好像只有該層的輸出用于正常的前饋網絡。此外,論文甚至指出可以使用單線性層作為合成梯度生成器。太厲害了,我們要嘗試一下。
2. 我們如何學習生成合成梯度的網絡?
那么問題就來了,我們如何學習產生合成梯度的神經網絡?事實證明,當我們進行全部的正反向傳播時,我們實際上得到了「正確的」梯度。我們可以用我們比較神經網絡的輸出和數據集的方法,將其與我們的「合成」梯度進行比較。因此,我們可以通過假設「真實梯度」來自于虛擬數據集來訓練我們的合成神經網絡。所以我們就可以像通常那樣訓練它們了。棒!
3. 如果我們的合成梯度網絡需要反饋,它有什么意義?
問得好!這個技術的全部意義是允許單個神經網絡訓練,而不用相互等待以完成前向與反向傳播。如果我們的合成梯度網絡需要等待完整的前向/反向傳播過程的話,那么我們又回到了原點并且還需要更大的計算量(這更糟了!)。為了找到答案,讓我們從論文中回顧一下。
請觀察左邊的第二部分??纯刺荻?Mi+2)是如何通過(fi+1)反向傳播到達 M(i+1)的?正如你所看到的,每一個合成梯度生成器實際上只使用了來自下一層的合成梯度進行訓練。因此,只有***一層實際上是在數據上訓練的。其他所有層,包括,合成梯度生成器網絡,均基于合成梯度訓練。因此,網絡只需等待來自下一層的合成梯度就可以訓練每個層。太棒了!
四、基線神經網絡
編程時間到!為了開始(所以我們有一個更簡單的參考框架),我將使用一個用反向傳播訓練的 vanilla 神經網絡,風格與 A Neural Network in 11 Lines of Python 相似。(所以如果你沒理解,去看那篇文章然后再回來)。然而, 我要添加一個附加層,但是這不應該妨礙理解。我只是在想,既然我們在減少依賴關系,更多的層可能會說明的更好。
就我們所訓練的數據集而言,我將使用二進制加法來生成合成數據集。因此,神經網絡將會采取兩個隨機二進制數并預測他們的和(也是二進制數)。好消息是,這使我們能夠靈活地根據需要增加任務的維度(難度)。以下是生成數據集的代碼。
這里是關于在該數據集的訓練 vanilla 神經網絡的代碼
現在,在這一點上,我覺得非常有必要做一些我在學習中幾乎從未做過的事情,增加一些面向對象的結構。通常,這會使網絡模糊一點,并使其更難(從高級別)讀懂正在進行什么(相對于只讀一個 python 腳本而言)。然而,因為本文是關于「解耦神經接口(Decoupled Neural Interfaces)」及其優(yōu)點,實際上很難解釋這些接口是否合理解耦。所以,為了更易學習,我首先將上述網絡轉化為完全相同的網絡,但會使用一個在后文中會轉化為 DNI 的「Layer」類對象。讓我們來看看這個 Layer 對象。
- class Layer(object):
- def __init__(self,input_dim, output_dim,nonlin,nonlin_deriv):
- self.weights = (np.random.randn(input_dim, output_dim) * 0.2) - 0.1
- self.nonlin = nonlin
- self.nonlin_deriv = nonlin_deriv
- def forward(self,input):
- self.input = input
- selfself.output = self.nonlin(self.input.dot(self.weights))
- return self.output
- def backward(self,output_delta):
- self.weight_output_delta = output_delta * self.nonlin_deriv(self.output)
- return self.weight_output_delta.dot(self.weights.T)
- def update(self,alpha=0.1):
- self.weights -= self.input.T.dot(self.weight_ou
在這個 Layer 類中,我們有幾個類變量。權重是我們用于從輸入到輸出的線性變換的矩陣(就像普通的線性層),根據需要,我們也可以包含一個非線性輸出函數,它將非線性放置在我們的輸出網絡中。如果我們不想要非線性,我們可以簡單地將此值設置為 lambda x:x。在我們的例子中,我們將傳遞給 "sigmoid" 函數。
我們傳遞的第二個函數是 nonlin_deriv,一個特殊的導函數。該函數需要從非線性中輸出,并將其轉化為導數。對于 sigmoid 來說,這非常容易(out * (1 - out)),其中「out」是 sigmoid 的輸出值。這個特定的功能幾乎存在于所有常見的神經網絡非線性中。
現在,我們來看看這個類中的各種方法。forward 正如其名字所示。它通過層向前傳播,首先通過一個線性變換,然后通過非線性函數。backward 接受了一個 output_delta 參數,它表示反向傳播期間從下一層返回的「真實梯度」(而非合成梯度),然后我們使用它來計算 self.weight_output_delta,它是我們權重輸出處的導數(僅在非線性內)。***,它反向傳播錯誤并傳入至上一層,并將其返回。
update 可能是最簡單的方法。它只需要在權重的輸出中使用導數,并使用它來進行權重更新。如果你對這些步驟有任何疑問,再一次地,查看 A Neural Network in 11 Lines of Python 再回來。如果你能全部理解,那么讓我們在訓練中看看我們的層對象。
- layer_1 = Layer(input_dim,layer_1_dim,sigmoid,sigmoid_out2deriv)
- layer_2 = Layer(layer_1_dim,layer_2_dim,sigmoid,sigmoid_out2deriv)
- layer_3 = Layer(layer_2_dim, output_dim,sigmoid, sigmoid_out2deriv)
- for iter in range(iterations):
- error = 0
- for batch_i in range(int(len(x) / batch_size)):
- batch_x = x[(batch_i * batch_size):(batch_i+1)*batch_size]
- batch_y = y[(batch_i * batch_size):(batch_i+1)*batch_size]
- layer_1layer_1_out = layer_1.forward(batch_x)
- layer_2layer_2_out = layer_2.forward(layer_1_out)
- layer_3layer_3_out = layer_3.forward(layer_2_out)
- layer_3_delta = layer_3_out - batch_y
- layer_2_delta = layer_3.backward(layer_3_delta)
- layer_1_delta = layer_2.backward(layer_2_delta)
- layer_1.backward(layer_1_delta)
- layer_1.update()
- layer_2.update()
- layer_3.update()
給定一個數據集 x 和 y,這就是我們使用我們的新層對象的方式。如果你把它與之前的腳本進行比較,幾乎是一模一樣。我只是更換了調用的神經網絡的版本。
所以,我們所做是將先前神經網絡的腳本代碼在類中分成不同的函數。下面,我們在實際中看看這個層。
如果你將以前的網絡和此網絡同時插入 Jupyter notebooks,你將會看到隨機種子會使這些網絡具有完全相同的值??雌饋?,Trinket.io 可能沒有***的隨機選取種子點以使這些網絡達到幾乎相同的值。然而,我向你保證,網絡是相同的。如果你認為這個網絡毫無意義,不要進行下一步的學習。在進行后面的學習前確保對這個抽象網絡的工作方式熟稔于心,因為下面將會變得更加復雜。
五、整合合成梯度
好的,所以現在我們將使用一個與上述非常相似的接口,唯一不同在于我們將所學到的關于合成梯度的知識整合入 Layer 對象中(并重命名為 DNI)。首先,我們要向你展示類,然后我會對其進行解釋。一探究竟吧!
- class DNI(object):
- def __init__(self,input_dim, output_dim,nonlin,nonlin_deriv,alpha = 0.1):
- # same as before
- self.weights = (np.random.randn(input_dim, output_dim) * 0.2) - 0.1
- self.nonlin = nonlin
- self.nonlin_deriv = nonlin_deriv
- # new stuff
- self.weights_synthetic_grads = (np.random.randn(output_dim,output_dim) * 0.2) - 0.1
- self.alpha = alpha
- # used to be just "forward", but now we update during the forward pass using Synthetic Gradients :)
- def forward_and_synthetic_update(self,input):
- # cache input
- self.input = input
- # forward propagate
- selfself.output = self.nonlin(self.input.dot(self.weights))
- # generate synthetic gradient via simple linear transformation
- selfself.synthetic_gradient = self.output.dot(self.weights_synthetic_grads)
- # update our regular weights using synthetic gradient
- selfself.weight_synthetic_gradient = self.synthetic_gradient * self.nonlin_deriv(self.output)
- self.weights += self.input.T.dot(self.weight_synthetic_gradient) * self.alpha
- # return backpropagated synthetic gradient (this is like the output of "backprop" method from the Layer class)
- # also return forward propagated output (feels weird i know... )
- return self.weight_synthetic_gradient.dot(self.weights.T), self.output
- # this is just like the "update" method from before... except it operates on the synthetic weights
- def update_synthetic_weights(self,true_gradient):
- selfself.synthetic_gradient_delta = self.synthetic_gradient - true_gradient
- self.weights_synthetic_grads += self.output.
那么,***個大的變化。我們有了一些新的類變量。其中唯一真正重要的是,self.weights_synthetic_grads 變量,這就是我們的合成生成器神經網絡(只是一個線性層,其實,,,就是一個矩陣)
前向和合成更新:前向方法已經變成 forward_and_synthetic_update。記得我們是如何不需要網絡的任何其他的部分來更新我們的權重嗎?這就是神奇的地方了。首先,前向傳播像正常一樣發(fā)生(22 行)。然后,我們通過非線性傳遞我們的輸出來生成我們的合成梯度。這部分可能是一個更復雜的網絡,但是相反,我們決定保持簡單,并只是用一個簡單的線性層生成我們的合成梯度。在我們得到我們的梯度時,我們繼續(xù)執(zhí)行并更新我們的普通權重(28 和 29 行)。最終,我們從權重的輸出反向傳播我們的合成梯度至其輸入,這樣我們可以將其傳入上一層。
更新合成梯度:好的,那么我們在「forward」方法的結尾處返回了梯度。這就是我們從下一層接受進 update_synthetic_gradient 方法的東西。那么,如果我們現在在第二層,那么第三層從其 forward_and_synthetic_update 方法中返回了一個梯度,并將其輸入進第二層的 update_synthetic_weights。然后,我們只需要像更新普通的神經網絡那樣更新我們的合成梯度就好。我們將輸入傳入合成梯度層(self.output),然后用 output delta 執(zhí)行均值外積運算(矩陣轉置 → 矩陣乘法)。這與在一個普通的神經網絡中學習沒什么不同,我們只是在 leau of data 中得到一些特殊的輸入和輸出。
讓我們在實踐中看一看它
訓練有點不一樣。較大的數據批量規(guī)模和較小的 alpha 值似乎性能更好,但積極的一面是,在訓練中只迭代了一半次數!(這可能很容易調整,但是仍然。。不錯)。一些結論。訓練似乎有些混亂(不會下降)。我不知道在 hood 那發(fā)生了什么事,但是當它收斂時,肯定很快。
原文:https://iamtrask.github.io/2017/03/21/synthetic-gradients/
【本文是51CTO專欄機構機器之心的原創(chuàng)譯文,微信公眾號“機器之心( id: almosthuman2014)”】