本文以pytorch1.10进行解读:torch — PyTorch 1.10 documentation
文本的操作在github上都有Shirley-Xie/pytorch_exercise · GitHub,且有运行结果。
1. 动态计算图
解决的问题:因为深度学习对各种张量操作种类和数量的增加,会导致各种想不到的问题,比如多个操作之间该并行还是顺序执行,底层的设备如何协同,如何避免冗余的操作等,这些问题都会影响我们算法的执行效率,甚至会出现一些bug。
计算图:由节点和边组成,节点表示张量或者Function,边表示张量和Function之间的依赖关系。例如下图表示y=wx+b。
使用计算图的好处不仅让计算看起来更加简洁,还有个更大的优势就是让梯度求导也变得更加方便。求导链式法则衍生的梯度累加规则,在图中可以更直观的计算出。张量的grad梯度不会自动清零,在需要的时候需要手动置零。
这里的动态主要有两重含义:
第一层含义是:计算图的正向传播是立即执行的。无需等待完整的计算图创建完毕,每条语句都会在计算图中动态添加节点和边,并立即执行正向传播得到计算结果。
第二层含义是:计算图在反向传播后立即销毁。下次调用需要重新构建计算图。如果在程序中使用了backward方法执行了反向传播,或者利用torch.autograd.grad方法计算了梯度,那么创建的计算图会被立即销毁,释放存储空间,下次调用需要重新创建。
下面在不同情况下对此进行讨论。
1.1 计算图中的张量
正向传播是立即执行的
import torch
w = torch.tensor([[3.0,1.0]],requires_grad=True)
b = torch.tensor([[3.0]],requires_grad=True)
X = torch.randn(10,2)
Y = torch.randn(10,1)
Y_hat = X@w.t() + b # Y_hat定义后其正向传播被立即执行,与其后面的loss创建语句无关
loss = torch.mean(torch.pow(Y_hat-Y,2))
print(loss.data)
print(Y_hat.data)
结果:
tensor(17.8969)
tensor([[3.2613],
[4.7322],
[4.5037],
[7.5899],
[7.0973],
[1.3287],
[6.1473],
[1.3492],
[1.3911],
[1.2150]])
反向传播后立即销毁
#计算图在反向传播后立即销毁,如果需要保留计算图, 需要设置retain_graph = True
loss.backward() #loss.backward(retain_graph = True)
#loss.backward() #如果再次执行反向传播将报错
1.2 计算图中的Function
计算图中的 张量我们已经比较熟悉了, 计算图中的另外一种节点是Function, 实际上就是 Pytorch中各种对张量操作的函数。这些Function和我们Python中的函数有一个较大的区别,那就是它同时包括正向计算逻辑和反向传播的逻辑。我们可以通过继承torch.autograd.Function来创建这种支持反向传播的Function。
class MyReLU(torch.autograd.Function):
#正向传播逻辑,可以用ctx存储一些值,供反向传播使用。
@staticmethod
def forward(ctx, input):
ctx.save_for_backward(input)
return input.clamp(min=0)
#反向传播逻辑
@staticmethod
def backward(ctx, grad_output):
input, = ctx.saved_tensors
grad_input = grad_output.clone()
grad_input[input < 0] = 0
return grad_input
w = torch.tensor([[3.0,1.0]],requires_grad=True)
b = torch.tensor([[3.0]],requires_grad=True)
X = torch.tensor([[-1.0,-1.0],[1.0,1.0]])
Y = torch.tensor([[2.0,3.0]])
relu = MyReLU.apply # relu现在也可以具有正向传播和反向传播功能
Y_hat = relu(X@w.t() + b)
loss = torch.mean(torch.pow(Y_hat-Y,2))
loss.backward()
print(w.grad)
print(b.grad)
# Y_hat的梯度函数即是我们自己所定义的 MyReLU.backward
print(Y_hat.grad_fn)
结果为:
tensor([[4.5000, 4.5000]])
tensor([[4.5000]])
<torch.autograd.function.MyReLUBackward object at 0x1162c8798>
1.3 叶子结点
1.3.1 叶子结点和grad的关系
在执行上面的代码时,我们发现Y_hat、loss的grad都是None,这并不是我们期待的。为什么会这样?因为他们不是叶子结点张量。在反向传播过程中,只有 tensor.is_leaf
来判断是否为叶子节点,只有叶节点的梯度值能够被保留下来。反向传播完了之后,非叶子节点的梯度是默认被释放掉的。Pytorch设计这样的规则主要是为了节约内存或者显存空间。
在Pytorch神经网络中,我们反向传播backward()就是为了求叶子节点的梯度。在pytorch中,神经网络层中的权值w的tensor均为叶子节点。它们的require_grad都是True,但它们都属于用户创建的,所以都是叶子节点。而反向传播backward()也就是为了求它们的梯度。
在调用backward()时,只有当requires_grad和is_leaf同时为真时,才会计算节点的梯度值。所以下面的例子中,只有w,b会计算结点的梯度。默认情况下,叶子结点的梯度会保存在tensor的grad中。
w = torch.tensor([[3.0,1.0]],requires_grad=True)
b = torch.tensor([[3.0]],requires_grad=True)
X = torch.randn(10,2)
Y = torch.randn(10,1)
Y_hat = X@w.t() + b
loss = torch.mean(torch.pow(Y_hat-Y,2))
loss.backward()
print("is_leaf:\n", w.is_leaf, X.is_leaf, b.is_leaf, Y.is_leaf, Y_hat.is_leaf,loss.is_leaf)
print("gradient:\n", w.grad, X.grad, b.grad, Y.grad, Y_hat.grad,loss.grad)
结果:
is_leaf:
True True True True False False
gradient:
tensor([[14.0855, 5.2406]]) None tensor([[8.6295]]) None None None
1.3.2 叶子结点的判断
1. requires_grad为False的张量,都约定俗成地归结为叶子张量。
就像我们训练模型的input,它们都是require_grad=False,因为他们不需要计算梯度(我们训练网络训练的是网络模型的权重,而不需要训练输入)。他们是计算图的起始点。一旦进行翻转一类的操作就不是叶子结点了。
a= torch.randn(10,2)
a.is_leaf# True
2. requires_grad为True的张量, 如果他们是由用户创建的,则它们是叶张量(leaf Tensor)。
例如各种网络层,nn.Linear(), nn.Conv2d()等, 他们是用户创建的,而且其网络参数也需要训练,所以requires_grad=True。这意味着它们不是运算的结果,因此gra_fn为None。
然而叶子结点很容易变为非叶子结点。
a= torch.randn(10,2,requires_grad=True)
print(a.is_leaf) #True
c= torch.randn(10,2,requires_grad=True).reshape(4,5)
print(c.is_leaf) # False
1.3.3 非叶子结点保存梯度
如果需要保留中间计算结果的梯度到grad属性中,可以使用 retain_grad方法。如果仅仅是为了调试代码查看梯度值,可以利用register_hook打印日志。
上面若是想保留y的梯度则在后面加上retain_grad()
# 正向传播
w = torch.tensor([[3.0,1.0]],requires_grad=True)
b = torch.tensor([[3.0]],requires_grad=True)
X = torch.randn(10,2)
Y = torch.randn(10,1)
Y_hat = X@w.t() + b
#非叶子节点梯度显示控制
Y_hat.register_hook(lambda grad: print('Y_hat grad: ', grad))
loss = torch.mean(torch.pow(Y_hat-Y,2))
loss.retain_grad() # retain_grad()
# 反向传播
loss.backward()
print("is_leaf:\n", w.is_leaf, X.is_leaf, b.is_leaf, Y.is_leaf, Y_hat.is_leaf,loss.is_leaf)
print("gradient:\n", w.grad, X.grad, b.grad, Y.grad, Y_hat.grad,loss.grad)
结果:
Y_hat grad: tensor([[-4.0340e-01],
[ 1.2399e-01],
[ 3.0519e-01],
[ 1.6016e+00],
[ 7.8939e-01],
[ 1.4119e+00],
[-5.5075e-04],
[ 6.1971e-01],
[ 3.9825e-01],
[-4.9616e-01]])
is_leaf:
True True True True False False
gradient:
tensor([[5.0101, 1.2299]]) None tensor([[4.3500]]) None None tensor(1.)
2. 自动微分
神经网络通常依赖反向传播求梯度来更新网络参数,求梯度过程通常是一件非常复杂而容易出错的事情。而深度学习框架可以帮助我们自动地完成这种求梯度运算。
Pytorch一般通过反向传播 backward 方法 实现这种求梯度计算。该方法求得的梯度将存在对应自变量张量的grad属性下。除此之外,也能够调用torch.autograd.grad 函数来实现求梯度计算。这就是Pytorch的自动微分机制。
2.1 利用backward方法张量求导
backward 方法通常在一个标量张量上调用,该方法求得的梯度将存在对应自变量张量的grad属性下。
torch.autograd.backward(tensors, grad_tensors=None, retain_graph=None, create_graph=False, grad_variables=None, inputs=None)
tensors表示用于求导的张量,如loss。
retain_graph表示保存计算图, 由于Pytorch采用了动态图机制,在每一次反向传播结束之后,计算图都会被释放掉。如果我们不想被释放,就要设置这个参数为True
create_graph表示创建导数计算图,用于高阶求导。
grad_tensors表示多梯度权重。如果有多个loss需要计算梯度的时候,就要设置这些loss的权重比例。
我们平时执行backward()的时候就是在调用这个函数。
# grad_tensors
w = torch.tensor([1.], requires_grad=True)
x = torch.tensor([2.], requires_grad=True)
a = torch.add(w, x)
b = torch.add(w, 1)
y0 = torch.mul(a, b) #dy0/dw = 5
y1 = torch.add(a, b) #dy1 /dw = 2
# 此处有多个梯度, 给两个梯度设置权重,最后得到的w的梯度就是带权重的这两个梯度之和。否则报错。
loss = torch.cat([y0, y1], dim=0)
grad_tensors = torch.tensor([1.,2.])
loss.backward(gradient=grad_tensors) #5+2*2=9
print(w.grad)
# 结果:tensor([9.])
2.2 autograd.grad方法高阶求导
torch.autograd.grad(outputs, inputs, grad_outputs=None, retain_graph=None, create_graph=False, only_inputs=True, allow_unused=False)
1. 高阶求导
# f(x) = a*x**2 + b*x + c的导数
x = torch.tensor(0.0,requires_grad = True) # x需要被求导
a = torch.tensor(1.0)
b = torch.tensor(-2.0)
c = torch.tensor(1.0)
y = a*torch.pow(x,2) + b*x + c
# create_graph 设置为 True 将允许创建更高阶的导数
dy_dx = torch.autograd.grad(y,x,create_graph=True)[0]
print(dy_dx.data)
# 求二阶导数
dy2_dx2 = torch.autograd.grad(dy_dx,x)[0]
print(dy2_dx2.data)
“结果”
tensor(-2.)
tensor(2.)
2. 多个自变量求导
x1 = torch.tensor(1.0,requires_grad = True) # x需要被求导
x2 = torch.tensor(2.0,requires_grad = True)
y1 = x1*x2
y2 = x1+x2
# 允许同时对多个自变量求导数
(dy1_dx1,dy1_dx2) = torch.autograd.grad(outputs=y1,inputs = [x1,x2],retain_graph = True)
print(dy1_dx1,dy1_dx2) # tensor(2.) tensor(1.)
# 如果有多个因变量,相当于把多个因变量的梯度结果求和
(dy12_dx1,dy12_dx2) = torch.autograd.grad(outputs=[y1,y2],inputs = [x1,x2])
print(dy12_dx1,dy12_dx2) # tensor(3.) tensor(2.)
结果:
tensor(2.) tensor(1.)
tensor(3.) tensor(2.)
参考:
Pytorch 叶子张量 leaf tensor (叶子节点) (detach)_pytorch 叶子节点_hxxjxw的博客-CSDN博客
GitHub - lyhue1991/eat_pytorch_in_20_days: Pytorch🍊🍉 is delicious, just eat it! 😋😋