參考https://github.com/chenyuntc/pytorch-book/tree/v1.0html
但願你們直接到上面的網址去查看代碼,下面是本人的筆記python
torch.autograd就是爲了方便用戶使用,專門開發的一套自動求導引擎,她可以根據輸入和前向傳播過程自動構建計算圖,並執行反向傳播c++
1.Variablegit
深度學習算法的本質是經過反向函數求導數,pytorch的Autograd模塊實現了此功能。在Tensor上的全部操做,Autograd都可以爲他們自動提供微分,避免手動計算的複雜過程github
其中Autograd.Variable是Autograd的核心類。Tensor被封裝成Variable後,能夠調用它的.backward實現反向傳播,自動計算全部梯度算法
Variable的構造函數須要傳入tensor,同時有兩個可選參數:api
Variable支持大多數tensor支持的函數,但其不支持部分inplace函數,由於這些函數會修改tensor自身,而在反向傳播中,variable須要緩存原來的tensor來計算梯度緩存
若是想要計算各個Variable的梯度,只須要調用根節點variable的backward方法,autograd會自動沿着計算圖反向傳播,計算每個葉子節點的梯度app
variable.backward(grad_variables=None, retain_graph=None, create_graph=None)的參數:函數
舉例:
from __future__ import print_function import torch as t from torch.autograd import Variable as V
生成a:
#從tensor中建立variable,指定須要求導 a = V(t.ones(3,4), requires_grad=True) a
返回:
tensor([[1., 1., 1., 1.], [1., 1., 1., 1.], [1., 1., 1., 1.]], requires_grad=True)
生成b:
b = V(t.zeros(3,4)) b
返回:
tensor([[0., 0., 0., 0.], [0., 0., 0., 0.], [0., 0., 0., 0.]])
生成c,實現對a,b的操做:
#函數的使用與tensor一致 #也能夠寫成c = a+b c = a.add(b) c
返回:
tensor([[1., 1., 1., 1.], [1., 1., 1., 1.], [1., 1., 1., 1.]], grad_fn=<AddBackward0>)
反向:
d = c.sum() d.backward()#反向傳播
⚠️
#注意tensor和variable二者操做的區別 #前者在取data後變成tensor,從tensor計算sum獲得float #後者計算sum後仍然是Variable c.data.sum(), c.sum()
返回:
(tensor(12.), tensor(12., grad_fn=<SumBackward0>))
查看梯度:
#反向傳播後就可以查看變量的梯度了 a.grad
返回:
tensor([[1., 1., 1., 1.], [1., 1., 1., 1.], [1., 1., 1., 1.]])
說明:
#此處雖然沒有指定c須要求導,但c依賴於a,而a須要求導 #所以c的requires_grad屬性會自動設置爲True a.requires_grad, b.requires_grad, c.requires_grad
返回:
(True, False, True)
#由用戶建立的variable是葉子節點,其對應的grad_fn是None a.is_leaf, b.is_leaf, c.is_leaf
返回:
(True, True, False)
#c.grad是None,c不是葉子節點,它的梯度是用來計算a的梯度的 #雖然c.requires_grad爲True,可是其梯度在計算完後會被釋放 c.grad is None #返回True
接下來咱們看看autograd計算的導數和咱們手動推導的區別
def f(x) : '''計算y''' y = x**2 * t.exp(x) return y def gradf(x): '''手動自動求導''' dx = 2*x*t.exp(x) + x**2*t.exp(x) return dx
生成x開始計算y:
x = V(t.randn(3,4), requires_grad=True) y = f(x) y
返回:
tensor([[0.0090, 0.3115, 1.2975, 0.4315], [0.5321, 0.3219, 0.1713, 0.3801], [0.1355, 0.0842, 0.1229, 0.4506]], grad_fn=<MulBackward0>)
反向傳播:
y.backward(t.ones(y.size())) #grad_variables形狀與y一致 x.grad
返回:
tensor([[-0.1716, 1.7068, 4.6518, -0.2922], [-0.0763, 1.7446, 1.1561, -0.3552], [ 0.9972, -0.4043, -0.4409, 2.1901]])
grad_variables爲何這樣設置,可見:pytorch的backward
#autograd的計算結果與利用公式的計算結果一致 gradf(x)
返回:
tensor([[-0.1716, 1.7068, 4.6518, -0.2922], [-0.0763, 1.7446, 1.1561, -0.3552], [ 0.9972, -0.4043, -0.4409, 2.1901]], grad_fn=<AddBackward0>)
2.計算圖
pytorch中autograd的底層採用了計算圖,其是一種有向無環圖(DAG),用於計算算子和變量之間的關係
如表達式z = wx +b可分解成y = wx, z = y+b,其計算圖以下所示:
通常用矩形表示算子,橢圓表示變量,即圖中的MUL和ADD都是算子,w,x,b爲變量
如這個有向無環圖,X和b是葉子節點,這些節點一般由用戶本身建立,不依賴於其餘變量。z稱爲根節點,是計算圖的最終目標。
利用鏈式法則很容易求得各個葉子節點的梯度:
有了計算圖,上述鏈式求導便可利用計算圖的反向傳播自動完成,其過程以下:
在pytorch實現中,autograd會隨着用戶的操做,記錄生成當前variable的全部操做,由此創建一個有向無環圖,每個變量在圖中的位置能夠經過其grad_fn屬性在圖中的位置推測獲得
用戶每進行一次 操做,相應的計算圖就會發生變化
每個前向傳播操做的函數都有與之對應的反向傳播函數用來計算輸入的各個variable的梯度,這些函數的函數名一般以Backward結尾,代碼實現:
x = V(t.ones(1)) b = V(t.rand(1),requires_grad=True) w = V(t.rand(1),requires_grad=True) y = w * x #等價於 y = w.mul(x) z = y + b #等價於 z = y.add(b)
查看requires_grad:
x.requires_grad,b.requires_grad,w.requires_grad
返回:
(False, True, True)
#雖然未指定y.requires_grad爲True,但因爲y依賴於須要求導的w #故而y.requires_grad自動設置爲True y.requires_grad #返回True
查看葉子節點:
x.is_leaf,b.is_leaf,w.is_leaf
返回:
(True, True, True)
y.is_leaf, z.is_leaf
返回:
(False, False)
查看grad_fn:
#grad_fn能夠查看這個variable的反向傳播函數 #z是add函數的輸出,因此它的反向傳播函數是AddBackward z.grad_fn
返回:
<AddBackward0 at 0x116776240>
next_functions:
#next_functions保存grad_fn的輸入,grad_fn的輸入是一個tuple #第一個是y,它是乘法mul的輸出,因此對應的反向傳播函數y.grad_fn是MulBackward #第二個是b,它是葉子節點,由用戶建立,grad_fn爲None,可是有 z.grad_fn.next_functions
返回:
((<MulBackward0 at 0x116776c50>, 0), (<AccumulateGrad at 0x116776828>, 0))
#Variable的grad_fn對應着圖中的function z.grad_fn.next_functions[0][0] == y.grad_fn #返回True
#第一個是w,葉子節點,須要求導,梯度是累加的 #第二個是x,葉子節點,不須要求導,因此爲None y.grad_fn.next_functions
返回:
((<AccumulateGrad at 0x116776ba8>, 0), (None, 0))
#葉子節點的grad_fn是None w.grad_fn, x.grad_fn
返回:
(None, None)
計算w的梯度時須要用到x的數值(由於),這些數值在前向過程當中會保存下來成buffer,在計算完梯度後會自動清空。
爲了可以屢次反向傳播,須要指定retain_graph來保留這些buffer
y.grad_fn.saved_variables
返回錯誤:
AttributeError: 'MulBackward0' object has no attribute 'saved_variables'
緣由確實是版本問題,PyTorch0.3 中把許多python的操做轉移到了C++中,saved_variables 如今是一個c++的對象,沒法經過python訪問。(https://github.com/chenyuntc/pytorch-book/issues/7)
能夠查看這裏進行學習https://github.com/chenyuntc/pytorch-book/blob/0.3/chapter3-Tensor和autograd/Autograd.ipynb,省掉上面的操做:
#使用retain_graph來保存buffer z.backward(retain_graph=True) w.grad
返回:
tensor([1.])
#屢次反向傳播,梯度累加,這就是w中AccumulateGrad標識的含義 z.backward() w.grad
返回:
tensor([2.])
PyTorch使用的是動態圖,它的計算圖在每次前向傳播時都是從頭開始構建,因此它可以使用Python控制語句(如for、if等)根據需求建立計算圖。這點在天然語言處理領域中頗有用,它意味着你不須要事先構建全部可能用到的圖的路徑,圖在運行時才構建。
def abs(x): if x.data[0] > 0:return x else: return -x x = V(t.ones(1),requires_grad=True) y = abs(x) y.backward() x.grad
返回:
tensor([1.])
x = V(-1*t.ones(1),requires_grad=True) y = abs(x) y.backward() x.grad
返回:
tensor([-1.])
若是不使用.float(),報錯:
RuntimeError: Only Tensors of floating point dtype can require gradients
若是使用data[0],報錯:
IndexError: invalid index of a 0-dim tensor. Use tensor.item() to convert a 0-dim tensor to a Python number
最終正確版:
def f(x): result = 1 for ii in x: if ii.data > 0:result = ii*result #這裏若是使用data[0]會報錯 return result x = V(t.arange(-2, 4).float(),requires_grad=True) #這裏若是不使用.float()會報錯 y = f(x) # y = x[3]*x[4]*x[5] y.backward() x.grad
返回:
tensor([0., 0., 0., 6., 3., 2.])
變量的requires_grad
屬性默認爲False,若是某一個節點requires_grad被設置爲True,那麼全部依賴它的節點requires_grad
都是True。這其實很好理解,對於,x.requires_grad = True,當須要計算
時,根據鏈式法則,
,天然也須要求
,因此y.requires_grad會被自動標爲True.
volatile=True
是另一個很重要的標識,它可以將全部依賴於它的節點所有都設爲volatile=True(默認爲True)
,其優先級比requires_grad=True
高。volatile=True
的節點不會求導,即便requires_grad=True
,也沒法進行反向傳播。對於不須要反向傳播的情景(如inference,即測試推理時),該參數可實現必定程度的速度提高,並節省約一半顯存,因其不須要分配空間計算梯度。
x = V(t.ones(1)) w = V(t.rand(1),requires_grad=True) y = x*w #y依賴於x,w,但x.volatile = True,w.requires_grad = True x.requires_grad, w.requires_grad, y.requires_grad
返回:
(False, True, True)
x.volatile, w.volatile, y.volatile
報錯:
/anaconda2/envs/deeplearning3/lib/python3.6/site-packages/ipykernel_launcher.py:1: UserWarning: volatile was removed (Variable.volatile is always False) """Entry point for launching an IPython kernel.
返回與預期不符:
(False, False, False)
該屬性已經在0.4版本中被移除了(我使用的是pytorch-1.0.1),可使用with torch.no_grad()
代替該功能,函數可使用裝飾器@torch.no_grad(),即:
>>> x = torch.tensor([1], requires_grad=True) >>> with torch.no_grad(): ... y = x * 2 >>> y.requires_grad False >>> @torch.no_grad() ... def doubler(x): ... return x * 2 >>> z = doubler(x) >>> z.requires_grad False
詳細內容可見https://pytorch.org/docs/master/autograd.html#locally-disable-grad
因此如今通常只使用requires_grad來查看,不使用volatile了
在反向傳播過程當中非葉子節點的導數計算完以後即被清空。若想查看這些變量的梯度,有兩種方法:
autograd.grad
和hook
方法都是很強大的工具,更詳細的用法參考官方api文檔,這裏舉例說明基礎的使用。推薦使用hook
方法,可是在實際使用中應儘可能避免修改grad的值。
x = V(t.ones(3),requires_grad=True) y = V(t.rand(3),requires_grad=True) y = x*w # y依賴於w,而w.requires_grad = True z = y.sum() x.requires_grad, w.requires_grad, y.requires_grad
返回:
(True, True, True)
#非葉子節點grad計算完後自動清空,因此y.grad爲None z.backward() x.grad, w.grad, y.grad
返回:
(tensor([0.8205, 0.8205, 0.8205]), tensor([3.]), None)
第一種方法:
#第一種方法:使用grad得到中間變量的梯度 x = V(t.ones(3), requires_grad=True) w = V(t.rand(3), requires_grad=True) y = x * w z = y.sum() #Z對y的梯度,隱式調用backward() t.autograd.grad(z, y)
返回:
(tensor([1., 1., 1.]),)
第二種方法:
#第二種方法:使用hook #hook是一個函數,輸入是梯度,不該該有返回值 def varaible_hook(grad): print('y的梯度:\r\n',grad) x = V(t.ones(3), requires_grad=True) w = V(t.rand(3), requires_grad=True) y = x * w #註冊hook hook_handle = y.register_hook(varaible_hook) #存儲的是那個值的梯度就對那個值進行註冊hook z = y.sum() z.backward() #除非你每次都要用hook,不然用完後記得移除hook hook_handle.remove()
返回:
y的梯度: tensor([1., 1., 1.])
最後再來看看variable中grad屬性和backward函數grad_variables
參數的含義,這裏直接下結論:
z.backward()
在必定程度上等價於y.backward(grad_y)。z.backward()
省略了grad_variables參數,是由於x = V(t.arange(0,3).float(),requires_grad=True) y = x**2 + x*2 z = y.sum() z.backward() #z是標量,即一個數,因此能夠省略參數 x.grad
返回:
tensor([2., 4., 6.])
x = V(t.arange(0,3).float(), requires_grad=True) y = x**2 + x*2 z = y.sum() y_grad_variables = V(t.Tensor([1,1,1])) # dz/dy,由於y不是標量,即一個數 y.backward(y_grad_variables) #從y開始反向傳播 x.grad
返回:
tensor([2., 4., 6.])
另外值得注意的是,只有對variable的操做才能使用autograd,若是對variable的data直接進行操做,將沒法使用反向傳播。除了對參數初始化,通常咱們不會修改variable.data的值。
在PyTorch中計算圖的特色可總結以下:
Function
。grad_fn
爲None。葉子節點中須要求導的variable,具備AccumulateGrad
標識,因其梯度是累加的。requires_grad
屬性默認爲False,若是某一個節點requires_grad被設置爲True,那麼全部依賴它的節點requires_grad
都爲True。volatile
屬性默認爲False,若是某一個variable的volatile
屬性被設爲True,那麼全部依賴它的節點volatile
屬性都爲True。volatile屬性爲True的節點不會求導,volatile的優先級比requires_grad
高。retain_graph
=True來保存這些緩存。autograd.grad
或hook
技術獲取非葉子節點的值。backward
的參數grad_variables
能夠當作鏈式求導的中間結果,若是是標量,能夠省略,默認爲13.擴展autograd
目前絕大多數函數均可以使用autograd
實現反向求導,但若是須要本身寫一個複雜的函數,不支持自動反向求導怎麼辦? 寫一個Function
,實現它的前向傳播和反向傳播代碼,Function
對應於計算圖中的矩形, 它接收參數,計算並返回結果。下面給出一個例子。
class Mul(Function): @staticmethod def forward(ctx, w, x, b, x_requires_grad=True): ctx.x_requires_grad = x_requires_grad ctx.save_for_backward(w, x)#存儲用來反向傳播的參數 output = w*x +b return output @staticmethod def backward(ctx, grad_output): w, x = ctx.save_variables grad_w = grad_output * x if ctx.x_requires_grad: grad_x = grad_output * w else: grad_x = None grad_b = grad_output * 1 return grad_w, grad_x, grad_b, None
上面這種寫法是有問題的:
NameError: name 'Function' is not defined
分析以下:
__init__
,forward和backward函數都是靜態方法grad_variables
Function的使用利用Function.apply(variable)
from torch.autograd import Function class MultiplyAdd(Function): @staticmethod def forward(ctx, w, x, b): print('type in forward', type(x)) ctx.save_for_backward(w, x)#存儲用來反向傳播的參數 output = w*x +b return output @staticmethod def backward(ctx, grad_output): w, x = ctx.saved_variables #deprecated,如今使用saved_tensors print('type in backward',type(x)) grad_w = grad_output * x grad_x = grad_output * w grad_b = grad_output * 1 return grad_w, grad_x, grad_b
調用方法一:
類名.apply(參數)
輸出變量.backward()
x = V(t.ones(1)) w = V(t.rand(1),requires_grad=True) b = V(t.rand(1),requires_grad=True) print('開始前向傳播') z = MultiplyAdd.apply(w, x, b) print('開始反向傳播') z.backward() # x不須要求導,中間過程仍是會計算它的導數,但隨後被清空 x.grad, w.grad, b.grad
警告:
/anaconda2/envs/deeplearning3/lib/python3.6/site-packages/ipykernel_launcher.py:13: DeprecationWarning: 'saved_variables' is deprecated; use 'saved_tensors' del sys.path[0]
返回:
開始前向傳播 type in forward <class 'torch.Tensor'> 開始反向傳播 type in backward <class 'torch.Tensor'> (None, tensor([1.]), tensor([1.]))
類名.apply(參數)
輸出變量.grad_fn.apply()
x = V(t.ones(1)) w = V(t.rand(1),requires_grad=True) b = V(t.rand(1),requires_grad=True) print('開始前向傳播') z = MultiplyAdd.apply(w, x, b) print('開始反向傳播') # 調用MultiplyAdd.backward # 會自動輸出grad_w, grad_x, grad_b z.grad_fn.apply(V(t.ones(1)))
返回:
開始前向傳播 type in forward <class 'torch.Tensor'> 開始反向傳播 type in backward <class 'torch.Tensor'> Out[68]: (tensor([1.]), tensor([0.8597], grad_fn=<MulBackward0>), tensor([1.]))
之因此forward函數的輸入是tensor,而backward函數的輸入是variable,是爲了實現高階求導。backward函數的輸入輸出雖然是variable,但在實際使用時autograd.Function會將輸入variable提取爲tensor,並將計算結果的tensor封裝成variable返回。在backward函數中,之因此也要對variable進行操做,是爲了可以計算梯度的梯度(backward of backward)。下面舉例說明,有關torch.autograd.grad的更詳細使用請參照文檔。
x = V(t.Tensor([5]),requires_grad=True) y = x**2 grad_x = t.autograd.grad(y,x,create_graph=True) grad_x # dy/dx = 2 * x
返回:
(tensor([10.], grad_fn=<MulBackward0>),)
grad_grad_x = t.autograd.grad(grad_x[0],x) grad_grad_x #二階導數 d(2x)/dx = 2
返回:
(tensor([2.]),)
這種設計雖然能讓autograd
具備高階求導功能,但其也限制了Tensor的使用,因autograd中反向傳播的函數只能利用當前已經有的Variable操做。這個設計是在0.2
版本新加入的,爲了更好的靈活性,也爲了兼容舊版本的代碼,PyTorch還提供了另一種擴展autograd的方法。
PyTorch提供了一個裝飾器@once_differentiable
,可以在backward函數中自動將輸入的variable提取成tensor,把計算結果的tensor自動封裝成variable。有了這個特性咱們就可以很方便的使用numpy/scipy中的函數,操做再也不侷限於variable所支持的操做。可是這種作法正如名字中所暗示的那樣只能求導一次,它打斷了反向傳播圖,再也不支持高階求導。
上面所描述的都是新式Function,還有個legacy Function,能夠帶有__init__
方法,forward
和backwad
函數也不須要聲明爲@staticmethod
,但隨着版本更迭,此類Function將愈來愈少遇到,在此不作更多介紹。
上面這些都是比較老的寫法了,如今都不使用了,如今的聲明都相似:
class StyleLoss(nn.Module): def __init__(self, target_feature): super(StyleLoss, self).__init__() self.target = gram_matrix(target_feature).detach() def forward(self, input): G = gram_matrix(input) self.loss = F.mse_loss(G, self.target) return input
backward方法都不本身寫了,都使用autograd
此外在實現了本身的Function以後,還可使用gradcheck
函數來檢測實現是否正確。gradcheck
經過數值逼近來計算梯度,可能具備必定的偏差,經過控制eps
的大小能夠控制容忍的偏差。
下面舉例說明如何利用Function實現sigmoid Function
class Sigmoid(Function): @staticmethod def forward(ctx, x): output = 1 / (1 + t.exp(-x)) ctx.save_for_backward(output) return output @staticmethod def backward(ctx, grad_output): output, = ctx.saved_tensors grad_x = output * (1 - output) *grad_output return grad_x
#採用數值逼近方式檢驗計算梯度的公式對不對 test_input =V(t.randn(3,4), requires_grad=True) t.autograd.gradcheck(Sigmoid.apply, (test_input,), eps=1e-3) #返回True
def f_sigmoid(x): y = Sigmoid.apply(x) y.backward(t.ones(x.size())) def f_naive(x): y = 1/(1 + t.exp(-x)) y.backward(t.ones(x.size())) def f_th(x): y = t.sigmoid(x) y.backward(t.ones(x.size()))
測試:
import time x = V(t.randn(100, 100), requires_grad=True) start_sigmoid = time.time() for i in range(100): f_sigmoid(x) end_sigmoid = time.time() print(end_sigmoid-start_sigmoid) start_naive = time.time() for i in range(100): f_naive(x) end_naive = time.time() print(end_naive-start_naive) start_th = time.time() for i in range(100): f_th(x) end_th = time.time() print(end_th-start_th)
返回:
0.01591801643371582 0.02293086051940918 0.010257244110107422
顯然f_sigmoid
要比單純利用autograd
加減和乘方操做實現的函數快很多,由於f_sigmoid的backward優化了反向傳播的過程。另外能夠看出系統實現的buildin接口(t.sigmoid)更快。
4.小試牛刀——用Variable實現線性迴歸
import torch as t from torch.autograd import Variable as V from matplotlib import pyplot as plt from IPython import display
#設置隨機數種子,爲了在不一樣人電腦上運行時下面的輸入一致 t.manual_seed(1000) def get_fake_data(batch_size=8): ''' 產生隨機數據:y = x*2 + 3,加上了一些噪聲''' x = t.rand(batch_size,1) * 20 y = x * 2 + (1 + t.randn(batch_size, 1))*3 return x, y
# 來看看產生x-y分佈是什麼樣的 x, y = get_fake_data() plt.scatter(x.squeeze().numpy(), y.squeeze().numpy())
圖示:
#隨機初始化參數 w = V(t.rand(1,1), requires_grad=True) b = V(t.zeros(1,1), requires_grad=True) lr = 0.001 #學習率 for ii in range(8000):#8000次迭代 x, y = get_fake_data() x, y = V(x), V(y) #forward:計算loss y_pred = x.mm(w) + b.expand_as(y) loss = 0.5 * (y_pred - y) ** 2 loss = loss.sum() # backward:手動計算梯度 loss.backward() # 更新參數 w.data.sub_(lr * w.grad.data) b.data.sub_(lr * b.grad.data) #梯度清零 w.grad.data.zero_() b.grad.data.zero_() if ii%1000 == 0: # 畫圖 display.clear_output(wait=True) x = t.arange(0, 20).float().view(-1, 1) y = x.mm(w.data) + b.data.expand_as(x) plt.plot(x.numpy(), y.numpy()) # predicted x2, y2 = get_fake_data(batch_size=20) plt.scatter(x2.numpy(), y2.numpy()) # true data plt.xlim(0,20) plt.ylim(0,41) plt.show() plt.pause(0.5) print(w.data.squeeze(), b.data.squeeze())
返回:
tensor(2.0516) tensor(2.9800)
圖示: