ช่วยปกป้อง Great Barrier Reef กับ TensorFlow บน Kaggle เข้าร่วมท้าทาย

สร้างop

หากคุณต้องการสร้าง op ที่ไม่อยู่ในไลบรารี TensorFlow ที่มีอยู่ เราขอแนะนำให้คุณลองเขียน op ใน Python เป็นองค์ประกอบของ Python ops หรือฟังก์ชันที่มีอยู่ก่อน หากไม่สามารถทำได้ คุณสามารถสร้าง C++ op แบบกำหนดเองได้ มีสาเหตุหลายประการที่คุณอาจต้องการสร้าง C++ op แบบกำหนดเอง:

  • การแสดงการดำเนินการของคุณเป็นองค์ประกอบของ ops ที่มีอยู่นั้นไม่ใช่เรื่องง่ายหรือเป็นไปไม่ได้
  • การแสดงการดำเนินการของคุณเป็นองค์ประกอบของพื้นฐานที่มีอยู่นั้นไม่มีประสิทธิภาพ
  • คุณต้องการรวมองค์ประกอบของ primitive ด้วยมือซึ่งคอมไพเลอร์ในอนาคตจะพบว่าการหลอมรวมทำได้ยาก

ตัวอย่างเช่น สมมติว่าคุณต้องการใช้บางอย่าง เช่น "การรวมค่ามัธยฐาน" ซึ่งคล้ายกับโอเปอเรเตอร์ "MaxPool" แต่ให้คำนวณค่ามัธยฐานเหนือหน้าต่างบานเลื่อนแทนค่าสูงสุด การทำเช่นนี้โดยใช้องค์ประกอบของการดำเนินการอาจเป็นไปได้ (เช่น การใช้ ExtractImagePatches และ TopK) แต่อาจไม่มีประสิทธิภาพหรือประสิทธิภาพหน่วยความจำเท่ากับการดำเนินการดั้งเดิม ซึ่งคุณสามารถทำสิ่งที่ฉลาดกว่าในการดำเนินการเดียวที่หลอมรวม และเช่นเคย โดยทั่วไปแล้ว อันดับแรก ควรพยายามแสดงสิ่งที่คุณต้องการโดยใช้องค์ประกอบของตัวดำเนินการ โดยเลือกให้เพิ่มการดำเนินการใหม่ก็ต่อเมื่อพบว่ายากหรือไม่มีประสิทธิภาพ

ในการรวม op ที่กำหนดเอง คุณจะต้อง:

  1. ลงทะเบียน op ใหม่ในไฟล์ C ++ การลงทะเบียน Op กำหนดอินเทอร์เฟซ (ข้อกำหนด) สำหรับฟังก์ชันการทำงานของ op ซึ่งเป็นอิสระจากการใช้งานของ op ตัวอย่างเช่น การลงทะเบียน op กำหนดชื่อของ op และอินพุตและเอาต์พุตของ op นอกจากนี้ยังกำหนดฟังก์ชันรูปร่างที่ใช้สำหรับการอนุมานรูปร่างเทนเซอร์
  2. ใช้ op ใน C ++ การนำ op ไปใช้นั้นเรียกว่าเคอร์เนล และเป็นการใช้งานที่เป็นรูปธรรมของข้อกำหนดที่คุณลงทะเบียนในขั้นตอนที่ 1 อาจมีเคอร์เนลหลายตัวสำหรับประเภทอินพุต / เอาต์พุตหรือสถาปัตยกรรมที่แตกต่างกัน (เช่น CPU, GPU)
  3. สร้างเครื่องห่อ Python (ไม่บังคับ) Wrapper นี้เป็น API สาธารณะที่ใช้สร้าง op ใน Python Wrapper เริ่มต้นถูกสร้างขึ้นจากการลงทะเบียน op ซึ่งสามารถใช้ได้โดยตรงหรือเพิ่มเข้าไป
  4. เขียนฟังก์ชันเพื่อคำนวณการไล่ระดับสีสำหรับ op (ไม่บังคับ)
  5. ทดสอบ op เรามักจะทำเช่นนี้ใน Python เพื่อความสะดวก แต่คุณสามารถทดสอบ op ใน C ++ ได้ หากคุณกำหนดไล่ระดับสีคุณสามารถตรวจสอบพวกเขาด้วยงูหลาม tf.test.compute_gradient_error ดู relu_op_test.py เป็นตัวอย่างที่ทดสอบฟังก์ชั่นข้าง Relu เหมือนผู้ประกอบการและการไล่ระดับสีของพวกเขา

ข้อกำหนดเบื้องต้น

กำหนดอินเทอร์เฟซ op

คุณกำหนดอินเทอร์เฟซของ op โดยการลงทะเบียนกับระบบ TensorFlow ในการลงทะเบียนคุณระบุชื่อของสหกรณ์ของคุณของปัจจัยการผลิต (ประเภทและชื่อ) และเอาท์พุท (ประเภทและชื่อ) เช่นเดียวกับ docstrings และใด ๆ attrs สหกรณ์อาจจำเป็นต้องใช้

เพื่อดูว่างานนี้สมมติว่าคุณต้องการสร้างสหกรณ์ที่ใช้เมตริกซ์ของหนึ่ง int32 และเอาท์พุทสำเนาของเมตริกซ์ที่มี แต่ชุดองค์ประกอบแรกที่จะเป็นศูนย์ การทำเช่นนี้สร้างไฟล์ชื่อ zero_out.cc แล้วเพิ่มการเรียกร้องให้มี REGISTER_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_zero ของจำนวนเต็ม 32 บิตเป็น input และผลเมตริกซ์ zeroed ของจำนวนเต็ม 32 บิต op ยังใช้ฟังก์ชันรูปร่างเพื่อให้แน่ใจว่าเทนเซอร์เอาต์พุตจะมีรูปร่างเดียวกับเทนเซอร์อินพุต ตัวอย่างเช่น หากอินพุตเป็นเทนเซอร์ของรูปร่าง [10, 20] ฟังก์ชันรูปร่างนี้จะระบุว่ารูปร่างเอาต์พุตเป็น [10, 20] ด้วย

