Pytorch 60分鐘入門之(二) Autograd:自動求導

Autograd:自動求導

Pytorch所有的神經網絡的核心是autograd庫. 我們先簡單瞭解一下, 然後訓練第一個神經網絡.
autograd 包提供了Tensor所有操作的自動求導(差分). 這是一個運行時定義(define-by-run)的架構, 意味着你的後向傳播是根據你的代碼怎樣運行來定義的. 每一次的迭代都可能不同.
讓我們看幾個例子.

Tensor

torch.Tensor是這個包的核心類. 如果將其屬性attribute.requires_grad設爲True, 它就會記錄所有的操作. 計算完成後, 調用.backward(), 會自動計算所有的梯度(gradients). 這個tensor的梯度會累加到.grad屬性中.
爲了阻止tensor記錄歷史, 可以調用.detach()將它和計算曆史脫鉤, 並阻止其下一步計算繼續記錄操作歷史.
爲了防止追蹤歷史(和使用存儲), 你可以將代碼塊放在torch.no_grad():中. 這在評估一個模型時特別有用, 因爲模型中有可訓練的參數, 他們的requires_gradTrue. 而在評估過程中不需要計算梯度.
還有一個類在autograd實現中特別重要, Function.
TensorFunction內部聯繫並且建立一張非環形的圖(acyclic graph). 這張圖將運算的全部歷史全部編碼. 每一個tensor都有一個.grad_fn指向創建此tensor的Function. 除非Tensor是用戶創建的, 其grad_fnNone.
如果你想計算導數, 你可以在一個Tensor上調用.backward(). 如果Tensor是個標量(也就是隻有一個元素), 你不需要指定任何參數. 但是如果它有不止一個元素, 你需要指定一個匹配大小的tensor作爲梯度參數.

import torch

創建一個requires_grad=True的tensor用來追蹤計算曆史.

x = torch.ones(2, 2, requires_grad=True)
print(x)

Out:

tensor([[1., 1.],
       			 [1., 1.]], requires_grad=True)

做一個tensor運算:

y = x + 2
print(y)

Out:

tensor([[3., 3.],
       			 [3., 3.]], grad_fn=<AddBackward0>)

y作爲一個操作的結果而被創建, 所以它有grad_fn.

print(y.grad_fn)

Out:

<AddBackward0 object at 0x7f6a7f893e10>

在y上做更多運算.

z = y * y * 3
out = z.mean()
print(z, out)

Out:

tensor([[27., 27.],
       			 [27., 27.]], grad_fn=<MulBackward0>) tensor(27., grad_fn=<MeanBackward0>)

.requires_grad_( ... )原址改變現有Tensor的requires_grad標誌位. 這個輸入標誌位默認是False.

a = torch.randn(2, 2)
a = ((a * 3) / (a - 1))
print(a.requires_grad)
a.requires_grad_(True)
print(a.requires_grad)
b = (a * a).sum()
print(b.grad_fn)

Out:

False
True
<SumBackward0 object at 0x7f6a7f8a2ba8>

Gradients

現在開始反向傳播. 因爲out只有一個標量, out.backward()等效於out.backward(torch.tensor(1.)).

out.backward()

打印梯度 d(out)/dx

print(x.grad)

Out:

tensor([[4.5000, 4.5000],
        		[4.5000, 4.5000]])

你應該得到一個4.5的矩陣. 令tensorout爲"oo". 我們得到o=12izio=\frac{1}{2}\sum_iz_i, zi=3(xi+2)2z_i=3(x_i+2)^2, zixi=1=27z_i|_{x_i=1}=27. 所以, oxi=32(xi+2)\frac{\partial o}{\partial x_i}=\frac{3}{2}(x_i+2), 所以, oxixi=1=92=4.5\frac{\partial o}{\partial x_i}|_{x_i=1}=\frac{9}{2}=4.5.

在數學上, 有一個向量函數y=f(x)\vec{y} = f(\vec{x}), 那麼y\vec{y}相對於x\vec{x}的梯度就是一個Jocobian 矩陣:
J=(y1x1...y1xnymx1...ymxn)J=\begin{pmatrix} \frac{\partial y_1}{\partial x_1} & ...&\frac{\partial y_1}{\partial x_n} \\ \vdots &\cdots&\vdots\\ \frac{\partial y_m}{\partial x_1} & ...&\frac{\partial y_m}{\partial x_n} \end{pmatrix}

一般地, torch.autograd是一個計算向量和Jacobian矩陣乘積的引擎. 也就是, 給定一個向量v=(v1,v2,,vm)Tv=(v_1, v_2, \cdots, v_m)^T, 計算乘積vTJv^T\cdot J. 如果vv是一個標量函數l=g(y)l=g(\vec y)的梯度, 也就是,v=(ly1,,ly1)v=(\frac{\partial l}{\partial y_1}, \cdots, \frac{\partial l}{\partial y_1}), 那麼根據鏈式法則(chain rule), 向量和Jacobian矩陣乘積的結果就是llx\vec{x}的梯度:

JTv=(y1x1...y1xnymx1...ymxn)(ly1lym)=(lx1lxn)J^T\cdot v=\begin{pmatrix} \frac{\partial y_1}{\partial x_1} & ...&\frac{\partial y_1}{\partial x_n} \\ \vdots &\ddots&\vdots\\ \frac{\partial y_m}{\partial x_1} & ...&\frac{\partial y_m}{\partial x_n} \end{pmatrix} \begin{pmatrix} \frac{\partial l}{\partial y_1} \\ \vdots \\ \frac{\partial l}{\partial y_m} \end{pmatrix}= \begin{pmatrix} \frac{\partial l}{\partial x_1} \\ \vdots \\ \frac{\partial l}{\partial x_n} \end{pmatrix}

(注意vTJv^T\cdot J輸出行向量, 可以看做是J^T\cdot v所輸出的列向量)

向量-Jacobian矩陣乘積的性質可以很方便的給非標量輸出的模型傳遞外部梯度.
現在看一個向量-Jacobian矩陣乘積的例子:

x = torch.randn(3, requires_grad=True)

y = x * 2
while y.data.norm() < 1000:
    y = y * 2

print(y)

Out:

tensor([  367.6527, -1195.3390,  -546.3927], grad_fn=<MulBackward0>)

Now in this case y is no longer a scalar. torch.autograd could not compute the full Jacobian directly, but if we just want the vector-Jacobian product, simply pass the vector to backward as argument:
這裏y不是標量. torch.autograd不能直接計算全部的Jacobian, 但是如果需要向量-Jacobian矩陣乘積, 簡單地傳遞一個向量參數給backward即可:

v = torch.tensor([0.1, 1.0, 0.0001], dtype=torch.float)
y.backward(v)

print(x.grad)

Out:

tensor([5.1200e+01, 5.1200e+02, 5.1200e-02])

設置.requires_grad=True, 阻止autograd追蹤Tensor的歷史. 將代碼塊放在with torch.no_grad():裏:

print(x.requires_grad)
print((x ** 2).requires_grad)

with torch.no_grad():
    print((x ** 2).requires_grad)

Out:

True
True
False

或者使用detach()獲取一個內容相同但不需要求梯度的新tensor:

print(x.requires_grad)
y = x.detach()
print(y.requires_grad)
print(x.eq(y).all())

Out:

True
False
tensor(True)

稍後閱讀:
autograd.Function的文檔在:https://pytorch.org/docs/stable/autograd.html#function

Total running time of the script: ( 0 minutes 3.509 seconds)

公式編輯語法: https://katex.org/docs/supported.html

補充1. 如何自定義autograd 和nn?

簡單來說, 需要繼承Function基類並override forward()和backward()函數. 這兩個函數都是static的.

官方文檔:
https://pytorch.org/docs/stable/notes/extending.html
補充解釋:
https://blog.csdn.net/Hungryof/article/details/78346304

補充2. 如何理解autograd.Function和Tensor的聯繫?

pytorch0.4之前版本有Variable和Tensor的區別. 現在兩者統一, 使用Tensor即可.
torch.Tensor有幾個屬性/方法和autograd相關:

  1. grad, 存儲梯度值(累加)
  2. requires_grad, (是否需要計算梯度)
  3. is_leaf, (是否葉節點, 是葉節點的tensor不從其他tensor運算得來)
  4. backward(), (計算當前tensor相對於葉節點tensor的梯度)
  5. detach(), 得到新tensor, 不再求梯度
  6. detach_(), 使tensor成爲一個葉節點.
  7. register_hook(), 註冊一個backward hook, 用以求梯度.
  8. retain_grad()

官方文檔:
https://pytorch.org/docs/stable/autograd.html

補充3. 如何理解自動求導和DAG圖的關係?

autograd機制能夠記錄作用於Tensor上的所有操作,生成一個動態計算圖。圖的葉子節點是輸入的數據,根節點是輸出的結果。當在根節點調用.backward()的時候就會從根到葉應用鏈式法則計算梯度。默認情況下,只有.requires_grad和is_leaf兩個屬性都爲True的節點纔會被計算導數,並存儲到grad中。

我們已經知道PyTorch使用動態計算圖(DAG)記錄計算的全過程,那麼DAG是怎樣建立的呢?一些博客認爲DAG的節點是Tensor(或說Variable),這其實是不準確的。DAG的節點是Function對象,邊表示數據依賴,從輸出指向輸入。因此Function類在PyTorch自動微分中位居核心地位,但是用戶通常不會直接去使用,導致人們對Function類瞭解並不多。

每當對Tensor施加一個運算的時候,就會產生一個Function對象,它產生運算的結果,記錄運算的發生,並且記錄運算的輸入。Tensor使用.grad_fn屬性記錄這個計算圖的入口。反向傳播過程中,autograd引擎會按照逆序,通過Function的backward依次計算梯度。
在這裏插入圖片描述
https://www.cnblogs.com/cocode/p/10746347.html

補充4. 爲什麼.grad設計成累加模式?

backward函數本身沒有返回值,它計算出來的梯度存放在葉子節點的grad屬性中。PyTorch文檔中提到,如果grad屬性不爲空,新計算出來的梯度值會直接加到舊值上面。

爲什麼不直接覆蓋舊的結果呢?這是因爲有些Tensor可能有多個輸出,那麼就需要調用多個backward。疊加的處理方式使得backward不需要考慮之前有沒有被計算過導數,只需要加上去就行了,這使得設計變得更簡單。因此我們用戶在反向傳播之前,常常需要用zero_grad函數對導數手動清零,確保計算出來的是正確的結果。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章