1. 程式人生 > 其它 >[Pytorch框架] 2.1.2 使用PyTorch計算梯度數值

[Pytorch框架] 2.1.2 使用PyTorch計算梯度數值

技術標籤:PyTorch

文章目錄

import torch
torch.__version__
'1.0.1.post2'

使用PyTorch計算梯度數值

PyTorch的Autograd模組實現了深度學習的演算法中的向傳播求導數,在張量(Tensor類)上的所有操作,Autograd都能為他們自動提供微分,簡化了手動計算導數的複雜過程。

在0.4以前的版本中,Pytorch使用Variable類來自動計算所有的梯度Variable類主要包含三個屬性:

data:儲存Variable所包含的Tensor;grad:儲存data對應的梯度,grad也是個Variable,而不是Tensor,它和data的形狀一樣;grad_fn:指向一個Function物件,這個Function用來反向傳播計算輸入的梯度。

從0.4起, Variable 正式合併入Tensor類,通過Variable巢狀實現的自動微分功能已經整合進入了Tensor類中。雖然為了程式碼的相容性還是可以使用Variable(tensor)這種方式進行巢狀,但是這個操作其實什麼都沒做。

所以,以後的程式碼建議直接使用Tensor類進行操作,因為官方文件中已經將Variable設定成過期模組。

要想通過Tensor類本身就支援了使用autograd功能,只需要設定.requries_grad=True

Variable類中的的grad和grad_fn屬性已經整合進入了Tensor類中

Autograd

在張量建立時,通過設定 requires_grad 標識為Ture來告訴Pytorch需要對該張量進行自動求導,PyTorch會記錄該張量的每一步操作歷史並自動計算

x = torch.rand(5, 5, requires_grad=True)
x
tensor([[0.0403, 0.5633, 0.2561, 0.4064, 0.9596],
        [0.6928, 0.1832, 0.5380, 0.6386, 0.8710],
        [0.5332, 0.8216, 0.8139, 0.1925, 0.4993],
        [0.2650, 0.6230, 0.5945, 0.3230, 0.0752],
        [0.0919, 0.4770, 0.4622, 0.6185, 0.2761]], requires_grad=True)
y = torch.rand(5, 5, requires_grad=True)
y
tensor([[0.2269, 0.7673, 0.8179, 0.5558, 0.0493],
        [0.7762, 0.9242, 0.2872, 0.0035, 0.4197],
        [0.4322, 0.5281, 0.9001, 0.7276, 0.3218],
        [0.5123, 0.6567, 0.9465, 0.0475, 0.9172],
        [0.9899, 0.9284, 0.5303, 0.1718, 0.3937]], requires_grad=True)

PyTorch會自動追蹤和記錄對與張量的所有操作,當計算完成後呼叫.backward()方法自動計算梯度並且將計算結果儲存到grad屬性中。

z=torch.sum(x+y)
z
tensor(25.6487, grad_fn=<SumBackward0>)

在張量進行操作後,grad_fn已經被賦予了一個新的函式,這個函式引用了一個建立了這個Tensor類的Function物件。
Tensor和Function互相連線生成了一個非迴圈圖,它記錄並且編碼了完整的計算曆史。每個張量都有一個.grad_fn屬性,如果這個張量是使用者手動建立的那麼這個張量的grad_fn是None。

下面我們來呼叫反向傳播函式,計算其梯度

簡單的自動求導

z.backward()
print(x.grad,y.grad)

tensor([[1., 1., 1., 1., 1.],
        [1., 1., 1., 1., 1.],
        [1., 1., 1., 1., 1.],
        [1., 1., 1., 1., 1.],
        [1., 1., 1., 1., 1.]]) tensor([[1., 1., 1., 1., 1.],
        [1., 1., 1., 1., 1.],
        [1., 1., 1., 1., 1.],
        [1., 1., 1., 1., 1.],
        [1., 1., 1., 1., 1.]])

如果Tensor類表示的是一個標量(即它包含一個元素的張量),則不需要為backward()指定任何引數,但是如果它有更多的元素,則需要指定一個gradient引數,它是形狀匹配的張量。
以上的 z.backward()相當於是z.backward(torch.tensor(1.))的簡寫。
這種引數常出現在影象分類中的單標籤分類,輸出一個標量代表影象的標籤。

複雜的自動求導