ใช้เคอร์เนลสำหรับop

หลังจากที่คุณกำหนดอินเทอร์เฟซแล้ว ให้จัดเตรียมการใช้งาน 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_flat = 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_flat(i) = 0;
    }

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

หลังจากติดตั้งเคอร์เนลแล้ว คุณต้องลงทะเบียนกับระบบ TensorFlow ในการลงทะเบียน คุณระบุข้อจำกัดที่แตกต่างกันซึ่งเคอร์เนลนี้จะรัน ตัวอย่างเช่น คุณอาจมีหนึ่งเคอร์เนลที่สร้างขึ้นสำหรับ CPU และอีกอันสำหรับ GPU

การทำเช่นนี้สำหรับ ZeroOut สหกรณ์เพิ่มต่อไปนี้ zero_out.cc :

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

เคอร์เนล CPU แบบมัลติเธรด

การเขียนเคอร์เนลซีพียูแบบมัลติเธรด, ฟังก์ชั่นที่แตกออกมาใน work_sharder.h สามารถนำมาใช้ นี้เศษฟังก์ชั่นฟังก์ชั่นการคำนวณข้ามหัวข้อที่กำหนดค่าที่จะใช้สำหรับภายใน-op เกลียว (ดู intra_op_parallelism_threads ใน config.proto )

เมล็ด GPU

เคอร์เนล GPU ถูกนำไปใช้ในสองส่วน: OpKernel และเคอร์เนล CUDA และโค้ดเรียกใช้

บางครั้งการนำ OpKernel ไปใช้นั้นเป็นเรื่องปกติระหว่างเคอร์เนล CPU และ GPU เช่น การตรวจสอบอินพุตและการจัดสรรเอาต์พุต ในกรณีนั้น การดำเนินการที่แนะนำคือ:

  1. กำหนดเทมเพลต OpKernel บนอุปกรณ์และประเภทดั้งเดิมของเทนเซอร์
  2. ในการคำนวณเอาท์พุตจริง ฟังก์ชัน Compute จะเรียกโครงสร้าง templated functor
  3. ความเชี่ยวชาญพิเศษของ functor นั้นสำหรับ CPUDevice ถูกกำหนดในไฟล์เดียวกัน แต่ความเชี่ยวชาญพิเศษสำหรับ GPUDevice ถูกกำหนดในไฟล์ .cu.cc เนื่องจากจะถูกคอมไพล์ด้วยคอมไพเลอร์ CUDA

นี่คือตัวอย่างการใช้งาน

// kernel_example.h
#ifndef KERNEL_EXAMPLE_H_
#define KERNEL_EXAMPLE_H_

#include <unsupported/Eigen/CXX11/Tensor>

template <typename Device, typename T>
struct ExampleFunctor {
  void operator()(const Device& d, int size, const T* in, T* out);
};

#if GOOGLE_CUDA
// Partially specialize functor for GpuDevice.
template <typename T>
struct ExampleFunctor<Eigen::GpuDevice, T> {
  void operator()(const Eigen::GpuDevice& d, int size, const T* in, T* out);
};
#endif

#endif KERNEL_EXAMPLE_H_
// kernel_example.cc
#include "kernel_example.h"

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

using namespace tensorflow;

using CPUDevice = Eigen::ThreadPoolDevice;
using GPUDevice = Eigen::GpuDevice;

REGISTER_OP("Example")
    .Attr("T: numbertype")
    .Input("input: T")
    .Output("input_times_two: T")
    .SetShapeFn([](::tensorflow::shape_inference::InferenceContext* c) {
      c->set_output(0, c->input(0));
      return Status::OK();
    });

// CPU specialization of actual computation.
template <typename T>
struct ExampleFunctor<CPUDevice, T> {
  void operator()(const CPUDevice& d, int size, const T* in, T* out) {
    for (int i = 0; i < size; ++i) {
      out[i] = 2 * in[i];
    }
  }
};

// OpKernel definition.
// template parameter <T> is the datatype of the tensors.
template <typename Device, typename T>
class ExampleOp : public OpKernel {
 public:
  explicit ExampleOp(OpKernelConstruction* context) : OpKernel(context) {}

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

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

    // Do the computation.
    OP_REQUIRES(context, input_tensor.NumElements() <= tensorflow::kint32max,
                errors::InvalidArgument("Too many elements in tensor"));
    ExampleFunctor<Device, T>()(
        context->eigen_device<Device>(),
        static_cast<int>(input_tensor.NumElements()),
        input_tensor.flat<T>().data(),
        output_tensor->flat<T>().data());
  }
};

// Register the CPU kernels.
#define REGISTER_CPU(T)                                          \
  REGISTER_KERNEL_BUILDER(                                       \
      Name("Example").Device(DEVICE_CPU).TypeConstraint<T>("T"), \
      ExampleOp<CPUDevice, T>);
REGISTER_CPU(float);
REGISTER_CPU(int32);

// Register the GPU kernels.
#ifdef GOOGLE_CUDA
#define REGISTER_GPU(T)                                          \
  /* Declare explicit instantiations in kernel_example.cu.cc. */ \
  extern template class ExampleFunctor<GPUDevice, T>;            \
  REGISTER_KERNEL_BUILDER(                                       \
      Name("Example").Device(DEVICE_GPU).TypeConstraint<T>("T"), \
      ExampleOp<GPUDevice, T>);
REGISTER_GPU(float);
REGISTER_GPU(int32);
#endif  // GOOGLE_CUDA
// kernel_example.cu.cc
#ifdef GOOGLE_CUDA
#define EIGEN_USE_GPU
#include "kernel_example.h"
#include "tensorflow/core/util/gpu_kernel_helper.h"

