天天看點

PyTorch 中 backward() 詳解

轉自:Pytorch中文網

接觸了PyTorch這麼長的時間,也玩了很多PyTorch的騷操作,都特别簡單直覺地實作了,但是有一個網絡訓練過程中的操作之前一直沒有仔細去考慮過,那就是

loss.backward()

,看到這個大家一定都很熟悉,loss是網絡的損失函數,是一個标量,你可能會說這不就是反向傳播嗎,有什麼好講的。

但是不知道大家思考過沒有,如果loss不是一個标量,而是一個向量,那麼

loss.backward()

是什麼結果呢?

大家可以去試試,寫一個簡單的小程式

1

2

3

4

5

import torch as t

from torch.autograd import Variable as v

x = v(t.ones(2, 2), requires_grad=True)

y = x   1

y.backward()

運作一下程式,恭喜你報錯了,錯誤顯示如下

PyTorch 中 backward() 詳解

我們來讀一讀這個錯誤是什麼意思。backward隻能被應用在一個标量上,也就是一個一維tensor,或者傳入跟變量相關的梯度。

嗯,前面一句話很簡單,backward應用在一個标量,平時我們也是這麼使用的,但是後面一句話,with gradient w.r.t variable是什麼鬼,傳入一個變量相關的梯度。不了解啊不了解,看不懂沒關系我們還可以做實驗來解決這個問題,俗話說自己動手豐衣足食(我也想做個伸手黨去看看别人寫的,然後不幸地是并沒有什麼人寫過這方面的東西)。

首先我們開始做一個簡單的實驗,就是複習一下标量的形式

1

2

3

4

5

6

7

8

9

10

11

12

13

14

# simple gradient

a = v(t.FloatTensor([2, 3]), requires_grad=True)

b = a   3

c = b * b * 3

out = c.mean()

out.backward()

