實現屬於自己的TensorFlow(二) - 梯度計算與反向傳播

前言

上一篇中介紹了計算圖以及前向傳播的實現,本文中將主要介紹對於模型優化非常重要的反向傳播演算法以及反向傳播演算法中梯度計算的實現。因為在計算梯度的時候需要涉及到矩陣梯度的計算,本文針對幾種常用操作的梯度計算和實現進行了較為詳細的介紹。如有錯誤歡迎指出。

首先先簡單總結一下, 實現反向傳播過程主要就是完成兩個任務:

  1. 實現不同操作輸出對輸入的梯度計算
  2. 實現根據鏈式法則計算損失函數對不同節點的梯度計算

再附上SimpleFlow的代碼地址:

PytLab/simpleflowgithub.com圖標

正文

反向傳播

對於我們構建的模型進行優化通常需要兩步:1.求損失函數針對變數的梯度;2.根據梯度信息進行參數優化(例如梯度下降). 那麼該如何使用我們構建的計算圖來計算損失函數對於圖中其他節點的梯度呢?通過鏈式法則。我們還是通過上篇中的表達式 Loss(x, y, z) = z(x + y) 對應的計算圖來說明:

我們把上面的操作節點使用字母進行標記,可以將每個操作看成一個函數,接受一個或兩個輸入有一個或者多個輸出, 則上面的表達式

Loss(x, y, z) = z(x+y)

可以寫成

Loss(x, y, z) = g(z, f(x, y))

那麼根據鏈式法則我們可以得到 Lossx 的導數為:

frac{partial Loss}{partial x} = frac{partial Loss}{partial g}frac{partial g}{partial f} frac{partial f}{partial x}

假設圖中的節點已經計算出了自己的輸出值,我們把節點的輸出值放到節點裡面如下:

然後再把鏈式法則的式子每一項一次計算,在圖中也就是從後向前進行計算:

1. frac{partial Loss}{partial g} = 1

2. frac{partial g}{partial f} = z = 6 (當然也可以計算出 frac{partial g}{partial z} = x + y = 5 ). 進而求出 frac{partial Loss}{partial f} = frac{partial Loss}{partial g}frac{partial g}{partial f} = 1 times z = 6

3. frac{partial f}{partial x} = 1 (同時也可以算出 frac{partial f}{partial y} = 1 ). 進而求出 frac{partial Loss}{partial x} = frac{partial Loss}{partial g}frac{partial g}{partial f}frac{partial f}{partial x} = 1 times z times 1 = 6

這樣從後向前逐級計算通過鏈式法則就可以計算出與損失值對其相關節點的梯度了。因此我們下一步要做的就是給定某個損失函數節點並計算它對於某一節點梯度計算。

下面在看一個不同的計算圖:

這裡的 x 節點有將輸出到兩個不同的節點中,此時我們需要計算所有從 gx 的路徑然後按照上面單挑路徑的鏈式法則計算方法計算每條路徑的梯度值,最終再將不同路徑的梯度求和即可。因此 Lossx 的梯度為:

frac{partial Loss}{partial x} = frac{partial g}{partial f}frac{partial f}{partial h}frac{partial h}{partial x} + frac{partial g}{partial f}frac{partial f}{partial l}frac{partial l}{partial x}

梯度計算

通過上面對反向傳播的介紹我們已經知道損失值對某個節點的梯度是怎麼求的(具體的實現方法在下篇博客中說明),下面就是如何求取針對某個節點上的梯度了,只要每個節點上的梯度計算出來沿著路徑反方向不斷乘下去就會得到你想要的節點的梯度了。本部分就介紹如何求損失值對具體某個節點的梯度值。

本部分我們就是干這麼一個事,首先我們先畫個節點:

f 節點可以看成一個函數 z=f(x,y) , 我們需要做的就是求 frac{partial f(x, y)}{partial x}frac{partial f(x, y)}{partial y} .

平方運算的梯度計算

我們先用一個平方運算(之所以不用求和和乘積/矩陣乘積來做例子,因為這裡面涉及到矩陣求導維度的處理,會在稍後進行總結, 而平方運算並不會涉及到維度的變化比較簡單):

class Square(Operation):n Square operation. n # ...n def compute_gradient(self, grad=None):n Compute the gradient for square operation wrt input value.n :param grad: The gradient of other operation wrt the square output.n :type grad: ndarray.n n input_value = self.input_nodes[0].output_valuen if grad is None:n grad = np.ones_like(self.output_value)n return grad*np.multiply(2.0, input_value)n

其中grad為損失值對Square輸出的梯度值,也就是上圖中的 frac{partial Loss}{partial z} 的值, 它的shape一定與Square的輸出值的shape一致

神經網路反向傳播的矩陣梯度計算

矩陣梯度的計算是實現反向傳播演算法重要的一部分, 但是在實現神經網路反向傳播的矩陣求導與許多公式列表上羅列出來的還是有差別的。

矩陣/向量求導