using namespace tensorflow;

using GPUDevice = Eigen::GpuDevice;

// Define the CUDA kernel.
template <typename T>
__global__ void ExampleCudaKernel(const int size, const T* in, T* out) {
  for (int i = blockIdx.x * blockDim.x + threadIdx.x; i < size;
       i += blockDim.x * gridDim.x) {
    out[i] = 2 * __ldg(in + i);
  }
}

// Define the GPU implementation that launches the CUDA kernel.
template <typename T>
void ExampleFunctor<GPUDevice, T>::operator()(
    const GPUDevice& d, int size, const T* in, T* out) {
  // Launch the cuda kernel.
  //
  // See core/util/gpu_kernel_helper.h for example of computing
  // block count and thread_per_block count.
  int block_count = 1024;
  int thread_per_block = 20;
  ExampleCudaKernel<T>
      <<<block_count, thread_per_block, 0, d.stream()>>>(size, in, out);
}

// Explicitly instantiate functors for the types of OpKernels registered.
template struct ExampleFunctor<GPUDevice, float>;
template struct ExampleFunctor<GPUDevice, int32>;

#endif  // GOOGLE_CUDA

สร้างห้องสมุดสหกรณ์

รวบรวม op โดยใช้คอมไพเลอร์ระบบของคุณ (การติดตั้งไบนารี TensorFlow)

คุณควรจะสามารถที่จะรวบรวม zero_out.cc กับ C++ คอมไพเลอร์เช่น g++ หรือ clang ที่มีอยู่บนระบบของคุณ แพ็คเกจไบนารี PIP จะติดตั้งไฟล์ส่วนหัวและไลบรารีที่คุณต้องการรวบรวม op ของคุณในตำแหน่งเฉพาะระบบ อย่างไรก็ตามห้องสมุด TensorFlow หลามให้ get_include ฟังก์ชั่นที่จะได้รับไดเรกทอรีส่วนหัวและ get_lib ไดเรกทอรีมีวัตถุที่ใช้ร่วมกันที่จะเชื่อมโยงกับ นี่คือผลลัพธ์ของฟังก์ชันเหล่านี้บนเครื่อง Ubuntu

$ python
>>> import tensorflow as tf
>>> tf.sysconfig.get_include()
'/usr/local/lib/python3.6/site-packages/tensorflow/include'
>>> tf.sysconfig.get_lib()
'/usr/local/lib/python3.6/site-packages/tensorflow'

สมมติว่าคุณมี g++ ติดตั้งที่นี่เป็นลำดับของคำสั่งที่คุณสามารถใช้เพื่อรวบรวม op ของคุณลงในห้องสมุดแบบไดนามิกที่

TF_CFLAGS=( $(python -c 'import tensorflow as tf; print(" ".join(tf.sysconfig.get_compile_flags()))') )
TF_LFLAGS=( $(python -c 'import tensorflow as tf; print(" ".join(tf.sysconfig.get_link_flags()))') )
g++ -std=c++14 -shared zero_out.cc -o zero_out.so -fPIC ${TF_CFLAGS[@]} ${TF_LFLAGS[@]} -O2

บน MacOS ธงเพิ่มเติม "dynamic_lookup -undefined" ถูกต้องเมื่อมีการสร้าง .so ไฟล์

หมายเหตุเกี่ยวกับ gcc รุ่น >=5 : GCC ใช้ c ++ ใหม่ ABI ตั้งแต่รุ่น 5 แพคเกจ pip ไบนารีมีอยู่บนเว็บไซต์ TensorFlow จะถูกสร้างขึ้นด้วย gcc4 ที่ใช้เก่า ABI หากคุณรวบรวมห้องสมุด op ของคุณกับ gcc>=5 เพิ่ม -D_GLIBCXX_USE_CXX11_ABI=0 บรรทัดคำสั่งที่จะทำให้ห้องสมุดเข้ากันได้กับ ABI เก่า

รวบรวม op โดยใช้ bazel (การติดตั้งซอร์ส TensorFlow)

หากคุณติดตั้งแหล่งที่มาของ TensorFlow คุณสามารถใช้ระบบบิลด์ของ TensorFlow เพื่อคอมไพล์ op ของคุณได้ วางแฟ้ม BUILD ที่มีดังต่อไปนี้การสร้างกฎ Bazel ใน 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

สำหรับการรวบรวม Example การดำเนินงานกับ CUDA Kernel คุณจำเป็นต้องใช้ gpu_srcs พารามิเตอร์ของ tf_custom_op_library วางแฟ้ม BUILD กับ Bazel สร้างกฎต่อไปนี้ในโฟลเดอร์ใหม่ภายใน tensorflow/core/user_ops ไดเรกทอรี (เช่น "example_gpu")

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

tf_custom_op_library(
    # kernel_example.cc  kernel_example.cu.cc  kernel_example.h
    name = "kernel_example.so",
    srcs = ["kernel_example.h", "kernel_example.cc"],
    gpu_srcs = ["kernel_example.cu.cc", "kernel_example.h"],
)

เรียกใช้คำสั่งต่อไปนี้เพื่อสร้าง kernel_example.so

$ bazel build --config opt //tensorflow/core/user_ops/example_gpu:kernel_example.so

ใช้ op ใน Python

TensorFlow หลาม API ให้ tf.load_op_library ฟังก์ชั่นในการโหลดแบบไดนามิกห้องสมุดและลงทะเบียนสหกรณ์ที่มีกรอบ TensorFlow load_op_library ผลตอบแทนโมดูลหลามที่มีห่อหลามสำหรับสหกรณ์และเคอร์เนล ดังนั้น เมื่อคุณสร้าง op แล้ว คุณสามารถทำสิ่งต่อไปนี้เพื่อเรียกใช้จาก Python:

