页面树结构

2017-11-09 ApacheCN 开源组织,第二期邀请成员活动,一起走的更远 : http://www.apachecn.org/member/209.html


MachineLearning 优酷地址 : http://i.youku.com/apachecn

转至元数据结尾
转至元数据起始

如果您想创建一个现有TensorFlow库未涵盖的操作,我们建议您首先尝试在Python中编写op作为现有Python操作或函数的组合。如果不可能,您可以创建一个自定义的C ++操作。您可能需要创建自定义C ++操作系统的几个原因:

  • 将您的操作表示为现有操作的组合是不容易或可能的。
  • 将您的操作表达为现有原语的组合不是有效的。
  • 你想手工融合未来的编译器会发现难以融合的原始图形。

例如,假设您想要实现类似于“中间池”的操作,类似于“MaxPool”操作符,但是计算中间人在滑动窗口而不是最大值。使用操作的组合可能是可能的(例如,使用ExtractImagePatches和TopK),但可能并不像本机操作那样具有性能或内存效率,您可以在单个融合操作中更聪明地执行某些操作。与往常一样,通常首先值得尝试使用操作员组合来表达您想要的内容,只有选择添加新操作(如果证明是困难或低效)。

要结合您的自定义操作,您需要:

  1. 在C ++文件中注册新的op。Op注册定义了op的功能的接口(规范),它独立于op的实现。例如,op注册定义了op的名称和op的输入和输出。它还定义用于张量形状推理的形状函数。
  2. 在C ++中实现op。op的实现被称为内核,它是您在步骤1中注册的规范的具体实现。可以为不同的输入/输出类型或体系结构(例如,CPU,GPU)提供多个内核。
  3. 创建一个Python包装(可选)。这个包装器是用于在Python中创建op的公共API。默认包装器是从op注册生成的,可以直接使用或添加到。
  4. 编写一个函数来计算op(可选)的梯度。
  5. 测试操作。我们通常在Python中为方便起见,但您也可以在C ++中测试op。如果定义渐变,您可以使用Python 渐变检查器来验证它们。看 relu_op_test.py一个例子来测试Relu类运算符及其梯度的前向函数。

先决条件:

 

定义op的界面

您可以通过使用TensorFlow系统注册操作来定义操作界面。在注册中,您可以指定op的名称,其输入(类型和名称)和输出(类型和名称)以及文档字符串和op可能需要的任何attrs

为了看看它是如何工作的,假设你想创建一个op,它接受张量,int32并输出张量的 副本,除了第一个元素设置为零。为此,创建一个名为的文件zero_out.cc。然后添加一个调用REGISTER_OP定义你的op的接口的 宏:

#include "tensorflow/core/framework/op.h"
#include "tensorflow/core/framework/shape_inference.h"

using namespace tensorflow;

REGISTER_OP("ZeroOut")
    .Input("to_zero: int32")
    .Output("zeroed: int32")
    .SetShapeFn([](::tensorflow::shape_inference::InferenceContext* c) {
      c->set_output(0, c->input(0));
      return Status::OK();
    }); 
ZeroOut操作将一个to_zero32位整数作为输入,并输出一个zeroed32位整数的张量。op也使用形状函数来确保输出张量与输入张量的形状相同。例如,如果输入是形状的张量[10,20],则该形状函数指定输出形状也是[10,20]。

关于命名的注释:op名称必须在CamelCase中,并且在二进制文件中注册的所有其他操作中必须是唯一的。

 

实现内核的操作

定义界面后,提供一个或多个op的实现。要创建这些内核之一,请创建一个扩展OpKernel和覆盖该Compute方法的类。该Compute方法提供一个context 类型的参数OpKernelContext*,从中可以访问有用的东西,如输入和输出张量。

将您的内核添加到上面创建的文件中。内核可能看起来像这样:

#include "tensorflow/core/framework/op_kernel.h"

using namespace tensorflow;

class ZeroOutOp : public OpKernel {
 public:
  explicit ZeroOutOp(OpKernelConstruction* context) : OpKernel(context) {}

  void Compute(OpKernelContext* context) override {
    // Grab the input tensor
    const Tensor& input_tensor = context->input(0);
    auto input = input_tensor.flat<int32>();

    // Create an output tensor
    Tensor* output_tensor = NULL;
    OP_REQUIRES_OK(context, context->allocate_output(0, input_tensor.shape(),
                                                     &output_tensor));
    auto output = output_tensor->flat<int32>();

    // Set all but the first element of the output tensor to 0.
    const int N = input.size();
    for (int i = 1; i < N; i++) {
      output(i) = 0;
    }

    // Preserve the first input value if possible.
    if (N > 0) output(0) = input(0);
  }
}; 
在实现内核之后,您可以使用TensorFlow系统进行注册。在注册中,您可以指定不同的约束条件来运行该内核。例如,您可能有一个内核为CPU而制作,另外一个用于GPU。