x = torch.rand(5, 5, requires_grad=True)
y = torch.rand(5, 5, requires_grad=True)
z= x**2+y**3
z
tensor([[3.3891e-01, 4.9468e-01, 8.0797e-02, 2.5656e-01, 2.9529e-01],
        [7.1946e-01, 1.6977e-02, 1.7965e-01, 3.2656e-01, 1.7665e-01],
        [3.1353e-01, 2.2096e-01, 1.2251e+00, 5.5087e-01, 5.9572e-02],
        [1.3015e+00, 3.8029e-01, 1.1103e+00, 4.0392e-01, 2.2055e-01],
        [8.8726e-02, 6.9701e-01, 8.0164e-01, 9.7221e-01, 4.2239e-04]],
       grad_fn=<AddBackward0>)
#我們的返回值不是一個標量,所以需要輸入一個大小相同的張量作為引數,這裡我們用ones_like函式根據x生成一個張量
z.backward(torch.ones_like(x))
print(x.grad)
tensor([[0.2087, 1.3554, 0.5560, 1.0009, 0.9931],
        [1.2655, 0.1223, 0.8008, 1.1127, 0.7261],
        [1.1052, 0.2579, 1.8006, 0.1544, 0.3646],
        [1.8855, 1.2296, 1.9061, 0.9313, 0.0648],
        [0.5952, 1.6190, 0.8430, 1.9213, 0.0322]])

我們可以使用with torch.no_grad()上下文管理器臨時禁止對已設定requires_grad=True的張量進行自動求導。這個方法在測試集計算準確率的時候會經常用到,例如:

with torch.no_grad():
    print((x +y*2).requires_grad)
False

使用.no_grad()進行巢狀後,程式碼不會跟蹤歷史記錄,也就是說儲存的這部分記錄會減少記憶體的使用量並且會加快少許的運算速度。

Autograd 過程解析

為了說明Pytorch的自動求導原理,我們來嘗試分析一下PyTorch的原始碼,雖然Pytorch的 Tensor和 TensorBase都是使用CPP來實現的,但是可以使用一些Python的一些方法檢視這些物件在Python的屬性和狀態。
Python的 dir() 返回引數的屬性、方法列表。z是一個Tensor變數,看看裡面有哪些成員變數。

