动态计算图介绍

Pytorch的计算图由节点和边组成,节点表示张量或者Function,边表示张量和Function之间的依赖关系。
Pytorch中的计算图是动态图。这里的动态主要有两重含义。
第一层含义是:计算图的正向传播是立即执行的。无需等待完整的计算图创建完毕,每条语句都会在计算图中动态添加节点和边,并立即执行正向传播得到计算结果。
第二层含义是:计算图在反向传播后立即销毁。下次调用需要重新构建计算图。如果在程序中使用了backward方法执行了反向传播,或者利用torch.autograd.grad方法计算了梯度,那么创建的计算图会被立即销毁,释放存储空间,下次调用需要重新创建。

  1. 计算图的正向传播是立即执行的 ```python 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)

  1. ![image.png](https://cdn.nlark.com/yuque/0/2022/png/1534487/1653620079553-91a3c108-016e-4268-9b1a-e18d9b4d6c2c.png?x-oss-process=image/format,png#clientId=u98ab34f3-370b-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=194&id=uea171b87&margin=%5Bobject%20Object%5D&name=image.png&originHeight=426&originWidth=348&originalType=binary&ratio=1&rotation=0&showTitle=false&size=597382&status=done&style=none&taskId=u69278d5a-f3e9-46e8-8ce9-41423a986df&title=&width=158.18181475331968)
  2. 2. 计算图在反向传播后立即销毁
  3. ```python
  4. import torch
  5. w = torch.tensor([[3.0,1.0]],requires_grad=True)
  6. b = torch.tensor([[3.0]],requires_grad=True)
  7. X = torch.randn(10,2)
  8. Y = torch.randn(10,1)
  9. Y_hat = X@w.t() + b # Y_hat定义后其正向传播被立即执行,与其后面的loss创建语句无关
  10. loss = torch.mean(torch.pow(Y_hat-Y,2))
  11. #计算图在反向传播后立即销毁,如果需要保留计算图, 需要设置retain_graph = True
  12. loss.backward() #loss.backward(retain_graph = True)
  13. #loss.backward() #如果再次执行反向传播将报错

计算图中的Function

计算图中的 张量我们已经比较熟悉了, 计算图中的另外一种节点是Function, 实际上就是 Pytorch中各种对张量操作的函数。
这些Function和我们Python中的函数有一个较大的区别,那就是它同时包括正向计算逻辑和反向传播的逻辑。
我们可以通过继承torch.autograd.Function来创建这种支持反向传播的Function。

  1. class MyReLU(torch.autograd.Function):
  2. #正向传播逻辑,可以用ctx存储一些值,供反向传播使用。
  3. @staticmethod
  4. def forward(ctx, input):
  5. ctx.save_for_backward(input)
  6. return input.clamp(min=0)
  7. #反向传播逻辑
  8. @staticmethod
  9. def backward(ctx, grad_output):
  10. input, = ctx.saved_tensors
  11. grad_input = grad_output.clone()
  12. grad_input[input < 0] = 0
  13. return grad_input
  14. import torch
  15. w = torch.tensor([[3.0,1.0]],requires_grad=True)
  16. b = torch.tensor([[3.0]],requires_grad=True)
  17. X = torch.tensor([[-1.0,-1.0],[1.0,1.0]])
  18. Y = torch.tensor([[2.0,3.0]])
  19. relu = MyReLU.apply # relu现在也可以具有正向传播和反向传播功能
  20. Y_hat = relu(X@w.t() + b)
  21. loss = torch.mean(torch.pow(Y_hat-Y,2))
  22. loss.backward()
  23. print(w.grad)
  24. print(b.grad)

image.png

  1. # Y_hat的梯度函数即是我们自己所定义的 MyReLU.backward
  2. print(Y_hat.grad_fn)

image.png


计算图与反向传播

了解了Function的功能,我们可以简单地理解一下反向传播的原理和过程。理解该部分原理需要一些高等数学中求导链式法则的基础知识。

  1. import torch
  2. x = torch.tensor(3.0,requires_grad=True)
  3. y1 = x + 1
  4. y2 = 2*x
  5. loss = (y1-y2)**2
  6. loss.backward()