要做这个ZeroOut操作,添加以下内容到zero_out.cc

REGISTER_KERNEL_BUILDER(Name("ZeroOut").Device(DEVICE_CPU), ZeroOutOp); 

重要提示:OpKernel的实例可能会同时访问。您的Compute方法必须是线程安全的。使用互斥锁保护对成员的任何访问。还是更好,不要通过课堂成员共享状态!考虑使用a ResourceMgr 来跟踪运行状态。

 

构建op库

使用系统编译器(TensorFlow二进制安装)编译op

你应该能够编译zero_out.cc一个C++编译器,如g++ 或clang您的系统上。二进制PIP包安装头文件和库,您需要在系统特定的位置编译op。但是,TensorFlow python库提供了get_include获取头目录的功能。这是在Ubuntu机器上的这个功能的输出。

$ python
>>> import tensorflow as tf
>>> tf.sysconfig.get_include()
'/usr/local/lib/python2.7/site-packages/tensorflow/include' 
假设您已经g++安装了,可以使用命令序列将op编译为动态库。
TF_INC=$(python -c 'import tensorflow as tf; print(tf.sysconfig.get_include())')

g++ -std=c++11 -shared zero_out.cc -o zero_out.so -fPIC -I $TF_INC -O2 
在Mac OS X上,构建.so文件时需要附加的标记“-undefined dynamic_lookup” 。

注意gcc版本5:gcc5使用新的C ++ ABI。TensorFlow网站上提供的二进制点包可以使用较老的ABI的gcc4。如果您使用gcc5编译您的op库,请添加 -D_GLIBCXX_USE_CXX11_ABI=0到命令行,使库与旧的abi兼容。此外,如果您使用从源码创建的TensorFlow包,请记住添加-cxxopt="-D_GLIBCXX_USE_CXX11_ABI=0" 为bazel命令来编译Python包。

使用bazel编译op(TensorFlow源安装)

如果您安装了TensorFlow源,可以使用TensorFlow的构建系统编译您的操作。在目录中放置一个带有以下Bazel构建规则的BUILD文件tensorflow/core/user_ops

load("//tensorflow:tensorflow.bzl", "tf_custom_op_library")

tf_custom_op_library(
    name = "zero_out.so",
    srcs = ["zero_out.cc"],
) 
运行以下命令进行构建zero_out.so
$ bazel build --config opt //tensorflow/core/user_ops:zero_out.so 

注意:尽管可以.so使用标准cc_library规则创建共享库(文件),但我们强烈建议您使用该tf_custom_op_library宏。它添加了一些所需的依赖关系,并执行检查以确保共享库与TensorFlow的插件加载机制兼容。

 

在Python中使用op

TensorFlow Python API提供了 tf.load_op_library加载动态库并使用TensorFlow框架注册op的功能。load_op_library返回一个包含op和内核的Python包装器的Python模块。因此,一旦构建了op,您可以执行以下操作来从Python运行:

import tensorflow as tf
zero_out_module = tf.load_op_library('zero_out.so')
with tf.Session(''):
  zero_out_module.zero_out([[1, 2], [3, 4]]).eval()

# Prints
array([[1, 0], [0, 0]], dtype=int32) 
请记住,生成的函数将被赋予一个snake_case名称(符合PEP8)。所以,如果您的op ZeroOut在C ++文件中命名,则将调用python函数zero_out

为了使操作可以作为importPython模块的常规功能,将load_op_library调用转换为Python源文件可能如下所示(请参阅zero_out_op_1.py):

import tensorflow as tf

_zero_out_module = tf.load_op_library('zero_out_op_kernel_1.so')
zero_out = _zero_out_module.zero_out 

验证操作是否正常

验证您已成功实施操作的好方法是为其编写测试。创建zero_out_op_test.py包含内容的文件 :

import tensorflow as tf

class ZeroOutTest(tf.test.TestCase):
  def testZeroOut(self):
    zero_out_module = tf.load_op_library('zero_out.so')
    with self.test_session():
      result = zero_out_module.zero_out([5, 4, 3, 2, 1])
      self.assertAllEqual(result.eval(), [5, 0, 0, 0, 0])

if __name__ == "__main__":
  tf.test.main() 
然后运行你的测试(假设你已经安装了tensorflow):
$ python zero_out_op_test.py 

将高级功能构建到您的操作系统中

现在你知道如何构建一个基本的(有限制的)操作和实现,我们将看一些更复杂的事情,你通常需要构建你的操作。这包括:

条件检查和验证

上面的例子假设操作适用于任何形状的张量。如果只适用于向量怎么办?这意味着添加检查到上述OpKernel实现。

  void Compute(OpKernelContext* context) override {
    // Grab the input tensor
    const Tensor& input_tensor = context->input(0);

    OP_REQUIRES(context, TensorShapeUtils::IsVector(input_tensor.shape()),
                errors::InvalidArgument("ZeroOut expects a 1-D vector."));
    // ...
  } 
