App下載

PyTorch 怎么自動計算梯度

重度健忘癥患者 2021-07-17 13:54:11 瀏覽數(shù) (2399)
反饋

在PyTorch中,torch.Tensor類是存儲和變換數(shù)據(jù)的重要工具,相比于Numpy,Tensor提供GPU計算和自動求梯度等更多功能,在深度學(xué)習(xí)中,我們經(jīng)常需要對函數(shù)求梯度(gradient)。PyTorch提供的autograd包能夠根據(jù)輸入和前向傳播過程自動構(gòu)建計算圖,并執(zhí)行反向傳播。本篇將介紹和總結(jié)如何使用autograd包來進(jìn)行自動求梯度的有關(guān)操作。

1. 概念

Tensor是這個pytorch的自動求導(dǎo)部分的核心類,如果將其屬性.requires_grad=True,它將開始追蹤(track) 在該tensor上的所有操作,從而實現(xiàn)利用鏈?zhǔn)椒▌t進(jìn)行的梯度傳播。完成計算后,可以調(diào)用.backward()來完成所有梯度計算。此Tensor的梯度將累積到.grad屬性中。

如果不想要被繼續(xù)對tensor進(jìn)行追蹤,可以調(diào)用.detach()將其從追蹤記錄中分離出來,接下來的梯度就傳不過去了。此外,還可以用with torch.no_grad()將不想被追蹤的操作代碼塊包裹起來,這種方法在評估模型的時候很常用,因為此時并不需要繼續(xù)對梯度進(jìn)行計算。

Function是另外一個很重要的類。Tensor和Function互相結(jié)合就可以構(gòu)建一個記錄有整個計算過程的有向無環(huán)圖(DAG)。每個Tensor都有一個.grad_fn屬性,該屬性即創(chuàng)建該Tensor的Function, 就是說該Tensor是不是通過某些運算得到的,若是,則grad_fn返回一個與這些運算相關(guān)的對象,否則是None。

2. 具體實現(xiàn)

2.1. 創(chuàng)建可自動求導(dǎo)的tensor

首先我們創(chuàng)建一個tensor,同時設(shè)置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這種直接創(chuàng)建的tensor 稱為葉子節(jié)點,葉子節(jié)點對應(yīng)的grad_fn是None。如果進(jìn)行一次運算操作:

y = x + 1
print(y)
print(y.grad_fn)
'''
tensor([[2., 2.],
        [2., 2.]], grad_fn=<AddBackward>)
<AddBackward object at 0x1100477b8>

'''

而y是通過一個加法操作創(chuàng)建的,所以它有一個為操作的grad_fn。

嘗試進(jìn)行更復(fù)雜的操作:

z = y ** 2
out = z.mean()
print(z, out)
'''
tensor([[4., 4.],
        [4., 4.]], grad_fn=<PowBackward0>) tensor(4., grad_fn=<MeanBackward0>)
'''

上面的out是一個標(biāo)量4,通常對于標(biāo)量直接使用out.backward()進(jìn)行求導(dǎo),不需要指定求導(dǎo)變量,后面進(jìn)行詳細(xì)說明。

也可以通過.requires_grad_()改變requires_grad屬性:

a = torch.randn(3, 2) # 缺失情況下默認(rèn) 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實現(xiàn)梯度求導(dǎo)的鏈?zhǔn)椒▌t,用來計算一些雅克比矩陣的乘積,即函數(shù)的一階導(dǎo)數(shù)的乘積。

注意: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的自動求導(dǎo)對于標(biāo)量比如上面的out.backward()十分方便,但是當(dāng)反向傳播的對象不是標(biāo)量時,需要在y.backward()種加入一個與out同形的Tensor,不允許張量對張量求導(dǎo),只允許標(biāo)量對張量求導(dǎo),求導(dǎo)結(jié)果是和自變量同形的張量。

這是為了避免向量(甚至更高維張量)對張量求導(dǎo),而轉(zhuǎn)換成標(biāo)量對張量求導(dǎo)。

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不是一個標(biāo)量,所以在調(diào)用 z.backward()時需要傳入一個和z同形的權(quán)重向量進(jìn)行加權(quán)求和得到一個標(biāo)量。

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,,無法調(diào)用反向傳播
'''
tensor(2.)
'''

這里結(jié)果為2,是因為我們沒有獲得y2的梯度,僅僅是對y1做了一次反向傳播,作為最后的梯度輸出。

2.4. 修改tensor的值