import tensorflow as tf
zero_out_module = tf.load_op_library('./zero_out.so')
print(zero_out_module.zero_out([[1, 2], [3, 4]]).numpy())

# Prints
array([[1, 0], [0, 0]], dtype=int32)

เก็บไว้ในใจ, ฟังก์ชั่นที่สร้างขึ้นจะได้รับชื่อ snake_case A (เพื่อให้สอดคล้องกับ PEP8 ) ดังนั้นถ้า op ของคุณเป็นชื่อ ZeroOut ในไฟล์ C ++, ฟังก์ชั่นหลามจะถูกเรียกว่า zero_out

เพื่อให้สหกรณ์สามารถใช้ได้เป็นฟังก์ชั่นปกติ import น่าจากโมดูลหลามมันมีประโยชน์อาจจะมี load_op_library โทรในแฟ้มแหล่งที่มาหลามดังนี้

import tensorflow as tf

zero_out_module = tf.load_op_library('./zero_out.so')
zero_out = zero_out_module.zero_out

ตรวจสอบว่า op ทำงาน

วิธีที่ดีในการตรวจสอบว่าคุณใช้งาน op ได้สำเร็จคือการเขียนการทดสอบ สร้างไฟล์ 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()

จากนั้นรันการทดสอบของคุณ (สมมติว่าคุณติดตั้งเทนเซอร์โฟลว์):

$ python zero_out_op_test.py

สร้างคุณสมบัติขั้นสูงในop .ของคุณ

ตอนนี้คุณรู้วิธีสร้าง op และการใช้งานพื้นฐาน (และค่อนข้างจำกัด) แล้ว เราจะมาดูสิ่งที่ซับซ้อนกว่าที่คุณมักจะต้องใช้เพื่อสร้างใน op ของคุณ ซึ่งรวมถึง:

การตรวจสอบตามเงื่อนไขและการตรวจสอบ

ตัวอย่างข้างต้นถือว่า op ใช้กับเทนเซอร์ของรูปร่างใดๆ เกิดอะไรขึ้นถ้ามันใช้กับเวกเตอร์เท่านั้น? นั่นหมายถึงการเพิ่มการตรวจสอบในการใช้งาน 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 แมโคร ใช้เวลาสามข้อโต้แย้ง:

  • context ซึ่งสามารถเป็น OpKernelContext หรือ OpKernelConstruction ชี้ (ดู tensorflow/core/framework/op_kernel.h ) สำหรับ SetStatus() วิธีการ
  • เงื่อนไข. ตัวอย่างเช่นมีฟังก์ชั่นสำหรับการตรวจสอบรูปร่างของเมตริกซ์ใน tensorflow/core/framework/tensor_shape.h
  • ข้อผิดพลาดของตัวเองซึ่งเป็นตัวแทนจาก Status วัตถุดู tensorflow/core/lib/core/status.h Status มีทั้งประเภท (บ่อย InvalidArgument แต่ดูรายการประเภท) และข้อความ ฟังก์ชั่นสำหรับการสร้างข้อผิดพลาดอาจพบได้ใน tensorflow/core/lib/core/errors.h

หรือถ้าคุณต้องการที่จะทดสอบว่า Status วัตถุกลับมาจากฟังก์ชั่นบางอย่างเป็นข้อผิดพลาดและถ้าเป็นเช่นนั้นกลับมาใช้ OP_REQUIRES_OK มาโครทั้งสองนี้กลับจากฟังก์ชันเมื่อเกิดข้อผิดพลาด

Op การลงทะเบียน

Attrs

Ops สามารถมี attrs ได้ ซึ่งค่าจะถูกตั้งค่าเมื่อมีการเพิ่ม op ลงในกราฟ สิ่งเหล่านี้ใช้เพื่อกำหนดค่า op และค่าสามารถเข้าถึงได้ทั้งภายในการใช้งานเคอร์เนลและในประเภทของอินพุตและเอาต์พุตในการลงทะเบียน op ควรใช้อินพุตแทน attr เมื่อเป็นไปได้ เนื่องจากอินพุตมีความยืดหยุ่นมากกว่า นี่เป็นเพราะว่า attr เป็นค่าคงที่และต้องกำหนดในช่วงเวลาที่สร้างกราฟ ในทางตรงกันข้าม อินพุตคือเทนเซอร์ที่มีค่าสามารถเป็นไดนามิกได้ กล่าวคือ อินพุตสามารถเปลี่ยนทุกขั้นตอน ตั้งค่าโดยใช้ฟีด ฯลฯ Attr ใช้สำหรับสิ่งที่ไม่สามารถทำได้ด้วยอินพุต: การกำหนดค่าใดๆ ที่ส่งผลต่อลายเซ็น (จำนวนหรือประเภทของอินพุตหรือเอาต์พุต) หรือที่สามารถทำได้' t เปลี่ยนจากขั้นตอนเป็นขั้นตอน

คุณกำหนด attr เมื่อคุณลงทะเบียนสหกรณ์โดยระบุชื่อและประเภทการใช้ Attr วิธีการซึ่งคาดว่าสเปคของรูปแบบ:

<name>: <attr-type-expr>

ที่ <name> เริ่มต้นด้วยตัวอักษรและสามารถประกอบด้วยตัวอักษรและตัวเลขและขีดและ <attr-type-expr> คือการแสดงออกประเภทของรูปแบบ ที่อธิบายด้านล่าง

ตัวอย่างเช่นถ้าคุณต้องการให้ ZeroOut สหกรณ์เพื่อรักษาดัชนีระบุโดยผู้ใช้แทนที่จะเป็นเพียงองค์ประกอบ 0 คุณสามารถลงทะเบียนสหกรณ์เช่นดังนั้น:

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

