自動求梯度
首先給大家介紹幾個基本概念:
方向?qū)?shù):是一個數(shù)宵距;反映的是f(x,y)在P0點沿方向v的變化率栽惶。
偏導(dǎo)數(shù):是多個數(shù)(每元有一個)谅河;是指多元函數(shù)沿坐標(biāo)軸方向的方向?qū)?shù),因此二元函數(shù)就有兩個偏導(dǎo)數(shù)蕉堰。
偏導(dǎo)函數(shù):是一個函數(shù)若锁;是一個關(guān)于點的偏導(dǎo)數(shù)的函數(shù)搁骑。
梯度:是一個向量;每個元素為函數(shù)對一元變量的偏導(dǎo)數(shù)又固;它既有大兄倨鳌(其大小為最大方向?qū)?shù)),也有方向仰冠。
摘自《方向?qū)?shù)與梯度》
梯度從本質(zhì)上來說也是導(dǎo)數(shù)的一種乏冀,實在不好理解可以把它當(dāng)成導(dǎo)數(shù),它的本質(zhì)也是反應(yīng)一個函數(shù)的變化洋只,我們經(jīng)常需要對函數(shù)求梯度(gradient)辆沦。PyTorch提供的autograd包能夠根據(jù)輸入和前向傳播過程自動構(gòu)建計算圖,并執(zhí)行反向傳播识虚。
概念
Tensor是PyTorch的核心類肢扯,如果將其屬性.requires_grad設(shè)置為True,它將開始追蹤(track)在其上的所有操作(這樣就可以利用鏈?zhǔn)椒▌t進(jìn)行梯度傳播了)担锤。完成計算后蔚晨,可以調(diào)用.backward()來完成所有梯度計算。此Tensor的梯度將累積到.grad屬性中肛循。
注意在y.backward()時铭腕,如果y是標(biāo)量,則不需要為backward()傳入任何參數(shù)多糠;否則累舷,需要傳入一個與y同形的Tensor。
如果不想要被繼續(xù)追蹤夹孔,可以調(diào)用.detach()將其從追蹤記錄中分離出來被盈,這樣就可以防止將來的計算被追蹤,這樣梯度就傳不過去了搭伤。此外害捕,還可以用with torch.no_grad()將不想被追蹤的操作代碼塊包裹起來,這種方法在評估模型的時候很常用闷畸,因為在評估模型時,我們并不需要計算可訓(xùn)練參數(shù)(requires_grad=True)的梯度吞滞。
Function是另外一個很重要的類佑菩。Tensor和Function互相結(jié)合就可以構(gòu)建一個記錄有整個計算過程的有向無環(huán)圖(DAG)盾沫。每個Tensor都有一個.grad_fn屬性,該屬性即創(chuàng)建該Tensor的Function, 就是說該Tensor是不是通過某些運(yùn)算得到的殿漠,若是赴精,則grad_fn返回一個與這些運(yùn)算相關(guān)的對象,否則是None绞幌。
測試
創(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
運(yùn)算操作:
y = x + 2
print(y)
print(y.grad_fn)
z = y * y * 3
out = z.mean()
print(z, out)
輸出如下:
tensor([[3., 3.],
[3., 3.]], grad_fn=<AddBackward>)
<AddBackward object at 0x1100477b8>
tensor([[27., 27.],
[27., 27.]], grad_fn=<MulBackward>) tensor(27., grad_fn=<MeanBackward1>)
注意x是直接創(chuàng)建的蕾哟,所以它沒有g(shù)rad_fn, 而y是通過一個加法操作創(chuàng)建的,所以它有一個為<AddBackward>的grad_fn莲蜘。
像x這種直接創(chuàng)建的稱為葉子節(jié)點谭确,葉子節(jié)點對應(yīng)的grad_fn是None。
print(x.is_leaf, y.is_leaf) # True False
通過.requires_grad_()來用in-place的方式改變requires_grad屬性:
a = torch.randn(2, 2) # 缺失情況下默認(rèn) 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>
上面是關(guān)于梯度的一些性質(zhì)票渠,下面介紹一些它的計算之類的逐哈。
因為out是一個標(biāo)量,所以調(diào)用backward()時不需要指定求導(dǎo)變量:
out.backward() # 等價于 out.backward(torch.tensor(1.))
print(x.grad)
輸出為:
tensor([[4.5000, 4.5000],
[4.5000, 4.5000]])
這點想必學(xué)過高等數(shù)學(xué)的人都可以輕松的驗證问顷,如果不行昂秃,可以問我。另外有一點需要特別注意杜窄,grad在反向傳播過程中是累加的(accumulated)肠骆,這意味著每一次運(yùn)行反向傳播,梯度都會累加之前的梯度塞耕,所以一般在反向傳播之前需把梯度清零蚀腿。
# 再來反向傳播一次,注意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是標(biāo)量唯咬,則不需要為backward()傳入任何參數(shù);否則畏浆,需要傳入一個與y同形的Tensor? 簡單來說就是為了避免向量(甚至更高維張量)對張量求導(dǎo)胆胰,而轉(zhuǎn)換成標(biāo)量對張量求導(dǎo)。舉個例子刻获,假設(shè)形狀為 m x n 的矩陣 X 經(jīng)過運(yùn)算得到了 p x q 的矩陣 Y蜀涨,Y 又經(jīng)過運(yùn)算得到了 s x t 的矩陣 Z。那么按照前面講的規(guī)則蝎毡,dZ/dY 應(yīng)該是一個 s x t x p x q 四維張量厚柳,dY/dX 是一個 p x q x m x n的四維張量。問題來了沐兵,怎樣反向傳播别垮?怎樣將兩個四維張量相乘?扎谎?碳想?這要怎么乘烧董??胧奔?就算能解決兩個四維張量怎么乘的問題赁温,四維和三維的張量又怎么乘宣增?導(dǎo)數(shù)的導(dǎo)數(shù)又怎么求饥努,這一連串的問題尸曼,感覺要瘋掉…… 為了避免這個問題,我們不允許張量對張量求導(dǎo)岩遗,只允許標(biāo)量對張量求導(dǎo)扇商,求導(dǎo)結(jié)果是和自變量同形的張量。所以必要時我們要把張量通過將所有張量的元素加權(quán)求和的方式轉(zhuǎn)換為標(biāo)量喘先,舉個例子钳吟,假設(shè)y由自變量x計算而來,w是和y同形的張量窘拯,則y.backward(w)的含義是:先計算l = torch.sum(y * w)红且,則l是個標(biāo)量,然后求l對自變量x的導(dǎo)數(shù)涤姊。
下面給大家舉幾個栗子:
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>)
由于現(xiàn)在 z 不是一個標(biāo)量暇番,所以在調(diào)用backward時需要傳入一個和z同形的權(quán)重向量進(jìn)行加權(quán)求和得到一個標(biāo)量。
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])
另外PyTorch中梯度是累加的思喊,如果在編碼過程中需要暫停梯度的記錄壁酬,也可以進(jìn)行操作:
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是沒有g(shù)rad_fn而且y2.requires_grad=False的恨课,而y3是有g(shù)rad_fn的舆乔。如果我們將y3對x求梯度的話會是多少呢?
y3.backward()
print(x.grad)
#輸出tensor(2.)
這里數(shù)學(xué)知識比較好的人可能又要發(fā)問了剂公,為什么和我算的結(jié)果不一樣希俩,是不是錯的?當(dāng)然不是纲辽,因為y2的定義是被torch.no_grad():包含的颜武,所以與y2有關(guān)的梯度是不會回傳的,只有與y1有關(guān)的梯度才會回傳拖吼,就是并沒有計算y2的鳞上。
另外,由于y2.requires_grad=False吊档,所以不能調(diào)用 y2.backward()篙议,會報錯:
RuntimeError: element 0 of tensors does not require grad and does not have a grad_fn
此外,如果你想要修改tensor的數(shù)值怠硼,但是又不希望被autograd記錄(即不會影響反向傳播)涡上,那么你可以對tensor.data進(jìn)行操作趾断。
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)
輸出為:
tensor([1.])
False
tensor([100.], requires_grad=True)
tensor([2.])