關於 ONNX 運算子可微性標籤的簡短指南

可微性標籤

每個運算子的 ONNX 運算子結構描述都包含每個輸入和輸出的可微性標籤。在本文件中,我們說明此標籤的意義,以及如何確保標籤的正確性。簡而言之,此標籤會識別運算子的可微分輸入和可微分輸出的集合。標籤的意義是,每個可微分輸出的偏導數,相對於每個可微分輸出都有定義。

定義可微性標籤的方法

運算子的可微性定義包含數個方面。

  • 可微分輸入,可以在 Gradient 的 xs 屬性中參考。

  • 可微分輸出,可以在 Gradient 的 y 屬性中參考。

  • 計算 Jacobian 矩陣 (或張量) 的數學方程式。變數 (輸入或輸出) 是否可微分,取決於數學。如果 Jacobian 矩陣 (或張量) 存在,則所考慮的運算子會有一些可微分的輸入和輸出。

有數種策略可以實作自動微分,例如正向累加、反向累加和雙變數。由於大多數深度學習架構都基於反向,因此審閱者應確保標籤的 PR 作者提供足夠的詳細資訊。我們在下面提出幾種方法來驗證 ONNX 運算子的可微性。

方法 1:重複使用現有的深度學習架構

第一種方法是顯示所考慮的運算子的反向運算存在於現有的架構中,例如 Pytorch 或 Tensorflow。在這種情況下,作者應提供可執行的 python 腳本,該腳本會計算所考慮運算子的反向傳遞。作者也應該指出如何將 Pytorch 或 Tensor 程式碼對應到 ONNX 格式 (例如,作者可以呼叫 torch.onnx.export 來儲存 ONNX 模型)。以下腳本顯示使用 Pytorch 的 ONNX Reshape 的可微性。

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:手動進行數學計算

第二種方法是正式證明從輸出到輸入的 Jacobian 矩陣 (或張量) 存在,並至少提供兩個數值範例。在這種情況下,審閱者應仔細檢查數學並確認數值結果是否正確。作者應新增足夠的詳細資訊,以便任何 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 的函式。然後,此函式的 Jacobian 矩陣為 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 全都可微分。透過將張量扁平化為 1 維向量,此範例可以在不需要形狀廣播時涵蓋所有張量。如果發生廣播,則廣播元素的梯度是其**非廣播**案例中所有相關元素梯度的總和。讓我們再次考慮上述範例。如果 B = [b]^T 成為一個 1 元素向量,則 B 可能會廣播到 [b1, b2]^TdL/dB = [dL/ db]^T = [dL/db1 + dL/db2]^T。對於高維張量,這實際上是沿著所有展開軸的 ReduceSum 運算。