pytorch系列3——动态计算图和自动微分

 本文以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! 😋😋

系统学习Pytorch笔记二:Pytorch的动态图、自动求导及逻辑回归-CSDN博客

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
今年的华为开发者大会 HDC 2020 上,除了**昇腾、鲲鹏等自研芯片硬件平台**之外,最令人期待的就是**深度学习框架 MindSpore 的开源**了。今天上午,华为 MindSpore **首席科学家陈雷**在活动中宣布这款产品正式开源,我们终于可以在开放平台上一睹它的真面目。 本文是根据机器之心报道的MindSpore 的开源介绍而整理的.md笔记 作为一款支持**端、边、云独立/协同的统一训练和推理框架,华为希望通过这款完整的软件堆栈,实现**一次性算子开发、一致的开发和调试体验**,以此帮助开发者实现**一次开发,应用在所有设备上平滑迁移**的能力。 三大创新能力:新编程范式,执行模式和协作方式 由**自动微分自动并行、数据处理**等功能构成 开发算法即代码、运行高效、部署态灵活**的**特点**, 三层核心:从下往上分别是**后端运行时、计算引擎及前端表示层**。 最大特点:采用了业界最新的 **Source-to-Source 自动微分**,它能**利用编译器及编程语言的底层技术**,进一步**优化以支持更好的微分表达**。主流深度学习框架中主要有**三种自动微分技术,才用的不是静态计算动态计算,而是基于**源码**转换:该技术源以**函数式编程框架**为基础,以**即时编译(JIT)**的方式**在<u>中间表达</u>(编译过程中程序的表达形式)上做自动微分变换**,支持**<u>复杂控制流场景、高阶函数和闭包</u>**。 MindSpore 主要概念就是张量、算子、单元和模型 其代码有两个比较突出的亮点:计算的调整,动态图与静态可以一行代码切换;自动并行特性,我们写的串行代码,只需要多加一行就能完成自动并行。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值