สร้างop

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

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

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

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

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

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

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

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

หากต้องการดูวิธีการทำงาน สมมติว่าคุณต้องการสร้าง 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_zero ของจำนวนเต็ม 32 บิตเป็นอินพุต และส่งออกเทนเซอร์ที่ 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 op ให้เพิ่มสิ่งต่อไปนี้ใน zero_out.cc :

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

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

หากต้องการเขียนเคอร์เนล CPU แบบมัลติเธรด คุณสามารถใช้ฟังก์ชัน Shard ใน work_sharder.h ได้ ฟังก์ชันนี้จะแบ่งฟังก์ชันการคำนวณข้ามเธรดที่กำหนดค่าเพื่อใช้สำหรับเธรดภายในออป (ดู intra_op_parallelism_threads ใน config.proto )

เคอร์เนล GPU

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

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

  1. กำหนดเทมเพลต OpKernel บนอุปกรณ์และประเภทดั้งเดิมของเทนเซอร์
  2. เมื่อต้องการคำนวณเอาต์พุตจริง ฟังก์ชันคำนวณจะเรียกโครงสร้างฟังก์ชันเทมเพลต
  3. ความเชี่ยวชาญพิเศษของฟังก์ชันสำหรับ 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

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

คุณควรจะสามารถคอมไพล์ zero_out.cc ด้วยคอมไพเลอร์ C++ เช่น g++ หรือ clang ที่มีอยู่ในระบบของคุณ แพ็คเกจ PIP ไบนารีจะติดตั้งไฟล์ส่วนหัวและไลบรารีที่คุณต้องการเพื่อรวบรวม op ของคุณในตำแหน่งเฉพาะของระบบ อย่างไรก็ตาม ไลบรารี Python ของ 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 จำเป็นต้องมีแฟล็กเพิ่มเติม "-unknown dynamic_lookup" เมื่อสร้างไฟล์ . .so

หมายเหตุเกี่ยวกับเวอร์ชัน gcc >=5 : gcc ใช้ C++ ABI ใหม่ตั้งแต่เวอร์ชัน 5 TensorFlow 2.8 และรุ่นก่อนหน้าสร้างขึ้นด้วย gcc4 ที่ใช้ ABI รุ่นเก่า หากคุณใช้ TensorFlow เวอร์ชันเหล่านี้และพยายามคอมไพล์ไลบรารี op ของคุณด้วย gcc>=5 ให้เพิ่ม -D_GLIBCXX_USE_CXX11_ABI=0 ลงในบรรทัดคำสั่งเพื่อทำให้ไลบรารีเข้ากันได้กับ ABI รุ่นเก่า แพ็คเกจ TensorFlow 2.9+ เข้ากันได้กับ 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 คุณต้องใช้พารามิเตอร์ 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 Python API มีฟังก์ชัน tf.load_op_library เพื่อโหลดไลบรารีแบบไดนามิกและลงทะเบียน op ด้วยเฟรมเวิร์ก TensorFlow load_op_library ส่งคืนโมดูล Python ที่มีตัวห่อ Python สำหรับ op และเคอร์เนล ดังนั้น เมื่อคุณสร้าง 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 (เพื่อให้สอดคล้องกับ PEP8 ) ดังนั้น หาก op ของคุณชื่อ ZeroOut ในไฟล์ C++ ฟังก์ชัน python จะถูกเรียกว่า zero_out

ในการทำให้ op พร้อมใช้งานเป็นฟังก์ชันปกติที่ import จากโมดูล Python อาจมีประโยชน์ที่จะมีการเรียก load_op_library ในไฟล์ต้นฉบับของ Python ดังต่อไปนี้:

import tensorflow as tf

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

ตรวจสอบว่าสหกรณ์ใช้งานได้

วิธีที่ดีในการตรวจสอบว่าคุณได้นำ 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 ใช้กับเทนเซอร์ที่มีรูปร่างใดๆ จะเกิดอะไรขึ้นถ้ามันใช้กับเวกเตอร์เท่านั้น? นั่นหมายถึงการเพิ่มการตรวจสอบการใช้งาน 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/platform/status.h Status มีทั้งประเภท (บ่อยครั้ง InvalidArgument แต่ดูรายการประเภท) และข้อความ ฟังก์ชั่นสำหรับการสร้างข้อผิดพลาดอาจพบได้ใน tensorflow/core/platform/errors.h

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

การลงทะเบียนสหกรณ์

คุณสมบัติ

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

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

<name>: <attr-type-expr>

โดยที่ <name> ขึ้นต้นด้วยตัวอักษรและสามารถประกอบด้วยอักขระตัวอักษรและตัวเลขและขีดล่างได้ และ <attr-type-expr> เป็นนิพจน์ประเภทของแบบฟอร์ม ที่อธิบายไว้ด้านล่าง

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

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 : A TensorShapeProto
  • 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> ได้รับการสนับสนุน 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> เป็นจำนวนธรรมชาติ ตัวอย่างเช่น การลงทะเบียน 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");

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

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

ความแตกต่าง

ประเภทความหลากหลาย

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

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

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

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

การตั้งชื่อ