(โปรดสังเกตว่าชุดของ ประเภทแอตทริบิวต์ จะแตกต่างจาก tf.DType ใช้สำหรับอินพุตและเอาต์พุต.)

เคอร์เนลของคุณก็จะสามารถเข้าถึง 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 : เป็น TensorShapeProto
  • list(<type>) : รายชื่อของ <type> ที่ <type> เป็นหนึ่งในประเภทดังกล่าวข้างต้น โปรดทราบว่า list(list(<type>)) ไม่ถูกต้อง

ดูเพิ่มเติม: op_def_builder.cc:FinalizeAttr สำหรับรายการที่ชัดเจน

ค่าเริ่มต้นและข้อจำกัด

Attrs อาจมีค่าเริ่มต้น และ attr บางประเภทอาจมีข้อจำกัด ต้องการกำหนด attr ที่มีข้อ จำกัด คุณสามารถใช้ต่อไป <attr-type-expr> S:

{'<string1>', '<string2>'} : ค่าต้องเป็นสตริงที่มีทั้งค่าที่ <string1> หรือ <string2> ชื่อของชนิด string เป็นนัยเมื่อคุณใช้รูปแบบนี้ สิ่งนี้เลียนแบบ enum:

REGISTER_OP("EnumExample")
    .Attr("e: {'apple', 'orange'}");

{<type1>, <type2>} : ค่าเป็นประเภท type และต้องเป็นหนึ่งใน <type1> หรือ <type2> ที่ <type1> และ <type2> ได้รับการสนับสนุน tf.DType คุณไม่ได้ระบุว่าประเภทของ attr เป็น type นี้จะบอกเป็นนัยเมื่อคุณมีรายการประเภทใน {...} } ยกตัวอย่างเช่นในกรณีนี้ attr t เป็นประเภทที่ต้องเป็น int32 เป็น float หรือ bool :

REGISTER_OP("RestrictedTypeExample")
    .Attr("t: {int32, float, bool}");

มีทางลัดสำหรับข้อจำกัดประเภททั่วไป:

  • numbertype : ประเภท type จำกัด เฉพาะตัวเลข (สตริงไม่ใช่และไม่ใช่บูล) ประเภท
  • realnumbertype : ชอบ numbertype โดยไม่ต้องประเภทที่ซับซ้อน
  • quantizedtype : ชอบ numbertype แต่เพียงชนิดจำนวนไท

รายการที่เฉพาะเจาะจงของชนิดได้รับอนุญาตจากเหล่านี้จะถูกกำหนดโดยฟังก์ชั่น (ชอบ NumberTypes() ) ใน tensorflow/core/framework/types.h ในตัวอย่างนี้ attr t จะต้องเป็นหนึ่งในประเภทที่เป็นตัวเลข:

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

สำหรับการดำเนินการนี้:

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

รายการสามารถรวมกับรายการอื่นและประเภทเดียว สหกรณ์ต่อไปนี้จะช่วยให้ attr t จะเป็นใด ๆ ของชนิดของตัวเลขหรือประเภทบูลไปนี้:

REGISTER_OP("NumberOrBooleanType")
    .Attr("t: {numbertype, bool}");

สำหรับการดำเนินการนี้:

tf.number_or_boolean_type(t=tf.int32)  # Valid
tf.number_or_boolean_type(t=tf.bool)   # Valid
tf.number_or_boolean_type(t=tf.string) # Invalid

int >= <n> : ค่าต้องเป็น int มีค่ามากกว่าหรือเท่ากับ <n> ที่ <n> เป็นจำนวนธรรมชาติ ยกตัวอย่างเช่นการลงทะเบียนสหกรณ์ระบุต่อไปว่า attr ต้องมีค่าที่มีอย่างน้อย a 2 :

REGISTER_OP("MinIntExample")
    .Attr("a: int >= 2");

list(<type>) >= <n> : รายการประเภท <type> มีความยาวมากกว่าหรือเท่ากับ <n> ยกตัวอย่างเช่นการลงทะเบียนสหกรณ์ระบุต่อไปว่า attr คือรายการของประเภท (ทั้ง a int32 หรือ float ) และจะต้องมีอย่างน้อย 3 ของพวกเขา

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

ในการตั้งค่าเริ่มต้นสำหรับ attr (ทำให้ตัวเลือกในรหัสที่สร้างขึ้น), เพิ่ม = <default> ไปที่สิ้นสุดเช่น:

REGISTER_OP("AttrDefaultExample")
    .Attr("i: int = 0");

นอกจากนี้ สามารถระบุทั้งข้อจำกัดและค่าเริ่มต้นได้:

REGISTER_OP("AttrConstraintAndDefaultExample")
    .Attr("i: int >= 1 = 1");

ไวยากรณ์ที่รองรับของค่าเริ่มต้นคือสิ่งที่จะใช้ในการแสดงโปรโตของคำจำกัดความ 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 การใช้งาน tf.DType

ความหลากหลาย

พิมพ์พหุสัณฐาน

สำหรับการปฏิบัติการที่สามารถใช้ชนิดที่แตกต่างกันเป็น input หรือผลิตประเภทแตกต่างกันออกคุณสามารถระบุ attr ใน การป้อนข้อมูลหรือผลลัพธ์ประเภท ในการลงทะเบียนสหกรณ์ โดยปกติแล้วคุณจะลงทะเบียน OpKernel สำหรับแต่ละประเภทได้รับการสนับสนุน

ตัวอย่างเช่นถ้าคุณต้องการให้ ZeroOut สหกรณ์ในการทำงานเกี่ยวกับการ float ในนอกจาก int32 s ลงทะเบียนสหกรณ์ของคุณอาจมีลักษณะดังนี้:

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

การลงทะเบียนสหกรณ์ของคุณตอนนี้ระบุว่าประเภทการป้อนข้อมูลของจะต้อง float หรือ int32 และว่าการส่งออกของมันจะเป็นชนิดเดียวกันเพราะทั้งสองมีประเภท T

