DeepMind提出用合成梯度取代反向傳播,讓網絡層可以獨立學習,加快訓練速度。讓我們和DeepMind數據科學家、Udacity深度學習導師Andrew Trask一起,基于numpy實現合成梯度。
TLDR本文將通過從頭實現DeepMind的Decoupled Neural Interfaces Using Synthetic Gradients論文中的技術,學習這一技術背后的直覺。
一、合成梯度概述
通常,神經網絡比較預測和數據集,以決定如何更新權重。它接著使用反向傳播找出每個權重移動的方向,使得預測更精確。然而,在合成梯度(Synthetic Gradient)的情況下,每層各自做出數據的“最佳猜測”,然后根據猜測更新其權重?!白罴巡聹y”稱為合成梯度。數據用來幫助更新每層的“猜測器”(合成梯度生成器)。在大多數情況下,這讓網絡層可以獨立學習,以加快訓練的速度。
上圖(來自論文)提供了一個直觀的表示(自左向右)。圓角方塊為網絡層,菱形為合成梯度生成器。
二、使用合成梯度
讓我們暫時忽略合成梯度是如何生成的,直接看看它們是如何使用的。上圖最左展示了如何更新神經網絡的第一層。第一層前向傳播至合成梯度生成器(Mi+1),合成梯度生成器返回一個梯度。網絡使用這個合成梯度代替真實的梯度(計算真實梯度需要一次完整的前向傳播和反向傳播)。接著照常更新權重,假裝合成梯度是真實梯度。如果你需要溫習下權重是如何根據梯度更新的,請參考我之前寫的基于Numpy實現神經網絡:反向傳播和梯度下降。
所以,簡單來說,合成梯度和平常的梯度一樣,而且出于一些神奇的原因,它們看起來很精確(在沒有查看數據的情況下)!看起來像魔法?讓我們看看它們是如何生成的。
三、生成合成梯度
好吧,這部分非常巧妙,坦白地說,它可以起效真令人驚訝。如何為一個神經網絡生成合成梯度?好吧,你當然需要另一個網絡!合成梯度生成器不過是一個神經網絡,該網絡經訓練可以接受一個網絡層的輸出,然后預測該網絡層的梯度。
邊注:Geoffrey Hinton的相關工作
事實上這讓我回想起幾年前Geoffrey Hinton的工作,隨機合成權重支持的深度學習網絡(arXiv:1411.0247)。基本上,你可以通過隨機生成矩陣進行反向傳播,仍然能夠完成學習。此外,他展示了這具有某種正則化效應。這肯定是一項有趣的工作。
好,回到合成梯度。論文同時提到其他相關信息可以用作合成梯度生成網絡的輸入,不過論文本身看起來在普通前饋網絡上只使用了網絡層的輸出作為生成器的輸入。此外,論文甚至聲稱單線性層可以用作合成梯度生成器。令人驚奇!我們將嘗試一下這個。
網絡如何學習生成梯度?
這提出了一個問題,生成合成梯度的網絡如何學習?當我們進行完整的前向傳播和反向傳播時,我們實際得到了“正確”的梯度。我們可以將其與“合成”梯度進行比較,就像我們通常比較神經網絡輸出和數據集一樣。因此,我們可以假裝“真梯度”來自某個神秘的數據集,以此訓練合成梯度網絡……所以我們像訓練平常的網絡一樣訓練。酷!
等一下……如果合成梯度網絡需要反向傳播……這還有什么意義?
很好的問題!這一技術的全部價值在于允許獨立訓練網絡層,無需等待所有網絡層完成前向傳播和反向傳播。如果合成梯度網絡需要等待完整的前向/反向傳播步驟,我們豈不是又回到了原點,而且需要進行的計算更多了(比原先還糟)。為了找到答案,讓我們重新看下論文中對網絡架構的可視化。
讓我們聚焦左邊的第二塊區域??吹搅藳]有?梯度(Mi+2)經fi+1反向傳播至Mi+2。如你所見,每個合成梯度生成器實際上僅僅使用下一層生成的合成梯度進行訓練。因此,只有最后一層實際在數據上訓練。其他層,包括合成梯度生成網絡,基于合成梯度訓練。因此,訓練每層的合成梯度生成網絡時,只需等待下一層的合成梯度(沒有其他依賴)。太酷了!
四、基線神經網絡
到了寫代碼的時間了!我將首先實現一個通過反向傳播進行訓練的原味神經網絡,風格與基于Numpy實現神經網絡:反向傳播中的類似。(所以,如果你有不明白的地方,可以先去閱讀我之前寫的文章,然后再回過頭來閱讀本文)。然而,我將額外增加一層,不過這不會造成理解問題。我只是覺得,既然我們在討論減少依賴,更多的網絡層可能有助于形成更好的解釋。
至于我們訓練的數據集,我們將使用二進制加法生成一個合成數據集(哈哈?。K?,網絡將接受兩個隨機的二進制數作為輸入,并預測兩者之和(也是一個二進制數)。這使我們可以方便地根據需要增加維度(大致相當于難度)。下面是生成數據集的代碼。
import numpy as np
import sys
def generate_dataset(output_dim = 8,num_examples=1000):
def int2vec(x,dim=output_dim):
out = np.zeros(dim)
binrep = np.array(list(np.binary_repr(x))).astype('int')
out[-len(binrep):] = binrep
return out
x_left_int = (np.random.rand(num_examples) * 2**(output_dim - 1)).astype('int')
x_right_int = (np.random.rand(num_examples) * 2**(output_dim - 1)).astype('int')
y_int = x_left_int + x_right_int
x = list()
for i in range(len(x_left_int)):
x.append(np.concatenate((int2vec(x_left_int[i]),int2vec(x_right_int[i]))))
y = list()
for i in range(len(y_int)):
y.append(int2vec(y_int[i]))
x = np.array(x)
y = np.array(y)
return (x,y)
num_examples = 1000
output_dim = 12
iterations = 1000
x,y = generate_dataset(num_examples=num_examples, output_dim = output_dim)
print("Input: two concatenated binary values:")
print(x[0])
print("\nOutput: binary value of their sum:")
print(y[0])
下面則是相應的神經網絡代碼:
batch_size = 10
alpha = 0.1
input_dim = len(x[0])
layer_1_dim = 128
layer_2_dim = 64
output_dim = len(y[0])
weights_0_1 = (np.random.randn(input_dim,layer_1_dim) * 0.2) - 0.1
weights_1_2 = (np.random.randn(layer_1_dim,layer_2_dim) * 0.2) - 0.1
weights_2_3 = (np.random.randn(layer_2_dim,output_dim) * 0.2) - 0.1
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_0 = batch_x
layer_1 = sigmoid(layer_0.dot(weights_0_1))
layer_2 = sigmoid(layer_1.dot(weights_1_2))
layer_3 = sigmoid(layer_2.dot(weights_2_3))
layer_3_delta = (layer_3 - batch_y) * layer_3 * (1 - layer_3)
layer_2_delta = layer_3_delta.dot(weights_2_3.T) * layer_2 * (1 - layer_2)
layer_1_delta = layer_2_delta.dot(weights_1_2.T) * layer_1 * (1 - layer_1)
weights_0_1 -= layer_0.T.dot(layer_1_delta) * alpha
weights_1_2 -= layer_1.T.dot(layer_2_delta) * alpha
weights_2_3 -= layer_2.T.dot(layer_3_delta) * alpha
error += (np.sum(np.abs(layer_3_delta)))
sys.stdout.write("\rIter:" + str(iter) + " Loss:" + str(error))
if(iter % 100 == 99):
print("")
現在,我真心覺得有必要做些我幾乎從不在學習時做的事,加上一點面向對象結構。通常,這會略微混淆網絡,更難看清代碼做了什么。然而,由于本文的主題是“解耦網絡接口”(Decoupled Neural Interfaces)及其優勢,如果不解耦這些接口的話,解釋起來會相當困難。因此,我將把上面的網絡轉換為一個Layer類,之后將進一步轉換為一個DNI(解耦網絡接口)。
classLayer(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
self.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_output_delta) * alpha
在這個Layer類中,我們有一些變量。weights是我們從輸入到輸出進行線性變換的矩陣(就像平常的線性層)。我們同時引入了一個輸出nonlin函數,給我們的網絡輸出加上了非線性。如果我們不想要非線性,我們可以直接將其值設為lambda x:x。在我們的情形中,我們將傳入sigmoid函數。
我們傳入的第二個函數是nonlin_deriv,這是一個導數。該函數將接受我們的非線性輸出,并將其轉換為導數。就sigmoid而言,它的值為(out * (1 - out)),其中out為sigmoid的輸出。
現在,讓我們看下類中的幾個方法。forward,顧名思義,前向傳播,首先通過一個線性轉換,接著通過一個非線性函數。backward接受一個output_delta參數,該參數表示從下一層經反向傳播返回的真實梯度(非合成梯度)。我們接著使用這個參數來計算self.weight_output_delta,也就是權重輸出的導數。最后,反向傳播發送給前一層的誤差,并返回誤差。
update也許是其中最簡單的函數。它直接接受權重輸出的導數,并使用它更新權重。如果有任何步驟不明白,請再次參考基于Numpy實現神經網絡:反向傳播。
接著,讓我們看看layer對象是如何用于訓練的。
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_1_out = layer_1.forward(batch_x)
layer_2_out = layer_2.forward(layer_1_out)
layer_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()
如果你將上面的代碼和之前的腳本對比,基本上所有事情發生在基本相同的地方。我只是用方法調用替換了腳本中的相應操作。
所以,我們實際上做的是從之前的腳本中提取步驟,將其切分為類中不同的函數。
如果你搞不明白這個新版本的網絡,不要繼續下去。確保你在繼續閱讀下文之前習慣這種抽象的方式,因為下面會變得更復雜。
五、基于層輸出的合成梯度
現在,我們將基于了解的合成梯度的知識改寫Layer類,將其重新命名為DNI。
class DNI(object):
def __init__(self,input_dim, output_dim,nonlin,nonlin_deriv,alpha = 0.1):
# 和之前一樣
self.weights = (np.random.randn(input_dim, output_dim) * 0.2) - 0.1
self.nonlin = nonlin
self.nonlin_deriv = nonlin_deriv
# 新東西
self.weights_synthetic_grads = (np.random.randn(output_dim,output_dim) * 0.2) - 0.1
self.alpha = alpha
# 之前僅僅是`forward`,現在我們在前向傳播中基于合成梯度更新權重
def forward_and_synthetic_update(self,input):
# 緩存輸入
self.input = input
# 前向傳播
self.output = self.nonlin(self.input.dot(self.weights))
# 基于簡單的線性變換生成合成梯度
self.synthetic_gradient = self.output.dot(self.weights_synthetic_grads)
# 使用合成梯度更新權重
self.weight_synthetic_gradient = self.synthetic_gradient * self.nonlin_deriv(self.output)
self.weights += self.input.T.dot(self.weight_synthetic_gradient) * self.alpha
# 返回反向傳播的合成梯度(這類似Layer類的backprop方法的輸出)
# 同時返回前向傳播的輸出(我知道這有點怪……)
return self.weight_synthetic_gradient.dot(self.weights.T), self.output
# 和之前的`update`方法類似……除了基于合成權重之外
def update_synthetic_weights(self,true_gradient):
self.synthetic_gradient_delta = self.synthetic_gradient - true_gradient
self.weights_synthetic_grads += self.output.T.dot(self.synthetic_gradient_delta) * self.alpha
我們有了一些新的變量。唯一關鍵的是self.weights_synthetic_grads,這是我們的合成梯度生成器神經網絡(只是一個線性層……也就是……一個矩陣)。
前向傳播和合成更新:forward方法變為forward_and_synthetic_update。還記得我們不需要網絡的其他部分來更新權重嗎?這就是魔法發生之處。首先,照常進行前向傳播。接著,我們通過將輸出傳給一個非線性生成合成梯度。這一部分本可以是一個更復雜的神經網絡,不過我們沒有這么做,而是決定保持簡單性,直接使用一個簡單的線性層生成我們的合成梯度。得到我們的梯度之后,我們繼續更新權重。最后,我們反向傳播合成梯度,以便發送給之前的層。
更新合成梯度:下一層的update_synthetic_gradient方法將接受上一層的forward_and_synthetic_update方法返回的梯度。所以,如果我們位于第二層,那么第三層的forward_and_synthetic_update方法返回的梯度將作為第二層的update_synthetic_weights的輸入。接著,我們直接更新合成權重,就像在普通的神經網絡中做的那樣。這和通常的神經網絡的學習沒什么兩樣,只不過我們使用了一些特別的輸入和輸出而已。
基于合成梯度方法訓練網絡,我發現它不像我預料的那樣收斂。我的意思是,它在收斂,但是收斂得非常慢。我仔細調查了一下,發現隱藏的表示(也就是梯度生成器的輸入)在開始時比較扁平和隨機。換句話說,兩個不同的訓練樣本在不同網絡層結果會有幾乎一樣的輸出表示。這大大增加了梯度生成器工作的難度。在論文中,作者使用的解決方案是批歸一化,批歸一化將所有網絡層輸出縮放至0均值和單位方差。此外,論文還提到你可以使用其他形式的梯度生成器輸入。對于我們的簡單玩具神經網絡而言,批歸一化會加入大量復雜度。因此,我嘗試了使用輸出數據集。這并沒有破壞解耦狀態(秉持了DNI的精神),但在開始階段給網絡提供了非常強力的信息。
進行了這一改動后,訓練起來快多了!思考哪些可以充當梯度生成器的優良輸入真是一項迷人的活動。也許輸入數據、輸出數據、批歸一化層輸出的某種組合會是最佳的(歡迎嘗試!)希望你喜歡這篇教程。
-
神經網絡
+關注
關注
42文章
4771瀏覽量
100766 -
生成器
+關注
關注
7文章
315瀏覽量
21011
原文標題:基于Numpy實現神經網絡:合成梯度
文章出處:【微信號:jqr_AI,微信公眾號:論智】歡迎添加關注!文章轉載請注明出處。
發布評論請先 登錄
相關推薦
評論