หากคุณต้องการสร้าง op ที่ไลบรารี TensorFlow ที่มีอยู่ไม่ครอบคลุม เราขอแนะนำให้คุณลองเขียน op ใน Python ให้เป็นองค์ประกอบของ ops หรือฟังก์ชันของ Python ที่มีอยู่ก่อน หากเป็นไปไม่ได้ คุณสามารถสร้าง C++ op แบบกำหนดเองได้ มีสาเหตุหลายประการที่คุณอาจต้องการสร้าง C++ op แบบกำหนดเอง:
- ไม่ใช่เรื่องง่ายหรือเป็นไปได้ที่จะแสดงการดำเนินงานของคุณโดยเป็นส่วนหนึ่งของปฏิบัติการที่มีอยู่
- การแสดงการดำเนินการของคุณโดยเป็นส่วนหนึ่งขององค์ประกอบดั้งเดิมที่มีอยู่นั้นไม่มีประสิทธิภาพ
- คุณต้องการหลอมรวมองค์ประกอบของ primitive ด้วยมือซึ่งคอมไพเลอร์ในอนาคตอาจพบว่าการหลอมรวมทำได้ยาก
ตัวอย่างเช่น ลองนึกภาพว่าคุณต้องการใช้บางอย่างเช่น "การรวมค่ามัธยฐาน" ซึ่งคล้ายกับตัวดำเนินการ "MaxPool" แต่คำนวณค่ามัธยฐานบนหน้าต่างบานเลื่อนแทนค่าสูงสุด การดำเนินการนี้โดยใช้องค์ประกอบของการดำเนินการอาจเป็นไปได้ (เช่น การใช้ ExtractImagePatches และ TopK) แต่อาจไม่มีประสิทธิภาพหรือประสิทธิภาพของหน่วยความจำเท่ากับการดำเนินการแบบเนทิฟ ซึ่งคุณสามารถทำอะไรที่ชาญฉลาดกว่าในการดำเนินการแบบหลอมรวมเพียงครั้งเดียว และเช่นเคย โดยทั่วไปแล้ว คุณควรพยายามแสดงสิ่งที่คุณต้องการโดยใช้องค์ประกอบของตัวดำเนินการเป็นอันดับแรก โดยเลือกที่จะเพิ่มการดำเนินการใหม่เท่านั้นหากพบว่าเป็นเรื่องยากหรือไม่มีประสิทธิภาพ
หากต้องการรวม Op ที่กำหนดเอง คุณจะต้อง:
- ลงทะเบียน op ใหม่ในไฟล์ C ++ การลงทะเบียน Op กำหนดอินเทอร์เฟซ (ข้อกำหนด) สำหรับการทำงานของ op ซึ่งไม่ขึ้นอยู่กับการใช้งานของ op ตัวอย่างเช่น การลงทะเบียน op จะกำหนดชื่อของ op และอินพุตและเอาต์พุตของ op นอกจากนี้ยังกำหนดฟังก์ชันรูปร่างที่ใช้สำหรับการอนุมานรูปร่างเทนเซอร์อีกด้วย
- ใช้งาน op ใน C ++ การใช้งาน op เรียกว่าเคอร์เนล และเป็นการใช้งานที่เป็นรูปธรรมของข้อกำหนดที่คุณลงทะเบียนไว้ในขั้นตอนที่ 1 สามารถมีได้หลายเคอร์เนลสำหรับประเภทอินพุต / เอาท์พุตหรือสถาปัตยกรรมที่แตกต่างกัน (เช่น CPU, GPU)
- สร้าง wrapper Python (ไม่บังคับ) Wrapper นี้เป็น API สาธารณะที่ใช้สร้าง op ใน Python Wrapper เริ่มต้นจะถูกสร้างขึ้นจากการลงทะเบียน op ซึ่งสามารถใช้ได้โดยตรงหรือเพิ่มเข้าไป
- เขียนฟังก์ชันเพื่อคำนวณการไล่ระดับสีสำหรับ op (ไม่บังคับ)
- ทดสอบปฏิบัติการ โดยปกติเราจะทำเช่นนี้ใน Python เพื่อความสะดวก แต่คุณสามารถทดสอบ op ใน C ++ ได้เช่นกัน หากคุณกำหนดการไล่ระดับสี คุณสามารถตรวจสอบได้ด้วย Python
tf.test.compute_gradient_error
ดูrelu_op_test.py
เป็นตัวอย่างที่ทดสอบฟังก์ชันไปข้างหน้าของตัวดำเนินการที่คล้าย Relu และการไล่ระดับสี
ข้อกำหนดเบื้องต้น
- มีความคุ้นเคยกับ C++ บ้าง
- ต้องติดตั้ง ไบนารี TensorFlow หรือต้อง ดาวน์โหลด TensorFlow source และสามารถสร้างได้
กำหนดอินเทอร์เฟซ 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 เช่น การตรวจสอบอินพุตและการจัดสรรเอาต์พุต ในกรณีดังกล่าว การดำเนินการที่แนะนำคือ:
- กำหนดเทมเพลต OpKernel บนอุปกรณ์และประเภทดั้งเดิมของเทนเซอร์
- เมื่อต้องการคำนวณเอาต์พุตจริง ฟังก์ชันคำนวณจะเรียกโครงสร้างฟังก์ชันเทมเพลต
- ความเชี่ยวชาญพิเศษของฟังก์ชันสำหรับ 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 ของคุณ ซึ่งรวมถึง:
- การตรวจสอบและการตรวจสอบตามเงื่อนไข
- การลงทะเบียนสหกรณ์
- รองรับจีพียู
- ใช้การไล่ระดับสีใน Python
- ฟังก์ชันรูปร่างใน C++
การตรวจสอบและการตรวจสอบตามเงื่อนไข
ตัวอย่างข้างต้นสันนิษฐานว่า 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
: ATensorShapeProto
-
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 opsREGISTER_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
มี อธิบายไว้ที่นี่
มีหลายวิธีในการรักษาความเข้ากันได้แบบย้อนหลัง
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");
คุณสามารถกำหนดข้อจำกัดให้กับ attr ที่มีข้อจำกัดน้อยลงได้อย่างปลอดภัย ตัวอย่างเช่น คุณสามารถเปลี่ยนจาก
{int32, int64}
เป็น{int32, int64, float}
หรือtype
หรือคุณอาจเปลี่ยนจาก{"apple", "orange"}
เป็น{"apple", "banana", "orange"}
หรือstring
คุณสามารถเปลี่ยนอินพุต / เอาท์พุตเดี่ยวเป็นอินพุต / เอาท์พุตรายการได้ ตราบใดที่ค่าเริ่มต้นสำหรับประเภทรายการตรงกับลายเซ็นเก่า
คุณสามารถเพิ่มอินพุต / เอาท์พุตรายการใหม่ได้ หากค่าเริ่มต้นเป็นว่างเปล่า
เนมสเปซการดำเนินการใหม่ใดๆ ที่คุณสร้างขึ้น โดยนำหน้าชื่อ op ด้วยสิ่งที่เป็นเอกลักษณ์สำหรับโปรเจ็กต์ของคุณ วิธีนี้จะหลีกเลี่ยงไม่ให้ op ของคุณขัดแย้งกับ ops ใด ๆ ที่อาจรวมอยู่ใน TensorFlow เวอร์ชันอนาคต
วางแผนล่วงหน้า! พยายามคาดการณ์การใช้งานในอนาคตสำหรับ 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 จากแหล่งที่มา