loss.backward()语句调用后,依次发生以下计算过程。

  1. loss自己的grad梯度赋值为1,即对自身的梯度为1。
  2. loss根据其自身梯度以及关联的backward方法,计算出其对应的自变量即y1和y2的梯度,将该值赋值到y1.grad和y2.grad。
  3. y2和y1根据其自身梯度以及关联的backward方法, 分别计算出其对应的自变量x的梯度,x.grad将其收到的多个梯度值累加。

(注意,1,2,3步骤的求梯度顺序和对多个梯度值的累加规则恰好是求导链式法则的程序表述)
正因为求导链式法则衍生的梯度累加规则,张量的grad梯度不会自动清零,在需要的时候需要手动置零。


叶子节点和非叶子节点

执行下面代码,我们会发现 loss.grad并不是我们期望的1,而是 None。
类似地 y1.grad 以及 y2.grad也是 None.
这是为什么呢?这是由于它们不是叶子节点张量。
在反向传播过程中,只有 is_leaf=True 的叶子节点,需要求导的张量的导数结果才会被最后保留下来。
那么什么是叶子节点张量呢?叶子节点张量需要满足两个条件。

  1. 叶子节点张量是由用户直接创建的张量,而非由某个Function通过计算得到的张量。
  2. 叶子节点张量的 requires_grad属性必须为True.

Pytorch设计这样的规则主要是为了节约内存或者显存空间,因为几乎所有的时候,用户只会关心他自己直接创建的张量的梯度。
所有依赖于叶子节点张量的张量, 其requires_grad 属性必定是True的,但其梯度值只在计算过程中被用到,不会最终存储到grad属性中。
如果需要保留中间计算结果的梯度到grad属性中,可以使用 retain_grad方法。 如果仅仅是为了调试代码查看梯度值,可以利用register_hook打印日志。

  1. import torch
  2. x = torch.tensor(3.0,requires_grad=True)
  3. y1 = x + 1
  4. y2 = 2*x
  5. loss = (y1-y2)**2
  6. loss.backward()
  7. print("loss.grad:", loss.grad)
  8. print("y1.grad:", y1.grad)
  9. print("y2.grad:", y2.grad)
  10. print(x.grad)

image.png

  1. print(x.is_leaf)
  2. print(y1.is_leaf)
  3. print(y2.is_leaf)
  4. print(loss.is_leaf)

image.png
利用retain_grad可以保留非叶子节点的梯度值,利用register_hook可以查看非叶子节点的梯度值。

  1. import torch
  2. #正向传播
  3. x = torch.tensor(3.0,requires_grad=True)
  4. y1 = x + 1
  5. y2 = 2*x
  6. loss = (y1-y2)**2
  7. #非叶子节点梯度显示控制
  8. y1.register_hook(lambda grad: print('y1 grad: ', grad))
  9. y2.register_hook(lambda grad: print('y2 grad: ', grad))
  10. loss.retain_grad()
  11. #反向传播
  12. loss.backward()
  13. print("loss.grad:", loss.grad)
  14. print("x.grad:", x.grad)

image.png


计算图在TensorBoard中的可视化

可以利用 torch.utils.tensorboard 将计算图导出到 TensorBoard进行可视化。

  1. from torch import nn
  2. class Net(nn.Module):
  3. def __init__(self):
  4. super(Net, self).__init__()
  5. self.w = nn.Parameter(torch.randn(2,1))
  6. self.b = nn.Parameter(torch.zeros(1,1))
  7. def forward(self, x):
  8. y = x@self.w + self.b
  9. return y
  10. net = Net()
  1. from torch.utils.tensorboard import SummaryWriter
  2. writer = SummaryWriter('./data/tensorboard')
  3. writer.add_graph(net,input_to_model = torch.rand(10,2))
  4. writer.close()
  1. %load_ext tensorboard
  2. # %tensorboard --logdir ./data/tensorboard
  1. from tensorboard import notebook
  2. notebook.list()
  1. #在tensorboard中查看模型
  2. notebook.start("--logdir ./data/tensorboard")

image.png