PyTorch由4个主要包装组成:
1.Torch:类似于Numpy的通用数组库,可以在将张量类型转换为(torch.cuda.TensorFloat)并在GPU上进行计算。
2.torch.autograd:用于构建计算图形并自动获取渐变的包
3.torch.nn:具有共同层和成本函数的神经网络库
4.torch.optim:具有通用优化算法(如SGD,Adam等)的优化包
Pytorch的autograd包会根据输入和前向传播(推理)过程自动构建计算图,并执行反向传播求参数的梯度。
关于autpgrad流程机制原理,我觉得这个博主讲的挺详细的,推荐看这个:
https://blog.csdn.net/MR_kdcon/article/details/108937374中的链接
这里顺带提一下,现在很多资料都有Variable这个变量,这个是老版本了。新版本中,torch.autograd.Variable 和 torch.Tensor 将同属一类。更确切地说,torch.Tensor 能够追踪日志并像旧版本的 Variable 那样运行; Variable 封装仍旧可以像以前一样工作,但返回的对象类型是 torch.Tensor。这意味着你的代码不再需要变量封装器。
所以我们想要导入autograd包的话只需要:import torch
我们之前已经知道了Tensor的某些属性,比如dtype、device,那么现在我们又可以继续学到4个属性
1、tensor.requires_grad
2、tensor.grad
3、tensor.grad_fn
4、tensor.data
说明如下:
requires_grad有True和False(默认值),True代表这个tensor将在计算图中被追踪其所有操作行为(加、乘、求平均、logistic、softmax.....),这么做的目的就是便于之后通过反向梯度传播计算梯度。tensor必须为浮点型
两者设置跟踪的方法
x = torch.ones(2, 3, requires_grad=True)
y = torch.ones(6)
print(y.requires_grad) # False
y.requires_grad_(True)
print(y.requires_grad) # True
note:
1、x.requires_grad_(True/False) 设置tensor的可导与不可导。但是需要注意的是,我只能够设置叶子变量,即leaf variable的这个方法,否则会出现以下错误:
RuntimeError: you can only change requires_grad flags of leaf variables.
2、只有当所有的“叶子变量”,即所谓的leaf variable都是不可求导的,那函数y才是不能求导的。
那么如果不想被跟踪咋办?
法1:
方法.detach可以将tensor从计算图中分离出来,切断后面的计算跟踪,这样反向传播的时候,梯度就传不过去了。但不影响正向传播。
法2:
with torch.no_grad: #包裹不想被跟踪的操作
...
...
可以将不想被跟踪的操作代码块包裹起来。但不影响正向传播。
那么如何计算梯度呢?
很简单,一句话就解决:loss.backward(),或者使用函数torch.autograd.backward()。一个是方法一个是函数,都可以实现一样的效果。
loss是损失函数,是你前向传播的最后计算结果,也是反向传播的开端。方法.backward()是用来完成计算图中所有的梯度计算。以一己之力解决所有梯度计算。
这里需要注意一下:loss.backward()中loss如果是0维张量(标量)则backward不需要任何实参。但如果loss是个非标量(向量或者多维张量),则backward()需要传入一个同样规模的Tensor,即loss.backward(loss_like),等价于先计算y = torch,sum(loss*loss_like),然后loss对y求偏导。
为啥要这么做呢?主要是为了避免张量对张量求导时产生的维数扩张,从而使得链式传播过程中难以匹配合适的相乘项,因此我们最希望是用标量去对张量求导,这样可以使得参数的求导结果和参数是同规模的,也便于后期的计算,如SGD、Adam等等。
梯度保存在哪?
所以接下来说第二个属性tensor.grad,这个属性用来保存计算好的梯度值
note:
grad在反向传播过程中是累加的,因此在每次loss.backward()之前,都需要把有关上一轮batch产生的梯度清零,即x.grad.data.zeros_()。然后再调用loss.backward()来计算这一轮batch的新梯度。
举个栗子:
a = torch.tensor(2,dtype=torch.float32, requires_grad=True)
b = torch.tensor(3, dtype=torch.float32, requires_grad=True)
d = torch.tensor(4, dtype=torch.float32, requires_grad=True)
e = torch.tensor(5, dtype=torch.float32, requires_grad=True)
c = a * b
f = c * d
g = e * c
f.backward()
print(a.grad) # tensor(12)
print(e.grad) # None
从这个例子我们可以看出,c节点有2个子分支,一路和e产生g,一路和d产生f,但是我只对f求反向传播,因此g那一路相当于不存在,因此最后对a的梯度只有d*b=3*4=12。
第三个属性tensor.grad_fn,这个属性就是保存tensor所经过的某些计算相关的对象,比如
x = torch.tensor([1, 2], dtype=torch.float32, requires_grad=True)
y = x + 2
print(y) # tensor([3., 4.], grad_fn=<AddBackward0>)
grad_fn就显示了经过了Add加法计算,返回这么个对象。另外自己创建的Tensor,这种叫叶子节点,叶子节点的grad_fn为None,后期经过计算得到的Tensor就会有类似上述例子的值。
note:
1、叶子节点不能进行in-place操作,但是除了requires_grad_()。
2、requires_grad=False的这种叶子张量grad_fn=None。
最后说一下最后一个属性 tensor.data
x.data 返回和 x的相同数据 Tensor,只是保存原张量的值,而且这个新的Tensor和原来的Tensor是共用数据的,一者改变,另一者也会跟着改变,而且新分离得到的Tensor的require s_grad = False, 即不可求导的,即脱离了计算图,切断被跟踪
note:
x.data 不能被 autograd 追踪求微分 。改变x.data,导致原来的张量x的值也跟着改变了,但是这种改变对于autograd是没有察觉的,它依然按照求导规则来求导,导致得出完全错误的导数值却浑然不知。它的风险性就是如果我再任意一个地方更改了某一个张量,求导的时候也没有通知我已经在某处更改了,导致得出的导数值完全不正确,可怕的是,你还不知道是错误的答案。这个问题可以用x.detach来解决,使用detach会产生RunTimeError,提示你x已经被改变了,接下去求导会出现错误。
x = torch.ones(1, requires_grad=True)
xx = x.detach()
print(xx) # tensor([1.])
print(xx.requires_grad) # False
#前向传播
y = x * x * 4
xx *= 100
#后向传播
y.backward()
print(x)
print(x.grad)
但话又说回来了,如果我们想要修改tensor的值,但又不希望被autograd记录在计算图中(即不影响反向传播),就对tensor.data操作吧。下面来看2个例子进一步加深对data的理解
x = torch.ones(1, requires_grad=True)
print(x.data) # tensor([1.])
print(x.data.requires_grad) # False
#前向传播
y = x * x * 4
x.data *= 100
#后向传播
y.backward()
print(x) # tensor([100.], requires_grad=True)
print(x.grad) # tensor([800.])
再看下面这个
x = torch.ones(1, requires_grad=True)
print(x.data) # tensor([1.])
print(x.data.requires_grad) # False
#前向传播
y = x * x * 4
x = x*100
#后向传播
y.backward()
print(x) # tensor([100.], requires_grad=True, grad_fn=<MulBackward0>)
print(x.grad) # None
这里我们想要去改变x的值,但我们从x.grad看出反向梯度计算出了错误,这一点从x.grad_fn可以看出,x进行了Mul乘法运算,我们在前向传播完成之后,即y=x*x*4,后向传播其实以完成了,就等着我们去调用backward调用,但这时候x又进行了Mul运算,使得整个后向传播收到了影响,因此产生了x.grad=None的结果。