dir(z)
['__abs__',
 '__add__',
 '__and__',
 '__array__',
 '__array_priority__',
 '__array_wrap__',
 '__bool__',
 '__class__',
 '__deepcopy__',
 '__delattr__',
 '__delitem__',
 '__dict__',
 '__dir__',
 '__div__',
 '__doc__',
 '__eq__',
 '__float__',
 '__floordiv__',
 '__format__',
 '__ge__',
 '__getattribute__',
 '__getitem__',
 '__gt__',
 '__hash__',
 '__iadd__',
 '__iand__',
 '__idiv__',
 '__ilshift__',
 '__imul__',
 '__index__',
 '__init__',
 '__init_subclass__',
 '__int__',
 '__invert__',
 '__ior__',
 '__ipow__',
 '__irshift__',
 '__isub__',
 '__iter__',
 '__itruediv__',
 '__ixor__',
 '__le__',
 '__len__',
 '__long__',
 '__lshift__',
 '__lt__',
 '__matmul__',
 '__mod__',
 '__module__',
 '__mul__',
 '__ne__',
 '__neg__',
 '__new__',
 '__nonzero__',
 '__or__',
 '__pow__',
 '__radd__',
 '__rdiv__',
 '__reduce__',
 '__reduce_ex__',
 '__repr__',
 '__reversed__',
 '__rfloordiv__',
 '__rmul__',
 '__rpow__',
 '__rshift__',
 '__rsub__',
 '__rtruediv__',
 '__setattr__',
 '__setitem__',
 '__setstate__',
 '__sizeof__',
 '__str__',
 '__sub__',
 '__subclasshook__',
 '__truediv__',
 '__weakref__',
 '__xor__',
 '_backward_hooks',
 '_base',
 '_cdata',
 '_coalesced_',
 '_dimI',
 '_dimV',
 '_grad',
 '_grad_fn',
 '_indices',
 '_make_subclass',
 '_nnz',
 '_values',
 '_version',
 'abs',
 'abs_',
 'acos',
 'acos_',
 'add',
 'add_',
 'addbmm',
 'addbmm_',
 'addcdiv',
 'addcdiv_',
 'addcmul',
 'addcmul_',
 'addmm',
 'addmm_',
 'addmv',
 'addmv_',
 'addr',
 'addr_',
 'all',
 'allclose',
 'any',
 'apply_',
 'argmax',
 'argmin',
 'argsort',
 'as_strided',
 'as_strided_',
 'asin',
 'asin_',
 'atan',
 'atan2',
 'atan2_',
 'atan_',
 'backward',
 'baddbmm',
 'baddbmm_',
 'bernoulli',
 'bernoulli_',
 'bincount',
 'bmm',
 'btrifact',
 'btrifact_with_info',
 'btrisolve',
 'byte',
 'cauchy_',
 'ceil',
 'ceil_',
 'char',
 'cholesky',
 'chunk',
 'clamp',
 'clamp_',
 'clamp_max',
 'clamp_max_',
 'clamp_min',
 'clamp_min_',
 'clone',
 'coalesce',
 'contiguous',
 'copy_',
 'cos',
 'cos_',
 'cosh',
 'cosh_',
 'cpu',
 'cross',
 'cuda',
 'cumprod',
 'cumsum',
 'data',
 'data_ptr',
 'dense_dim',
 'det',
 'detach',
 'detach_',
 'device',
 'diag',
 'diag_embed',
 'diagflat',
 'diagonal',
 'digamma',
 'digamma_',
 'dim',
 'dist',
 'div',
 'div_',
 'dot',
 'double',
 'dtype',
 'eig',
 'element_size',
 'eq',
 'eq_',
 'equal',
 'erf',
 'erf_',
 'erfc',
 'erfc_',
 'erfinv',
 'erfinv_',
 'exp',
 'exp_',
 'expand',
 'expand_as',
 'expm1',
 'expm1_',
 'exponential_',
 'fft',
 'fill_',
 'flatten',
 'flip',
 'float',
 'floor',
 'floor_',
 'fmod',
 'fmod_',
 'frac',
 'frac_',
 'gather',
 'ge',
 'ge_',
 'gels',
 'geometric_',
 'geqrf',
 'ger',
 'gesv',
 'get_device',
 'grad',
 'grad_fn',
 'gt',
 'gt_',
 'half',
 'hardshrink',
 'histc',
 'ifft',
 'index_add',
 'index_add_',
 'index_copy',
 'index_copy_',
 'index_fill',
 'index_fill_',
 'index_put',
 'index_put_',
 'index_select',
 'indices',
 'int',
 'inverse',
 'irfft',
 'is_coalesced',
 'is_complex',
 'is_contiguous',
 'is_cuda',
 'is_distributed',
 'is_floating_point',
 'is_leaf',
 'is_nonzero',
 'is_pinned',
 'is_same_size',
 'is_set_to',
 'is_shared',
 'is_signed',
 'is_sparse',
 'isclose',
 'item',
 'kthvalue',
 'layout',
 'le',
 'le_',
 'lerp',
 'lerp_',
 'lgamma',
 'lgamma_',
 'log',
 'log10',
 'log10_',
 'log1p',
 'log1p_',
 'log2',
 'log2_',
 'log_',
 'log_normal_',
 'log_softmax',
 'logdet',
 'logsumexp',
 'long',
 'lt',
 'lt_',
 'map2_',
 'map_',
 'masked_fill',
 'masked_fill_',
 'masked_scatter',
 'masked_scatter_',
 'masked_select',
 'matmul',
 'matrix_power',
 'max',
 'mean',
 'median',
 'min',
 'mm',
 'mode',
 'mul',
 'mul_',
 'multinomial',
 'mv',
 'mvlgamma',
 'mvlgamma_',
 'name',
 'narrow',
 'narrow_copy',
 'ndimension',
 'ne',
 'ne_',
 'neg',
 'neg_',
 'nelement',
 'new',
 'new_empty',
 'new_full',
 'new_ones',
 'new_tensor',
 'new_zeros',
 'nonzero',
 'norm',
 'normal_',
 'numel',
 'numpy',
 'orgqr',
 'ormqr',
 'output_nr',
 'permute',
 'pin_memory',
 'pinverse',
 'polygamma',
 'polygamma_',
 'potrf',
 'potri',
 'potrs',
 'pow',
 'pow_',
 'prelu',
 'prod',
 'pstrf',
 'put_',
 'qr',
 'random_',
 'reciprocal',
 'reciprocal_',
 'record_stream',
 'register_hook',
 'reinforce',
 'relu',
 'relu_',
 'remainder',
 'remainder_',
 'renorm',
 'renorm_',
 'repeat',
 'requires_grad',
 'requires_grad_',
 'reshape',
 'reshape_as',
 'resize',
 'resize_',
 'resize_as',
 'resize_as_',
 'retain_grad',
 'rfft',
 'roll',
 'rot90',
 'round',
 'round_',
 'rsqrt',
 'rsqrt_',
 'scatter',
 'scatter_',
 'scatter_add',
 'scatter_add_',
 'select',
 'set_',
 'shape',
 'share_memory_',
 'short',
 'sigmoid',
 'sigmoid_',
 'sign',
 'sign_',
 'sin',
 'sin_',
 'sinh',
 'sinh_',
 'size',
 'slogdet',
 'smm',
 'softmax',
 'sort',
 'sparse_dim',
 'sparse_mask',
 'sparse_resize_',
 'sparse_resize_and_clear_',
 'split',
 'split_with_sizes',
 'sqrt',
 'sqrt_',
 'squeeze',
 'squeeze_',
 'sspaddmm',
 'std',
 'stft',
 'storage',
 'storage_offset',
 'storage_type',
 'stride',
 'sub',
 'sub_',
 'sum',
 'svd',
 'symeig',
 't',
 't_',
 'take',
 'tan',
 'tan_',
 'tanh',
 'tanh_',
 'to',
 'to_dense',
 'to_sparse',
 'tolist',
 'topk',
 'trace',
 'transpose',
 'transpose_',
 'tril',
 'tril_',
 'triu',
 'triu_',
 'trtrs',
 'trunc',
 'trunc_',
 'type',
 'type_as',
 'unbind',
 'unfold',
 'uniform_',
 'unique',
 'unsqueeze',
 'unsqueeze_',
 'values',
 'var',
 'view',
 'view_as',
 'where',
 'zero_']

