2.6 Autograd
瞭解計算圖後,我們可以開始學習autograd。這裡再次回顧pytorch官網的一張示意圖
在進行h2h、i2h、next_h、loss的計算過程中,逐步搭建計算圖,同時針對每一個變數(tensor)都存儲計算梯度所必備的grad_fn,便於自動求導系統使用。當計算到根節點後,在根節點調用.backward()函數,即可自動反向傳播計算計算圖中所有節點的梯度。這就是pytorch自動求導機制,其中涉及張量類、計算圖、grad_fn、鏈式求導法則等基礎概念,大家可以自行補充學習。
autograd 官方定義
來看看官方文檔中對autograd的解釋:
Conceptually, autograd keeps a record of data (tensors) and all executed operations (along with the resulting new tensors) in a directed acyclic graph (DAG) consisting of Function objects. In this DAG, leaves are the input tensors, roots are the output tensors. By tracing this graph from roots to leaves, you can automatically compute the gradients using the chain rule.
In a forward pass, autograd does two things simultaneously:
- run the requested operation to compute a resulting tensor
- maintain the operation’s gradient function in the DAG.
The backward pass kicks off when .backward() is called on the DAG root. autograd then:
- computes the gradients from each .grad_fn,
- accumulates them in the respective tensor’s .grad attribute
- using the chain rule, propagates all the way to the leaf tensors.
from: https://pytorch.org/tutorials/beginner/basics/autogradqs_tutorial.html#more-on-computational-graphs
劃重點:
- 自動求導機制通過有向無環圖(directed acyclic graph ,DAG)實現
- 在DAG中,記錄資料(對應tensor.data)以及操作(對應tensor.grad_fn)
- 操作在pytorch中統稱為Function,如加法、減法、乘法、ReLU、conv、Pooling等,統統是Function
autograd 的使用
autograd的使用有很多方法,這裡重點講解一下三個,並在最後匯總一些知識點。更多API推薦閱讀官方文檔
- torch.autograd.backward
- torch.autograd.grad
- torch.autograd.Function
torch.autograd.backward
backward函數是使用頻率最高的自動求導函數,沒有之一。99%的訓練代碼中都會用它進行梯度求導,然後更新權重。
使用方法可以參考第二章第二節-新冠肺炎分類的代碼,loss.backward()就可以完成計算圖中所有張量的梯度求解。
雖然絕大多數都是直接使用,但是backward()裡邊還有一些高級參數,值得瞭解。
torch.autograd.backward(tensors, grad_tensors=None, retain_graph=None, create_graph=False, grad_variables=None, inputs=None)
- tensors (Sequence[Tensor] or Tensor) – 用於求導的張量。如上例的loss。
- grad_tensors (Sequence[Tensor or None] or Tensor, optional) – 雅克比向量積中使用,詳細作用請看代碼演示。
- retain_graph (bool, optional) – 是否需要保留計算圖。pytorch的機制是在方向傳播結束時,計算圖釋放以節省記憶體。大家可以嘗試連續使用loss.backward(),就會報錯。如果需要多次求導,則在執行backward()時,retain_graph=True。
- create_graph (bool, optional) – 是否創建計算圖,用於高階求導。
- inputs (Sequence[Tensor] or Tensor, optional) – Inputs w.r.t. which the gradient be will accumulated into .grad. All other Tensors will be ignored. If not provided, the gradient is accumulated into all the leaf Tensors that were used to compute the attr::tensors.
補充說明:我們使用時候都是在張量上直接調用.backward()函數,但這裡卻是torch.autograd.backward,為什麼不一樣呢? 其實Tensor.backward()介面內部調用了autograd.backward。
請看使用示例
retain_grad參數使用
對比兩個程式碼片段,仔細閱讀pytorch報錯資訊。
##### retain_graph=True
import torch
w = torch.tensor([1.], requires_grad=True)
x = torch.tensor([2.], requires_grad=True)
a = torch.add(w, x)
b = torch.add(w, 1)
y = torch.mul(a, b)
y.backward(retain_graph=True)
print(w.grad)
y.backward()
print(w.grad)
Copy
tensor([5.])
tensor([10.])
Copy
運行上面程式碼片段可以看到是正常的,下面這個程式碼片段就會報錯,報錯資訊提示非常明確:Trying to backward through the graph a second time。並且還給出了解決方法: Specify retain_graph=True if you need to backward through the graph a second time 。
這也是pytorch代碼寫得好的地方,出現錯誤不要慌,仔細看看報錯資訊,裡邊可能會有解決問題的方法。
##### retain_graph=False
import torch
w = torch.tensor([1.], requires_grad=True)
x = torch.tensor([2.], requires_grad=True)
a = torch.add(w, x)
b = torch.add(w, 1)
y = torch.mul(a, b)
y.backward()
print(w.grad)
y.backward()
print(w.grad)
Copy
tensor([5.])
---------------------------------------------------------------------------
RuntimeError Traceback (most recent call last)
<ipython-input-9-64bccc64184d> in <module>
10 y.backward()
11 print(w.grad)
---> 12 y.backward()
13 print(w.grad)
D:\Anaconda_data\envs\pytorch_1.10_gpu\lib\site-packages\torch\_tensor.py in backward(self, gradient, retain_graph, create_graph, inputs)
305 create_graph=create_graph,
306 inputs=inputs)
--> 307 torch.autograd.backward(self, gradient, retain_graph, create_graph, inputs=inputs)
308
309 def register_hook(self, hook):
D:\Anaconda_data\envs\pytorch_1.10_gpu\lib\site-packages\torch\autograd\__init__.py in backward(tensors, grad_tensors, retain_graph, create_graph, grad_variables, inputs)
154 Variable._execution_engine.run_backward(
155 tensors, grad_tensors_, retain_graph, create_graph, inputs,
--> 156 allow_unreachable=True, accumulate_grad=True) # allow_unreachable flag
157
158
RuntimeError: Trying to backward through the graph a second time (or directly access saved tensors after they have already been freed). Saved intermediate values of the graph are freed when you call .backward() or autograd.grad(). Specify retain_graph=True if you need to backward through the graph a second time or if you need to access saved tensors after calling backward.
Copy
grad_tensors使用
w = torch.tensor([1.], requires_grad=True)
x = torch.tensor([2.], requires_grad=True)
a = torch.add(w, x)
b = torch.add(w, 1)
y0 = torch.mul(a, b) # y0 = (x+w) * (w+1) dy0/dw = 2w + x + 1
y1 = torch.add(a, b) # y1 = (x+w) + (w+1) dy1/dw = 2
loss = torch.cat([y0, y1], dim=0) # [y0, y1]
grad_tensors = torch.tensor([1., 2.])
loss.backward(gradient=grad_tensors) # Tensor.backward中的 gradient 傳入 torch.autograd.backward()中的grad_tensors
# w = 1* (dy0/dw) + 2*(dy1/dw)
# w = 1* (2w + x + 1) + 2*(w)
# w = 1* (5) + 2*(2)
# w = 9
print(w.grad)
Copy
tensor([9.])
Copy
torch.autograd.grad
torch.autograd.grad(outputs, inputs, grad_outputs=None, retain_graph=None, create_graph=False, only_inputs=True, allow_unused=False)
功能:計算outputs對inputs的導數
主要參數:
- outputs (sequence of Tensor) – 用於求導的張量,如loss
- inputs (sequence of Tensor) – 所要計算導數的張量
- grad_outputs (sequence of Tensor) – 雅克比向量積中使用。
- retain_graph (bool, optional) – 是否需要保留計算圖。pytorch的機制是在方向傳播結束時,計算圖釋放以節省記憶體。大家可以嘗試連續使用loss.backward(),就會報錯。如果需要多次求導,則在執行backward()時,retain_graph=True。
- create_graph (bool, optional) – 是否創建計算圖,用於高階求導。
- allow_unused (bool, optional) – 是否需要指示,計算梯度時未使用的張量是錯誤的。
此函數使用上比較簡單,請看案例:
import torch
x = torch.tensor([3.], requires_grad=True)
y = torch.pow(x, 2) # y = x**2
# 一階導數
grad_1 = torch.autograd.grad(y, x, create_graph=True) # grad_1 = dy/dx = 2x = 2 * 3 = 6
print(grad_1)
# 二階導數
grad_2 = torch.autograd.grad(grad_1[0], x) # grad_2 = d(dy/dx)/dx = d(2x)/dx = 2
print(grad_2)
Copy
(tensor([6.], grad_fn=<MulBackward0>),)
(tensor([2.]),)
Copy
torch.autograd.Function
有的時候,想要實現自己的一些操作(op),如特殊的數學函數、pytorch的module中沒有的網路層,那就需要自己寫一個Function,在Function中定義好forward的計算公式、backward的計算公式,然後將這些op組合到模型中,模型就可以用autograd完成梯度求取。
這個概念還是很抽象,平時用得不多,但是自己想要自訂網路時,常常需要自己寫op,那麼它就很好用了,為了讓大家掌握自訂op——Function的寫法,特地從多處收集了四個案例,大家多運行代碼體會Function如何寫。
案例1: exp
案例1:來自 https://pytorch.org/docs/stable/autograd.html#function
假設需要一個計算指數的功能,並且能組合到模型中,實現autograd,那麼可以這樣實現
第一步:繼承Function
第二步:實現forward
第三步:實現backward
注意事項:
- forward和backward函數第一個參數為ctx,它的作用類似於類函數的self一樣,更詳細解釋可參考如下: In the forward pass we receive a Tensor containing the input and return a Tensor containing the output. ctx is a context object that can be used to stash information for backward computation. You can cache arbitrary objects for use in the backward pass using the ctx.save_for_backward method.
- backward函數返回的參數個數與forward的輸入參數個數相同, 即,傳入該op的參數,都需要給它們計算對應的梯度。
import torch
from torch.autograd.function import Function
class Exp(Function):
@staticmethod
def forward(ctx, i):
# ============== step1: 函數功能實現 ==============
result = i.exp()
# ============== step1: 函數功能實現 ==============
# ============== step2: 結果保存,用於反向傳播 ==============
ctx.save_for_backward(result)
# ============== step2: 結果保存,用於反向傳播 ==============
return result
@staticmethod
def backward(ctx, grad_output):
# ============== step1: 取出結果,用於反向傳播 ==============
result, = ctx.saved_tensors
# ============== step1: 取出結果,用於反向傳播 ==============
# ============== step2: 反向傳播公式實現 ==============
grad_results = grad_output * result
# ============== step2: 反向傳播公式實現 ==============
return grad_results
x = torch.tensor([1.], requires_grad=True)
y = Exp.apply(x) # 需要使用apply方法調用自訂autograd function
print(y) # y = e^x = e^1 = 2.7183
y.backward()
print(x.grad) # 反傳梯度, x.grad = dy/dx = e^x = e^1 = 2.7183
# 關於本例子更詳細解釋,推薦閱讀 https://zhuanlan.zhihu.com/p/321449610
Copy
tensor([2.7183], grad_fn=<ExpBackward>)
tensor([2.7183])
Copy
從代碼裡可以看到,y這個張量的 grad_fn 是 ExpBackward,正是我們自己實現的函數,這表明當y求梯度時,會調用ExpBackward這個函數進行計算
這也是張量的grad_fn的作用所在
案例2:為梯度乘以一定係數 Gradcoeff
案例2來自: https://zhuanlan.zhihu.com/p/321449610
功能是反向傳梯度時乘以一個自訂係數
class GradCoeff(Function):
@staticmethod
def forward(ctx, x, coeff):
# ============== step1: 函數功能實現 ==============
ctx.coeff = coeff # 將coeff存為ctx的成員變數
x.view_as(x)
# ============== step1: 函數功能實現 ==============
return x
@staticmethod
def backward(ctx, grad_output):
return ctx.coeff * grad_output, None # backward的輸出個數,應與forward的輸入個數相同,此處coeff不需要梯度,因此返回None
# 嘗試使用
x = torch.tensor([2.], requires_grad=True)
ret = GradCoeff.apply(x, -0.1) # 前向需要同時提供x及coeff,設置coeff為-0.1
ret = ret ** 2
print(ret) # 注意看: ret.grad_fn
ret.backward()
print(x.grad)
Copy
tensor([4.], grad_fn=<PowBackward0>)
tensor([-0.4000])
Copy
在這裡需要注意 backward函數返回的參數個數與forward的輸入參數個數相同
即,傳入該op的參數,都需要給它們計算對應的梯度。
案例3:勒讓德多項式
案例來自:https://github.com/excelkks/blog
假設多項式為:$y = a+bx+cx^2+dx^3$時,用兩步替代該過程 $y= a+b\times P_3(c+dx), P_3(x) = \frac{1}{2}(5x^3-3x)$
import torch
import math
from torch.autograd.function import Function
class LegendrePolynomial3(Function):
@staticmethod
def forward(ctx, x):
"""
In the forward pass we receive a Tensor containing the input and return
a Tensor containing the output. ctx is a context object that can be used
to stash information for backward computation. You can cache arbitrary
objects for use in the backward pass using the ctx.save_for_backward method.
"""
y = 0.5 * (5 * x ** 3 - 3 * x)
ctx.save_for_backward(x)
return y
@staticmethod
def backward(ctx, grad_output):
"""
In the backward pass we receive a Tensor containing the gradient of the loss
with respect to the output, and we need to compute the gradient of the loss
with respect to the input.
"""
ret, = ctx.saved_tensors
return grad_output * 1.5 * (5 * ret ** 2 - 1)
a, b, c, d = 1, 2, 1, 2
x = 1
P3 = LegendrePolynomial3.apply
y_pred = a + b * P3(c + d * x)
print(y_pred)
Copy
127.0
Copy
案例4:手動實現2D卷積
案例來自:https://pytorch.org/tutorials/intermediate/custom_function_conv_bn_tutorial.html
案例本是卷積與BN的融合實現,此處僅觀察Function的使用,更詳細的內容,十分推薦閱讀原文章
下面看如何實現conv_2d的
import torch
from torch.autograd.function import once_differentiable
import torch.nn.functional as F
def convolution_backward(grad_out, X, weight):
"""
將反向傳播功能用函數包裝起來,返回的參數個數與forward接收的參數個數保持一致,為2個
"""
grad_input = F.conv2d(X.transpose(0, 1), grad_out.transpose(0, 1)).transpose(0, 1)
grad_X = F.conv_transpose2d(grad_out, weight)
return grad_X, grad_input
class MyConv2D(torch.autograd.Function):
@staticmethod
def forward(ctx, X, weight):
ctx.save_for_backward(X, weight)
# ============== step1: 函數功能實現 ==============
ret = F.conv2d(X, weight)
# ============== step1: 函數功能實現 ==============
return ret
@staticmethod
def backward(ctx, grad_out):
X, weight = ctx.saved_tensors
return convolution_backward(grad_out, X, weight)
Copy
weight = torch.rand(5, 3, 3, 3, requires_grad=True, dtype=torch.double)
X = torch.rand(10, 3, 7, 7, requires_grad=True, dtype=torch.double)
torch.autograd.gradcheck(Conv2D.apply, (X, weight)) # gradcheck 功能請自行瞭解,通常寫完Function會用它檢查一下
y = Conv2D.apply(X, weight)
label = torch.randn_like(y)
loss = F.mse_loss(y, label)
print(weight.grad)
loss.backward()
print(weight.grad)
Copy
None
tensor([[[[1.4503, 1.3995, 1.4427],
[1.4725, 1.4247, 1.4995],
[1.4584, 1.4395, 1.5462]],
......
[[1.4645, 1.4461, 1.3604],
[1.4523, 1.4556, 1.3755],
[1.4204, 1.4346, 1.4323]]]], dtype=torch.float64)
Copy
autograd相關的知識點
autograd使用過程中還有很多需要注意的地方,在這裡做個小匯總。
- 知識點一:梯度不會自動清零
- 知識點二: 依賴于葉子結點的結點,requires_grad默認為True
- 知識點三: 葉子結點不可執行in-place
- 知識點四: detach 的作用
- 知識點五: with torch.no_grad()的作用
知識點一:梯度不會自動清零
import torch
w = torch.tensor([1.], requires_grad=True)
x = torch.tensor([2.], requires_grad=True)
for i in range(4):
a = torch.add(w, x)
b = torch.add(w, 1)
y = torch.mul(a, b)
y.backward()
print(w.grad) # 梯度不會自動清零,資料會累加, 通常需要採用 optimizer.zero_grad() 完成對參數的梯度清零
# w.grad.zero_()
Copy
tensor([5.])
tensor([5.])
tensor([5.])
tensor([5.])
Copy
知識點二:依賴于葉子結點的結點,requires_grad默認為True
結點的運算依賴于葉子結點的話,它一定是要計算梯度的,因為葉子結點梯度的計算是從後向前傳播的,因此與其相關的結點均需要計算梯度,這點還是很好理解的。
import torch
w = torch.tensor([1.], requires_grad=True) #
x = torch.tensor([2.], requires_grad=True)
a = torch.add(w, x)
b = torch.add(w, 1)
y = torch.mul(a, b)
print(a.requires_grad, b.requires_grad, y.requires_grad)
print(a.is_leaf, b.is_leaf, y.is_leaf)
Copy
True True True
False False False
Copy
知識點三:葉子張量不可以執行in-place操作
葉子結點不可執行in-place,因為計算圖的backward過程都依賴于葉子結點的計算,可以回顧計算圖當中的例子,所有的偏微分計算所需要用到的資料都是基於w和x(葉子結點),因此葉子結點不允許in-place操作。
a = torch.ones((1, ))
print(id(a), a)
a = a + torch.ones((1, ))
print(id(a), a)
a += torch.ones((1, ))
print(id(a), a)
Copy
2361561191752 tensor([1.])
2362180999432 tensor([2.])
2362180999432 tensor([3.])
Copy
w = torch.tensor([1.], requires_grad=True)
x = torch.tensor([2.], requires_grad=True)
a = torch.add(w, x)
b = torch.add(w, 1)
y = torch.mul(a, b)
w.add_(1)
y.backward()
Copy
---------------------------------------------------------------------------
RuntimeError Traceback (most recent call last)
<ipython-input-41-7e2ec3c17fc3> in <module>
6 y = torch.mul(a, b)
7
----> 8 w.add_(1)
9
10 y.backward()
RuntimeError: a leaf Variable that requires grad is being used in an in-place operation.
Copy
知識點四:detach 的作用
通過以上知識,我們知道計算圖中的張量是不能隨便修改的,否則會造成計算圖的backward計算錯誤,那有沒有其他方法能修改呢?當然有,那就是detach()
detach的作用是:從計算圖中剝離出“資料”,並以一個新張量的形式返回,並且新張量與舊張量共用資料,簡單的可理解為做了一個別名。 請看下例的w,detach後對w_detach修改資料,w同步地被改為了999
w = torch.tensor([1.], requires_grad=True)
x = torch.tensor([2.], requires_grad=True)
a = torch.add(w, x)
b = torch.add(w, 1)
y = torch.mul(a, b)
y.backward()
w_detach = w.detach()
w_detach.data[0] = 999
print(w)
Copy
tensor([999.], requires_grad=True)
Copy
知識點五:with torch.no_grad()的作用
autograd自動構建計算圖過程中會保存一系列中間變數,以便於backward的計算,這就必然需要花費額外的記憶體和時間。
而並不是所有情況下都需要backward,例如推理的時候,因此可以採用上下文管理器——torch.no_grad()來管理上下文,讓pytorch不記錄相應的變數,以加快速度和節省空間。
詳見:https://pytorch.org/docs/stable/generated/torch.no_grad.html?highlight=no_grad#torch.no_grad
小結
本章終於結束,本章目的是為大家介紹pytorch的核心模組,包括pytorch代碼庫結構,以便於今後閱讀源碼,知道從哪裡找代碼;包括第一個分類模型訓練,便於大家理解模型訓練過程;包括核心資料結構——張量,便於理解整個pytorch的資料;包括計算圖與autograd,便於大家熟悉自動微分的過程及自訂op的方法。
下一章將通過借助covid-19任務,詳細介紹pytorch的資料讀取機制,以及各種資料形式的讀取,包括csv形式、txt形式、雜亂資料夾形式等一切關於資料讀取、載入、操作的模組都將涉及。
小記:動筆一個多月,才寫了兩章,尤其autograd和tensor寫了大半個月,希望後面能有更多時間精力早日完成,加油!2022年1月18日