PyTorch 如何自動計算梯度
在PyTorch中,torch.Tensor類是存儲和變換數據的重要工具,相比於Numpy,Tensor提供GPU計算和自動求梯度等更多功能,在深度學習中,我們經常需要對函數求梯度(gradient)。
PyTorch提供的autograd包能夠根據輸入和前向傳播過程自動構建計算圖,並執行反向傳播。
本篇將介紹和總結如何使用autograd包來進行自動求梯度的有關操作。
1. 概念
Tensor是這個pytorch的自動求導部分的核心類,如果將其屬性.requires_grad=True,它將開始追蹤(track) 在該tensor上的所有操作,從而實現利用鏈式法則進行的梯度傳播。完成計算後,可以調用.backward()來完成所有梯度計算。此Tensor的梯度將累積到.grad屬性中。
如果不想要被繼續對tensor進行追蹤,可以調用.detach()將其從追蹤記錄中分離出來,接下來的梯度就傳不過去瞭。此外,還可以用with torch.no_grad()將不想被追蹤的操作代碼塊包裹起來,這種方法在評估模型的時候很常用,因為此時並不需要繼續對梯度進行計算。
Function是另外一個很重要的類。Tensor和Function互相結合就可以構建一個記錄有整個計算過程的有向無環圖(DAG)。每個Tensor都有一個.grad_fn屬性,該屬性即創建該Tensor的Function, 就是說該Tensor是不是通過某些運算得到的,若是,則grad_fn返回一個與這些運算相關的對象,否則是None。
2. 具體實現
2.1. 創建可自動求導的tensor
首先我們創建一個tensor,同時設置requires_grad=True:
x = torch.ones(2, 2, requires_grad=True) print(x) print(x.grad_fn) '''
輸出:
tensor([[1., 1.],
[1., 1.]], requires_grad=True)
None
”’
像x這種直接創建的tensor 稱為葉子節點,葉子節點對應的grad_fn是None。如果進行一次運算操作:
y = x + 1 print(y) print(y.grad_fn) ''' tensor([[2., 2.], [2., 2.]], grad_fn=<AddBackward>) <AddBackward object at 0x1100477b8> '''
而y是通過一個加法操作創建的,所以它有一個為操作的grad_fn。
嘗試進行更復雜的操作:
z = y ** 2 out = z.mean() print(z, out) ''' tensor([[4., 4.], [4., 4.]], grad_fn=<PowBackward0>) tensor(4., grad_fn=<MeanBackward0>) '''
上面的out是一個標量4,通常對於標量直接使用out.backward()進行求導,不需要指定求導變量,後面進行詳細說明。
也可以通過.requires_grad_()改變requires_grad屬性:
a = torch.randn(3, 2) # 缺失情況下默認 requires_grad = False a = (a ** 2) print(a.requires_grad) # False a.requires_grad_(True) #使用in-place操作,改變屬性 print(a.requires_grad) # True b = (a * a).sum() print(b.grad_fn) ''' False True <SumBackward0 object at 0x7fd8c16edd30> '''
2.2. 梯度計算
torch.autograd實現梯度求導的鏈式法則,用來計算一些雅克比矩陣的乘積,即函數的一階導數的乘積。
註意:grad在反向傳播過程中是累加的(accumulated),每一次運行反向傳播,梯度都會累加之前的梯度,所以一般在反向傳播之前需把梯度清零x.grad.data.zero_()。
x = torch.ones(2, 2, requires_grad=True) y = x + 1 z = y ** 2 out = z.mean() print(z, out) out.backward() print(x.grad) # 註意grad是累加的 out2 = x.sum() out2.backward() print(out2) print(x.grad) out3 = x.sum() x.grad.data.zero_() out3.backward() print(out3) print(x.grad) ''' tensor([[4., 4.], [4., 4.]], grad_fn=<PowBackward0>) tensor(4., grad_fn=<MeanBackward0>) tensor([[1., 1.], [1., 1.]]) tensor(4., grad_fn=<SumBackward0>) tensor([[2., 2.], [2., 2.]]) tensor(4., grad_fn=<SumBackward0>) tensor([[1., 1.], [1., 1.]]) '''
Tensor的自動求導對於標量比如上面的out.backward()十分方便,但是當反向傳播的對象不是標量時,需要在y.backward()種加入一個與out同形的Tensor,不允許張量對張量求導,隻允許標量對張量求導,求導結果是和自變量同形的張量。
這是為瞭避免向量(甚至更高維張量)對張量求導,而轉換成標量對張量求導。
x = torch.tensor([1.0, 2.0, 3.0, 4.0], requires_grad=True) y = 2 * x z = y.view(2, 2) print(z) ''' tensor([[2., 4.], [6., 8.]], grad_fn=<ViewBackward>) '''
顯然上面的tensor z不是一個標量,所以在調用 z.backward()時需要傳入一個和z同形的權重向量進行加權求和得到一個標量。
c = torch.tensor([[1.0, 0.1], [0.01, 0.001]], dtype=torch.float) z.backward(c) print(x.grad) ''' tensor([[2., 4.], [6., 8.]], grad_fn=<ViewBackward>) tensor([2.0000, 0.2000, 0.0200, 0.0020]) '''
2.3 停止梯度追蹤
我們可以使用detach()或者torch.no_grad()語句停止梯度追蹤:
x = torch.tensor(1.0, requires_grad=True) y1 = x ** 2 with torch.no_grad(): y2 = x ** 3 y3 = y1 + y2 print(x.requires_grad) print(y1, y1.requires_grad) # True print(y2, y2.requires_grad) # False print(y3, y3.requires_grad) # True ''' True tensor(1., grad_fn=<PowBackward0>) True tensor(1.) False tensor(2., grad_fn=<ThAddBackward>) True '''
我們嘗試計算梯度:
y3.backward() print(x.grad) # y2.backward() #這句會報錯,因為此時 y2.requires_grad=False,,無法調用反向傳播 ''' tensor(2.) '''
這裡結果為2,是因為我們沒有獲得y2的梯度,僅僅是對y1做瞭一次反向傳播,作為最後的梯度輸出。
2.4. 修改tensor的值
如果我們想要修改tensor的數值,但是不希望保存在autograd的記錄中,require s_grad = False, 即不影響到正在進行的反向傳播,那麼可以用tensor.data進行操作。但是這種操作需要註意可能會產生一些問題,比如標量為0
x = torch.ones(1,requires_grad=True) print(x.data) # 仍然是一個tensor print(x.data.requires_grad) # 但是已經是獨立於計算圖之外 y = 2 * x x.data *= 100 # 隻改變瞭值,不會記錄在計算圖,所以不會影響梯度傳播 y.backward() print(x) # 更改data的值也會影響tensor的值 print(x.grad)
pytorch0.4以後保留瞭.data() 但是官方文檔建議使用.detach(),因為使用x.detach時,任何in-place變化都會使backward報錯,因此.detach()是從梯度計算中排除子圖的更安全方法。
如下面的例子:
torch.tensor([1,2,3.], requires_grad = True) out = a.sigmoid() c = out.detach() c.zero_() # in-place為0 ,tensor([ 0., 0., 0.]) print(out) # modified by c.zero_() !! tensor([ 0., 0., 0.]) out.sum().backward() # Requires the original value of out, but that was overwritten by c.zero_() ''' RuntimeError: one of the variables needed for gradient computation has been modified by an inplace operation ''' a = torch.tensor([1,2,3.], requires_grad = True) out = a.sigmoid() c = out.data c.zero_() # tensor([ 0., 0., 0.]) print(out) # out was modified by c.zero_() tensor([ 0., 0., 0.]) out.sum().backward() a.grad # 這麼做不會報錯,但是a已經被改變,最後計算的梯度實際是錯誤的 ''' tensor([ 0., 0., 0.]) '''
補充:pytorch如何計算導數_Pytorch 自動求梯度(autograd)
深度學習其實就是一個最優化問題,找到最小的loss值,因為自變量過多,想要找到最小值非常困難。所以就出現瞭很多最優化方法,梯度下降就是一個非常典型的例子。本文針對python的pytorch庫中的自動求梯度進行瞭詳細的解釋
Tensor
pytorch裡面的tensor可以用來存儲向量或者標量。
torch.tensor(1) # 標量 torch.tensor([1]) # 1*1 的向量
tensor還可以指定數據類型,以及數據存儲的位置(可以存在顯存裡,硬件加速)
torch.tensor([1,2], dtype=torch.float64)
梯度
在數學裡,梯度的定義如下:
可以看出,自變量相對於因變量的每一個偏導乘以相應的單位向量,最後相加,即為最後的梯度向量。
在pytorch裡面,我們無法直接定義函數,也無法直接求得梯度向量的表達式。更多的時候,我們其實隻是求得瞭函數的在某一個點處相對於自變量的偏導。
我們先假設一個一元函數:y = x^2 + 3x +1,在pytorch裡面,我們假設x = 2, 那麼
>>> x = torch.tensor(2, dtype=torch.float64, requires_grad=True) >>> y = x * x + 3 * x + 1 >>> y.backward() >>> x.grad tensor(7., dtype=torch.float64)
可以看出,最後y相對於x的導數在x=2的地方為7。在數學裡進行驗證,那麼就是
y’ = 2*x + 3, 當x=2時,y’ = 2 * 2 + 3 = 7, 完全符合torch自動求得的梯度值。
接下來計算二元函數時的情況:
>>> x1 = torch.tensor(1.0) >>> x2 = torch.tensor(2.0, requires_grad=True) >>> y = 3*x1*x1 + 9 * x2 >>> y.backward() tensor(6.) >>> x2.grad tensor(9.)
可以看出,我們可以求得y相對於x2的偏導數。
以上討論的都是標量的情況,接下來討論自變量為向量的情況。
mat1 = torch.tensor([[1,2,3]], dtype=torch.float64, requires_grad=True) >>> mat2 tensor([[1.], [2.], [3.]], dtype=torch.float64, requires_grad=True)
mat1是一個1×3的矩陣,mat2是一個3×1的矩陣,他們倆的叉乘為一個1×1的矩陣。在pytorch裡面,可以直接對其進行backward,從而求得相對於mat1或者是mat2的梯度值。
>>> y = torch.mm(mat1, mat2) >>> y.backward() >>> mat1.grad tensor([[1., 2., 3.]], dtype=torch.float64) >>> mat2.grad tensor([[1.], [2.], [3.]], dtype=torch.float64)
其實可以把mat1中的每一個元素當成一個自變量,那麼相對於mat1的梯度向量,就是分別對3個x進行求偏導。
相當於是y = mat1[0] * mat2[0] + mat1[1] * mat2[1] + mat1[2] * mat2[2]
然後分別求y對於mat1,mat2每個元素的偏導。
另外,如果我們最後輸出的是一個N x M 的一個向量,我們要計算這個向量相對於自變量向量的偏導,那麼我們就需要在backward函數的參數裡傳入參數。
如上圖所述,其實pytorch的autograd核心就是計算一個 vector-jacobian 乘積, jacobian就是因變量向量相對於自變量向量的偏導組成的矩陣,vector相當於是因變量向量到一個標量的函數的偏導。最後就是標量相對於一個向量的梯度向量。
總結
最後,其實神經網絡就是尋求一個擬合函數,但是因為參數過多,所以不得不借助每一點的梯度來一點一點的接近最佳的LOSS值,pytorch擁有動態的計算圖,存儲記憶對向量的每一個函數操作,最後通過反向傳播來計算梯度,這可以說是pytorch的核心。
所以深入瞭解如果利用pytorch進行自動梯度計算非常重要。
以上為個人經驗,希望能給大傢一個參考,也希望大傢多多支持WalkonNet。
推薦閱讀:
- pytorch_detach 切斷網絡反傳方式
- Pytorch中的backward()多個loss函數用法
- pytorch中.numpy()、.item()、.cpu()、.detach()以及.data的使用方法
- pytorch 如何打印網絡回傳梯度
- pytorch 禁止/允許計算局部梯度的操作