pytorch计算图Computation_graph是什么

发布于:2025-04-09 ⋅ 阅读:(28) ⋅ 点赞:(0)


引言:计算图(数据流图)类似于数学中的流程图和数据结构中的图

一、AI系统中的计算图(宏观)

在这里插入图片描述

为各类神经网络计算提供统一的描述

  • 基本数据结构 :Tensor 张量(边)

  • 基本运算单元:Operator 算子(节点)

  1. 由最基本的代数算子组成
  2. 根据深度学习结构组成复杂算子
  3. N个输入Tensor,M个输出Tensor

深度学习训练流程主要计算阶段:
1.前向计算;
2.反向计算;
3.更新可学习的权重参数
自动微分:原子操作构成的复杂前向计算程序,关注自动生成高效的反向计算程序
在这里插入图片描述

二、动态计算图(微观)

Pytorch的计算图由节点和边组成,节点表示张量或者Function,边表示张量和Function之间的依赖关系。

Pytorch中的计算图是动态图。这里的动态主要有两重含义。

第一层含义是:计算图的正向传播是立即执行的。无需等待完整的计算图创建完毕,每条语句都会在计算图中动态添加节点和边,并立即执行正向传播得到计算结果。

第二层含义是:计算图在反向传播后立即销毁。下次调用需要重新构建计算图。如果在程序中使用了backward方法执行了反向传播,或者利用torch.autograd.grad方法计算了梯度,那么创建的计算图会被立即销毁,释放存储空间,下次调用需要重新创建。

dtype 该张量存储的值类型,可选类型见:torch.dtype
device 该张量存放的设备类型,cpu/gpu
data 该张量节点存储的值
requires_grad 表示autograd时是否需要计算此tensor的梯度,默认Falsegrad存储梯度的值,初始为None
grad_fn反向传播时,用来计算梯度的函数
is_leaf 该张量节点在计算图中是否为叶子节点

  1. 输入数据
  2. 建立模型
  3. 定义损失函数和训练方法
  4. 初始化和启动TensorFlow会话
  5. 训练

2.1 张量计算图

张量本身也支持微分计算。这种可微分性其实不仅体现在我们可以使用grad函数对其进行可导,更重要的是这种可微分性会体现在可微分张量参与的所有运算中

import numpy as np
import torch 

requires_grad属性:可微分性

#构建可微分张量
x = torch.tensor(1., requires_grad=True)
x
tensor(1., requires_grad=True)
#构建函数关系
y = x ** 2
y
tensor(1., grad_fn=<PowBackward0>)

grad_fn属性:存储Tensor微分函数
我们发现,此时张量y具有了一个grad_fn属性,并且取值为,我们可以查看该属性

y.grad_fn #y的梯度函数是平方函数
<PowBackward0 at 0x1521ce260>

grad_fn其实是存储了一个Tensor的微分函数,或者说grad_fn存储了可微分张量在进行计算的过程中函数关系,此处x到y其实就是进行了幂运算

#但x作为初始张量,并没有grad_fn 属性
x.grad_fn #None

也就是相当于x,y不仅同样拥有张量的取值,并且同样可微,还额外存储了x到y的函数计算信息,我们再尝试围绕y创建新的函数关系,z=y+1

z = y + 1
z
tensor(2., grad_fn=<AddBackward0>)
z.requires_grad #True

True
z.grad_fn #加法函数
<AddBackward0 at 0x1521cc430>

不难发现,z也同时存储了张量计算数值、z是可微的,并且z还存储了和y的计算关系(add)。据此我们可以知道,在PyTorch的张量计算过程中,如果我们设置初始张量是可微的,则在计算过程中,每一个由原张量计算得出的新张量都是可微的,并且还会保存此前一步的函数关系,这也就是所谓的回溯机制。而根据这个回溯机制,我们就能非常清楚掌握张量的每一步计算,并据此绘制张量计算图。

借助回溯机制,我们就能将张量的复杂计算过程抽象为一张图(Graph),例如此前我们定义的x、、z三个张量,三者的计算关系就可以由下图进行表示。
在这里插入图片描述

2.2 计算图的定义

上图就是用于记录可微分张量计算关系的张量计算图,图由节点和有向边构成,其中节点表示张量,边表示函数计算关系,方向则表示实际运算方向,张量计算图本质是有向无环图。

2.3 节点类型

在张量计算图中,虽然每个节点都表示可微分张量,但节点和节点之间却略有不同。就像在前例中,y和z保存了函数计算关系,但x没有,而在实际计算关系中,我们不难发现z是所有计算的终点,因此,虽然x、y、都是节点,但每个节点却并不一样。此处我们可以将节点分为三类,分别是:
a):叶节点,也就是初始输入的可微分张量,前例中x就是叶节点;
b):输出节点,也就是最后计算得出的张量,前例中z就是输出节点;
c):中间节点,在一张计算图中,除了叶节点和输出节点,其他都是中间节点,前例中y就是中间节点。
当然,在一张计算图中,可以有多个叶节点和中间节点,但大多数情况下,只有一个输出节点,若存在多个输出结果,我们也往往会将其保存在一个张量中。

2.4 计算图的动态性

值得一提的是,PyTorch的计算图是动态计算图,会根据可微分张量的计算过程自动生成,并且伴随着新张量或运算的加入不断更新,这使得PyTorch的计算图更加灵活高效,并且更加易于构建,相比于先构件图后执行计算的部分框架(如老版本的TensorFlow),动态图也更加适用于面向对象编程。
在这里插入图片描述

2.5 计算图的正向传播是立即执行的

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)) #mean是均值函数,pow是幂函数 

