自動求梯度
在深度學習中,我們經常需要對函數求梯度(gradient)。PyTorch提供的autograd包能夠根據輸入和前向傳播過程自動構建計算圖,並執行反向傳播。本節將介紹如何使用autograd包來進行自動求梯度的有關操作。
概念
上一節介紹的Tensor
是這個包的核心類,如果將其屬性.requires_grad
設置為True
,它將開始追蹤(track)在其上的所有操作(這樣就可以利用鏈式法則進行梯度傳播了)。完成計算后,可以調用.backward()
來完成所有梯度計算。此Tensor
的梯度將累積到.grad
屬性中。
注意在
y.backward()
時,如果y
是標量,則不需要為backward()
傳入任何參數;否則,需要傳入一個與y
同形的Tensor
。
如果不想要被繼續追蹤,可以調用.detach()
將其從追蹤記錄中分離出來,這樣就可以防止將來的計算被追蹤,這樣梯度就傳不過去了。此外,還可以用with torch.no_grad()
將不想被追蹤的操作代碼塊包裹起來,這種方法在評估模型的時候很常用,因為在評估模型時,我們並不需要計算可訓練參數(requires_grad=True
)的梯度。
Function
是另外一個很重要的類。Tensor
和Function
互相結合就可以構建一個記錄有整個計算過程的有向無環圖(DAG)。每個Tensor
都有一個.grad_fn
屬性,該屬性即創建該Tensor
的Function
, 就是說該Tensor
是不是通過某些運算得到的,若是,則grad_fn
返回一個與這些運算相關的對象,否則是None。
下面通過一些例子來理解這些概念。
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
再做一下運算操作:
y = x + 2
print(y)
print(y.grad_fn)
輸出:
tensor([[3., 3.],
[3., 3.]], grad_fn=<AddBackward>)
<AddBackward object at 0x1100477b8>
注意x是直接創建的,所以它沒有grad_fn
, 而y是通過一個加法操作創建的,所以它有一個為<AddBackward>
的grad_fn
。
像x這種直接創建的稱為葉子節點,葉子節點對應的grad_fn
是None
。
print(x.is_leaf, y.is_leaf) # True False
再來點復雜度運算操作:
z = y * y * 3
out = z.mean()
print(z, out)
輸出:
tensor([[27., 27.],
[27., 27.]], grad_fn=<MulBackward>) tensor(27., grad_fn=<MeanBackward1>)
通過.requires_grad_()
來用in-place的方式改變requires_grad
屬性:
a = torch.randn(2, 2) # 缺失情況下默認 requires_grad = False
a = ((a * 3) / (a - 1))
print(a.requires_grad) # False
a.requires_grad_(True)
print(a.requires_grad) # True
b = (a * a).sum()
print(b.grad_fn)
輸出:
False
True
<SumBackward0 object at 0x118f50cc0>
梯度
因為out
是一個標量,所以調用backward()
時不需要指定求導變量:
out.backward() # 等價於 out.backward(torch.tensor(1.))
我們來看看out
關於x
的梯度 \(\frac{d(out)}{dx}\):
print(x.grad)
輸出:
tensor([[4.5000, 4.5000],
[4.5000, 4.5000]])
我們令out
為 \(o\) , 因為
所以
所以上面的輸出是正確的。
數學上,如果有一個函數值和自變量都為向量的函數 \(\vec{y}=f(\vec{x})\), 那么 \(\vec{y}\) 關於 \(\vec{x}\) 的梯度就是一個雅可比矩陣(Jacobian matrix):
而torch.autograd
這個包就是用來計算一些雅克比矩陣的乘積的。例如,如果 \(v\) 是一個標量函數的 \(l=g\left(\vec{y}\right)\) 的梯度:
那么根據鏈式法則我們有 \(l\) 關於 \(\vec{x}\) 的雅克比矩陣就為:
注意:grad在反向傳播過程中是累加的(accumulated),這意味着每一次運行反向傳播,梯度都會累加之前的梯度,所以一般在反向傳播之前需把梯度清零。
# 再來反向傳播一次,注意grad是累加的
out2 = x.sum()
out2.backward()
print(x.grad)
out3 = x.sum()
x.grad.data.zero_()
out3.backward()
print(x.grad)
輸出:
tensor([[5.5000, 5.5000],
[5.5000, 5.5000]])
tensor([[1., 1.],
[1., 1.]])
為什么在
y.backward()
時,如果y
是標量,則不需要為backward()
傳入任何參數;否則,需要傳入一個與y
同形的Tensor
?
簡單來說就是為了避免向量(甚至更高維張量)對張量求導,而轉換成標量對張量求導。舉個例子,假設形狀為m x n
的矩陣 X 經過運算得到了p x q
的矩陣 Y,Y 又經過運算得到了s x t
的矩陣 Z。那么按照前面講的規則,dZ/dY 應該是一個s x t x p x q
四維張量,dY/dX 是一個p x q x m x n
的四維張量。問題來了,怎樣反向傳播?怎樣將兩個四維張量相乘???這要怎么乘???就算能解決兩個四維張量怎么乘的問題,四維和三維的張量又怎么乘?導數的導數又怎么求,這一連串的問題,感覺要瘋掉……
為了避免這個問題,我們不允許張量對張量求導,只允許標量對張量求導,求導結果是和自變量同形的張量。所以必要時我們要把張量通過將所有張量的元素加權求和的方式轉換為標量,舉個例子,假設y
由自變量x
計算而來,w
是和y
同形的張量,則y.backward(w)
的含義是:先計算l = torch.sum(y * w)
,則l
是個標量,然后求l
對自變量x
的導數。
參考
來看一些實際例子。
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>)
現在 z
不是一個標量,所以在調用backward
時需要傳入一個和z
同形的權重向量進行加權求和得到一個標量。
v = torch.tensor([[1.0, 0.1], [0.01, 0.001]], dtype=torch.float)
z.backward(v)
print(x.grad)
輸出:
tensor([2.0000, 0.2000, 0.0200, 0.0020])
注意,x.grad
是和x
同形的張量。
再來看看中斷梯度追蹤的例子:
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
可以看到,上面的y2
是沒有grad_fn
而且y2.requires_grad=False
的,而y3
是有grad_fn
的。如果我們將y3
對x
求梯度的話會是多少呢?
y3.backward()
print(x.grad)
輸出:
tensor(2.)
為什么是2呢?$ y_3 = y_1 + y_2 = x^2 + x^3$,當 \(x=1\) 時 \(\frac {dy_3} {dx}\) 不應該是5嗎?事實上,由於 \(y_2\) 的定義是被torch.no_grad():
包裹的,所以與 \(y_2\) 有關的梯度是不會回傳的,只有與 \(y_1\) 有關的梯度才會回傳,即 \(x^2\) 對 \(x\) 的梯度。
上面提到,y2.requires_grad=False
,所以不能調用 y2.backward()
,會報錯:
RuntimeError: element 0 of tensors does not require grad and does not have a grad_fn
此外,如果我們想要修改tensor
的數值,但是又不希望被autograd
記錄(即不會影響反向傳播),那么我么可以對tensor.data
進行操作。
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)
輸出:
tensor([1.])
False
tensor([100.], requires_grad=True)
tensor([2.])
注: 本文主要參考PyTorch官方文檔,與原書同一節有很大不同。