print(\'*\'*10)

print(\'=====simple gradient======\')

print(\'input\')

print(a.data)

print(\'compute result is\')

print(out.data[0])

print(\'input gradients are\')

print(a.grad.data)

很簡單,我們把數學表達式寫出來,傳入的參數x1=2,x2=3

x1=2,x2=3

,特别注意

Variable

裡面預設的參數

requires_grad=False

,是以這裡我們要重新傳入

requires_grad=True

讓它成為一個葉子節點。

那麼我們對其求偏導也很簡單(分别為15,18) 這樣依靠簡單的微積分知識我們就能夠算出他們的結果,運作一下程式,確定結果一緻,ok。

PyTorch 中 backward() 詳解

下面我們研究一下如何能夠對非标量的情況下使用backward,下面開始做實驗(瞎試)。

1

2

3

4

m = v(t.FloatTensor([[2, 3]]), requires_grad=True)

n = v(t.zeros(1, 2))

n[0, 0] = m[0, 0] ** 2

n[0, 1] = m[0, 1] ** 3

第一想法就是裡面這個參數是要求梯度的對象,我們這樣調用

n.backward(m.data)

,有有報錯诶,是不是成功了,我真的是個天才,這麼難的東西都能想到,等等,我好想看到了一個很神奇的結果。

PyTorch 中 backward() 詳解

這是什麼鬼,這跟說好的結果不一樣啊,我們想要的結果是4和27,現在給我們的結果是8和81,為什麼會出現這樣神奇的結果呢,想不通啊。我們看看我們傳入的參數是

m.data

,這是一個(2, 3)的向量,我們希望得到的梯度是(4, 27),好像(4×2=8, 27×3=81),我的内心毫無波動,甚至有點想笑,似乎backward将我傳入的參數

m.data

乘上了得到的梯度,既然要乘上我傳入的參數,那麼我就給你傳入1,這樣總能得到我想要的結果了吧,

n.backward(t.FloatTensor([[1, 1]]))

,看看結果呢

PyTorch 中 backward() 詳解

哇,跟我們想要的結果一樣诶,撒花,我們解決了一個大問題,就是這麼簡單,扔進去一個1就可以了,這個問題也沒有那麼難嘛,哈哈哈。

似乎又有一點不對,如果這麼簡單那麼寫PyTorch的人為什麼不把這一步直接內建進去,那我們不就不會遇到這個問題了嘛。

我們來試試另外一種情況

1

2

3

4

5

6

m = v(t.FloatTensor([[2, 3]]), requires_grad=True)

j = t.zeros(2 ,2)

k = v(t.zeros(1, 2))

m.grad.data.zero_()

k[0, 0] = m[0, 0] ** 2 + 3 * m[0 ,1]

k[0, 1] = m[0, 1] ** 2 + 2 * m[0, 0]

麼我們直接對k反向傳播

k.backward(t.FloatTensor([[1, 1]])

,結果是什麼呢?

首先我們手動算一算結果是什麼:4,3,2,6,我們是希望能夠得到上面四個結果,這個時候你可能已經開始懷疑了,能夠得到這4個結果嗎?我們可以輸出結果來看看

PyTorch 中 backward() 詳解

非常遺憾,我們隻得到了兩個結果,并且數值并不對,這個時候你就會疑惑了,到底是哪裡出了問題呢,為什麼會得到這樣的結果呢?

經過不斷地嘗試,我終于發現了其中的奧秘,

k.backward(parameters)

接受的參數

parameters

必須要和

k

的大小一模一樣,然後作為

k

的系數傳回去,什麼意思呢,我們通過上面的例子來解釋這個問題你就知道了。

我們已經知道我們得到的k=(k1,k2)

k=(k1,k2)

,以及傳入的參數是1和1,那麼是如何得到這6和9這兩個結果的呢?

我們知道了這個操作具體是怎麼完成的,我們就可以求求我們需要的這個jacobian矩陣了,非常簡單。

1

2

3

4

5

6

7

8

9

10

11

12

13

# jacobian

j = t.zeros(2 ,2)

k = v(t.zeros(1, 2))

m.grad.data.zero_()

k[0, 0] = m[0, 0] ** 2 + 3 * m[0 ,1]

k[0, 1] = m[0, 1] ** 2 + 2 * m[0, 0]

k.backward(t.FloatTensor([[1, 0]]), retain_variables=True)

j[:, 0] = m.grad.data

m.grad.data.zero_()

k.backward(t.FloatTensor([[0, 1]]))

j[:, 1] = m.grad.data

print('jacobian matrix is')

print(j)

我們可以得到如下結果

PyTorch 中 backward() 詳解

這裡我們要注意

backward()

裡面另外的一個參數

retain_variables=True

,這個參數預設是False,也就是反向傳播之後這個計算圖的記憶體會被釋放,這樣就沒辦法進行第二次反向傳播了,是以我們需要設定為True,因為這裡我們需要進行兩次反向傳播求得jacobian矩陣。

最後我們再舉一個矩陣乘法的例子試驗一下我們的結果

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

x = t.FloatTensor([2, 1]).view(1, 2)

x = v(x, requires_grad=True)

y = v(t.FloatTensor([[1, 2], [3, 4]]))

z = t.mm(x, y)

jacobian = t.zeros((2, 2))

z.backward(t.FloatTensor([[1, 0]]), retain_variables=True)  # dz1/dx1, dz2/dx1

jacobian[:, 0] = x.grad.data

x.grad.data.zero_()

z.backward(t.FloatTensor([[0, 1]]))  # dz1/dx2, dz2/dx2

jacobian[:, 1] = x.grad.data

print('=========jacobian========')

print('x')

print(x.data)

print('y')

print(y.data)

print('compute result')

print(z.data)

print('jacobian matrix is')

print(jacobian)

上面是代碼,仔細閱讀,作為一個小練習回顧一下本篇文章講的内容,媽媽再也不用擔心我不會用

backward

了。

繼續閱讀