2020年3月30日 星期一

PyTorch 特殊函數之求導

torch.max() / torch.min()

對 torch.max() 進行 backward() 時,對輸入的 tensor 中數值最大的元素的 gradient 會貢獻 1,其餘則為 0。

對 torch.min() 進行 backward() 時,對數值最小的元素的 gradient 會貢獻 1,其餘則為 0。

程式
a = torch.tensor([1., 2., 3.], requires_grad=True)
b = torch.max(a)
b.backward()
a.grad

輸出
tensor([0., 0., 1.])

torch.abs()

對 torch.abs() 進行求導時,對輸入的 tensor 中正的元素的 gradient 貢獻為 1,對負的元素的 gradient 貢獻為 -1,對元素值為 0 的 gradient 貢獻為 0。

程式
a = torch.tensor([-1., -2., 3., 0.], requires_grad=True)
b = torch.abs(a)
b.backward(torch.ones_like(b))
a.grad

輸出
tensor([-1., -1., 1., 0.])

torch.where()

d = torch.where(a, b, c)
a 通常是一個 bool type 的張量,在 a 張量裡面是 True 的位置,b 該位置的元素值會傳到 d 的相應位置,在 a 張量裡面是 False 的位置,c 該位置的元素值會傳到 d 的相應位置。當我們對 where 進行 backward 時,在 b 和 c 當中有傳到 d 的那些位置的 grad 是 1,其餘位置的 grad 是 0。

程式
a = torch.tensor([True, False, False, True, True])
b = torch.tensor([1., 2., 3., 4., 5.], requires_grad=True)
c = torch.tensor([2., 4., 6., 8., 10.], requires_grad=True)
d = torch.where(a, b, c)
d
d.backward(torch.ones_like(d))
b.grad
c.grad

輸出
tensor([1., 4., 6., 4., 5.], grad_fn=<SWhereBackward>)   # d
tensor([1., 0., 0., 1., 1.])   # b.grad
tensor([0., 1., 1., 0., 0.])   # c.grad

求導結果為 0 的函數

sign, ceil

不可求導的函數

argmax, argmin, lt, le, eq, ne, ge, gt

當一條運算路徑中有經過不可求導函數時,在進行 backward() 時,這條路徑就無法進行求導運算了。

PyTorch add function


在 PyTorch 中的 add function,可以實現將兩個張量相加,可以寫成 c = torch.add(a, b),也可以直接寫成 c = a + b。

其中 a 和 b 分別可以是一個張量加上一個純量,結果 c 就會是把純量加到張量中的每一個元素上。

a 和 b 也可以是兩個張量,但是若要實現兩個張量的相加,a 和 b 的 size 就有特定的限制 (broadcastable)。

以下幾種 a 和 b 的 size 是允許相加的:

Size of a:  (5)
Size of b:  (3, 2, 1, 5)

Size of a:  (1, 5)
Size of b:  (3, 2, 1, 5)

Size of a:  (2, 1, 5)
Size of b:  (3, 2, 1, 5)

Size of a:  (3, 2, 1, 5)
Size of b:  (3, 2, 1, 5)

由以上的例子可以看出,其中一個張量 (a) 的維度必須小於或等於另一個張量 (b),而且 a 的 size 必須與另一個張量 b 的最後幾個維度的 size 相同。

以下幾種 a 和 b 也是允許相加的:

Size of a:  (3, 2, 2, 5)
Size of b:  (1, 5)

Size of a:  (3, 2, 2, 5)
Size of b:  (1, 1, 5)

Size of a:  (3, 2, 2, 5)
Size of b:  (2, 1, 5)

Size of a:  (3, 2, 2, 5)
Size of b:  (3, 1, 1, 5)

Size of a:  (1, 5)
Size of b:  (2, 1)

Size of a:  (1, 1, 1, 5)
Size of b:  (1, 2, 1, 1)

由以上的例子可以看出,在其中一個張量的維度中,可以允許一個或多個維度的大小為 1。在大小為 1 的那個維度中,b (a) 的所有元素會被廣播到 a (b) 裡面同一個維度中所有的 index 上並執行加法運算。


2020年3月26日 星期四

PyTorch 中 weight 和 weight.data 的差異


當我們做完 backpropagation 得到 weight 和 bias 的 grad 之後,要進行 weight 和 bias 的更新時,有兩種不同的 coding style。

(1) 使用 torch.no_grad()
with torch.no_grad():
    weight -= learning_rate * weight.grad
    bias -= learning_rate * bias.grad

(2) 使用 weight.data 和 weight.grad.data
weight.data -= learning_rate * weight.grad.data
bias.data -= learning_rate * bias.grad.data

這兩種方法有何區別呢?根據 PyTorch 官方網站給的解釋:
Recall that tensor.data gives a tensor that shares the storage with tensor, but doesn't track history.
 tensor 和 tensor.data 共享同一個儲存空間,但是當我們使用 tensor.data 時,PyTorch 不會追蹤運算的歷史,也就是當我們進行 backward() 時,在 tensor.data 中不會進行 gradient 的計算。


Reference
https://pytorch.org/tutorials/beginner/examples_autograd/two_layer_net_autograd.html

PyTorch 的 nn 和 nn.functional 的差別


在 nn 裡面實現了很多搭建神經網路時常用的 building blocks 如 Conv2D、ReLu 等等。但是相同的名稱在 nn.functional 裡面也能找到,那它們之間有何差別?

差別在於 nn 裡面的 building blocks 是用 Python 的 Class 來實現的,他繼承了nn.Module 這個 Class,像 weight 和 bias 等 variable 會存在 nn.Conv2D 裡面。但是對於 nn.functional.Conv2D 來說,因為它只是一個 function 而不是 class,function 本身是沒有狀態的 (state),所有一切 variable 在 function 結束以後就消失了,因此它的 weight 和 bias 等需要保留的 variable 就必須從外面傳入。

這也是為什麼,當我們創建一個 network 時,nn.Conv2D 要寫在 self.__init__ 裡面,但是 nn.functional.relu 卻不用。


Reference
https://discuss.pytorch.org/t/what-is-the-difference-between-torch-nn-and-torch-nn-functional/33597

2020年3月25日 星期三

PyTorch 的 backward function


在呼叫 t.backward() 時,若 t 是一個純量,就不需要傳入任何的引數。但是當 t 是一個張量的時候,就需要傳入一個和 t 同樣 size 的張量,這個張量是做什麼用的?

假設我們的 t = [t1, t2],這個張量的運算圖裡面有一個 leaf node 是 x = [x1, x2]。如果在呼叫 backward 時,我們傳入 [1, 1],PyTorch 會將 t 當中的每一個元素 (t1 和 t2) 都對 x1 進行偏微分,並且將所有的結果加起來得到 x.grad = [g1, g2] 中的 g1。

此時如果我們只想要知道 t1 對 x1 的偏微分結果,就要在呼叫 backward 時,傳入引數 [1, 0]。

這個傳入的引數,可以看成一個權重,可以控制 t 裡面每一個元素對偏微分的影響程度。在針對一個運算圖進行 backward 時,每一個 operation 的 backward function 會接收它的下游所傳來的 gradient,因為在進行 back propagation 時,上游的 gradient 是透過微分的鏈鎖律 (chain rule) 算出來的,也就是 backward function 的 input 會和該 operation 的 gradient 相乘,去得到上游的 gradient。


Reference
Sherlock, "PyTorch中的backward," https://zhuanlan.zhihu.com/p/27808095.