โดยทั่วไปอินพุต เอาต์พุต และ attrs ควรได้รับชื่อ Snake_case ข้อยกเว้นประการหนึ่งคือ attrs ที่ใช้เป็นประเภทของอินพุตหรือในประเภทของเอาต์พุต Attr เหล่านั้นสามารถอนุมานได้เมื่อมีการเพิ่ม 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 ) แอตทริบิวต์ที่อนุมานเหล่านั้นจะได้รับชื่อที่เป็นตัวพิมพ์ใหญ่หรือ 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`.
  """
พิมพ์ตัวอย่างความหลากหลาย
#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 ให้กับ 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++ แทนได้ คุณจะยังคงมีการลงทะเบียนเคอร์เนลหนึ่งครั้ง (การเรียก 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 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 และใช้ int attr N เพื่อระบุความยาวของรายการ

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

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

ตามค่าเริ่มต้น รายการเทนเซอร์จะมีความยาวขั้นต่ำ 1 คุณสามารถเปลี่ยนค่าเริ่มต้นนั้นได้โดยใช้ ข้อจำกัด ">=" ใน attr ที่เกี่ยวข้อง ในตัวอย่างถัดไป อินพุตคือรายการของเทนเซอร์ int32 อย่างน้อย 2 ตัว:

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 type หรือ list(type) (โดยมีข้อจำกัดประเภทที่เป็นไปได้) ไวยากรณ์นี้อนุญาตให้ใช้ polymorphic ops

    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 ตามตัวอย่างแรก op นี้ยอมรับรายการ int32 tensor:

    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 ใด ๆ ที่ใช้ในประเภทของอินพุตจะถูกอนุมาน ตามธรรมเนียมแล้ว attr ที่อนุมานเหล่านั้นจะใช้ชื่อตัวพิมพ์ใหญ่ (เช่น T หรือ N ) มิฉะนั้นอินพุต เอาต์พุต และ attrs จะมีชื่อเหมือนพารามิเตอร์ฟังก์ชัน (เช่น num_outputs ) สำหรับรายละเอียดเพิ่มเติม ดู ส่วนก่อนหน้าเกี่ยวกับการตั้งชื่อ

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

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

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

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

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

  1. Attrs ใหม่ใด ๆ ที่เพิ่มให้กับการดำเนินการจะต้องมีการกำหนดค่าเริ่มต้น และด้วยค่าเริ่มต้นนั้น 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. เนมสเปซการดำเนินการใหม่ใดๆ ที่คุณสร้างขึ้น โดยนำหน้าชื่อ op ด้วยสิ่งที่เป็นเอกลักษณ์สำหรับโปรเจ็กต์ของคุณ วิธีนี้จะหลีกเลี่ยงไม่ให้ op ของคุณขัดแย้งกับ ops ใด ๆ ที่อาจรวมอยู่ใน TensorFlow เวอร์ชันอนาคต

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

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

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

รองรับจีพียู

คุณสามารถใช้ 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 เท่านั้น

สิ่งหนึ่งที่ควรทราบ แม้ว่าจะใช้ pad เวอร์ชันเคอร์เนล GPU แต่ก็ยังต้องการอินพุต "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 เพื่อใช้งาน op tf_custom_op_library ยอมรับอาร์กิวเมนต์ gpu_srcs ซึ่งสามารถระบุรายการไฟล์ต้นฉบับที่มีเคอร์เนล CUDA (ไฟล์ *.cu.cc ) ได้ หากต้องการใช้กับการติดตั้ง TensorFlow แบบไบนารี เคอร์เนล CUDA จะต้องได้รับการคอมไพล์ด้วยคอมไพเลอร์ nvcc ของ NVIDIA ต่อไปนี้เป็นลำดับของคำสั่งที่คุณสามารถใช้เพื่อคอมไพล์ 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 ที่สร้างไว้ข้างต้นสามารถโหลดได้ตามปกติใน Python โดยใช้ฟังก์ชัน 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 จะใช้การสร้างความแตกต่างแบบอัตโนมัติ (การเผยแพร่ย้อนกลับ) เพื่อเพิ่ม Ops ใหม่ที่แสดงถึงการไล่ระดับสีโดยคำนึงถึง Ops ที่มีอยู่ เพื่อให้การสร้างความแตกต่างโดยอัตโนมัติทำงานได้สำหรับ ops ใหม่ คุณต้องลงทะเบียนฟังก์ชันการไล่ระดับสีซึ่งจะคำนวณการไล่ระดับสีโดยคำนึงถึงอินพุตของ ops ที่ได้รับการไล่ระดับสีโดยคำนึงถึงเอาต์พุตของ ops

ในทางคณิตศาสตร์ ถ้า op คำนวณ \(y = f(x)\) op การไล่ระดับสีที่ลงทะเบียนไว้จะแปลงการไล่ระดับสี \(\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 :

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

  • หาก op มีเอาต์พุตหลายรายการ ฟังก์ชันการไล่ระดับสีจะใช้ op และ grads โดยที่ grads คือรายการของการไล่ระดับสีที่เกี่ยวข้องกับแต่ละเอาต์พุต ผลลัพธ์ของฟังก์ชันไล่ระดับสีต้องเป็นรายการวัตถุ Tensor ที่แสดงถึงการไล่ระดับสีโดยสัมพันธ์กับอินพุตแต่ละรายการ

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

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

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

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

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

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

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

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

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