基於Numpy實現神經網路:合成梯度
作者:Andrew Trask
編譯:weakish
編者按:DeepMind提出用合成梯度取代反向傳播,讓網路層可以獨立學習,加快訓練速度。讓我們和DeepMind資料科學家、Udacity深度學習導師Andrew Trask一起,基於numpy實現合成梯度。授權編譯,轉載請保留作者署名&連結。
TLDR
本文將透過從頭實現DeepMind的Decoupled Neural Interfaces Using Synthetic Gradients論文中的技術,學習這一技術背後的直覺。
當我寫完新文章後,我通常會發推說一下。如果你對我的文章感興趣,歡迎關注 @iamtrask,也歡迎向我反饋。
一、合成梯度概述
通常,神經網路比較預測和資料集,以決定如何更新權重。它接著使用反向傳播找出每個權重移動的方向,使得預測更精確。然而,在合成梯度(Synthetic Gradient)的情況下,每層各自做出資料的“最佳猜測”,然後根據猜測更新其權重。“最佳猜測”稱為合成梯度。資料用來幫助更新每層的“猜測器”(合成梯度生成器)。在大多數情況下,這讓網路層可以獨立學習,以加快訓練的速度。
上圖(來自論文)提供了一個直觀的表示(自左向右)。圓角方塊為網路層,菱形為合成梯度生成器。
二、使用合成梯度
讓我們暫時忽略合成梯度是如何生成的,直接看看它們是如何使用的。上圖最左展示瞭如何更新神經網路的第一層。第一層前向傳播至合成梯度生成器(Mi+1),合成梯度生成器返回一個梯度。網路使用這個合成梯度
代替
真實的梯度(計算真實梯度需要一次完整的前向傳播和反向傳播)。接著照常更新權重,假裝合成梯度是真實梯度。如果你需要溫習下權重是如何根據梯度更新的,請參考我之前寫的
基於Numpy實現神經網路:反向傳播和梯度下降。
相關閱讀:
基於Numpy實現神經網路:反向傳播
基於Numpy實現神經網路:梯度下降
所以,簡單來說,合成梯度和平常的梯度一樣,而且出於一些神奇的原因,它們看起來很精確(在沒有檢視資料的情況下)!看起來像魔法?讓我們看看它們是如何生成的。
三、生成合成梯度
好吧,這部分非常巧妙,坦白地說,它可以起效真令人驚訝。如何為一個神經網路生成合成梯度?好吧,你當然需要另一個網路!合成梯度生成器不過是一個神經網路,該網路經訓練可以接受一個網路層的輸出,然後預測該網路層的梯度。
邊注:Geoffrey Hinton的相關工作
事實上這讓我回想起幾年前Geoffrey Hinton的工作,隨機合成權重支援的深度學習網路(arXiv:1411。0247)。基本上,你可以透過隨機生成矩陣進行反向傳播,仍然能夠完成學習。此外,他展示了這具有某種正則化效應。這肯定是一項有趣的工作。
好,回到合成梯度。論文同時提到其他相關資訊可以用作合成梯度生成網路的輸入,不過論文字身看起來在普通前饋網路上只使用了網路層的輸出作為生成器的輸入。此外,論文甚至聲稱
單線性層
可以用作合成梯度生成器。令人驚奇!我們將嘗試一下這個。
網路如何學習生成梯度?
這提出了一個問題,生成合成梯度的網路如何學習?當我們進行完整的前向傳播和反向傳播時,我們實際得到了“正確”的梯度。我們可以將其與“合成”梯度進行比較,就像我們通常比較神經網路輸出和資料集一樣。因此,我們可以假裝“真梯度”來自某個神秘的資料集,以此訓練合成梯度網路……所以我們像訓練平常的網路一樣訓練。酷!
等一下……如果合成梯度網路需要反向傳播……這還有什麼意義?
很好的問題!這一技術的全部價值在於允許獨立訓練網路層,無需等待所有網路層完成前向傳播和反向傳播。如果合成梯度網路需要等待完整的前向/反向傳播步驟,我們豈不是又回到了原點,而且需要進行的計算更多了(比原先還糟)。為了找到答案,讓我們重新看下論文中對網路架構的視覺化。
讓我們聚焦左邊的第二塊區域。看到了沒有?梯度(Mi+2)經fi+1反向傳播至Mi+2。如你所見,每個合成梯度生成器
實際上
僅僅使用下一層生成的合成梯度進行訓練。因此,
只有最後一層
實際在資料上訓練。其他層,包括合成梯度生成網路,基於合成梯度訓練。因此,訓練每層的合成梯度生成網路時,只需等待下一層的合成梯度(沒有其他依賴)。太酷了!
四、基線神經網路
到了寫程式碼的時間了!我將首先實現一個透過反向傳播進行訓練的原味神經網路,風格與基於Numpy實現神經網路:反向傳播中的類似。(所以,如果你有不明白的地方,可以先去閱讀我之前寫的文章,然後再回過頭來閱讀本文)。然而,我將額外增加一層,不過這不會造成理解問題。我只是覺得,既然我們在討論減少依賴,更多的網路層可能有助於形成更好的解釋。
至於我們訓練的資料集,我們將使用二進位制加法生成一個合成數據集(哈哈!)。所以,網路將接受兩個隨機的二進位制數作為輸入,並預測兩者之和(也是一個二進位制數)。這使我們可以方便地根據需要增加維度(大致相當於難度)。下面是生成資料集的程式碼。
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(解耦網路介面)。
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
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的精神),但在開始階段給網路提供了非常強力的資訊。
進行了這一改動後,訓練起來快多了!思考哪些可以充當梯度生成器的優良輸入真是一項迷人的活動。也許輸入資料、輸出資料、批歸一化層輸出的某種組合會是最佳的(歡迎嘗試!)希望你喜歡這篇教程。
原文地址:
https://
iamtrask。github。io/2017
/03/21/synthetic-gradients/