print(loss.data)
print(Y_hat.data)
tensor(29.3997)
tensor([[4.0577],
        [4.0333],
        [1.8358],
        [0.8279],
        [7.7642],
        [9.1330],
        [9.1387],
        [2.9540],
        [1.9430],
        [1.0640]])

2.6 计算图在反向传播后立即销毁

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))

#计算图在反向传播后立即销毁,如果需要保留计算图, 需要设置retain_graph = True
loss.backward()  #loss.backward(retain_graph = True) 

#loss.backward() #如果再次执行反向传播将报错

2.7 计算图中的Function

计算图中的 张量我们已经比较熟悉了, 计算图中的另外一种节点是Function, 实际上就是 Pytorch中各种对张量操作的函数。

这些Function和我们Python中的函数有一个较大的区别,那就是它同时包括正向计算逻辑和反向传播的逻辑。

我们可以通过继承torch.autograd.Function来创建这种支持反向传播的Function

class MyReLU(torch.autograd.Function): #自定义的激活函数 
   
    #正向传播逻辑,可以用ctx存储一些值,供反向传播使用。
    @staticmethod #@staticmethod是一个装饰器,表示该方法是一个静态方法 
    def forward(ctx, input):
        ctx.save_for_backward(input) #save_for_backward是一个方法,用于保存输入张量,以便在反向传播时使用
        return input.clamp(min=0) #clamp是一个张量操作函数,min=0表示小于0的值都置为0

    #反向传播逻辑
    @staticmethod
    def backward(ctx, grad_output):
        input, = ctx.saved_tensors #saved_tensors是一个属性,用于获取保存的张量
        grad_input = grad_output.clone() #clone是一个方法,用于复制张量
        grad_input[input < 0] = 0 #grad_input是一个张量,用于存储反向传播的梯度 
        return grad_input
    
# d loss /d x = grad_output
# d loss /d y = grad_input
# d loss /d x = (d loss/ dy) * (d y / d x) 
import torch 
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)
tensor([[4.5000, 4.5000]])
tensor([[4.5000]])
# Y_hat的梯度函数即是我们自己所定义的 MyReLU.backward

print(Y_hat.grad_fn)
<torch.autograd.function.MyReLUBackward object at 0x1521b0d50>

2.8 计算图与反向传播

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

import torch 

x = torch.tensor(3.0,requires_grad=True)
y1 = x + 1
y2 = 2*x
loss = (y1-y2)**2

loss.backward()
x.grad 
#x的梯度是4.0
# 反向传播的梯度是loss对x的偏导数

# d loss /dx = (d loss /d y1) * (d y1 /x) + (d loss /d y2) * (d y2 /x) 
#y1 = 4
#y2 = 6
#2*(-2)*1 + 2*(2)*2 = -4 + 8 = 4
tensor(4.)

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梯度不会自动清零,在需要的时候需要手动置零。

2.9 叶子节点和非叶子节点

执行下面代码,我们会发现 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打印日志。

import torch 

x = torch.tensor(3.0,requires_grad=True)
y1 = x + 1
y2 = 2*x
loss = (y1-y2)**2

loss.backward()
print("loss.grad:", loss.grad)
print("y1.grad:", y1.grad)
print("y2.grad:", y2.grad)
print(x.grad)
loss.grad: None
y1.grad: None
y2.grad: None
tensor(4.)
print(x.is_leaf)
print(y1.is_leaf)
print(y2.is_leaf)
print(loss.is_leaf)
True
False
False
False

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

import torch 

#正向传播
x = torch.tensor(3.0,requires_grad=True)
y1 = x + 1
y2 = 2*x
loss = (y1-y2)**2

#非叶子节点梯度显示控制
y1.register_hook(lambda grad: print('y1 grad: ', grad))
y2.register_hook(lambda grad: print('y2 grad: ', grad))
loss.retain_grad()

#反向传播
loss.backward()
print("loss.grad:", loss.grad)
print("x.grad:", x.grad)
y2 grad:  tensor(4.)
y1 grad:  tensor(-4.)
loss.grad: tensor(1.)
x.grad: tensor(4.)

三、计算图在TensorBoard中的可视化

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

from torch import nn
class Net(nn.Module):
    def __init__(self):
        super(Net, self).__init__()
        self.w = nn.Parameter(torch.randn(2,1)) #nn.Parameter是一个类,用于定义模型的参数
        self.b = nn.Parameter(torch.zeros(1,1)) 

    def forward(self, x):
        y = x @ self.w + self.b #@是矩阵乘法运算符
        return y 
net = Net() #实例化模型    
from torch.utils.tensorboard import SummaryWriter
writer = SummaryWriter('runs/tensorboard_example') #创建一个SummaryWriter对象,用于记录训练过程中的数据w
writer.add_graph(net,input_to_model=torch.rand(10,2)) #add_graph是一个方法,用于添加模型图
writer.close() #关闭SummaryWriter对象
#tensorboard --logdir=runs/tensorboard_example

from tensorboard import notebook
notebook.list() #列出所有的tensorboard实例
notebook.start('--logdir=runs/tensorboard_example') #启动tensorboard实例

在这里插入图片描述

总结

Pytorch的计算图由节点和边组成,节点表示张量或者Function,边表示张量和Function之间的依赖关系。
Pytorch构建动态计算图,通过反向传播来实现自动微分机制

参考链接:

  1. https://www.bilibili.com/video/BV1rR4y197HM?vd_source=41eda4a1e91bb010366913e2b99886b9
  2. https://github.com/lyhue1991/eat_pytorch_in_20_days

网站公告

今日签到

点亮在社区的每一天
去签到