这声明输入是向量,InvalidArgument如果不是,则返回已设置 状态。该 OP_REQUIRES有三个参数:

或者,如果要测试Status从某个函数返回的对象是否是错误,并且如果返回它,请使用OP_REQUIRES_OK。这两个宏都从函数返回错误。

操作注册

ATTRS

操作可以具有attrs,当操作被添加到图形时,其值被设置。这些用于配置op,并且它们的值可以在内核实现中以及在op注册中的输入和输出的类型中访问。最好使用输入代替attr,因为输入更灵活。这是因为attrs是常量,必须在图形构建时定义。相反,输入是Tensors,其值可以是动态的; 也就是说,输入可以改变每一步,使用feed等设置。Attrs用于不能用输入做的事情:影响签名(输入或输出的数量或类型)的任何配置,或者可以“从一步一步地改变。

您在注册op时定义一个attr,通过使用该Attr方法指定其名称和类型,该方法期望具有以下格式的规范:

<name>: <attr-type-expr> 
其中<name>以字母开头并且可以由字母数字字符和下划线,并且<attr-type-expr>是具有以下形式的式表达如下所述

例如,如果您希望ZeroOutop保留用户指定的索引,而不是仅第0个元素,则可以注册op:

REGISTER_OP("ZeroOut")
    .Attr("preserve_index: int") 
    .Input("to_zero: int32") 
    .Output("zeroed: int32"); 

(请注意,属性类型的集合不同于用于输入和输出的 张量类型。)

您的内核可以通过参数访问其构造函数中的这个attr context :

class ZeroOutOp : public OpKernel {
 public:
  explicit ZeroOutOp(OpKernelConstruction* context) : OpKernel(context) {
    // Get the index of the value to preserve
    OP_REQUIRES_OK(context,
                   context->GetAttr("preserve_index", &preserve_index_));
    // Check that preserve_index is positive
    OP_REQUIRES(context, preserve_index_ >= 0,
                errors::InvalidArgument("Need preserve_index >= 0, got ",
                                        preserve_index_));
  }
  void Compute(OpKernelContext* context) override {
    // ...
  }
 private:
  int preserve_index_;
}; 
然后可以在该Compute方法中使用:
  void Compute(OpKernelContext* context) override {
    // ...


    // We're using saved attr to validate potentially dynamic input
    // So we check that preserve_index is in range
    OP_REQUIRES(context, preserve_index_ < input.dimension(0),
                errors::InvalidArgument("preserve_index out of range"));

    // Set all the elements of the output tensor to 0
    const int N = input.size();
    for (int i = 0; i < N; i++) {
      output_flat(i) = 0;
    }

    // Preserve the requested input value
    output_flat(preserve_index_) = input(preserve_index_);
  } 

Attr类型

attr中支持以下类型:

  • string:任何字节序列(不需要为UTF8)。
  • int:有符号整数。
  • float:浮点数。
  • bool: 对或错。
  • type:其中一个(非参考)值DataType
  • shape:A TensorShapeProto
  • tensor:A TensorProto
  • list(<type>):列表<type>,其中<type>以上类型之一。注意这list(list(<type>))是无效的。

另见:op_def_builder.cc:FinalizeAttr一个明确的列表。

默认值和约束

Attrs可能有默认值,某些类型的attrs可以有约束。要定义具有约束的attr,可以使用以下<attr-type-expr>s:

  • {'<string1>', '<string2>'}:该值必须是一个字符串,它具有该值<string1><string2>string当您使用此语法时,隐含类型的名称,。这个模拟一个枚举:
REGISTER_OP("EnumExample")
   
.Attr("e: {'apple', 'orange'}");
  • {<type1>, <type2>}:该值是类型type,并且必须是其中一个, <type1><type2>在哪里<type1><type2>受支持的 张量类型。您不指定attr的类型type。当您有类型列表时,这是隐含的{...}。例如,在这种情况下,attr t是一个必须是a int32,a float或a的类型bool
REGISTER_OP("RestrictedTypeExample")
   
.Attr("t: {int32, float, bool}");
  • 有常见类型约束的捷径

    • numbertype:类型type限制为数字(非字符串和非bool)类型。
    • realnumbertypenumbertype没有复杂的类型。
    • quantizedtype:像numbertype只是量化的数字类型。

    这些允许的类型的具体列表由函数(如NumberTypes())中 定义tensorflow/core/framework/types.h。在这个例子中,attr t必须是数字类型之一:

    c++ REGISTER_OP("NumberType") .Attr("t: numbertype");

    对于这个操作:

    python tf.number_type(t=tf.int32) # Valid tf.number_type(t=tf.bool) # Invalid

  • int >= <n>:该值必须是一个int大于或等于 <n>,其中<n>是自然数。

例如,以下op注册指定attr的值a必须至少为2

REGISTER_OP("MinIntExample")
   
.Attr("a: int >= 2");
  • list(<type>) >= <n><type>长度大于或等于的类型列表<n>