如果我們想要修改tensor的數(shù)值,但是不希望保存在autograd的記錄中,require s_grad = False, 即不影響到正在進(jìn)行的反向傳播,那么可以用tensor.data進(jìn)行操作。但是這種操作需要注意可能會產(chǎn)生一些問題,比如標(biāo)量為0

x = torch.ones(1,requires_grad=True)

print(x.data) # 仍然是一個tensor
print(x.data.requires_grad) # 但是已經(jīng)是獨立于計算圖之外

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已經(jīng)被改變,最后計算的梯度實際是錯誤的
'''
tensor([ 0.,  0.,  0.])
'''

補充:pytorch如何計算導(dǎo)數(shù)_Pytorch 自動求梯度(autograd)

深度學(xué)習(xí)其實就是一個最優(yōu)化問題,找到最小的loss值,因為自變量過多,想要找到最小值非常困難。所以就出現(xiàn)了很多最優(yōu)化方法,梯度下降就是一個非常典型的例子。本文針對python的pytorch庫中的自動求梯度進(jìn)行了詳細(xì)的解釋

Tensor

pytorch里面的tensor可以用來存儲向量或者標(biāo)量。

torch.tensor(1) # 標(biāo)量
torch.tensor([1]) # 1*1 的向量

tensor還可以指定數(shù)據(jù)類型,以及數(shù)據(jù)存儲的位置(可以存在顯存里,硬件加速)

torch.tensor([1,2], dtype=torch.float64)

梯度

在數(shù)學(xué)里,梯度的定義如下:

公式

可以看出,自變量相對于因變量的每一個偏導(dǎo)乘以相應(yīng)的單位向量,最后相加,即為最后的梯度向量。

在pytorch里面,我們無法直接定義函數(shù),也無法直接求得梯度向量的表達(dá)式。更多的時候,我們其實只是求得了函數(shù)的在某一個點處相對于自變量的偏導(dǎo)。

我們先假設(shè)一個一元函數(shù):y = x^2 + 3x +1,在pytorch里面,我們假設(shè)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的導(dǎo)數(shù)在x=2的地方為7。在數(shù)學(xué)里進(jìn)行驗證,那么就是

y' = 2*x + 3, 當(dāng)x=2時,y' = 2 * 2 + 3 = 7, 完全符合torch自動求得的梯度值。

接下來計算二元函數(shù)時的情況:

>>> 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的偏導(dǎo)數(shù)。

以上討論的都是標(biāo)量的情況,接下來討論自變量為向量的情況。

mat1 = torch.tensor([[1,2,3]], dtype=torch.float64, requires_grad=True)
>>> mat2
tensor([[1.],
        [2.],
        [3.]], dtype=torch.float64, requires_grad=True)

mat1是一個1x3的矩陣,mat2是一個3x1的矩陣,他們倆的叉乘為一個1x1的矩陣。在pytorch里面,可以直接對其進(jìn)行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中的每一個元素當(dāng)成一個自變量,那么相對于mat1的梯度向量,就是分別對3個x進(jìn)行求偏導(dǎo)。

相當(dāng)于是y = mat1[0] * mat2[0] + mat1[1] * mat2[1] + mat1[2] * mat2[2]

然后分別求y對于mat1,mat2每個元素的偏導(dǎo)。

另外,如果我們最后輸出的是一個N x M 的一個向量,我們要計算這個向量相對于自變量向量的偏導(dǎo),那么我們就需要在backward函數(shù)的參數(shù)里傳入?yún)?shù)。

論文介紹

如上圖所述,其實pytorch的autograd核心就是計算一個 vector-jacobian 乘積, jacobian就是因變量向量相對于自變量向量的偏導(dǎo)組成的矩陣,vector相當(dāng)于是因變量向量到一個標(biāo)量的函數(shù)的偏導(dǎo)。最后就是標(biāo)量相對于一個向量的梯度向量。

總結(jié)

最后,其實神經(jīng)網(wǎng)絡(luò)就是尋求一個擬合函數(shù),但是因為參數(shù)過多,所以不得不借助每一點的梯度來一點一點的接近最佳的LOSS值,pytorch擁有動態(tài)的計算圖,存儲記憶對向量的每一個函數(shù)操作,最后通過反向傳播來計算梯度,這可以說是pytorch的核心。

所以深入了解如果利用pytorch進(jìn)行自動梯度計算非常重要。

以上就是如何使用autograd包進(jìn)行自動求梯度的全部內(nèi)容了,希望能給大家一個參考,也希望大家多多支持W3Cschool


0 人點贊