張量求導規則 $\frac{\partial y}{\partial x}$
1. 規則 $1$:形狀規則
a. 只要 $y$ 或 $x$ 中有一個是標量,那么導數 $\frac{\partial y}{\partial x}$ 的形狀和非標量的形狀一致。
b. 如果 $y$ 和 $x$ 都是非標量,那么把 $y$ 拆成一個個標量元素,先求每個標量元素對 $x$ 的導數,結果張量的形狀規則按 $a$ 中要求,
然后所有標量元素的求導結果按 $y$ 的形狀排列。
舉個例子:$y$ 的形狀為 $(3,4)$,$x$ 的形狀為 $(4,8,1)$,那么求導結果的張量形狀為?按照形狀規則,$y$ 中每個標量元素對 $x$ 的求導結果形狀與 $x$ 相同,
即$(4,8,1)$,所有標量對 $x$ 的求導結果再按 $y$ 的形狀排列,所以整個求導結果張量的形狀為 $(3,4,4,8,1)$。
這個規則是最基本也是最重要的形狀規則,無論是向量對向量求導、向量對矩陣求導還是矩陣對矩陣求導,最終都可以分解為標量對矩陣求導的組合,
就可以判斷出求導結果的形狀。
2. 規則 $2$:當 $y, x$ 都是列向量且 $y = Wx$,有 $\frac{\partial y}{\partial x} = W$;當 $y, x$ 都是行向量且 $y = xW$,有 $\frac{\partial y}{\partial x} = W^{T}$。
在 Pytorch 只有向量乘向量或矩陣乘矩陣,不存在矩陣和向量相乘,如果出現矩陣和向量相乘,Pytorch 內部會對向量增加一個前置尺寸后者后置尺寸,
使向量變成一個矩陣以滿足形狀要求,本質還是矩陣乘矩陣,運算完成后,刪除輸出結果的尺寸,結果就還是一個向量。
形狀為 $(m,)$ 的向量 $y$ 對形狀為 $(n,)$ 的向量 $x$ 求導,根據形狀規則 $1$,向量 $y$ 的每個標量元素對向量 $x$ 的求導結果的形狀為 $(n,)$,
即與 $x$ 相同,最后在前面加上 $y$ 的形狀,所以結果張量的形狀為 $(m,n)$,
3. 現在關注向量對矩陣求導,存在兩種關系:
1)列向量 $y$ 對矩陣 $W$ 求導,其中 $y_{n \times 1} = W_{n \times m}x_{m \times 1}$
根據向量 $y(n \times 1)$ 和矩陣 $W(n \times m)$ 的大小,$\frac{\partial y}{\partial W}$ 是個三維張量,形狀為 $n \times (n \times m)$。形式如下:
按照形狀規則 $1$,$\frac{\partial y_{i}}{\partial W}$ 的形狀和矩陣 $W$ 的形狀是一樣的。
2)行向量 $y$ 對矩陣 $W$ 求導,其中 $y_{1 \times n} = x_{1 \times m}W_{m \times n}$
根據向量 $y(1 \times n)$ 和矩陣 $W(m \times n)$ 的大小,$\frac{\partial y}{\partial W}$ 是個三維張量,形狀為 $n \times (m \times n)$。形式如下:
按照規則 $1$,$\frac{\partial y_{i}}{\partial W}$ 的形狀和矩陣 $W$ 的形狀是一樣的。
對於誤差函數 $J$,它是 $y$ 的標量函數。下面我們來求一求 $\frac{\partial J}{\partial W}$。
1)列向量 $y$ 對矩陣 $W$ 求導,其中 $y_{n \times 1} = W_{n \times m}x_{m \times 1}$
2)行向量 $y$ 對矩陣 $W$ 求導,其中 $y_{1 \times n} = x_{1 \times m}W_{m \times n}$
規則 $3$ 總結如下:
a. 當 $y, x$ 都是列向量且 $y = Wx$,$l$ 是 $y$ 的標量函數,有 $\frac{\partial l}{\partial W} = \frac{\partial l}{\partial y} \cdot x^{T}$。
b. 當 $y, x$ 都是行向量且 $y = xW$,$l$ 是 $y$ 的標量函數,有 $\frac{\partial l}{\partial W} = x^{T} \cdot \frac{\partial l}{\partial y}$。
4. 假設 $X$ 是 $d \times m$ 的矩陣,$W$ 是 $n \times d$ 的矩陣,而 $Y = WX$ 是 $n \times m$ 的矩陣,以 $d = 3, m = 2, n = 2$ 為例。
我們根據規則 $1$(形狀規則),很容易知道 $\frac{\partial Y}{\partial W}$ 的形狀為 $(2,2,2,3)$,$\frac{\partial Y}{\partial X}$ 的形狀為 $(2,2,3,2)$。
誤差函數 $l$ 是 Y 的標量函數,比起求 $\frac{\partial Y}{\partial W}$ 和 $\frac{\partial Y}{\partial X}$,我們更有興趣求 $\frac{\partial l}{\partial W}$ 和 $\frac{\partial l}{\partial X}$。要推導出矩陣鏈式法則還需回到基本的標量鏈式法則。
首先關注 $\frac{\partial l}{\partial X}$,一個個元素來看。$Y=WX$ 對應的全連接神經網絡如下圖(只畫出一個樣本)。
將這六項帶入矩陣 $\frac{\partial l}{\partial X}$ 整理得到
同理得到 $\frac{\partial l}{\partial W}$
規則 $4$ 總結為:$Y, X$ 是矩陣且 $Y = WX$,$l$ 是 $Y$ 的標量函數,有 $\frac{\partial l}{\partial W} = \frac{\partial l}{\partial Y} \cdot X^{T}$,$\frac{\partial l}{\partial X} = W^{T} \cdot \frac{\partial l}{\partial Y}$。
5. 函數 $y = f(x)$ 都是作用在元素層面上,比如一些基本函數 $y = exp(x)$ 和 $y = \sin(x)$,還有神經網絡用的函數 $y = sigmod(x)$ 和 $y = relu(x)$,
它們都是標量進標量出、向量進向量出 (常見)、矩陣進矩陣出 (常見)、張量進張量出。
拿 $y = \sin(x)$ 舉例,整個推導可以用規則 $2$,即向量對向量求導那一套,但由於 $y$ 和 $x$ 一一對應,因此 $y$ 和 $x$ 是一樣的形狀,且 $y_{i}$ 只與 $x_{i}$ 有關。
這種元素層面的函數求得的偏導數都是對角矩陣。
再次把誤差函數 $l$ 請出來 ($l$ 是 $y$ 的標量函數),通常更感興趣的是求 $\frac{\partial l}{\partial x}$。對某個 $x_{i}$,根據鏈式法則得到
規則 $5$ 可以總結為:當函數 $y = f(x)$ 是在元素層面操作,$l$ 是 $y$ 的標量函數,有 $\frac{\partial l}{\partial x} = \frac{\partial l}{\partial y} \otimes f^{'}(x)$。
計算圖、前向傳播和反向傳播
先約定如下符號:慣例是用小括號 $(i)$ 上標表示第 $i$ 個數據,用中括號 $[L]$ 上標表示神經網絡的第 $L$ 層。
$x = (x_{1},x_{2},...,x_{n})$:輸入特征向量
$y = (y_{1},y_{2},...,y_{m})$:輸出標簽
$(x,y)$:單個樣本點
$\left \{ \left ( x^{(1)},y^{(1)} \right ), \left ( x^{(2)},y^{(2)} \right ),\cdots, \left ( x^{(k)},y^{(k)} \right )\right \}$:數據集,$k$ 表示數據集中樣本點個數
$L = 0,1,2,...,M$:表示神經網絡的第幾層
$n^{[0]},n^{[1]},...,n^{[M]}$:每一層的節點個數
$W^{[1]},W^{[2]},...,W^{[M]}$:每兩層之間的權重矩陣,矩陣 $W^{L}$ 的大小是 $n^{[L-1]} \times n^{[L]}$
$b^{[1]},b^{[2]},...,b^{[M]}$:每一層的偏置向量(輸入層是沒有偏置的)
$z^{[1]},z^{[2]},...,z^{[M]}$:每一層神經元線性求和部分的結果所組成的向量
$a^{[1]},a^{[2]},...,a^{[M]}$:每一層的輸出向量
$f_{L}(z^{[L]})$:每一層的轉換函數
$J$:誤差函數
下面舉個例子來講述神經網絡正向傳播和反向傳播的過程。
1. 神經網絡只有單數據點 $(x, y)$,輸入 $x$ 只有單特征,輸出 $y$ 也只有一個標簽,即 $x,y$ 都是標量
因為 $x,y$ 是標量,所以網絡中的參數 $w^{[1]},b^{[1]},w^{[2]},b^{[2]}$ 和各層網絡輸出 $z^{[1]},a^{[1]},z^{[2]},a^{[2]}$ 都是標量。
在開始訓練神經網絡的時候,會隨機初始化參數 $w^{[1]},b^{[1]},w^{[2]},b^{[2]}$。要利用隨機梯度下降法進行參數調整,及必須知道輸出 $J$
向神經網絡喂一個樣本點 $(x,y)$,傳播過程如下:
在前向傳播的過程中,每一步的計算都會利用到前面的結果,在反向傳播的過程中,每一步的計算也是在已經計算的結果上進行的,也會利用到
前向傳播過程中得到的數據。
以下幾個量稱為中間梯度:
$$\frac{\partial J}{\partial a^{[2]}}, \;\;\; \frac{\partial J}{\partial z^{[2]}}, \;\;\; \frac{\partial J}{\partial a^{[1]}}, \;\;\; \frac{\partial J}{\partial z^{[1]}}$$
以下幾個量稱為局部梯度:
$$\frac{\partial a^{[2]}}{\partial z^{[2]}}, \;\;\; \frac{\partial z^{[2]}}{\partial w^{[2]}}, \;\;\; \frac{\partial z^{[2]}}{\partial b^{[2]}},
\;\;\;\;\;\;\;\;\ \frac{\partial a^{[1]}}{\partial z^{[1]}}, \;\;\; \frac{\partial z^{[1]}}{\partial w^{[1]}}, \;\;\; \frac{\partial z^{[1]}}{\partial b^{[1]}}$$
以下幾個量稱為最終梯度,也是進行梯度下降法調整參數所需要的量:
$$\frac{\partial J}{\partial w^{[1]}}, \;\;\; \frac{\partial J}{\partial b^{[1]}}, \;\;\; \frac{\partial J}{\partial w^{[2]}}, \;\;\; \frac{\partial J}{\partial b^{[2]}}$$
了解了前向傳播和反向傳播的原理,下面來看一下 Pytorch 是怎么實現這個過程的。
首先來看一下 torch.Tensor 這個類的一些屬性:
1)is_leaf:每一個 Tensor 都是計算圖(下面介紹)中的一個節點,這個值用來表明該 Tensor 是否為葉子節點。
2)requires_grad: 用於判斷該 tensor 是否需要被跟蹤,用以計算梯度,默認為 False。當為 False 的時候,就意味着這個變量是不需要
計算輸出(調用了 backward 的變量)關於它的導數的,在這個變量上發生的操作不會被記錄,如 grad_fn 依然還是 None。或者說,這個節
點不會被加到計算圖中,是游離於計算圖之外的。但是如果依賴於該節點的其它張量節點的 requires_grad = True,那不管你是否設置該
值,它會自動置為 True。若沒有依賴關系,那所有 requires_grad = False 的張量(Tensor)都是孤立的節點,即都為葉張量(leaf Tensor)。
3)grad:初始為 None,requires_grad = False 時為 None,否則當某 out 節點調用 out.backward() 時,會存放計算后的 $\frac{\partial out}{\partial x}$ 梯度值,
梯度值不會自動清空,因此每次在計算 backward 時都需要將前一時刻的梯度歸零,否則梯度值會一直累加。
4)grad_fn:指向用於 *Backward 函數的地址。因為葉子張量不是運算的結果,因此對應的屬性 grad_fn 的值為 None,而非葉子節點是由運
算產生的,所以它會記錄創建了這個 Tensor 的 Function,grad_fn 就是這個 function 的引用。舉個例子:
我們首先定義一個張量 $x$,那么它是一個葉張量,對這個張量執行 $y = f(x)$,則 $y$ 是輸出節點張量,函數 $f$ 定義為 $y = e^{x}$,函數 $f$ 定義如下:
class ExpBackward(torch.autograd.Function): """ 接受一個context ctx作為第一個參數,之后傳入包含輸入的張量,這個函數定義 Function 的計算規則 """ @staticmethod def forward(ctx, i): result = torch.exp(i) ctx.save_for_backward(result) # 保存前向傳播的計算結果 tensors,在 backward 階段可以進行獲取。 return result """ 接受一個context ctx作為第一個參數,然后是第二個參數 grad_output,即反向傳播上一次計算的梯度值,這個函數定義 Function 的求導規則 """ @staticmethod def backward(ctx, grad_output): result, = ctx.saved_tensors return grad_output * result # 在已經計算的梯度基礎上再乘以局部梯度就可得到輸出關於該節點的梯度。
此時輸出張量 $y$ 的 grad_fn = ExpBackward。像 $+,-,/,*$ 等基本操作,Pytorch 內部都實現了對應的 Function,張量之間進行這些
操作時,也會引用對應的 Function。
在 Pytorch 和 tensorflow 中,底層結構都是由 Tensor 組成的計算圖,計算圖是用來描述運算的有向無環圖。
下面用一個例子來說明 Pytorch 是怎么構建計算圖的。
import torch a = torch.tensor(2.0, requires_grad=True) b = torch.tensor(3.0, requires_grad=True) c = a + b d = torch.tensor(4.0, requires_grad=True) e = c * d e.backward() # 執行求導 print(a.grad) # a.grad 即導數 d(e)/d(a) 的值 """ tensor(4.) """
調用 e.backward()
執行求導,為什么會更新到 a.grad?
過程是這樣的:當我們執行 e.backward()
的時候。這個操作將調用 e 里面的 grad_fn
這個屬性,這個屬性保存的是操作函數的引用,每個
操作函數都實現了 forward 和 backward 方法,所以執行 e.backward() 后會執行 $e$ 這個張量節點所對應 Function 的 backward 方法,
Function 在前向傳播中保存了輸入的張量節點,所以計算出梯度后,就會將梯度值保存在對應的輸入張量節點 grad 屬性中,這便實現了梯度往
回傳播,然后再調用輸入張量節點所引用的 Function,就這樣逐層往回計算,直到葉子節點......grad_fn 保存的 Function 和 Function 中保
存的輸入張量都相當於圖的邊,反向傳播就是沿着這樣的路徑往回走,而這個路徑是前向傳播過程建立的。
用圓形表示 Tensor 節點,用方形表示 Function 節點,那么這個例子在前向傳播過程中建立的計算圖如下:
黑線代表前向傳播建立動態圖的過程。紅線代表反向傳播進行梯度計算的過程。
Function 節點不僅是溝通前向傳播和反向傳播的橋梁,也是溝通輸入張量和輸出張量的橋梁。
2. 神經網絡有 $m$ 個數據點 $(X, Y)$,輸入 $X$ 有多特征,輸出 $Y$ 有多類別,即 $X,Y$ 都是矩陣