例如,以下op注册指定了attr a是类型列表(int32或者float),并且必须至少有3个:

REGISTER_OP("TypeListExample")
   
.Attr("a: list({int32, float}) >= 3");

要设置一个attr的默认值(使其在生成的代码中可选),添加= <default>到最后,如:

REGISTER_OP("AttrDefaultExample")
    .Attr("i: int = 0"); 
支持的默认值的语法是在得到的GraphDef定义的原始表示中使用的语法。

以下是为所有类型指定默认值的示例:

REGISTER_OP("AttrDefaultExampleForAllTypes")
   .Attr("s: string = 'foo'")
   .Attr("i: int = 0")
   .Attr("f: float = 1.0")
   .Attr("b: bool = true")
   .Attr("ty: type = DT_INT32")
   .Attr("sh: shape = { dim { size: 1 } dim { size: 2 } }")
   .Attr("te: tensor = { dtype: DT_INT32 int_val: 5 }")
   .Attr("l_empty: list(int) = []")
   .Attr("l_int: list(int) = [2, 3, 5, 7]"); 
请注意,类型的值type使用类型DT_*名称

多态性

类型多态性

对于可以将不同类型作为输入或产生不同输出类型的操作,您可以在操作注册中 的输入或输出类型中指定一个attr。通常,您将为每种受支持的类型注册一个。  OpKernel 

例如,如果您希望ZeroOutop在floats之外工作int32,您的操作注册可能如下所示:

REGISTER_OP("ZeroOut")
    .Attr("T: {float, int32}")
    .Input("to_zero: T") 
    .Output("zeroed: T"); 
您的op注册现在指定输入的类型必须是float或 int32,并且其输出将是相同的类型,因为它们都有类型T

关于命名的注释:输入,输出和attrs通常应该给出snake_case名称。一个例外是用作输入类型或输入类型的attrs。当op被添加到图形中时,可以推断这些attrs,因此不会出现在op的函数中。例如,ZeroOut的最后一个定义将生成一个如下所示的Python函数:

def zero_out(to_zero, name=None):
 
"""...
  Args:
    to_zero: A `Tensor`. Must be one of the following types:
        `float32`, `int32`.
    name: A name for the operation (optional).

  Returns:
    A `Tensor`. Has the same type as `to_zero`.
  """

如果to_zero通过了int32张量,那么T会自动设置 int32(实际上DT_INT32)。那些推断的attrs被赋予了大写或CamelCase的名字。

将此与具有确定输出类型的类型attr的op进行比较:

REGISTER_OP("StringToNumber")
   
.Input("string_tensor: string")
   
.Output("output: out_type")
   
.Attr("out_type: {float, int32} = DT_FLOAT");
   
.Doc(R"doc(
Converts each string in the input Tensor to the specified numeric type.
)doc"
);

在这种情况下,用户必须指定输出类型,如生成的Python:

def string_to_number(string_tensor, out_type=None, name=None):
 
"""Converts each string in the input Tensor to the specified numeric type.

  Args:
    string_tensor: A `Tensor` of type `string`.
    out_type: An optional `tf.DType` from: `tf.float32, tf.int32`.
      Defaults to `tf.float32`.
    name: A name for the operation (optional).

  Returns:
    A `Tensor` of type `out_type`.
  """

 

#include "tensorflow/core/framework/op_kernel.h"

class ZeroOutInt32Op : public OpKernel {
  // as before
};

class ZeroOutFloatOp : public OpKernel {
 public:
  explicit ZeroOutFloatOp(OpKernelConstruction* context)
      : OpKernel(context) {}

  void Compute(OpKernelContext* context) override {
    // Grab the input tensor
    const Tensor& input_tensor = context->input(0);
    auto input = input_tensor.flat<float>();

    // Create an output tensor
    Tensor* output = NULL;
    OP_REQUIRES_OK(context,
                   context->allocate_output(0, input_tensor.shape(), &output));
    auto output_flat = output->template flat<float>();

    // Set all the elements of the output tensor to 0
    const int N = input.size();
    for (int i = 0; i < N; i++) {
      output_flat(i) = 0;
    }

    // Preserve the first input value
    if (N > 0) output_flat(0) = input(0);
  }
};

// Note that TypeConstraint<int32>("T") means that attr "T" (defined
// in the op registration above) must be "int32" to use this template
// instantiation.
REGISTER_KERNEL_BUILDER(
    Name("ZeroOut")
    .Device(DEVICE_CPU)
    .TypeConstraint<int32>("T"),
    ZeroOutOpInt32);
REGISTER_KERNEL_BUILDER(
    Name("ZeroOut")
    .Device(DEVICE_CPU)
    .TypeConstraint<float>("T"),
    ZeroOutFloatOp);

 

为了保持向后兼容性,您应该在将attr添加到现有操作系统时指定默认值

 

