圖是一類(lèi)比較常見(jiàn)的數(shù)據(jù)結(jié)構(gòu),在pytorch中可以使用autograd來(lái)計(jì)算圖,那么autograd計(jì)算圖有什么特點(diǎn)嗎?今天小編帶來(lái)了一篇pytorch的autograd計(jì)算圖的特點(diǎn)說(shuō)明,希望能給小伙伴帶來(lái)一定的幫助。
在PyTorch實(shí)現(xiàn)中,autograd會(huì)隨著用戶的操作,記錄生成當(dāng)前variable的所有操作,并由此建立一個(gè)有向無(wú)環(huán)圖。用戶每進(jìn)行一個(gè)操作,相應(yīng)的計(jì)算圖就會(huì)發(fā)生改變。
更底層的實(shí)現(xiàn)中,圖中記錄了操作Function,每一個(gè)變量在圖中的位置可通過(guò)其grad_fn屬性在圖中的位置推測(cè)得到。在反向傳播過(guò)程中,autograd沿著這個(gè)圖從當(dāng)前變量(根節(jié)點(diǎn) extbf{z}z)溯源,可以利用鏈?zhǔn)角髮?dǎo)法則計(jì)算所有葉子節(jié)點(diǎn)的梯度。
每一個(gè)前向傳播操作的函數(shù)都有與之對(duì)應(yīng)的反向傳播函數(shù)用來(lái)計(jì)算輸入的各個(gè)variable的梯度,這些函數(shù)的函數(shù)名通常以Backward結(jié)尾。
下面結(jié)合代碼學(xué)習(xí)autograd的實(shí)現(xiàn)細(xì)節(jié)。
在PyTorch中計(jì)算圖的特點(diǎn)可總結(jié)如下:
autograd根據(jù)用戶對(duì)variable的操作構(gòu)建其計(jì)算圖。對(duì)變量的操作抽象為Function
對(duì)于那些不是任何函數(shù)(Function)的輸出,由用戶創(chuàng)建的節(jié)點(diǎn)稱(chēng)為葉子節(jié)點(diǎn),葉子節(jié)點(diǎn)的grad_fn為None。葉子節(jié)點(diǎn)中需要求導(dǎo)的variable,具有AccumulateGrad標(biāo)識(shí),因其梯度是累加的
variable默認(rèn)是不需要求導(dǎo)的,即requires_grad屬性默認(rèn)為False,如果某一個(gè)節(jié)點(diǎn)requires_grad被設(shè)置為T(mén)rue,那么所有依賴(lài)它的節(jié)點(diǎn)requires_grad都為T(mén)rue
variable的volatile屬性默認(rèn)為False,如果某一個(gè)variable的volatile屬性被設(shè)為T(mén)rue,那么所有依賴(lài)它的節(jié)點(diǎn)volatile屬性都為T(mén)rue。volatile屬性為T(mén)rue的節(jié)點(diǎn)不會(huì)求導(dǎo),volatile的優(yōu)先級(jí)比requires_grad高。
多次反向傳播時(shí),梯度是累加的。反向傳播的中間緩存會(huì)被清空,為進(jìn)行多次反向傳播需指定retain_graph=True來(lái)保存這些緩存
非葉子節(jié)點(diǎn)的梯度計(jì)算完之后即被清空,可以使用autograd.grad或hook技術(shù)獲取非葉子節(jié)點(diǎn)的值
variable的grad與data形狀一致,應(yīng)避免直接修改variable.data,因?yàn)閷?duì)data的直接操作無(wú)法利用autograd進(jìn)行反向傳播
反向傳播函數(shù)backward的參數(shù)grad_variables可以看成鏈?zhǔn)角髮?dǎo)的中間結(jié)果,如果是標(biāo)量,可以省略,默認(rèn)為1
PyTorch采用動(dòng)態(tài)圖設(shè)計(jì),可以很方便地查看中間層的輸出,動(dòng)態(tài)的設(shè)計(jì)計(jì)算圖結(jié)構(gòu)
在 e.backward() 執(zhí)行求導(dǎo)時(shí),系統(tǒng)遍歷 e.grad_fn.next_functions ,分別執(zhí)行求導(dǎo)。
如果 e.grad_fn.next_functions 中有哪個(gè)是 AccumulateGrad ,則把結(jié)果保存到 AccumulateGrad 的variable引用的變量中。
否則,遞歸遍歷這個(gè)function的 next_functions ,執(zhí)行求導(dǎo)過(guò)程。
最終到達(dá)所有的葉節(jié)點(diǎn),求導(dǎo)結(jié)束。同時(shí),所有的葉節(jié)點(diǎn)的 grad 變量都得到了相應(yīng)的更新。
他們之間的關(guān)系如下圖所示:
例子:
x = torch.randn(5, 5)
y = torch.randn(5, 5)
z = torch.randn((5, 5), requires_grad=True)
a = x + z
print(a.requires_grad)
可以z是一個(gè)標(biāo)量,當(dāng)調(diào)用它的backward方法后會(huì)根據(jù)鏈?zhǔn)椒▌t自動(dòng)計(jì)算出葉子節(jié)點(diǎn)的梯度值。
但是如果遇到z是一個(gè)向量或者是一個(gè)矩陣的情況,這個(gè)時(shí)候又該怎么計(jì)算梯度呢?這種情況我們需要定義grad_tensor來(lái)計(jì)算矩陣的梯度。在介紹為什么使用之前我們先看一下源代碼中backward的接口是如何定義的:
torch.autograd.backward(
tensors,
grad_tensors=None,
retain_graph=None,
create_graph=False,
grad_variables=None)
grad_tensors作用
x = torch.ones(2,requires_grad=True)
z = x + 2
z.backward()
>>> ...
RuntimeError: grad can be implicitly created only for scalar outputs
當(dāng)我們運(yùn)行上面的代碼的話會(huì)報(bào)錯(cuò),報(bào)錯(cuò)信息為RuntimeError: grad can be implicitly created only for scalar outputs。
x = torch.ones(2,requires_grad=True)
z = x + 2
z.sum().backward()
print(x.grad)
>>> tensor([1., 1.])
我們?cè)僮屑?xì)想想,對(duì)z求和不就是等價(jià)于z點(diǎn)乘一個(gè)一樣維度的全為1的矩陣嗎?即sum(Z)=dot(Z,I),而這個(gè)I也就是我們需要傳入的grad_tensors參數(shù)。(點(diǎn)乘只是相對(duì)于一維向量而言的,對(duì)于矩陣或更高為的張量,可以看做是對(duì)每一個(gè)維度做點(diǎn)乘)
代碼如下:
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.]])
補(bǔ)充:PyTorch的計(jì)算圖和自動(dòng)求導(dǎo)機(jī)制
自動(dòng)求導(dǎo)機(jī)制簡(jiǎn)介
PyTorch會(huì)根據(jù)計(jì)算過(guò)程自動(dòng)生成動(dòng)態(tài)圖,然后根據(jù)動(dòng)態(tài)圖的創(chuàng)建過(guò)程進(jìn)行反向傳播,計(jì)算每個(gè)節(jié)點(diǎn)的梯度值。
為了能夠記錄張量的梯度,首先需要在創(chuàng)建張量的時(shí)候設(shè)置一個(gè)參數(shù)requires_grad=True,意味著這個(gè)張量將會(huì)加入到計(jì)算圖中,作為計(jì)算圖的葉子節(jié)點(diǎn)參與計(jì)算,最后輸出根節(jié)點(diǎn)。
對(duì)于PyTorch來(lái)說(shuō),每個(gè)張量都有一個(gè)grad_fn方法,包含創(chuàng)建該張量的運(yùn)算的導(dǎo)數(shù)信息。在反向傳播的過(guò)程中,通過(guò)傳入后一層的神經(jīng)網(wǎng)絡(luò)的梯度,該函數(shù)會(huì)計(jì)算出參與運(yùn)算的所有張量的梯度。
同時(shí),PyTorch提供了一個(gè)專(zhuān)門(mén)用來(lái)做自動(dòng)求導(dǎo)的包torch.autograd。它包含兩個(gè)重要的函數(shù),即torch.autograd.bakward和torch.autograd.grad。
torch.autograd.bakward通過(guò)傳入根節(jié)點(diǎn)的張量以及初始梯度張量,可以計(jì)算產(chǎn)生該根節(jié)點(diǎn)的所有對(duì)應(yīng)葉子節(jié)點(diǎn)的梯度。當(dāng)張量為標(biāo)量張量時(shí),可以不傳入梯度張量,這是默認(rèn)會(huì)設(shè)置初始梯度張量為1.當(dāng)計(jì)算梯度張量時(shí),原先建立起來(lái)的計(jì)算圖會(huì)被自動(dòng)釋放,如果需要再次做自動(dòng)求導(dǎo),因?yàn)橛?jì)算圖已經(jīng)不存在,就會(huì)報(bào)錯(cuò)。如果要在反向傳播的時(shí)候保留計(jì)算圖,可以設(shè)置retain_graph=True。
另外,在自動(dòng)求導(dǎo)的時(shí)候默認(rèn)不會(huì)建立反向傳播的計(jì)算圖,如果需要在反向傳播的計(jì)算的同時(shí)建立梯度張量的計(jì)算圖,可以設(shè)置create_graph=True。對(duì)于一個(gè)可求導(dǎo)的張量來(lái)說(shuō),也可以調(diào)用該張量?jī)?nèi)部的backward方法。
自動(dòng)求導(dǎo)機(jī)制實(shí)例
定義一個(gè)函數(shù)f(x)=x2,則它的導(dǎo)數(shù)f'(x)=2x。于是可以創(chuàng)建一個(gè)可導(dǎo)的張量來(lái)測(cè)試具體的導(dǎo)數(shù)。
t1 = torch.randn(3, 3, requires_grad=True) # 定義一個(gè)3×3的張量
print(t1)
t2 = t1.pow(2).sum() # 計(jì)算張量的所有分量的平方和
t2.backward() # 反向傳播
print(t1.grad) # 梯度是原始分量的2倍
t2 = t1.pow(2).sum() # 再次計(jì)算張量的所有分量的平方和
t2.backward() # 再次反向傳播
print(t1.grad) # 梯度累積
print(t1.grad.zero_()) # 單個(gè)張量清零梯度的方法
得到的結(jié)果:
tensor([[-1.8170, -1.4907, 0.4560],
[ 0.9244, 0.0798, -1.2246],
[ 1.7800, 0.0367, -2.5998]], requires_grad=True)
tensor([[-3.6340, -2.9814, 0.9120],
[ 1.8488, 0.1597, -2.4492],
[ 3.5600, 0.0735, -5.1996]])
tensor([[ -7.2681, -5.9628, 1.8239],
[ 3.6975, 0.3193, -4.8983],
[ 7.1201, 0.1469, -10.3992]])
tensor([[0., 0., 0.],
[0., 0., 0.],
[0., 0., 0.]])
需要注意的一點(diǎn)是: 張量綁定的梯度張量在不清空的情況下會(huì)逐漸累積。這在例如一次性求很多Mini-batch的累積梯度時(shí)是有用的,但在一般情況下,需要注意將張量的梯度清零。
梯度函數(shù)的使用
如果不需要求出當(dāng)前張量對(duì)所有產(chǎn)生該張量的葉子節(jié)點(diǎn)的梯度,可以使用torch.autograd.grad函數(shù)。
這個(gè)函數(shù)的參數(shù)是兩個(gè)張量,第一個(gè)張量是計(jì)算圖的張量列表,第二個(gè)參數(shù)是需要對(duì)計(jì)算圖求導(dǎo)的張量。最后輸出的結(jié)果是第一個(gè)張量對(duì)第二個(gè)張量求導(dǎo)的結(jié)果。
這個(gè)函數(shù)不會(huì)改變?nèi)~子節(jié)點(diǎn)的grad屬性,同樣該函數(shù)在反向傳播求導(dǎo)的時(shí)候釋放計(jì)算圖,如果要保留計(jì)算圖需要設(shè)置retain_graph=True。
另外有時(shí)候會(huì)碰到一種情況:求到的兩個(gè)張量之間在計(jì)算圖上沒(méi)有關(guān)聯(lián)。在這種情況下需要設(shè)置allow_unused=True,結(jié)果會(huì)返回分量全為0的梯度張量。
t1 = torch.randn(3, 3, requires_grad=True)
print(t1)
t2 = t1.pow(2).sum()
print(torch.autograd.grad(t2, t1))
得到的結(jié)果為:
tensor([[ 0.5952, 0.1209, 0.5190],
[ 0.4602, -0.6943, -0.7853],
[-0.1460, -0.1406, -0.7081]], requires_grad=True)
(tensor([[ 1.1904, 0.2418, 1.0379],
[ 0.9204, -1.3885, -1.5706],
[-0.2919, -0.2812, -1.4161]])
計(jì)算圖構(gòu)建的啟用和禁用
由于計(jì)算圖的構(gòu)建需要消耗內(nèi)存和計(jì)算資源,在一些情況下計(jì)算圖并不是必要的,所以可以使用torch.no_grad這個(gè)上下文管理器,對(duì)該管理器作用域中的神經(jīng)網(wǎng)絡(luò)計(jì)算不構(gòu)建任何的計(jì)算圖。
還有一種情況是對(duì)于一個(gè)張量,在反向傳播的時(shí)候可能不需要讓梯度通過(guò)這個(gè)張量的節(jié)點(diǎn),也就是新建的計(jì)算圖需要和原來(lái)的計(jì)算圖分離,使用張量的detach方法,可以返回一個(gè)新的張量,該張量會(huì)成為一個(gè)新的計(jì)算圖的葉子結(jié)點(diǎn)。
總結(jié)
PyTorch使用動(dòng)態(tài)計(jì)算圖,該計(jì)算圖的特點(diǎn)是靈活。雖然在構(gòu)件計(jì)算圖的時(shí)候有性能開(kāi)銷(xiāo),但PyTorch本身的優(yōu)化抵消了一部分開(kāi)銷(xiāo),盡可能讓計(jì)算圖的構(gòu)建和釋放過(guò)程代價(jià)最小,因此,相對(duì)于靜態(tài)圖的框架來(lái)說(shuō),PyTorch本身的運(yùn)算速度并不慢。
有了計(jì)算圖之后,就可以很方便地通過(guò)自動(dòng)微分機(jī)制進(jìn)行反向傳播的計(jì)算,從而獲得計(jì)算圖葉子節(jié)點(diǎn)的梯度。在訓(xùn)練深度學(xué)習(xí)模型的時(shí)候,可以通過(guò)對(duì)損失函數(shù)的反向傳播,計(jì)算所有參數(shù)的梯度,隨后在優(yōu)化器中優(yōu)化這些梯度。
以上就是pytorch的autograd計(jì)算圖的特點(diǎn)說(shuō)明的全部?jī)?nèi)容,希望能給大家一個(gè)參考,也希望大家多多支持W3Cschool。