Autograd

神经网络的训练过程离不开 反向传播算法,在反向传播过程中,需要获取 loss 函数对模型参数的梯度,用于更新参数。

OneFlow 提供了自动求导机制,可自动计算神经网络中参数的梯度。

本文将先介绍计算图的基本概念,它有利于理解 OneFlow 自动求导的常见设置及限制,再介绍 OneFlow 中与自动求导有关的常见接口。

计算图

张量与算子,共同组成计算图,如以下代码:

  1. import oneflow as flow
  2. def loss(y_pred, y):
  3. return flow.sum(1/2*(y_pred-y)**2)
  4. x = flow.ones(1, 5) # 输入
  5. w = flow.randn(5, 3, requires_grad=True)
  6. b = flow.randn(1, 3, requires_grad=True)
  7. z = flow.matmul(x, w) + b
  8. y = flow.zeros(1, 3) # label
  9. l = loss(z,y)

它对应的计算图如下:

todo

计算图中,像 xwby 这种只有输出,没有输入的节点称为 叶子节点;像 loss 这种只有输入没有输出的节点,称为 根节点

反向传播过程中,需要求得 lwb 的梯度,以更新这两个模型参数。因此,我们在创建它们时,设置 requires_gradTrue

自动求梯度

backward 与梯度

在反向传播的过程中,需要得到 l 分别对 wb 的梯度 Autograd - 图2Autograd - 图3。我们只需要对 l 调用 backward() 方法,然后 OneFlow 就会自动计算梯度,并且存放到 wbgrad 成员中。

  1. l.backward()
  2. print(w.grad)
  3. print(b.grad)
  1. tensor([[0.9397, 2.5428, 2.5377],
  2. [0.9397, 2.5428, 2.5377],
  3. [0.9397, 2.5428, 2.5377],
  4. [0.9397, 2.5428, 2.5377],
  5. [0.9397, 2.5428, 2.5377]], dtype=oneflow.float32)
  6. tensor([[0.9397, 2.5428, 2.5377]], dtype=oneflow.float32)

对非叶子节点求梯度

默认情况下,只有 requires_grad=True 的叶子节点的梯度会被保留。非叶子节点的 grad 属性默认在 backward 执行过程中,会自动释放,不能查看。

如果想保留并查看非叶子节点的梯度,可以调用 Tensor.retain_grad 方法:

  1. from math import pi
  2. n1 = flow.tensor(pi/2, requires_grad=True)
  3. n2 = flow.sin(n1)
  4. n2.retain_grad()
  5. n3 = flow.pow(n2, 2)
  6. n3.backward()
  7. print(n1.grad)
  8. print(n2.grad)

以上代码,既求 Autograd - 图4,也求 Autograd - 图5

输出:

  1. tensor(-8.7423e-08, dtype=oneflow.float32)
  2. tensor(2., dtype=oneflow.float32)

对一个计算图多次 backward()

默认情况下,对于给定的计算图,只能调用 backward() 一次。比如,以下代码会报错:

  1. n1 = flow.tensor(10., requires_grad=True)
  2. n2 = flow.pow(n1, 2)
  3. n2.backward()
  4. n2.backward()

报错信息:

Maybe you try to backward through the node a second time. Specify retain_graph=True when calling .backward() or autograd.grad() the first time.

如果想要在同一个计算图上调用多次 backward(),需要在调用时设置 retain_graph=True

  1. n1 = flow.tensor(10., requires_grad=True)
  2. n2 = flow.pow(n1, 2)
  3. n2.backward(retain_graph=True)
  4. print(n1.grad)
  5. n2.backward()
  6. print(n1.grad)

输出:

  1. tensor(20., dtype=oneflow.float32)
  2. tensor(40., dtype=oneflow.float32)

以上输出可知,OneFlow 会 累加 多次 backward() 计算得到的梯度。 如果想清空梯度,可以调用 zeros_ 方法:

  1. n1 = flow.tensor(10., requires_grad=True)
  2. n2 = flow.pow(n1, 2)
  3. n2.backward(retain_graph=True)
  4. print(n1.grad)
  5. n1.grad.zeros_()
  6. n2.backward()
  7. print(n1.grad)

输出:

  1. tensor(20., dtype=oneflow.float32)
  2. tensor(20., dtype=oneflow.float32)

不记录某个 Tensor 的梯度

默认情况下,OneFlow 会 tracing requires_gradTrue 的 Tensor,自动求梯度。 不过有些情况可能并不需要 OneFlow 这样做,比如只是想试一试前向推理。那么可以使用 oneflow.no_gradoneflow.Tensor.detach 方法设置。

  1. z = flow.matmul(x, w)+b
  2. print(z.requires_grad)
  3. with flow.no_grad():
  4. z = flow.matmul(x, w)+b
  5. print(z.requires_grad)

输出:

  1. True
  2. False
  1. z_det = z.detach()
  2. print(z_det.requires_grad)

输出:

  1. False

输出不是标量时如何求梯度

通常,调用 backward() 方法的是神经网络的 loss,是一个标量。

但是,如果不是标量,对 Tensor 调用 backward() 时会报错。

  1. x = flow.randn(1, 2, requires_grad=True)
  2. y = 3*x + 1
  3. y.backward()

报错信息:

Check failed: IsScalarTensor(*outputs.at(i)) Grad can be implicitly created only for scalar outputs

而对 ysum 后可以求梯度:

  1. x = flow.randn(1, 2, requires_grad=True)
  2. y = 3*x + 1
  3. y = y.sum()
  4. y.backward()
  5. print(x.grad)

输出:

  1. tensor([[3., 3.]], dtype=oneflow.float32)

错误原因及解决方法的分析请参考下文 “扩展阅读” 部分。

扩展阅读

x 张量中有两个元素,记作 Autograd - 图6Autograd - 图7y 张量中的两个元素记作 Autograd - 图8Autograd - 图9,并且两者的关系是:

Autograd - 图10

Autograd - 图11

此时,想直接求 Autograd - 图12

Autograd - 图13

在数学上是没有意义的,因此当然就报错了。 实际上,当用户调用 y.backward() 时,其实想要的结果通常是:

Autograd - 图14

当对 y 进行 sum 运算后:

Autograd - 图15

此时,调用 backward() 时,对 Autograd - 图16Autograd - 图17 可求梯度:

Autograd - 图18

Autograd - 图19

除了使用 sum 之外,还可以使用更通用方法,即 Vector Jacobian Product(VJP) 完成非标量的根节点的梯度计算。依然用上文的例子,在反向传播过程中,OneFlow 会根据计算图生成雅可比矩阵:

Autograd - 图20

只需提供一个与 Autograd - 图21 大小一致的向量 Autograd - 图22,即可计算 VJP:

Autograd - 图23

若向量 Autograd - 图24 是反向传播中上一层的梯度,VJP 的结果刚好是当前层要求的梯度。

backward 方法是可以接受一个张量做参数的,该参数就是 VJP 中的 Autograd - 图25,理解以上道理后,还可以使用以下的方式对张量求梯度:

  1. x = flow.randn(1, 2, requires_grad=True)
  2. y = 3*x + 1
  3. y.backward(flow.ones_like(y))
  4. print(x.grad)

输出:

  1. tensor([[3., 3.]], dtype=oneflow.float32)

外部链接

为正常使用来必力评论功能请激活JavaScript