REGISTER_OP("ZeroOut")
  .Attr("T: {float, int32} = DT_INT32") 
  .Input("to_zero: T") 
  .Output("zeroed: T") 

 

假设你想添加更多的类型,比如说double

REGISTER_OP("ZeroOut")
    .Attr("T: {float, double, int32}")
    .Input("to_zero: T") 
    .Output("zeroed: T");

 

而不是OpKernel用上面的冗余代码编写另一个代码,通常你可以使用C ++模板。您将仍然有一个内核注册(REGISTER_KERNEL_BUILDER调用)每个重载。

template <typename T>
class ZeroOutOp : public OpKernel {
 public:
  explicit ZeroOutOp(OpKernelConstruction* context) : OpKernel(context) {}

  void Compute(OpKernelContext* context) override {
    // Grab the input tensor
    const Tensor& input_tensor = context->input(0);
    auto input = input_tensor.flat<T>();

    // Create an output tensor
    Tensor* output = NULL;
    OP_REQUIRES_OK(context,
                   context->allocate_output(0, input_tensor.shape(), &output));
    auto output_flat = output->template flat<T>();

    // Set all the elements of the output tensor to 0
    const int N = input.size();
    for (int i = 0; i < N; i++) {
      output_flat(i) = 0;
    }

    // Preserve the first input value
    if (N > 0) output_flat(0) = input(0);
  }
};

// Note that TypeConstraint<int32>("T") means that attr "T" (defined
// in the op registration above) must be "int32" to use this template
// instantiation.
REGISTER_KERNEL_BUILDER(
    Name("ZeroOut")
    .Device(DEVICE_CPU)
    .TypeConstraint<int32>("T"),
    ZeroOutOp<int32>);
REGISTER_KERNEL_BUILDER(
    Name("ZeroOut")
    .Device(DEVICE_CPU)
    .TypeConstraint<float>("T"),
    ZeroOutOp<float>);
REGISTER_KERNEL_BUILDER(
    Name("ZeroOut")
    .Device(DEVICE_CPU)
    .TypeConstraint<double>("T"),
    ZeroOutOp<double>); 
如果您有超过一对夫妇超载,您可以将注册放在宏中。
#include "tensorflow/core/framework/op_kernel.h"

#define REGISTER_KERNEL(type) \
 REGISTER_KERNEL_BUILDER( \
 Name("ZeroOut").Device(DEVICE_CPU).TypeConstraint<type>("T"), \
 ZeroOutOp<type>)

REGISTER_KERNEL(int32);
REGISTER_KERNEL(float);
REGISTER_KERNEL(double);

#undef REGISTER_KERNEL
根据您注册内核的类型列表,您可能可以使用以下提供的宏tensorflow/core/framework/register_types.h
#include "tensorflow/core/framework/op_kernel.h"
#include "tensorflow/core/framework/register_types.h"

REGISTER_OP("ZeroOut")
 .Attr("T: realnumbertype")
 .Input("to_zero: T")
 .Output("zeroed: T");

template <typename T>
class ZeroOutOp : public OpKernel { ... };

#define REGISTER_KERNEL(type) \
 REGISTER_KERNEL_BUILDER( \
 Name("ZeroOut").Device(DEVICE_CPU).TypeConstraint<type>("T"), \
 ZeroOutOp<type>)

TF_CALL_REAL_NUMBER_TYPES(REGISTER_KERNEL);

#undef REGISTER_KERNEL
列出输入和输出

除了能够接受或产生不同类型之外,操作可以消耗或产生可变数量的张量。

在下一个示例中,attr T保存一个类型的列表,并用作输入in和输出的类型out。输入和输出是该类型的张量列表(输出中的张量的数量和类型与输入相同,因为两者都有类型T)。

REGISTER_OP("PolymorphicListExample")
    .Attr("T: list(type)")
    .Input("in: T")
    .Output("out: T"); 
您还可以对列表中指定的类型设置限制。在下一种情况下,输入是一张floatdouble张量的列表。op接受例如输入类型(float, double, float),在这种情况下,输出类型也将是(float, double, float)
REGISTER_OP("ListTypeRestrictionExample")
    .Attr("T: list({float, double})")
    .Input("in: T")
    .Output("out: T");
如果您希望列表中的所有张量类型相同,则可能会执行以下操作:
REGISTER_OP("IntListInputExample")
    .Attr("N: int")
    .Input("in: N * int32")
    .Output("out: int32");
这接受int32张量列表,并使用intattr N指定列表的长度。

这也可以是类型多态的。在下一个示例中,输入是"N"相同(但未指定)类型("T")的张量列表(长度),输出是匹配类型的单张张量:

REGISTER_OP("SameListInputExample")
    .Attr("N: int")
    .Attr("T: type")
    .Input("in: N * T")
    .Output("out: T");
默认情况下,张名单有1的最小长度您可以更改使用默认的 ">="对相应的ATTR约束。在下一个示例中,输入是至少两个int32张量的列表:
REGISTER_OP("MinLengthIntListExample")
    .Attr("N: int >= 2")
    .Input("in: N * int32")
    .Output("out: int32"); 
