平常都是無腦使用backward,每次看到別人的代碼里使用諸如autograd.grad這種方法的時候就有點抵觸,今天花了點時間了解了一下原理,寫下筆記以供以后參考。以下筆記基於Pytorch1.0
Tensor
Pytorch中所有的計算其實都可以回歸到Tensor上,所以有必要重新認識一下Tensor。如果我們需要計算某個Tensor的導數,那么我們需要設置其.requires_grad
屬性為True
。為方便說明,在本文中對於這種我們自己定義的變量,我們稱之為葉子節點(leaf nodes),而基於葉子節點得到的中間或最終變量則可稱之為結果節點。例如下面例子中的x
則是葉子節點,y
則是結果節點。
x = torch.rand(3, requires_grad=True)
y = x**2
z = x + x
另外一個Tensor中通常會記錄如下圖中所示的屬性:
data
: 即存儲的數據信息requires_grad
: 設置為True
則表示該Tensor需要求導grad
: 該Tensor的梯度值,每次在計算backward時都需要將前一時刻的梯度歸零,否則梯度值會一直累加,這個會在后面講到。grad_fn
: 葉子節點通常為None,只有結果節點的grad_fn才有效,用於指示梯度函數是哪種類型。例如上面示例代碼中的y.grad_fn=<PowBackward0 at 0x213550af048>, z.grad_fn=<AddBackward0 at 0x2135df11be0>
is_leaf
: 用來指示該Tensor是否是葉子節點。
torch.autograd.backward
有如下代碼:
x = torch.tensor(1.0, requires_grad=True)
y = torch.tensor(2.0, requires_grad=True)
z = x**2+y
z.backward()
print(z, x.grad, y.grad)
>>> tensor(3., grad_fn=<AddBackward0>) tensor(2.) tensor(1.)
可以z是一個標量,當調用它的backward方法后會根據鏈式法則自動計算出葉子節點的梯度值。
但是如果遇到z是一個向量或者是一個矩陣的情況,這個時候又該怎么計算梯度呢?這種情況我們需要定義grad_tensor
來計算矩陣的梯度。在介紹為什么使用之前我們先看一下源代碼中backward的接口是如何定義的:
torch.autograd.backward(
tensors,
grad_tensors=None,
retain_graph=None,
create_graph=False,
grad_variables=None)
tensor
: 用於計算梯度的tensor。也就是說這兩種方式是等價的:torch.autograd.backward(z) == z.backward()
grad_tensors
: 在計算矩陣的梯度時會用到。他其實也是一個tensor,shape一般需要和前面的tensor
保持一致。retain_graph
: 通常在調用一次backward后,pytorch會自動把計算圖銷毀,所以要想對某個變量重復調用backward,則需要將該參數設置為True
create_graph
: 當設置為True
的時候可以用來計算更高階的梯度grad_variables
: 這個官方說法是grad_variables' is deprecated. Use 'grad_tensors' instead.也就是說這個參數后面版本中應該會丟棄,直接使用grad_tensors
就好了。
好了,參數大致作用都介紹了,下面我們看看pytorch為什么設計了grad_tensors
這么一個參數,以及它有什么用呢?
還是用代碼做個示例
x = torch.ones(2,requires_grad=True)
z = x + 2
z.backward()
>>> ...
RuntimeError: grad can be implicitly created only for scalar outputs
當我們運行上面的代碼的話會報錯,報錯信息為RuntimeError: grad can be implicitly created only for scalar outputs。
上面的報錯信息意思是只有對標量輸出它才會計算梯度,而求一個矩陣對另一矩陣的導數束手無策。
那么我們只要想辦法把矩陣轉變成一個標量不就好了?比如我們可以對z求和,然后用求和得到的標量在對x求導,這樣不會對結果有影響,例如:
我們可以看到對z求和后再計算梯度沒有報錯,結果也與預期一樣:
x = torch.ones(2,requires_grad=True)
z = x + 2
z.sum().backward()
print(x.grad)
>>> tensor([1., 1.])
我們再仔細想想,對z求和不就是等價於z點乘一個一樣維度的全為1的矩陣嗎?即\(sum(Z)=dot(Z,I)\),而這個I也就是我們需要傳入的grad_tensors
參數。(點乘只是相對於一維向量而言的,對於矩陣或更高為的張量,可以看做是對每一個維度做點乘)
代碼如下:
x = torch.ones(2,requires_grad=True)
z = x + 2
z.backward(torch.ones_like(z)) # grad_tensors需要與輸入tensor大小一致
print(x.grad)
>>> tensor([1., 1.])
弄個再復雜一點的:
x = torch.tensor([2., 1.], requires_grad=True).view(1, 2)
y = torch.tensor([[1., 2.], [3., 4.]], requires_grad=True)
z = torch.mm(x, y)
print(f"z:{z}")
z.backward(torch.Tensor([[1., 0]]), retain_graph=True)
print(f"x.grad: {x.grad}")
print(f"y.grad: {y.grad}")
>>> z:tensor([[5., 8.]], grad_fn=<MmBackward>)
x.grad: tensor([[1., 3.]])
y.grad: tensor([[2., 0.],
[1., 0.]])
結果解釋如下:
總結:
說了這么多,grad_tensors
的作用其實可以簡單地理解成在求梯度時的權重,因為可能不同值的梯度對結果影響程度不同,所以pytorch弄了個這種接口,而沒有固定為全是1。引用自知乎上的一個評論:如果從最后一個節點(總loss)來backward,這種實現(torch.sum(y*w))的意義就具體化為 multiple loss term with difference weights 這種需求了吧。
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
: 葉子節點grad_outputs
: 類似於backward
方法中的grad_tensors
retain_graph
: 同上create_graph
: 同上only_inputs
: 默認為True
, 如果為True
, 則只會返回指定input
的梯度值。 若為False
,則會計算所有葉子節點的梯度,並且將計算得到的梯度累加到各自的.grad
屬性上去。allow_unused
: 默認為False
, 即必須要指定input
,如果沒有指定的話則報錯。
參考
- PyTorch 中 backward() 詳解
- PyTorch 的backward 為什么有一個grad_variables 參數?
- AUTOMATIC DIFFERENTIATION PACKAGE - TORCH.AUTOGRAD
2019-9-18