添加操作

如何在SameDiff图中添加微分函数和其他操作。

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等)导入计算图时,这些属性需要映射到我们内部使用的约定。attributeAdaptersForFunctionmappingsForFunctionpropertiesForFunctionresolvePropertiesFromSameDiffBeforExecution方法是您希望在开始时查看的内容。

定义属性并正确映射后,分别为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实例的工厂:

public DifferentialFunctionFactory f() {
    return sameDiff.f();
}

这在许多地方都有使用,并允许您访问当前在SameDiff中注册的所有微分函数。把这家工厂看作是一个操作提供者。下面是一个将sum暴露给DifferentialFunctionFactory的示例:

public SDVariable sum(...) {
    return new Sum(...).outputVariables()[0];
}

我们故意省略了函数参数。注意,我们所做的只是重定向到ND4J中其他地方定义的Sum操作,然后返回第一个输出变量(SDVariable类型,将在第二个中讨论)。现在不考虑实现细节,这允许您从任何可以访问微分函数工厂的地方调用f().sum(...)。例如,当实现SameDiff op x并且函数工厂中已经有x_bp时,可以重写xdoDiff

@Override
public List<SDVariable> doDiff(List<SDVariable> grad) {
    ...
    return Arrays.asList(f().x_bp(...));
}

SameDiff中图的构建与执行

请参阅GitHub上的samediff模块。

不足为奇,这就是魔法发生的地方。这个模块具有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如下:

public SDVariable add(double sameDiffVariable) {
    return add(sameDiff.generateNewVarName(new AddOp().opName(),0),sameDiffVariable);
}

允许对两个SameDiff变量调用c=a.add(b),其结果可由c.eval()访问。

SameDiff

SameDiff类是该模块的主要工作程序,它汇集了迄今为止讨论的大多数概念。有点不幸的是,反过来也是正确的,SameDiff实例在某种程度上是所有其他SameDiff模块抽象的一部分(这就是为什么您已经多次看到它)。一般来说,SameDiff是自动微分的主要入口点,您可以使用它来定义一个符号图,该图对SDVariables执行操作。一旦构建,SameDiff图可以通过几种方式运行,例如exec()execandResult()

让自己相信调用SameDiff()会创建很多很多东西!本质上,SameDiff将收集并允许您访问(就getter和setter而言)

  • 图的所有微分函数及其所有属性,可以通过各种方式(如名称或id)访问。

  • 所述功能的所有输入和输出信息。

  • 所有函数属性以及如何映射它们、propertiesToResolvepropertiesForFunction都特别值得注意。

SameDiff也是向SameDiff模块公开新操作的地方。实际上,您可以为DifferentialFunctionFactory实例f()中的各个操作编写一个小包装器。下面是交叉积的一个例子:

public SDVariable cross(SDVariable a, SDVariable b) {
    return cross(null, a, b);
}

public SDVariable cross(String name, SDVariable a, SDVariable b) {
    SDVariable ret = f().cross(a, b);
    return updateVariableNameAndReference(ret, name);
}

SameDiff 执行示例和测试

在这一点上,查看并运行几个示例可能是一个好主意。SameDiff测试是一个很好的来源。下面是一个如何将两个SameDiff变量相乘的示例

SameDiff sd = SameDiff.create();

INDArray inArr = Nd4j.linspace(1, n, n).reshape(inOrder, d0, d1, d2);
INDArray inMul2Exp = inArr.mul(2);

SDVariable in = sd.var("in", inArr);
SDVariable inMul2 = in.mul(2.0);

sd.exec();

这个例子取自SameDiffTests,它是一个主要的测试源,在这里您还可以找到一些完整的端到端的例子。

你发现测试的第二个地方是samediff 。无论何时向SameDiff添加新操作,都要为前向传播和梯度检查添加测试。

第三组相关测试存储在imports中,包含用于导入TensorFlow和ONNX图的测试。另外,这些导入测试的资源是在我们的TFOpsTests项目中生成的。

创建和公开新的SameDiff操作

我们已经看到了DifferentialFunctionFactory和SameDiff如何获取ND4J操作,以便在不同级别将它们暴露给SameDiff。至于实际实现这些操作,您需要知道一些事情。在libnd4j中,您可以找到两类操作,这里将详细介绍这两类操作。我们将演示如何实现这两种操作类型。

所有的操作都是在这里进行的,而且大多数情况下,很明显要把操作放在哪里。要特别注意层,这是为深度学习层实现(如Conv2D)保留的。这些高级操作基于模块的概念,类似于pytorch中的模块或TensorFlow中的层。这些层操作实现还提供了更多涉及操作实现的源。

实现遗留操作

遗留(或XYZ)操作是具有特征“XYZ”签名的老一代ND4J操作。下面是如何在ND4J中通过包装libn4j中的cos遗留操作来实现cosine:cosine实现。说到SameDiff,遗留操作的好处是它们已经在ND4J中可用,但是需要通过SameDiff特定的功能来增强才能通过测试。因为余弦函数没有任何属性,所以这个实现很简单。使此操作SameDiff兼容的部分包括:

  • 指定SameDiff构造函数 这里

  • 你实现 doDiff 这里

  • 此处指定SameDiff opName、TensorFlow tensorflowName和ONNX onnxName

如果仔细观察,这只是事实的一部分,因为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来提供属性适配器。

  • 通过添加initFromTensorFlowinitFromOnnx(使用addArgs),为TensorFlow和ONNX添加import一行。

  • 测试,测试,测试

最后更新于