同样的语法与"list(type)"attrs 一起使用:
REGISTER_OP("MinimumLengthPolymorphicListExample")
    .Attr("T: list(type) >= 3")
    .Input("in: T")
    .Output("out: T"); 

输入和输出

总结上述,操作注册可以有多个输入和输出:

REGISTER_OP("MultipleInsAndOuts")
 .Input("y: int32")
 .Input("z: float")
 .Output("a: string")
 .Output("b: int32");
每个输入或输出规格的形式如下:
<name>: <io-type-expr>
其中<name>以字母开头,可以由字母数字字符和下划线组成。<io-type-expr>是以下类型表达式之一:
  • <type>,其中<type>是一个支持的输入类型(例如floatint32, string)。这指定给定类型的单张量。

请参阅 支持的Tensor类型列表

REGISTER_OP("BuiltInTypesExample")
   
.Input("integers: int32")
   
.Input("complex_numbers: complex64");
  • <attr-type>在哪里<attr-type>有一个Attr的类型 typelist(type)(可能的类型限制)的名称。此语法允许多态操作
REGISTER_OP("PolymorphicSingleInput")
   
.Attr("T: type")
   
.Input("in: T");

REGISTER_OP
("RestrictedPolymorphicSingleInput")
   
.Attr("T: {int32, int64}")
   
.Input("in: T");

引用类型的attr list(type)可以接受一系列张量。

REGISTER_OP("ArbitraryTensorSequenceExample")
   
.Attr("T: list(type)")
   
.Input("in: T")
   
.Output("out: T");

REGISTER_OP
("RestrictedTensorSequenceExample")
   
.Attr("T: list({int32, int64})")
   
.Input("in: T")
   
.Output("out: T");

请注意,输出out中的张量的数量和类型与输入中相同in,因为两者都是类型T

  • 对于具有相同类型的张量序列:<number> * <type>其中具有类型<number>Attr的名称在哪里int。的<type>可以是 特定类型的像int32float,或者与一个类型attr的名称type。作为第一个例子,这个op接受一个int32张量列表:
REGISTER_OP("Int32SequenceExample")
   
.Attr("NumTensors: int")
   
.Input("in: NumTensors * int32")

而这个op接受任何类型的张量列表,只要它们都是一样的:

REGISTER_OP("SameTypeSequenceExample")
   
.Attr("NumTensors: int")
   
.Attr("T: type")
   
.Input("in: NumTensors * T")
  • 对于一个张量的引用:Ref(<type>)其中<type>之一是以前的类型之一。

关于命名的注释:将推断在输入类型中使用的任何attr。按照惯例,推断的attrs使用资本名称(如TN)。否则,输入,输出和attrs具有函数参数(如num_outputs)的名称。有关更多详细信息,请参阅 前面的命名注释

有关详细信息,请参阅 tensorflow/core/framework/op_def_builder.h

向后兼容性

假设你已经写了一个很好的,自定义的操作,并与其他人分享,所以你有快乐的客户使用你的操作。但是,您想以某种方式对op进行更改。

一般来说,对现有的签入规范的更改必须是向后兼容的:更改操作规范不能破坏GraphDef由旧规范构建的先前的序列化协议缓冲区。的细节GraphDef兼容性是 这里描述

有几种保留向后兼容性的方法。

  1. 添加到操作中的任何新attrs必须定义默认值,并且使用该默认值,op必须具有原始行为。要将操作从不多态转换为多态,您必须为新类型attr赋予一个默认值,以便默认保留原始签名。例如,如果您的操作是:

    c++ REGISTER_OP("MyGeneralUnaryOp") .Input("in: float") .Output("out: float");

您可以使用以下向后兼容的方式使其变为多态:

c++ REGISTER_OP("MyGeneralUnaryOp") .Input("in: T") .Output("out: T") .Attr("T: numerictype = DT_FLOAT");

 

  1. 您可以安全地对attr做出限制,限制较少。例如,您可以从或更改{int32, int64}为。或者,你可以从改变到或。{int32, int64, float} type {"apple", "orange"} {"apple", "banana", "orange"} string 

  2. 只要列表类型的默认值与旧签名相匹配,您可以将单个输入/输出更改为列表输入/输出。

  3. 如果默认为空,可以添加新的列表输入/输出。

  4. 通过将op名称前缀到您的项目独有的名称,命名空间您创建的任何新操作。这样做避免了您的操作与任何可能包含在未来版本的TensorFlow中的操作相冲突。

  5. 未雨绸缪!尝试预测未来的用途。某些签名更改无法以兼容的方式进行(例如,将相同类型的列表列入不同类型的列表)。

安全和不安全更改的完整列表可以在中找到tensorflow/core/framework/op_compatibility_test.cc。如果您无法使向后兼容的操作更改,则使用新的语义创建一个新名称的新操作。