การตั้งชื่อ

อินพุต เอาต์พุต และ attrs โดยทั่วไปควรกำหนดชื่อ snake_case ข้อยกเว้นประการหนึ่งคือ attrs ที่ใช้เป็นประเภทของอินพุตหรือในประเภทของเอาต์พุต สามารถอนุมานได้เมื่อเพิ่ม op ลงในกราฟ ดังนั้นจึงไม่ปรากฏในฟังก์ชันของ 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

เปรียบเทียบกับ op ที่มีประเภท attr ที่กำหนดประเภทเอาต์พุต:

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`.
  """
พิมพ์ ตัวอย่าง polymorphism
#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"),
    ZeroOutInt32Op);
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
รายการอินพุตและเอาต์พุต

นอกจากความสามารถในการยอมรับหรือผลิตประเภทต่างๆ แล้ว ops ยังสามารถใช้หรือสร้างเมตริกซ์จำนวนตัวแปรได้อีกด้วย

ในตัวอย่างถัดไป attr T ถือรายการประเภทและถูกนำมาใช้เป็นชนิดของทั้งสองที่นำเข้า in และเอาท์พุท out อินพุตและเอาต์พุตคือรายการของเทนเซอร์ของประเภทนั้น (และจำนวนและประเภทของเทนเซอร์ในการส่งออกที่เป็นเช่นเดียวกับการป้อนข้อมูลเนื่องจากทั้งสองมีประเภท T )

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

คุณยังสามารถวางข้อจำกัดเกี่ยวกับประเภทที่สามารถระบุในรายการได้ ในกรณีต่อไปนี้เข้าเป็นรายการของ float และ double เทนเซอร์ สหกรณ์ยอมรับตัวอย่างเช่นรูปแบบการใส่ (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 เทนเซอร์และใช้ int attr N การระบุความยาวของรายการ

นี้สามารถทำ ประเภท polymorphic เช่นกัน ในตัวอย่างต่อไปใส่เป็นรายชื่อของเทนเซอร์ (ที่มีความยาว "N" ) ของที่เหมือนกัน ( แต่ไม่ได้ระบุ) ประเภท ( "T" ) และการส่งออกเป็นเมตริกซ์เดียวของชนิดที่ตรงกับ:

REGISTER_OP("SameListInputExample")
    .Attr("N: int")
    .Attr("T: type")
    .Input("in: N * T")
    .Output("out: T");

โดยค่าเริ่มต้นรายการเมตริกซ์มีความยาวไม่ต่ำกว่า 1 คุณสามารถเปลี่ยนค่าเริ่มต้นว่าการใช้ "> =" ข้อ จำกัด ใน attr ที่สอดคล้องกัน ">=" ในตัวอย่างต่อไปนี้เข้าเป็นรายการอย่างน้อย 2 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");

อินพุตและเอาต์พุต

เพื่อสรุปข้างต้น การลงทะเบียน op สามารถมีอินพุตและเอาต์พุตได้หลายรายการ:

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> เป็นชนิดที่ได้รับการสนับสนุนการป้อนข้อมูล (เช่น float , int32 , string ) ระบุเมตริกซ์เดียวของประเภทที่กำหนด

    ดู tf.DType

    REGISTER_OP("BuiltInTypesExample")
        .Input("integers: int32")
        .Input("complex_numbers: complex64");
    
  • <attr-type> ที่ <attr-type> เป็นชื่อของนั้น attr กับประเภท type หรือ list(type) (ที่มีข้อ จำกัด ประเภทเป็นไปได้) รูปแบบนี้จะช่วยให้การ ปฏิบัติการ polymorphic

    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> สามารถเป็นได้ทั้ง tf.DType หรือชื่อของ attr กับชนิด type ในฐานะที่เป็นตัวอย่างของแรกแย้มยิ้มนี้ยอมรับรายการ 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 สรุปใช้ชื่อทุน (เช่น T หรือ N ) มิฉะนั้นปัจจัยการผลิตผลผลิตและ attrs มีชื่อเช่นค่าฟังก์ชัน (เช่น num_outputs ) สำหรับรายละเอียดเพิ่มเติมโปรดดูที่ ส่วนก่อนหน้านี้ในการตั้งชื่อ

สำหรับรายละเอียดเพิ่มเติมโปรดดูที่ tensorflow/core/framework/op_def_builder.h

ความเข้ากันได้ย้อนหลัง

สมมติว่าคุณได้เขียน op แบบกำหนดเองที่ดีและแชร์กับผู้อื่น เพื่อให้คุณมีลูกค้าที่มีความสุขโดยใช้การดำเนินการของคุณ อย่างไรก็ตาม คุณต้องการเปลี่ยนแปลงการดำเนินการในทางใดทางหนึ่ง

โดยทั่วไปแล้วการเปลี่ยนแปลงที่มีอยู่ตรวจสอบในรายละเอียดจะต้องย้อนกลับเข้ากันได้: การเปลี่ยนแปลงคุณสมบัติของสหกรณ์จะต้องไม่ทำลายก่อนเนื่อง GraphDef บัฟเฟอร์โปรโตคอลสร้างจากข้อกำหนดเก่า รายละเอียดของการ GraphDef เข้ากันได้มีการ อธิบายไว้ที่นี่

มีหลายวิธีในการรักษาความเข้ากันได้แบบย้อนหลัง

  1. Attr ใหม่ใดๆ ที่เพิ่มลงในการดำเนินการต้องมีค่าเริ่มต้นที่กำหนดไว้ และด้วยค่าเริ่มต้นนั้น op ต้องมีพฤติกรรมเดิม ในการเปลี่ยนการดำเนินงานจากการไม่ polymorphic เพื่อ polymorphic คุณจะต้องให้ค่าเริ่มต้นชนิด attr ใหม่เพื่อรักษาลายเซ็นต้นฉบับโดยค่าเริ่มต้น ตัวอย่างเช่น หากการดำเนินการของคุณคือ:

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

    คุณสามารถสร้าง polymorphic ด้วยวิธีที่เข้ากันได้แบบย้อนหลังโดยใช้:

    REGISTER_OP("MyGeneralUnaryOp")
        .Input("in: T")
        .Output("out: T")
        .Attr("T: numerictype = DT_FLOAT");
    
  2. คุณสามารถสร้างข้อจำกัดในการ attr น้อยลงได้อย่างปลอดภัย ตัวอย่างเช่นคุณสามารถเปลี่ยนจาก {int32, int64} เพื่อ {int32, int64, float} หรือ type หรือคุณอาจมีการเปลี่ยนแปลงจาก {"apple", "orange"} เพื่อ {"apple", "banana", "orange"} หรือ string

  3. คุณสามารถเปลี่ยนอินพุต / เอาต์พุตเดี่ยวเป็นรายการอินพุต / เอาต์พุต ตราบใดที่ค่าเริ่มต้นสำหรับประเภทรายการตรงกับลายเซ็นเก่า

  4. คุณสามารถเพิ่มรายการอินพุต / เอาต์พุตใหม่ได้ หากค่าเริ่มต้นเป็นว่างเปล่า

  5. เนมสเปซ ops ใหม่ใดๆ ที่คุณสร้างขึ้น โดยนำหน้าชื่อ op ด้วยชื่อเฉพาะสำหรับโปรเจ็กต์ของคุณ วิธีนี้จะช่วยป้องกันไม่ให้ op ของคุณชนกับ ops ที่อาจรวมอยู่ใน TensorFlow เวอร์ชันต่อๆ ไป

  6. วางแผนล่วงหน้า! พยายามคาดการณ์การใช้งานในอนาคตสำหรับ op การเปลี่ยนแปลงลายเซ็นบางอย่างไม่สามารถทำได้ในลักษณะที่เข้ากันได้ (เช่น การสร้างรายการประเภทเดียวกันลงในรายการประเภทต่างๆ)

รายการเต็มรูปแบบของการเปลี่ยนแปลงที่ปลอดภัยและไม่ปลอดภัยสามารถพบได้ใน tensorflow/core/framework/op_compatibility_test.cc หากคุณไม่สามารถเปลี่ยนแปลงการดำเนินการที่เข้ากันได้แบบย้อนหลัง ให้สร้างการดำเนินการใหม่โดยใช้ชื่อใหม่ที่มีความหมายใหม่

นอกจากนี้ยังทราบว่าในขณะที่การเปลี่ยนแปลงเหล่านี้สามารถรักษา GraphDef เข้ากันได้รหัสงูใหญ่ที่สร้างขึ้นอาจมีการเปลี่ยนแปลงในทางที่ไม่เข้ากันกับสายที่เก่า Python API อาจเข้ากันได้โดยการเปลี่ยนแปลงอย่างระมัดระวังในตัวห่อหุ้ม Python ที่เขียนด้วยลายมือ โดยคงลายเซ็นเก่าไว้ ยกเว้นการเพิ่มอาร์กิวเมนต์ทางเลือกใหม่ต่อท้าย โดยทั่วไปการเปลี่ยนแปลงที่เข้ากันไม่อาจทำได้เมื่อ TensorFlow การเปลี่ยนแปลงรุ่นที่สำคัญและต้องเป็นไปตาม GraphDef หมายรุ่น

รองรับ GPU

คุณสามารถใช้ OpKernels ที่แตกต่างกันและลงทะเบียนสำหรับ CPU และ GPU อีกเช่นเดียวกับคุณสามารถ ลงทะเบียนเมล็ดชนิดที่แตกต่างกัน มีหลายตัวอย่างของเมล็ดด้วยการสนับสนุน GPU ในมี tensorflow/core/kernels/ / ขอให้สังเกตเมล็ดบางส่วนมีรุ่น CPU ใน .cc แฟ้มรุ่น 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++14 -c -o cuda_op_kernel.cu.o cuda_op_kernel.cu.cc \
  ${TF_CFLAGS[@]} -D GOOGLE_CUDA=1 -x cu -Xcompiler -fPIC

g++ -std=c++14 -shared -o cuda_op_kernel.so cuda_op_kernel.cc \
  cuda_op_kernel.cu.o ${TF_CFLAGS[@]} -fPIC -lcudart ${TF_LFLAGS[@]}

cuda_op_kernel.so ผลิตดังกล่าวข้างต้นสามารถโหลดได้ตามปกติในหลามใช้ tf.load_op_library ฟังก์ชั่น

โปรดทราบว่าถ้าห้องสมุด CUDA ของคุณไม่ได้ติดตั้งใน /usr/local/lib64 คุณจะต้องระบุเส้นทางที่ชัดเจนในครั้งที่สอง (g ++) คำสั่งดังกล่าว ตัวอย่างเช่นเพิ่ม -L /usr/local/cuda-8.0/lib64/ ถ้า CUDA ของคุณจะถูกติดตั้งใน /usr/local/cuda-8.0

ใช้การไล่ระดับสีใน Python

จากกราฟของ ops TensorFlow ใช้การสร้างความแตกต่างอัตโนมัติ (backpropagation) เพื่อเพิ่ม ops ใหม่ที่แสดงการไล่ระดับสีที่สัมพันธ์กับ ops ที่มีอยู่ ในการทำให้การแยกความแตกต่างอัตโนมัติทำงานสำหรับ ops ใหม่ คุณต้องลงทะเบียนฟังก์ชันการไล่ระดับสีซึ่งคำนวณการไล่ระดับสีที่สัมพันธ์กับอินพุตของ ops ที่มีการไล่ระดับสีที่เกี่ยวกับเอาต์พุตของ ops

ศาสตร์ถ้าสหกรณ์คำนวณ \(y = f(x)\) ลาดสหกรณ์แปรรูปจดทะเบียนการไล่ระดับสี \(\partial L/ \partial y\) ของการสูญเสีย \(L\) ด้วยความเคารพ\(y\) เข้าสู่การไล่ระดับสี \(\partial L/ \partial x\) ด้วยความเคารพ \(x\) ผ่านกฎลูกโซ่:

\[\frac{\partial L}{\partial x} = \frac{\partial L}{\partial y} \frac{\partial y}{\partial x} = \frac{\partial L}{\partial y} \frac{\partial f}{\partial 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 :

  • สำหรับสหกรณ์กับการส่งออก, ฟังก์ชั่นการไล่ระดับสีจะใช้ tf.Operation , op และ tf.Tensor grad และสร้าง Ops ใหม่ออกของเทนเซอร์ op.inputs[i] , op.outputs[i] และ grad ข้อมูลเกี่ยวกับ attrs ใด ๆ ที่สามารถพบได้ผ่าน tf.Operation.get_attr

  • หากสหกรณ์มีผลหลายฟังก์ชั่นการไล่ระดับสีที่จะนำ op และ grads ที่ grads เป็นรายการของการไล่ระดับสีที่เกี่ยวกับการส่งออกในแต่ละ ผลของฟังก์ชั่นการไล่ระดับสีต้องเป็นรายการของ Tensor วัตถุที่เป็นตัวแทนของการไล่ระดับสีที่เกี่ยวกับการป้อนข้อมูลแต่ละ

  • ถ้าไม่มีที่ดีที่กำหนดลาดสำหรับการป้อนข้อมูลบางอย่างเช่นปัจจัยการผลิตจำนวนเต็มใช้เป็นดัชนีลาดกลับสอดคล้องกันควรจะ None ตัวอย่างเช่นสำหรับสหกรณ์การเมตริกซ์ลอยจุด x และดัชนีจำนวนเต็ม i ฟังก์ชั่นการไล่ระดับสีจะ return [x_grad, None]

  • หากไม่มีการไล่ระดับสีที่มีความหมายสำหรับ op เลย คุณมักจะไม่ต้องลงทะเบียนการไล่ระดับสีใดๆ และตราบเท่าที่ไม่จำเป็นต้องใช้การไล่ระดับสีของ op คุณก็ไม่เป็นไร ในบางกรณี op ไม่มีการไล่ระดับสีที่กำหนดไว้อย่างดี แต่สามารถเกี่ยวข้องกับการคำนวณการไล่ระดับสีได้ ที่นี่คุณสามารถใช้ ops.NotDifferentiable การศูนย์การเผยแพร่โดยอัตโนมัติไปข้างหลัง

โปรดทราบว่าในขณะที่เรียกใช้ฟังก์ชันการไล่ระดับสี จะมีเฉพาะกราฟการไหลของข้อมูลของ ops เท่านั้น ไม่ใช่ข้อมูลเทนเซอร์เอง ดังนั้น การคำนวณทั้งหมดจะต้องดำเนินการโดยใช้ tensorflow ops อื่น ๆ เพื่อรันในเวลาดำเนินการของกราฟ

ฟังก์ชันรูปร่างใน C++

TensorFlow API มีคุณลักษณะที่เรียกว่า "การอนุมานรูปร่าง" ซึ่งให้ข้อมูลเกี่ยวกับรูปร่างของเมตริกซ์โดยไม่ต้องดำเนินการกับกราฟ รูปร่างอนุมานได้รับการสนับสนุนจาก "ฟังก์ชั่นรูปร่าง" ที่ลงทะเบียนสำหรับแต่ละประเภทสหกรณ์ใน c ++ REGISTER_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)

มีจำนวนของฟังก์ชั่นรูปร่างทั่วไปที่นำไปใช้กับ Ops เป็นจำนวนมากเช่นมี 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) มีรูปร่างตรงกับมิติหนึ่ง (หรือถ้ารูปร่างการป้อนข้อมูลที่ไม่เป็นที่รู้จักรูปร่างออกจะเป็นเวกเตอร์ที่มีมิติที่ไม่รู้จักหนึ่ง)

หากคุณคือสหกรณ์ polymorphic กับปัจจัยหลาย คุณสามารถใช้สมาชิกของ 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 ช่วยให้โทรไปยืนยันว่าทั้งสองรูปทรงเหมือนกันแม้ว่าหนึ่งหรือทั้งสองของพวกเขาไม่ได้มีข้อมูลที่สมบูรณ์ ฟังก์ชัน Shape ถูกกำหนดไว้สำหรับ TensorFlow ops หลักทั้งหมด และให้ตัวอย่างการใช้งานที่แตกต่างกันมากมาย

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_OK และ INFER_ERROR เป็นความลับเล็ก ๆ น้อย ๆ แต่พยายามที่จะมีขนาดกะทัดรัดในการเป็นตัวแทนข้อกำหนดอินพุทและเอาท์พุทรูปร่างในการทดสอบ. สำหรับตอนนี้ให้ดูที่ความคิดเห็นโดยรอบในการทดสอบเหล่านั้นจะได้รับความรู้สึกของสเปสตริงรูปร่าง)

สร้างแพ็คเกจ pip สำหรับ op . ที่คุณกำหนดเอง

เพื่อสร้าง pip แพคเกจสำหรับสหกรณ์ของคุณดู tensorflow / กำหนดเอง-op ตัวอย่างเช่น คู่มือนี้แสดงวิธีสร้าง ops ที่กำหนดเองจากแพ็คเกจ pip ของ TensorFlow แทนที่จะสร้าง TensorFlow จากแหล่งที่มา