首先先看下矩陣的求導,其實矩陣的求導本質上就是目標矩陣中的元素對變數矩陣中的元素求偏導,至於求導後的導數矩陣的形狀大都也都是為了形式上的美觀方便求導之後的繼續使用。所以不必被那些複雜的矩陣求導形式迷惑了雙眼。這裡上傳了一份矩陣求導公式法則的列表PDF版本,可以一步一步通過(行/列)向量對標量求導再到(行/列)向量對(行/列)向量求導再到矩陣對矩陣的求導逐漸擴展。

例如標量 y 對矩陣 X = left[ begin{matrix}x_{11} & x_{12}  x_{21} & x_{22} end{matrix} right] 求導, 我們就對標量 y 對於 X 的所有元素求偏導,最終得到一個導數矩陣,矩陣形狀同 X 相同:

frac{mathrm{d}y}{mathrm{d}X} = left[ begin{matrix} frac{partial y}{partial x_{11}} & frac{partial y}{partial x_{12}}  frac{partial y}{partial x_{21}} & frac{partial y}{partial x_{22}} end{matrix} right]

神經網路反向傳播中的矩陣求導

之所以把矩陣求導分成兩部分,是因為在實現矩陣求導的時候發現做反向傳播的時候的矩陣求導與矩陣求導公式的形式上還是有區別的。所謂的區別就是,我們在神經網路進行矩陣求導的時候其實是Loss(損失)函數對節點中的矩陣進行求導,而損失函數是標量,那每次我們對計算圖中的每個節點計算梯度的時候其實是計算的標量(損失值)對矩陣(節點輸出值)的求導. 也就是說在進行反向傳播的時候我們用的只是矩陣求導中的一種,即標量對矩陣的求導, 也就是上面舉的例子的形式。再進一步其實就是損失函數對矩陣中每個元素進行求偏導的過程,通俗的講就是計算圖中矩陣中的每個元素對損失值的一個影響程度。因此這樣計算出來的導數矩陣的形狀與變數的形狀一定是一致的。

直觀上理解就是計算圖中對向量/矩陣求導的時候計算的是矩陣中的元素對損失值影響程度的大小,其形狀與矩陣形狀相同。

求和操作的梯度計算

現在我們以求和操作的梯度計算為例說明反向傳播過程中矩陣求導的實現方法。

對於求和操作: C = A + b , 其中 A = left[ begin{matrix} a_{11} & a_{12}  a_{21} & a_{22} end{matrix} right] , b = b_{0} , 則 C = left[ begin{matrix} a_{11} + b_0 & a_{12} + b_0  a_{21} + b_0 & a_{22} + b_0 end{matrix} right] , 損失值 LC 梯度矩陣為 G = left[ begin{matrix} frac{partial L}{partial c_{11}} & frac{partial L}{partial c_{12}}  frac{partial L}{partial c_{21}} & frac{partial L}{partial c_{22}} end{matrix} right]

下面我們計算 frac{partial L}{partial b} , 根據我們之前說的這個梯度的維度(形狀)應該與 b 相同,也就是一個標量,那麼具體要怎麼計算呢?我們分成兩部分來處理:

1. 先計算對於 C=A+Bfrac{partial L}{partial B} 的梯度值,其中 B = left[ begin{matrix} b_0 & b_0  b_0 & b_0 end{matrix} right] 是通過對 b 進行廣播操作得到的

frac{partial L}{partial B} = left[ begin{matrix} frac{partial L}{c_{11}} frac{partial c_{11}}{partial b_0} & frac{partial L}{c_{12}} frac{partial c_{12}}{partial b_0}  frac{partial L}{c_{21}} frac{partial c_{21}}{partial b_0} & frac{partial L}{c_{22}} frac{partial c_{22}}{partial b_0}  end{matrix} right] = left[ begin{matrix} frac{partial L}{c_{11}} times 1 & frac{partial L}{c_{12}} times 1  frac{partial L}{c_{21}} times 1 & frac{partial L}{c_{22}} times 1  end{matrix} right] = frac{partial L}{partial C} = G

2. 計算 Lb 的梯度 frac{partial L}{partial b} 。因為 B 是對 b 的一次廣播操作,雖然是用的是矩陣的形式,本質上是將 b 複製了4份然後再進行操作的,因此將 frac{partial L}{partial B} 中的每個元素進行累加就是 frac{partial L}{partial b} 的值了。

則梯度的值為:

frac{partial L}{partial b} = sum_{i=1}^{2} sum_{j=1}^{2} frac{partial L}{partial c_{ij}}

針對此例 b 是一個標量,使用矩陣表示的話可以表示成:

frac{partial L}{partial b} = left[ begin{matrix} 1 & 1 end{matrix} right] G left[ begin{matrix} 1  1 end{matrix} right]

b 是一個長度為2的列向量,型如 left[ begin{matrix} b_0  b_0end{matrix} right] 則需要將 G 中的每一列進行相加得到與 b 形狀相同的梯度向量:

frac{partial L}{partial b} = left[ begin{matrix} frac{partial L}{partial c_{11}} + frac{partial L}{partial c_{12}}  frac{partial L}{partial c_{21}} + frac{partial L}{partial c_{22}} end{matrix} right]

下面是求和操作梯度計算的Python實現:

class Add(object):n # ...nn def compute_gradient(self, grad=None):n Compute the gradients for this operation wrt input values.nn :param grad: The gradient of other operation wrt the addition output.n :type grad: number or a ndarray, default value is 1.0.n n x, y = [node.output_value for node in self.input_nodes]nn if grad is None:n grad = np.ones_like(self.output_value)nn grad_wrt_x = gradn while np.ndim(grad_wrt_x) > len(np.shape(x)):n grad_wrt_x = np.sum(grad_wrt_x, axis=0)n for axis, size in enumerate(np.shape(x)):n if size == 1:n grad_wrt_x = np.sum(grad_wrt_x, axis=axis, keepdims=True)nn grad_wrt_y = gradn while np.ndim(grad_wrt_y) > len(np.shape(y)):n grad_wrt_y = np.sum(grad_wrt_y, axis=0)n for axis, size in enumerate(np.shape(y)):n if size == 1:n grad_wrt_y = np.sum(grad_wrt_y, axis=axis, keepdims=True)nn return [grad_wrt_x, grad_wrt_y]n

其中grad參數就是上面公式中的 G ,它的shape應該與該節點的輸出值(output_value的形狀一直)。

矩陣乘梯度的計算

這部分主要介紹如何在反向傳播求梯度中運用維度分析來幫助我們快速獲取梯度。先上一個矩陣乘操作的例子:

C = AB

其中, CM times K 的矩陣, AM times N 的矩陣, BN times K 的矩陣。

損失值 LC 的梯度為 G = frac{partial L}{partial C} ,其形狀與矩陣 C 相同同為 M times K

通過維度分析可以通過我們標量求導的知識再稍微對矩陣的形狀進行處理(左乘,右乘,轉置)來出正確的梯度。當然如果需要分析每個元素的導數也是可以的,可以參考這篇神經網路中利用矩陣進行反向傳播運算的實質, 下面我們主要使用維度分析來快速計算反向傳播中矩陣乘節點中矩陣對矩陣的導數。

若我們想求 frac{partial L}{partial B} , 根據標量計算的鏈式法則應該有: frac{partial L}{partial B} = frac{partial L}{partial C} frac{partial C}{partial A}

根據向量已知的 frac{partial L}{partial C} 的形狀為 M times K (與 C 形狀相同), frac{partial L}{partial B} 的形狀為 N times K (與 B 形狀相同), 因此 frac{partial C}{partial B} 應該是一個 N times M 的矩陣,而且我們發現上面乘積的式子寫反了,把順序調換一下就是: frac{partial L}{partial B} = frac{partial C}{partial B} frac{partial L}{partial C}

根據我們在標量求導的規則里, C 對於 B 求導應該是 A , 但是 A 是一個 M times N 的矩陣而我們現在需要一個 N times M 的矩陣,那麼就將 A 轉置一下唄,於是就得到: frac{partial L}{partial B} = frac{partial C}{partial B} frac{partial L}{partial C} = A^{T}G

同理也可以通過維度分析得到 LA 的梯度為 GB^{T}

下面是矩陣乘操作梯度計算的Python實現:

class MatMul(Operation):n # ...nn def compute_gradient(self, grad=None):n Compute and return the gradient for matrix multiplication.nn :param grad: The gradient of other operation wrt the matmul output.n :type grad: number or a ndarray, default value is 1.0.n n # Get input values.n x, y = [node.output_value for node in self.input_nodes]nn # Default gradient wrt the matmul output.n if grad is None:n grad = np.ones_like(self.output_value)nn # Gradients wrt inputs.n dfdx = np.dot(grad, np.transpose(y))n dfdy = np.dot(np.transpose(x), grad)nn return [dfdx, dfdy]n

其他操作的梯度計算

這裡就不一一介紹了其他操作的梯度計算了,類似的我們根據維度分析以及理解反向傳播里矩陣梯度其實就是標量求梯度放到了矩陣的規則里的一種變形的本質,其他梯度也可以推導並實現出來了。

在simpleflow里目前實現了求和,乘法,矩陣乘法,平方,Sigmoid,Reduce Sum以及Log等操作的梯度實現,可以參考:

https://github.com/PytLab/simpleflow/blob/master/simpleflow/operations.pygithub.com

總結

本文介紹了通過計算圖的反向傳播快速計算梯度的原理以及每個節點相應梯度的計算和實現,有了每個節點的梯度計算我們就可以通過實現反向傳播演算法來實現損失函數對所有節點的梯度計算了,下一篇中將會總結通過廣度優先搜索實現圖中節點梯度的計算以及梯度下降優化器的實現。

參考

  • deepideas.net/deep-lear
  • zhuanlan.zhihu.com/p/25
  • blog.csdn.net/magic_ant
  • zhihu.com/question/4702

推薦閱讀:

目前,人工智慧語音在說中文時的語氣感覺上還比較機械,怎樣使人工智慧語音的語氣更自然一些?
梯度下降法的神經網路容易收斂到局部最優,為什麼應用廣泛?
向量 L1 範數最小化問題?
如何形象又有趣的講解對抗神經網路(GAN)是什麼?

TAG:深度学习DeepLearning | 机器学习 | TensorFlow |