返回很多,我們直接排除掉一些Python中特殊方法(以__開頭和結束的)和私有方法(以_開頭的,直接看幾個比較主要的屬性:
.is_leaf:記錄是否是葉子節點。通過這個屬性來確定這個變數的型別
在官方文件中所說的“graph leaves”,“leaf variables”,都是指像xy這樣的手動建立的、而非運算得到的變數,這些變數成為建立變數。
z這樣的,是通過計算後得到的結果稱為結果變數。

一個變數是建立變數還是結果變數是通過.is_leaf來獲取的。

print("x.is_leaf="+str(x.is_leaf))
print("z.is_leaf="+str(z.is_leaf))
x.is_leaf=True
z.is_leaf=False

x是手動建立的沒有通過計算,所以他被認為是一個葉子節點也就是一個建立變數,而z是通過xy的一系列計算得到的,所以不是葉子結點也就是結果變數。

為什麼我們執行z.backward()方法會更新x.grady.grad呢?
.grad_fn屬性記錄的就是這部分的操作,雖然.backward()方法也是CPP實現的,但是可以通過Python來進行簡單的探索。

grad_fn:記錄並且編碼了完整的計算曆史

z.grad_fn
<AddBackward0 at 0x120840a90>

grad_fn是一個AddBackward0型別的變數 AddBackward0這個類也是用Cpp來寫的,但是我們從名字裡就能夠大概知道,他是加法(ADD)的反反向傳播(Backward),看看裡面有些什麼東西

dir(z.grad_fn)
['__call__',
 '__class__',
 '__delattr__',
 '__dir__',
 '__doc__',
 '__eq__',
 '__format__',
 '__ge__',
 '__getattribute__',
 '__gt__',
 '__hash__',
 '__init__',
 '__init_subclass__',
 '__le__',
 '__lt__',
 '__ne__',
 '__new__',
 '__reduce__',
 '__reduce_ex__',
 '__repr__',
 '__setattr__',
 '__sizeof__',
 '__str__',
 '__subclasshook__',
 '_register_hook_dict',
 'metadata',
 'name',
 'next_functions',
 'register_hook',
 'requires_grad']

next_functions就是grad_fn的精華

z.grad_fn.next_functions
((<PowBackward0 at 0x1208409b0>, 0), (<PowBackward0 at 0x1208408d0>, 0))

next_functions是一個tuple of tuple of PowBackward0 and int。

為什麼是2個tuple ?
因為我們的操作是z= x**2+y**3 剛才的AddBackward0是相加,而前面的操作是乘方 PowBackward0。tuple第一個元素就是x相關的操作記錄

xg = z.grad_fn.next_functions[0][0]
dir(xg)
['__call__',
 '__class__',
 '__delattr__',
 '__dir__',
 '__doc__',
 '__eq__',
 '__format__',
 '__ge__',
 '__getattribute__',
 '__gt__',
 '__hash__',
 '__init__',
 '__init_subclass__',
 '__le__',
 '__lt__',
 '__ne__',
 '__new__',
 '__reduce__',
 '__reduce_ex__',
 '__repr__',
 '__setattr__',
 '__sizeof__',
 '__str__',
 '__subclasshook__',
 '_register_hook_dict',
 'metadata',
 'name',
 'next_functions',
 'register_hook',
 'requires_grad']

繼續深挖

x_leaf=xg.next_functions[0][0]
type(x_leaf)
AccumulateGrad

在PyTorch的反向圖計算中,AccumulateGrad型別代表的就是葉子節點型別,也就是計算圖終止節點。AccumulateGrad類中有一個.variable屬性指向葉子節點。

x_leaf.variable
tensor([[0.1044, 0.6777, 0.2780, 0.5005, 0.4966],
        [0.6328, 0.0611, 0.4004, 0.5564, 0.3631],
        [0.5526, 0.1290, 0.9003, 0.0772, 0.1823],
        [0.9428, 0.6148, 0.9530, 0.4657, 0.0324],
        [0.2976, 0.8095, 0.4215, 0.9606, 0.0161]], requires_grad=True)

這個.variable的屬性就是我們的生成的變數x

print("x_leaf.variable的id:"+str(id(x_leaf.variable)))
print("x的id:"+str(id(x)))
x_leaf.variable的id:4840553424
x的id:4840553424
assert(id(x_leaf.variable)==id(x))

這樣整個規程就很清晰了:

  1. 當我們執行z.backward()的時候。這個操作將呼叫z裡面的grad_fn這個屬性,執行求導的操作。
  2. 這個操作將遍歷grad_fn的next_functions,然後分別取出裡面的Function(AccumulateGrad),執行求導操作。這部分是一個遞迴的過程直到最後型別為葉子節點。
  3. 計算出結果以後,將結果儲存到他們對應的variable 這個變數所引用的物件(x和y)的 grad這個屬性裡面。
  4. 求導結束。所有的葉節點的grad變數都得到了相應的更新

最終當我們執行完c.backward()之後,a和b裡面的grad值就得到了更新。

擴充套件Autograd

如果需要自定義autograd擴充套件新的功能,就需要擴充套件Function類。因為Function使用autograd來計算結果和梯度,並對操作歷史進行編碼。
在Function類中最主要的方法就是forward()backward()他們分別代表了前向傳播和反向傳播。

一個自定義的Function需要一下三個方法:

__init__ (optional):如果這個操作需要額外的引數則需要定義這個Function的建構函式,不需要的話可以忽略。

forward():執行前向傳播的計算程式碼

backward():反向傳播時梯度計算的程式碼。 引數的個數和forward返回值的個數一樣,每個引數代表傳回到此操作的梯度。
# 引入Function便於擴充套件
from torch.autograd.function import Function
# 定義一個乘以常數的操作(輸入引數是張量)
# 方法必須是靜態方法,所以要加上@staticmethod 
class MulConstant(Function):
    @staticmethod 
    def forward(ctx, tensor, constant):
        # ctx 用來儲存資訊這裡類似self,並且ctx的屬性可以在backward中呼叫
        ctx.constant=constant
        return tensor *constant
    @staticmethod
    def backward(ctx, grad_output):
        # 返回的引數要與輸入的引數一樣.
        # 第一個輸入為3x3的張量,第二個為一個常數
        # 常數的梯度必須是 None.
        return grad_output, None 

定義完我們的新操作後,我們來進行測試

a=torch.rand(3,3,requires_grad=True)
b=MulConstant.apply(a,5)
print("a:"+str(a))
print("b:"+str(b)) # b為a的元素乘以5
a:tensor([[0.0118, 0.1434, 0.8669],
        [0.1817, 0.8904, 0.5852],
        [0.7364, 0.5234, 0.9677]], requires_grad=True)
b:tensor([[0.0588, 0.7169, 4.3347],
        [0.9084, 4.4520, 2.9259],
        [3.6820, 2.6171, 4.8386]], grad_fn=<MulConstantBackward>)

反向傳播,返回值不是標量,所以backward方法需要引數

b.backward(torch.ones_like(a))
a.grad
tensor([[1., 1., 1.],
        [1., 1., 1.],
        [1., 1., 1.]])

梯度因為1