另请注意,虽然这些更改可以保持GraphDef兼容性,但生成的Python代码可能会以与旧的调用者不兼容的方式更改。通过保留旧签名,除了可能添加新的可选参数,Python API可能会通过手写的Python包装器中的仔细更改来保持兼容。一般不兼容的更改只能在TensorFlow更改主要版本时进行,并且必须符合GraphDef版本语义

GPU支持

您可以实现不同的OpKernels并注册一个CPU,另一个用于GPU,就像您可以注册不同类型的内核一样。有几个支持GPU的内核的例子 tensorflow/core/kernels/。注意一些内核在一个.cc文件中有一个CPU版本,一个文件中的GPU版本,一个文件中_gpu.cu.cc共享的一些代码.h

例如,tf.pad拥有一切,但在GPU内核tensorflow/core/kernels/pad_op.cc。GPU内核在tensorflow/core/kernels/pad_op_gpu.cu.cc,而共享代码是一个定义的模板类tensorflow/core/kernels/pad_op.h。我们以这种方式组织代码有两个原因:它允许您在CPU和GPU实现之间共享通用代码,并将GPU实现放在单独的文件中,以便只能由GPU编译器进行编译。

有一件事要注意,即使使用GPU内核版本pad,仍然需要"paddings"在CPU内存中输入。要将输入或输出标记在CPU上,请添加HostMemory()对内核注册的调用,例如:

#define REGISTER_GPU_KERNEL(T)                         \
  REGISTER_KERNEL_BUILDER(Name("Pad")                  \
                              .Device(DEVICE_GPU)      \
                              .TypeConstraint<T>("T")  \
                              .HostMemory("paddings"), \
                          PadOp<GPUDevice, T>) 

编译GPU设备的内核

看一下 cuda_op_kernel.cu.cc ,使用一个CUDA内核来实现一个操作。在 tf_custom_op_library接受一个gpu_srcs在其中含有CUDA内核(源文件的列表参数*.cu.cc可指定文件)。要使用TensorFlow的二进制安装,CUDA内核必须使用NVIDIA的nvcc编译器进行编译。以下是可以将cuda_op_kernel.cu.cc和 cuda_op_kernel.cc编译 为单个可动态加载库的命令序列 :

nvcc -std=c++11 -c -o cuda_op_kernel.cu.o cuda_op_kernel.cu.cc \
-I $TF_INC -D GOOGLE_CUDA=1 -x cu -Xcompiler -fPIC

g++ -std=c++11 -shared -o cuda_op_kernel.so cuda_op_kernel.cc \
cuda_op_kernel.cu.o -I $TF_INC -fPIC -lcudart 
cuda_op_kernel.so可以像往常一样在Python中加载,使用该 tf.load_op_library功能。

请注意,如果未安装CUDA库/usr/local/lib64,则需要在上面的第二个(g ++)命令中明确指定路径。例如,-L /usr/local/cuda-8.0/lib64/如果您的CUDA已安装 ,请添加/usr/local/cuda-8.0

在Python中实现渐变

给定一个操作图,TensorFlow使用自动差分(反向传播)来添加表示相对于现有操作的渐变的新操作(参见 渐变计算)。为了对新操作进行自动差异化,您必须注册梯度函数,该梯度函数相对于给定梯度的运算输入相对于运算输出计算梯度。

数学上,如果一个操作计算  ÿ=F(X) 注册梯度op转换梯度  ∂大号/∂ÿ 的损失  大号 关于  ÿ 渐变  ∂大号/∂X 关于  X 通过链条规则:

 

在输入的情况下ZeroOut只有一个条目会影响输出,所以相对于输入的渐变是一个稀疏的“一个热”张量。这表示如下:

from tensorflow.python.framework import ops
from tensorflow.python.ops import array_ops
from tensorflow.python.ops import sparse_ops

@ops.RegisterGradient("ZeroOut")
def _zero_out_grad(op, grad):
 """The gradients for `zero_out`.

 Args:
 op: The `zero_out` `Operation` that we are differentiating, which we can use
 to find the inputs and outputs of the original op.
 grad: Gradient with respect to the output of the `zero_out` op.

 Returns:
 Gradients with respect to the input of `zero_out`.
 """
 to_zero = op.inputs[0]
 shape = array_ops.shape(to_zero)
 index = array_ops.zeros_like(shape)
 first_grad = array_ops.reshape(grad, [-1])[0]
 to_zero_grad = sparse_ops.sparse_to_dense([index], shape, first_grad, 0)
 return [to_zero_grad] # List of one Tensor, since we have one input
