关于 ONNX 操作符的可微分标签的简短指南

可微分标签

每个操作符的 ONNX 操作符模式都包含一个可微分标签,用于表示每个输入和输出。在本文件中,我们将解释此标签的含义以及如何确保标签的正确性。简而言之,该标签标识操作符的可微分输入和可微分输出集。标签的含义是,每个可微分输出的偏导数相对于每个可微分输出都定义。

定义可微分标签的方法

操作符的可微分定义包含几个方面。

  • 可微分输入,可以在 Gradient 的 xs 属性中引用。

  • 可微分输出,可以在 Gradient 的 y 属性中引用。

  • 计算雅可比矩阵(或张量)的数学方程式。如果一个变量(输入或输出)是可微分的,则由数学判断。如果雅可比矩阵(或张量)存在,则所考虑的操作符具有一些可微分输入和输出。

有几种实现自动微分的策略,例如前向累积、后向累积和对偶变量。由于大多数深度学习框架都是基于后向的,因此审阅者应确保标签的 PR 作者提供有关该方面的足够详细信息。我们将在下面介绍几种方法来验证 ONNX 操作符的可微分性。

方法 1:重用现有的深度学习框架

第一种方法是表明所考虑的操作符的后向操作在 Pytorch 或 Tensorflow 等现有框架中存在。在这种情况下,作者应提供一个可运行的 python 脚本,用于计算所考虑操作符的后向传递。作者还应指出如何将 Pytorch 或 Tensor 代码映射到 ONNX 格式(例如,作者可以调用 torch.onnx.export 来保存 ONNX 模型)。以下脚本显示了 ONNX Reshape 使用 Pytorch 的可微分性。

import torch
import torch.nn as nn

# A single-operator model. It's literally a Pytorch Reshape.
# Note that Pytorch Reshape can be directly mapped to ONNX Reshape.
class MyModel(nn.Module):
  def __init__(self):
    super(MyModel, self).__init__()

  def forward(self, x):
    y = torch.reshape(x, (x.numel(),))
    y.retain_grad()
    return y

model = MyModel()

x = torch.tensor([[1., -1.], [1., 1.]], requires_grad=True)
y = model(x)
dy = torch.tensor([1., 2., 3., 4.])

torch.autograd.backward([y],
  grad_tensors=[dy],
  retain_graph=True,
  create_graph=True,
  grad_variables=None)

# This example shows the input and the output in Pytorch are differentiable.
# From the exported ONNX model below, we also see that "x" is the first input
# of ONNX Reshape and "y" the output of ONNX Reshape. Therefore, we can say
# the first input and the output of ONNX Reshape are differentiable.
print(x.grad)
print(y.grad)

with open('model.onnx', 'wb') as f:
  torch.onnx.export(model, x, f)

方法 2:手动进行数学运算

第二种方法是正式证明雅可比矩阵(或张量)从输出到输入的存在性,并至少提供两个数值示例。在这种情况下,审阅者应检查数学并确认数值结果是否正确。作者应添加足够的详细信息,以便任何 STEM 毕业生都能轻松地审查它。

例如,为了显示 Add 的可微分性,作者可以先写下它的方程式

C = A + B

为了简单起见,假设 AB 是相同形状的向量。

A = [a1, a2]^T
B = [b1, b2]^T
C = [c1, c2]^T

这里我们使用符号 ^T 来表示附加矩阵或向量的转置。令 X = [a1, a2, b1, b2]^TY = [c1, c2]^T,并将 Add 视为将 X 映射到 Y 的函数。然后,该函数的雅可比矩阵是一个 4x2 矩阵,

J = [[dc1/da1, dc2/da1],
     [dc1/da2, dc2/da2],
     [dc1/db1, dc2/db1],
     [dc1/db2, dc2/db2]]
  = [[1, 0],
     [0, 1],
     [1, 0],
     [0, 1]]

If

dL/dC = [dL/dc1, dL/dc2]^T,

然后 dL/dA = [dL/da1, dL/da2]^TdL/dB = [dL/db1, dL/db2]^T 可以从 中的元素计算出来

  [[dL/da1], [dL/da2], [dL/db1], [dL/db2]]
= J * dL/dC
= [[dL/dc1], [dL/dc2], [dL/dc1], [dL/dc2]]

其中 * 是标准矩阵乘法。如果 dL/dC = [0.2, 0.8]^T,则 dL/dA = [0.2, 0.8]^TdL/dB = [0.2, 0.8]^T。请注意,从 dL/dC 计算 dL/dAdL/dB 的过程通常称为操作符的后向。我们可以看到 Add 的后向操作将 dL/dC 作为输入,并生成两个输出 dL/dAdL/dB。因此,ABC 都是可微分的。通过将张量展平为一维向量,此示例可以扩展到覆盖所有张量,前提是不需要形状广播。如果发生了广播,则广播元素的梯度是其在**非广播**情况下的所有关联元素的梯度之和。让我们再次考虑上面的示例。如果 B = [b]^T 变成一个 1 元素向量,则 B 可能会广播到 [b1, b2]^T,并且 dL/dB = [dL/ db]^T = [dL/db1 + dL/db2]^T。对于高维张量,这实际上是沿着所有扩展轴进行的 ReduceSum 操作。