序言:在訓練一個神經網絡時,梯度的計算是一個關鍵的步驟,它為神經網絡的優化提供了關鍵數據。但是在面臨復雜神經網絡的時候導數的計算就成為一個難題,要求人們解出復雜、高維的方程是不現實的。這就是自動微分出現的原因,當前最流行的深度學習框架如PyTorch、Tensorflow等都提供了自動微分的支持,讓人們只需要很少的工作就能神奇般地自動計算出復雜函數的梯度。
PyTorch的autograd簡介
Tensor
是PyTorch實現多維數組計算和自動微分的關鍵數據結構。一方面,它類似於numpy的ndarray,用戶可以對Tensor
進行各種數學運算;另一方面,當設置.requires_grad = True
之后,在其上進行的各種操作就會被記錄下來,用於后續的梯度計算,其內部實現機制被成為動態計算圖(dynamic computation graph)。
Variable
變量:在PyTorch早期版本中,Tensor
只負責多維數組的運算,自動微分的職責是Variable
完成的,因此經常可以看到因而產生的包裝代碼。而在0.4.0版本之后,二者的功能進行了合並,使得自動微分的使用更加簡單了。
autograd機制能夠記錄作用於Tensor
上的所有操作,生成一個動態計算圖。圖的葉子節點是輸入的數據,根節點是輸出的結果。當在根節點調用.backward()
的時候就會從根到葉應用鏈式法則計算梯度。默認情況下,只有.requires_grad
和is_leaf
兩個屬性都為True
的節點才會被計算導數,並存儲到grad
中。
動態計算圖本質上是一個有向無環圖,因此“葉”和“根”的稱呼是不太准確的,但是這種簡稱可以幫助理解,PyTorch的文檔中仍然采用這種說法。
requires_grad
屬性
requires_grad
屬性默認為False,也就是Tensor
變量默認是不需要求導的。如果一個節點的requires_grad
是True,那么所有依賴它的節點requires_grad
也會是True。換言之,如果一個節點依賴的所有節點都不需要求導,那么它的requires_grad
也會是False。在反向傳播的過程中,該節點所在的子圖會被排除在外。
>>> x = torch.randn(5, 5) # requires_grad=False by default >>> y = torch.randn(5, 5) # requires_grad=False by default >>> z = torch.randn((5, 5), requires_grad=True) >>> a = x + y >>> a.requires_grad False >>> b = a + z >>> b.requires_grad True
Function
類
我們已經知道PyTorch使用動態計算圖(DAG)記錄計算的全過程,那么DAG是怎樣建立的呢?一些博客認為DAG的節點是Tensor(或說Variable),這其實是不准確的。DAG的節點是Function對象,邊表示數據依賴,從輸出指向輸入。因此Function類在PyTorch自動微分中位居核心地位,但是用戶通常不會直接去使用,導致人們對Function類了解並不多。
每當對Tensor施加一個運算的時候,就會產生一個Function對象,它產生運算的結果,記錄運算的發生,並且記錄運算的輸入。Tensor
使用.grad_fn
屬性記錄這個計算圖的入口。反向傳播過程中,autograd引擎會按照逆序,通過Function的backward依次計算梯度。
backward
函數
backward函數是反向傳播的入口點,在需要被求導的節點上調用backward函數會計算梯度值到相應的節點上。backward需要一個重要的參數grad_tensor,但如果節點只含有一個標量值,這個參數就可以省略(例如最普遍的loss.backward()
與loss.backward(torch.tensor(1))
等價),否則就會報如下的錯誤:
Backward should be called only on a scalar (i.e. 1-element tensor) or with gradient w.r.t. the variable
要理解這個參數的內涵首先要從數學角度認識梯度運算。如果有一個向量函數$\vec{y}=f(\vec{x})$,那么$\vec{y}$相對於$\vec{x}$的梯度是一個雅克比矩陣(Jacobian matrix):
$$\begin{split}J=\left(\begin{array}{ccc} \frac{\partial y_{1}}{\partial x_{1}} & \cdots & \frac{\partial y_{1}}{\partial x_{n}}\\ \vdots & \ddots & \vdots\\ \frac{\partial y_{m}}{\partial x_{1}} & \cdots & \frac{\partial y_{m}}{\partial x_{n}} \end{array}\right)\end{split}$$
本文討論的主角torch.autograd本質上是一個向量-雅克比乘積(*vector-Jacobian product*)的計算引擎,即計算$v^{T}\cdot J$,而所謂的參數grad_tensor就是這里的$v$。由定義易知,參數grad_tensor需要與Tensor本身有相同的size。通過恰當地設置grad_tensor,容易計算任意的$\frac{\partial y_{m}}{\partial x_{n}}$求導組合。
反向傳播過程中一般用來傳遞上游傳來的梯度,從而實現鏈式法則,簡單的推導如下所示:
$$\begin{split}J^{T}\cdot v=\left(\begin{array}{ccc} \frac{\partial y_{1}}{\partial x_{1}} & \cdots & \frac{\partial y_{m}}{\partial x_{1}}\\ \vdots & \ddots & \vdots\\ \frac{\partial y_{1}}{\partial x_{n}} & \cdots & \frac{\partial y_{m}}{\partial x_{n}} \end{array}\right)\left(\begin{array}{c} \frac{\partial l}{\partial y_{1}}\\ \vdots\\ \frac{\partial l}{\partial y_{m}} \end{array}\right)=\left(\begin{array}{c} \frac{\partial l}{\partial x_{1}}\\ \vdots\\ \frac{\partial l}{\partial x_{n}} \end{array}\right)\end{split}$$
(注:這里的計算結果被轉置為列向量以方便查看)
注意:梯度是累加的
backward函數本身沒有返回值,它計算出來的梯度存放在葉子節點的grad屬性中。PyTorch文檔中提到,如果grad屬性不為空,新計算出來的梯度值會直接加到舊值上面。
為什么不直接覆蓋舊的結果呢?這是因為有些Tensor可能有多個輸出,那么就需要調用多個backward。疊加的處理方式使得backward不需要考慮之前有沒有被計算過導數,只需要加上去就行了,這使得設計變得更簡單。因此我們用戶在反向傳播之前,常常需要用zero_grad函數對導數手動清零,確保計算出來的是正確的結果。