关于使用以下功能注册渐变功能的详细信息 tf.RegisterGradient
  • 对于具有一个输出运算,梯度函数将采取 以及 和建立新的OPS出张量 , 和。有关任何attrs的信息可以通过 。tf.Operation op tf.Tensorgrad op.inputs[i] op.outputs[i] grad tf.Operation.get_attr 

  • 如果运具有多个输出,梯度函数将采取op和 grads,其中grads是梯度相对于各个输出的列表。梯度函数的结果必须是表示Tensor相对于每个输入的梯度的对象的列表。

  • 如果对于某些输入没有明确定义的梯度,例如用作索引的整数输入,则相应的返回梯度应为None。例如,对于使用浮点张量x和整数索引i,渐变函数将会return [x_grad, None]

  • 如果op没有任何有意义的渐变,你通常不必注册任何渐变,只要op的渐变不再需要,你会很好。在某些情况下,op没有明确定义的梯度,但可以参与梯度的计算。这里可以ops.NotDifferentiable用来自动向后传播零。

请注意,在调用梯度函数时,只有ops的数据流图可用,而不是张量数据本身。因此,所有计算都必须使用其他张量流op执行,以在图形执行时运行。

C ++中的形状函数

TensorFlow API具有称为“形状推断”的功能,它提供有关张量的形状的信息,而不必执行图形。形状推断由C ++ REGISTER_OP声明中每个op类型注册的“形状函数” 支持,并执行两个角色:在图形构造期间声明输入的形状是兼容的,并指定输出的形状。

形状函数定义为类上的操作 shape_inference::InferenceContext。例如,在ZeroOut的形状函数中:

.SetShapeFn([](::tensorflow::shape_inference::InferenceContext* c) {
 c->set_output(0, c->input(0));
 return Status::OK();
 });
c->set_output(0, c->input(0));声明第一个输出的形状应该被设置为第一个输入的形状。如果输出是按照上述示例的索引选择的,则第二个参数set_output应该是一个ShapeHandle对象。您可以ShapeHandle通过其默认构造函数创建一个空对象。ShapeHandle具有索引的输入的对象idx可以通过获得c->input(idx)

有许多常见的形状函数适用于许多操作,例如shape_inference::UnchangedShape可以在common_shape_fns.h找到并使用如下:

REGISTER_OP("ZeroOut")
 .Input("to_zero: int32")
 .Output("zeroed: int32")
 .SetShapeFn(::tensorflow::shape_inference::UnchangedShape);
形状函数也可以约束输入的形状。对于ZeroOut具有向量形状约束的版本 ,形状函数将如下所示:
.SetShapeFn([](::tensorflow::shape_inference::InferenceContext* c) {
 ::tensorflow::shape_inference::ShapeHandle input;
 TF_RETURN_IF_ERROR(c->WithRank(c->input(0), 1, &input));
 c->set_output(0, input);
 return Status::OK();
 });
WithRank呼叫将验证输入形状c->input(0)与恰好一个维度的形状(或者如果输入的形状是未知的,则输出的形状将与一个未知尺寸的向量)。

如果您的op 与多个输入是多态的,您可以使用成员InferenceContext来确定要检查的形状数量,并Merge验证形状是否兼容(或者,访问指示长度的访问属性InferenceContext::GetAttr,其中提供对属性的访问的op)。

.SetShapeFn([](::tensorflow::shape_inference::InferenceContext* c) {
 ::tensorflow::shape_inference::ShapeHandle input;
 ::tensorflow::shape_inference::ShapeHandle output;
 for (size_t i = 0; i < c->num_inputs(); ++i) {
 TF_RETURN_IF_ERROR(c->WithRank(c->input(i), 2, &input));
 TF_RETURN_IF_ERROR(c->Merge(output, input, &output));
 }
 c->set_output(0, output);
 return Status::OK();
 });
由于形状推理是可选的特征,并且张量的形状可以动态变化,形状函数必须对于任何输入的不完整形状信息是鲁棒的。该Merge方法InferenceContext 允许调用者断言两个形状是相同的,即使它们中的一个或两者没有完整的信息。形状函数定义为所有核心TensorFlow操作,并提供许多不同的用法示例。

所述InferenceContext类具有许多可被用于定义形状的功能操作的功能。例如,您可以使用InferenceContext::Dim和 验证特定维度具有非常特定的值InferenceContext::WithValue; 您可以指定输出尺寸是使用InferenceContext::Add和 的两个输入尺寸的总和/乘积InferenceContext::Multiply。看到InferenceContext你可以指定的所有各种形状操作的类。以下示例将第一个输出的形状设置为(n,3),其中第一个输入具有形状(n,...)

.SetShapeFn([](::tensorflow::shape_inference::InferenceContext* c) {
 c->set_output(0, c->Matrix(c->Dim(c->input(0), 0), 3));
 return Status::OK();
});
如果您有一个复杂的形状函数,您应该考虑添加一个测试,以验证各种输入形状组合产生预期的输出形状组合。您可以在我们的核心操作测试中看到如何写这些测试的例子 。(语法INFER_OKINFER_ERROR一些有点神秘,但是在测试中尝试表示输入和输出形状规范的紧凑性,现在看到这些测试中的周围注释,以获得形状字符串规范的感觉)。

  • 无标签