如何在SameDiff图中添加微分函数和其他操作。
要开始使用SameDiff,请熟悉GitHub上ND4J API的autodiff模块。
不管好坏,SameDiff代码只组织在几个关键的地方。对于SameDiff的基本使用和测试,以下模块是关键。我们将更详细地讨论其中的一些。
functions
: 这个模块有基本的构建块来构建SameDiff变量和图。
execution
: 拥有与SameDiff图执行相关的所有内容。
gradcheck
: 用于检查SameDiff梯度的实用程序功能,其结构类似于DL4J中的相应工具。
loss
: SameDiff的损失函数
samediff
: 主要用于定义、设置和运行SameDiff操作和图形的SameDiff模块。
functions
模块中的微分函数请参阅GitHub上的functions
模块。
functions
模块的中心抽象是DifferentialFunction
,它几乎是SameDiff中所有内容的基础。在数学上,我们在SameDiff中所做的是建立一个有向无环图,它的节点是微分函数,我们可以计算梯度。在这方面,DifferentialFunction
构成了一个基本层次上的SameDiff图。
注意,每个DifferentialFunction
函数都有一个SameDiff实例。我们稍后再讨论SameDiff
和这段关系。另外,虽然只有很少的关键抽象,但它们实际上在任何地方都被使用,所以几乎不可能单独讨论SameDiff概念。最后,我们将讨论每个部分。
每个微分函数都有属性。在最简单的情况下,微分函数只有一个名字。根据所讨论的操作,通常会有更多的属性(考虑卷积中的步幅或内核大小)。当我们从其他项目(TensorFlow、ONNX等)导入计算图时,这些属性需要映射到我们内部使用的约定。attributeAdaptersForFunction
、mappingsForFunction
、propertiesForFunction
和resolvePropertiesFromSameDiffBeforExecution
方法是您希望在开始时查看的内容。
定义属性并正确映射后,分别为TensorFlow和ONNX import调用initFromTensorFlow和initFromOnnx。稍后,当我们讨论构建SameDiff操作时,将详细介绍这一点。
使用函数属性对输入列表执行微分函数,并生成一个或多个输出变量。您可以访问许多帮助函数来设置或访问这些变量:
args()
: 返回所有输入变量。
arg()
: 返回第一个输入变量(唯一一个用于一元操作)。
larg()
与 rarg()
: 返回二进制操作的第一个和第二个(读“left”和“right”)参数
outputVariables()
: 返回所有输出变量的列表。这取决于操作,可以动态计算。正如我们稍后将看到的,要获得具有单个输出的ops的结果,我们将调用.outputVariables()[0]。
处理输出变量是很棘手的,也是使用和扩展SameDiff的一个陷阱。例如,可能需要为微分函数实现calculateOutputShape
,但如果实现不正确,则可能导致难以调试的失败。(注意,SameDiff最终将调用libnd4j
中的操作执行,动态自定义操作要么推断输出形状,要么需要提供正确的输出形状。)
微分函数的自动微分是用一种方法实现的:doDiff。每个操作都必须提供doDiff的实现。如果您正在为libnd4j
op x实现SameDiff
操作,并且幸运地找到了x_bp
(如“反向传播”),那么您可以使用它,并且doDiff
实现基本上是自由的。
您还将看到内部使用的diff
实现并调用doDiff
。
重要的是,每个微分函数都可以通过调用f()
访问factory(DifferentialFunctionFactory
的实例)。更准确地说,这将返回微分函数具有的SameDiff实例的工厂:
这在许多地方都有使用,并允许您访问当前在SameDiff中注册的所有微分函数。把这家工厂看作是一个操作提供者。下面是一个将sum
暴露给DifferentialFunctionFactory
的示例:
我们故意省略了函数参数。注意,我们所做的只是重定向到ND4J中其他地方定义的Sum操作,然后返回第一个输出变量(SDVariable
类型,将在第二个中讨论)。现在不考虑实现细节,这允许您从任何可以访问微分函数工厂的地方调用f().sum(...)
。例如,当实现SameDiff op x
并且函数工厂中已经有x_bp
时,可以重写x
的doDiff
SameDiff
中图的构建与执行请参阅GitHub上的samediff
模块。
不足为奇,这就是魔法发生的地方。这个模块具有SameDiff操作的核心结构。首先,让我们看看构成SameDiff操作的变量。
SDVariable
(读取SameDiff变量)是DifferentialFunction
的扩展,它对SameDiff的作用就像INDArray
对老ND4J的作用一样,特别是SameDiff图对这些变量进行操作,每个单独的操作都会接收并输出一个SDVariable列表。SDVariable带有一个名称,配备一个SameDiff实例,具有形状信息,并且知道如何使用ND4J WeightInitScheme
初始化自身。您还可以找到一些助手来设置和获取这些属性。
SDVariable
可以做的少数事情之一就是DifferentialFunction
不能通过调用eval()
评估其结果并返回底层的INDArray
。这将在内部运行SameDiff并获取结果。类似的getter是getArr(),您可以在任何时候调用它来获取此变量的当前值。此功能广泛用于测试,以断言正确的结果。SDVariable
还可以通过gradient()
访问其当前梯度。初始化时不会有任何梯度,通常在稍后的点计算。
除了这些方法之外,SDVariable还提供了具体操作的方法(在这方面与DifferentialFunctionFactory
有点相似)。例如,定义add
如下:
允许对两个SameDiff变量调用c=a.add(b)
,其结果可由c.eval()
访问。
SameDiff
类是该模块的主要工作程序,它汇集了迄今为止讨论的大多数概念。有点不幸的是,反过来也是正确的,SameDiff
实例在某种程度上是所有其他SameDiff
模块抽象的一部分(这就是为什么您已经多次看到它)。一般来说,SameDiff
是自动微分的主要入口点,您可以使用它来定义一个符号图,该图对SDVariables
执行操作。一旦构建,SameDiff
图可以通过几种方式运行,例如exec()
和execandResult()
。
让自己相信调用SameDiff()
会创建很多很多东西!本质上,SameDiff将收集并允许您访问(就getter和setter而言)
图的所有微分函数及其所有属性,可以通过各种方式(如名称或id)访问。
所述功能的所有输入和输出信息。
所有函数属性以及如何映射它们、propertiesToResolve
和propertiesForFunction
都特别值得注意。
SameDiff
也是向SameDiff
模块公开新操作的地方。实际上,您可以为DifferentialFunctionFactory
实例f()
中的各个操作编写一个小包装器。下面是交叉积的一个例子:
在这一点上,查看并运行几个示例可能是一个好主意。SameDiff
测试是一个很好的来源。下面是一个如何将两个SameDiff
变量相乘的示例
这个例子取自SameDiffTests,它是一个主要的测试源,在这里您还可以找到一些完整的端到端的例子。
你发现测试的第二个地方是samediff 。无论何时向SameDiff添加新操作,都要为前向传播和梯度检查添加测试。
第三组相关测试存储在imports中,包含用于导入TensorFlow和ONNX图的测试。另外,这些导入测试的资源是在我们的TFOpsTests项目中生成的。
我们已经看到了DifferentialFunctionFactory和SameDiff如何获取ND4J操作,以便在不同级别将它们暴露给SameDiff。至于实际实现这些操作,您需要知道一些事情。在libnd4j中,您可以找到两类操作,这里将详细介绍这两类操作。我们将演示如何实现这两种操作类型。
所有的操作都是在这里进行的,而且大多数情况下,很明显要把操作放在哪里。要特别注意层,这是为深度学习层实现(如Conv2D)保留的。这些高级操作基于模块的概念,类似于pytorch中的模块或TensorFlow中的层。这些层操作实现还提供了更多涉及操作实现的源。
遗留(或XYZ)操作是具有特征“XYZ”签名的老一代ND4J操作。下面是如何在ND4J中通过包装libn4j中的cos遗留操作来实现cosine:cosine实现。说到SameDiff,遗留操作的好处是它们已经在ND4J中可用,但是需要通过SameDiff特定的功能来增强才能通过测试。因为余弦函数没有任何属性,所以这个实现很简单。使此操作SameDiff兼容的部分包括:
如果仔细观察,这只是事实的一部分,因为Cos
扩展了实现其他SameDiff功能的BaseTransformOp
。(注意,BaseTransformOp
是一个BaseOp
,它早期扩展了DifferentialFunction
。)例如,calculateOutputShape
就是在这里实现的。如果您想实现一个新的转换,也可以简单地从BaseTransformOp
继承。对于其他的操作类型,如reductions等,也可以使用操作基类,这意味着您只需要解决上面的三个要点。
在极少数情况下,您需要从头开始编写一个遗留操作,您需要从libn4j中找到相应的操作编号,可以在legacy-ops.h
中找到。
DynamicCustomOp
是libnd4j中的一种新操作,所有最近添加的操作都是这样实现的。ND4J中的这种操作类型直接扩展了DifferentialFunction
。
这里是从DynamicCustomOp
继承的BatchToSpace
操作的示例:
BatchToSpace由两个属性(block和crops)初始化。请注意,blocks和crops都是整数类型的,它们是如何通过调用addIArgument
添加到操作的整数参数中的。对于float参数和其他类型,请改用addTArgument
。
操作获取自己的名称和要导入的名称,
和doDiff
被实现
BatchToSpace操作在这里集成到DifferentialFunctionFactory
中,在这里暴露在SameDiff
中并在这里测试。
BatchToSpace当前唯一缺少的是属性映射。我们调用这个操作block和crops的属性,但是在ONNX或TensorFlow中,它们的调用和存储方式可能完全不同。要查找正确映射的差异,请参见Tensorflow的ops.proto和ONNX的onnxops.json。
让我们看看另一个正确执行属性映射的操作,即DynamicPartition
。这个操作只有一个属性,在SameDiff中称为numPartitions
。要映射和使用此属性,请执行以下操作:
实现一个名为addArgs
的小助手方法,该方法用于op的构造函数和导入助手中,下面一行我们将讨论。这是没有必要的,但鼓励这样做,并一致称之为addArgs
,为了清晰。
重写initFromTensorFlow
方法,该方法使用TFGraphMapper
实例为我们映射属性,并使用addArgs
添加参数。注意,由于ONNX在编写本文时不支持动态分区(因此没有onnxName
),因此也没有initFromOnnx
方法,其工作方式与initFromTensorFlow
几乎相同。
为了使TensorFlow导入可以工作,我们还需要重写mappingsForFunction。这个映射示例非常简单,它所做的只是将TensorFlow的属性名称 num_partitions
映射到我们的名称 numPartitions
。
请注意,虽然DynamicPartition
具有正确的属性映射,但它当前没有一个有效的doDiff
实现。
作为最后一个例子,我们展示了一个更有趣的属性映射设置,即Dilation2d。正如您在mappingsForFunction
中看到的,这个操作不仅要映射更多的属性,而且属性还带有属性值,如attributeAdaptersForFunction
中定义的。我们之所以选择显示此操作,是因为它具有属性映射,但既不向DifferentialFunctionFactory
公开,也不向SameDiff
公开。
因此,所示的三个DynamicCustomOp示例都有自己的缺陷,并代表了必须为SameDiff完成的工作的示例。总之,要添加新的SameDiff 操作,您需要:
在ND4J中创建扩展DifferentialFunction
的新操作。具体如何设置此实现取决于
操作生成 (遗留与动态定制)
操作类型 (变换、缩减等)
定义自己的操作名,以及TensorFlow和ONNX名称。
定义必要的SameDiff构造函数
使用addArgs
以可重用的方式添加操作参数。
首先在DifferentialFunctionFactory
中公开操作,然后将其包装在SameDiff
(或变量方法的SDVariable
)中。
实现doDiff
的自动微分。
重写mappingsForFunction
以映射TensorFlow和ONNX的属性
If necessary, also provide an attribute adapter by overriding attributeAdaptersForFunction
.如果需要,还可以通过重写attributeAdaptersForFunction
来提供属性适配器。
通过添加initFromTensorFlow
和initFromOnnx
(使用addArgs
),为TensorFlow和ONNX添加import一行。
测试,测试,测试