属性可以使用下面的类型:
string: 任何二进制字节流 (UTF8 不是必须的).
int: 一个有型整数.
float: 一个浮点数.
bool: 真或假.
type: DataType 非引用类型之一.
shape: 一个 TensorShapeProto.
tensor: 一个 TensorProto.
list(<type>): <type> 列表, 其中 <type> 是上述类型之一. 注意 list(list(<type>)) 是无效的.
权威的列表以 op_def_builder.cc:FinalizeAttr 为准.
属性可能有默认值, 一些类型的属性可以有约束条件. 为了定义一个有约束条件的属性, 你可以使用下列的 <attr-type-expr> 形式:
{'<string1>', '<string2>'}: 属性值必须是一个字符串, 取值可以为 <string1> 或 <string2>. 值的语法已经暗示了值的类型为 string, 已经暗示了. 下述语句模拟了一个枚举值:
REGISTER_OP("EnumExample")
.Attr("e: {'apple', 'orange'}");
{<type1>, <type2>}: 值是 type 类型, 且必须为 <type1> 或 <type2> 之一, 当然 <type1> 和 <type2> 必须都是有效的 tensor 类型. 你无须指定属性的类型为 type, 而是通过 {...} 语句给出一个类型列表. 例如, 在下面的例子里, 属性 t 的类型必须为 int32, float, 或 bool:
REGISTER_OP("RestrictedTypeExample")
.Attr("t: {int32, float, bool}");
这里有一些常见类型约束条件的快捷方式:
numbertype: 限制类型为数字类型, 即非 string 非 bool 的类型.
realnumbertype: 与 numbertype 区别是不支持复杂类型.
quantizedtype: 与 numbertype 区别是只支持量化数值 (quantized number type).
这些类型的列表在 tensorflow/core/framework/types.h 文件中通过函数定义 (如 NumberTypes()). 本例中属性 t 必须为某种数字类型:
REGISTER_OP("NumberType")
.Attr("t: numbertype");
对于这个 Op:
tf.number_type(t=tf.int32) # 有效 tf.number_type(t=tf.bool) # 无效
int >= <n>: 值必须是一个整数, 且取值大于等于 <n>, <n> 是一个自然数.
例如, 下列 Op 注册操作指定了属性 a 的取值至少为 2.
REGISTER_OP("MinIntExample")
.Attr("a: int >= 2");
list(<type>) >= <n>: 一个 <type> 类型列表, 列表长度必须大于等于 <n>.
例如, 下面的 Op 注册操作指定属性 a 是一个列表, 列表中的元素类型是 int32 或 float列表长度至少为3.
REGISTER_OP("TypeListExample")
.Attr("a: list({int32, float}) >= 3");
通过添加 = <default> 到约束条件末尾, 给一个属性设置默认值 (使其在自动生成的代码里 变成可选属性), 如下:
REGISTER_OP("AttrDefaultExample")
.Attr("i: int = 0");
默认值支持的语法将在最终 GraphDef 定义的 protobuf 表示中被使用.
下面是给所有类型赋予默认值的例子:
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]");
请特别注意那些类型值里面包含的 DT_* 名称.
对于那些可以使用不同类型输入或产生不同类型输出的 Op, 可以注册 Op 时为输入/输出类型里指定一个属性. 一般紧接着, 会为每一个支持的类型注册一个 OpKernel.
例如, 除了 int32 外, 想要 ZeroOut Op 支持 float, 注册代码如下:
REGISTER_OP("ZeroOut")
.Attr("T: {float, int32}")
.Input("to_zero: <b>T</b>")
.Output("zeroed: <b>T</b>");
这段 Op 注册代码现在指定了输入的类型必须为 float 或 int32, 而且 既然输入和输出制定了同样的类型 T, 输出也同样如此.
def zero_out(to_zero, name=None):
"""...
参数:
to_zero: 一个 `Tensor`. 必须为下列类型之一:
`float32`, `int32`.
name: 操作的名字 (可选).
返回值:
一个 `Tensor`, 类型和 `to_zero` 一样.
"""
如果输入的 to_zero 是一个 int32 的tensor, 然后 T 将被自动 设置为 int32 (实际上是 DT_INT32). 那些推导出的属性的名称字母全大写 或采用驼峰命名法.
下面是一个输出类型自动推断的例子, 读者可以对比一下:
REGISTER_OP("StringToNumber")
.Input("string_tensor: string")
.Output("output: out_type")
.Attr("out_type: {float, int32}");
.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):
"""将输入 Tensor 中的每一个字符串转化成指定的数字类型
参数:
string_tensor: 一个 `string` 类型的 `Tensor`.
out_type: 一个可选的 `tf.DType`, 取值为 `tf.float32, tf.int32`.
默认值是 `tf.float32`.
name: 操作的名称 (可选).
返回值:
一个 `out_type` 类型的 `Tensor`.
"""
#include "tensorflow/core/framework/op_kernel.h"
class ZeroOutInt32Op : public OpKernel {
// 和之前一样
};
class ZeroOutFloatOp : public OpKernel {
public:
explicit ZeroOutFloatOp(OpKernelConstruction * context)
: OpKernel(context) {}
void Compute(OpKernelContext * context) override {
// 获取输入 tensor
const Tensor& input_tensor = context->input(0);
auto input = input_tensor.flat<float>();
// 创建一个输出 tensor
Tensor * output = NULL;
OP_REQUIRES_OK(context,
context->allocate_output(0, input_tensor.shape(), &output));
auto output_flat = output->template flat<float>();
// 设置输出 tensor 的所有元素为 0
const int N = input.size();
for (int i = 0; i < N; i++) {
output_flat(i) = 0;
}<br/>
// 保留第一个输入值
if (N > 0) output_flat(0) = input(0);
}
};
// 注意, TypeConstraint<int32>("T") 意味着属性 "T" (在上面 Op 注册代码中
// 定义的) 必须是 "int32", 才能实例化.
REGISTER_KERNEL_BUILDER(
Name("ZeroOut")
.Device(DEVICE_CPU)
.TypeConstraint<int32>("T"),
ZeroOutOpInt32);
REGISTER_KERNEL_BUILDER(
Name("ZeroOut")
.Device(DEVICE_CPU)
.TypeConstraint<float>("T"),
ZeroOutFloatOp);
为了保持向后兼容性, 你在为一个 已有的 op 添加属性时, 必须指定一个默认值:
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++ 模板作为替代. 当然, 仍然需要为每一个重载版本定义一个 keneral 注册 (REGISTER\_KERNEL\_BUILDER 调用).
template <typename T>;
class ZeroOutOp : public OpKernel {
public:
explicit ZeroOutOp(OpKernelConstruction* context) : OpKernel(context) {}
void Compute(OpKernelContext* context) override {
// 获取输入 tensor
const Tensor& input_tensor = context->input(0);
auto input = input_tensor.flat<T>();
// 创建一个输出 tensor
Tensor* output = NULL;
OP_REQUIRES_OK(context,
context->allocate_output(0, input_tensor.shape(), &output));
auto output_flat = output->template flat<T>();
// 设置输出 tensor 的所有元素为 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);
}
};
};<br/>
// 注意, TypeConstraint<int32>("T") 意味着属性 "T" (在上面 Op 注册代码中
// 定义的) 必须是 "int32", 才能实例化. </b>
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
取决于注册 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
列表输入和输出
除了能够使用不同类型的 tensor 作为输入或输出, Op 还支持使用多个 tensor 作为输入或输出.
在接下来的例子里, 属性 T 存储了一个类型列表, 并同时作为输入 in 和输出 out 的类型. 输入和输出均为指定类型的 tensor 列表. 既然输入和输出的类型均为 T, 它们的 tensor 数量和类型 是一致的.
REGISTER_OP("PolymorphicListExample")
.Attr("T: list(type)")
.Input("in: T")
.Output("out: T");
可以为列表中可存放的类型设置约束条件. 在下一个例子中, 输入是 float 和 double 类型的 tensor 列表. 例如, 这个 Op 可接受的 输入类型为 (float, double, float) 的数据, 且在此情况下, 输出类型同样 为 (float, double, float).
REGISTER_OP("ListTypeRestrictionExample")
.Attr("T: list({float, double})")
.Input("in: T")
.Output("out: T");
如果想要一个列表中的所有 tensor 是同一类型, 你需要写下列代码:
REGISTER_OP("IntListInputExample")
.Attr("N: int")
.Input("in: N * int32")
.Output("out: int32");
这段代码接受 int32 tensor 列表, 并用一个 int 属性 N 来指定列表的长度.
这也可用于类型推断. 在下一个例子中, 输入是一个 tensor 列表, 长度为 "N", 类型为 "T", 输出是单个 "T" 的 tensor:
REGISTER_OP("SameListInputExample")
.Attr("N: int")
.Attr("T: type")
.Input("in: N * T")
.Output("out: T");
默认情况下, tensor 列表的最小长度为1. 这个约束条件可以通过 为指定的属性增加一个 ">=" 约束来变更:
REGISTER_OP("MinLengthIntListExample")
.Attr("N: int >= 2")
.Input("in: N * int32")
.Output("out: int32");
同样的语法也适用于 "list(type)" 属性:
REGISTER_OP("MinimumLengthPolymorphicListExample")
.Attr("T: list(type) >= 3")
.Input("in: T")
.Output("out: T");