หากคุณต้องการสร้าง op ที่ไม่อยู่ในไลบรารี TensorFlow ที่มีอยู่ เราขอแนะนำให้คุณลองเขียน op ใน Python เป็นองค์ประกอบของ Python ops หรือฟังก์ชันที่มีอยู่ก่อน หากไม่สามารถทำได้ คุณสามารถสร้าง C++ op แบบกำหนดเองได้ มีหลายสาเหตุที่คุณอาจต้องการสร้าง C++ op แบบกำหนดเอง:
- การแสดงปฏิบัติการของคุณเป็นองค์ประกอบของปฏิบัติการที่มีอยู่นั้นไม่ใช่เรื่องง่ายหรือเป็นไปได้
- การแสดงการดำเนินการของคุณเป็นองค์ประกอบของสิ่งดั้งเดิมที่มีอยู่นั้นไม่มีประสิทธิภาพ
- คุณต้องการผสมส่วนประกอบขององค์ประกอบดั้งเดิมด้วยมือ ซึ่งคอมไพเลอร์ในอนาคตจะพบว่าการหลอมรวมทำได้ยาก
ตัวอย่างเช่น สมมติว่าคุณต้องการใช้บางอย่างเช่น "การรวมค่ามัธยฐาน" ซึ่งคล้ายกับตัวดำเนินการ "MaxPool" แต่คำนวณค่ามัธยฐานผ่านหน้าต่างเลื่อนแทนค่าสูงสุด การทำเช่นนี้โดยใช้ส่วนประกอบของการดำเนินการอาจทำได้ (เช่น การใช้ ExtractImagePatches และ TopK) แต่อาจไม่มีประสิทธิภาพหรือประหยัดหน่วยความจำเท่ากับการดำเนินการแบบเนทีฟ ซึ่งคุณสามารถทำสิ่งที่ฉลาดกว่าในการดำเนินการแบบรวมเดียว เช่นเคย อันดับแรกควรพยายามแสดงสิ่งที่คุณต้องการโดยใช้องค์ประกอบตัวดำเนินการ โดยเลือกที่จะเพิ่มการดำเนินการใหม่หากพบว่ายากหรือไม่มีประสิทธิภาพเท่านั้น
ในการรวม op แบบกำหนดเองของคุณ คุณจะต้อง:
- ลงทะเบียน op ใหม่ในไฟล์ C ++ การลงทะเบียน Op กำหนดอินเทอร์เฟซ (ข้อกำหนด) สำหรับฟังก์ชันการทำงานของ op ซึ่งไม่ขึ้นกับการใช้งานของ op ตัวอย่างเช่น การลงทะเบียน op จะกำหนดชื่อของ op และอินพุตและเอาต์พุตของ op นอกจากนี้ยังกำหนดฟังก์ชันรูปร่างที่ใช้สำหรับการอนุมานรูปร่างของเทนเซอร์
- ใช้ op ใน C ++ การนำ op ไปใช้เรียกว่าเคอร์เนล และเป็นการนำข้อมูลจำเพาะที่คุณลงทะเบียนในขั้นตอนที่ 1 ไปใช้อย่างเป็นรูปธรรม อาจมีเคอร์เนลหลายตัวสำหรับประเภทหรือสถาปัตยกรรมอินพุต / เอาท์พุตที่แตกต่างกัน (เช่น CPU, GPU)
- สร้าง Python wrapper (ไม่บังคับ) wrapper นี้เป็น API สาธารณะที่ใช้สร้าง op ใน Python wrapper เริ่มต้นถูกสร้างขึ้นจากการลงทะเบียน op ซึ่งสามารถใช้ได้โดยตรงหรือเพิ่มเข้าไป
- เขียนฟังก์ชันเพื่อคำนวณการไล่ระดับสีสำหรับ op (ทางเลือก)
- ทดสอบ op. เรามักจะทำสิ่งนี้ใน Python เพื่อความสะดวก แต่คุณสามารถทดสอบ op ใน C++ ได้เช่นกัน หากคุณกำหนดการไล่ระดับสี คุณสามารถตรวจสอบได้ด้วย Python
tf.test.compute_gradient_error
ดูrelu_op_test.py
เป็นตัวอย่างที่ทดสอบฟังก์ชันไปข้างหน้าของตัวดำเนินการที่เหมือน Relu และการไล่ระดับสี
ข้อกำหนดเบื้องต้น
- มีความคุ้นเคยกับ C++ อยู่บ้าง
- ต้องติดตั้ง ไบนารี TensorFlow หรือต้อง ดาวน์โหลดซอร์ส TensorFlow และสามารถสร้างได้
กำหนดอินเทอร์เฟซ op
คุณกำหนดอินเทอร์เฟซของ op โดยลงทะเบียนกับระบบ TensorFlow ในการลงทะเบียน คุณต้องระบุชื่อ op อินพุต (ประเภทและชื่อ) และเอาต์พุต (ประเภทและชื่อ) ตลอดจน docstrings และ attrs ใดๆ ที่ op อาจต้องการ
หากต้องการดูวิธีการทำงาน สมมติว่าคุณต้องการสร้าง op ที่ใช้เทนเซอร์ int32
s และส่งออกสำเนาของเทนเซอร์ โดยตั้งค่าองค์ประกอบทั้งหมดยกเว้นองค์ประกอบแรกเป็นศูนย์ ในการทำเช่นนี้ ให้สร้างไฟล์ชื่อ 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
op นี้ใช้หนึ่งเทนเซอร์ 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 บนอุปกรณ์และประเภทดั้งเดิมของเทนเซอร์
- ในการคำนวณเอาต์พุตจริง ฟังก์ชัน Compute จะเรียกโครงสร้าง templated functor
- ความเชี่ยวชาญพิเศษของ 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
รวบรวม 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 จำเป็นต้องมีค่าสถานะเพิ่มเติม "-undefined 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 ใช้งานได้
วิธีที่ดีในการยืนยันว่าคุณใช้ 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()
จากนั้นเรียกใช้การทดสอบของคุณ (สมมติว่าคุณติดตั้ง tensorflow):
$ python zero_out_op_test.py
สร้างคุณสมบัติขั้นสูงใน op ของคุณ
ตอนนี้คุณรู้วิธีสร้าง op พื้นฐาน (และค่อนข้างจำกัด) และการใช้งานแล้ว เราจะมาดูสิ่งที่ซับซ้อนกว่าที่คุณมักจะต้องสร้างใน op ของคุณ ซึ่งรวมถึง:
- การตรวจสอบและการตรวจสอบตามเงื่อนไข
- ลงทะเบียนออป
- รองรับ GPU
- ใช้การไล่ระดับสีใน 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 เป็นค่าคงที่และต้องกำหนด ณ เวลาที่สร้างกราฟ ในทางตรงกันข้าม อินพุตคือ Tensor ซึ่งค่าสามารถเป็นไดนามิกได้ นั่นคือ อินพุตสามารถเปลี่ยนทุกขั้นตอน ตั้งค่าโดยใช้ฟีด ฯลฯ Attrs ใช้สำหรับสิ่งที่ไม่สามารถทำได้ด้วยอินพุต: การกำหนดค่าใด ๆ ที่ส่งผลต่อลายเซ็น (จำนวนหรือประเภทของอินพุตหรือเอาต์พุต) หรือที่สามารถ' t เปลี่ยนจากทีละขั้นตอน
คุณกำหนด 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>
ต่อไปนี้:
{'<string1>', '<string2>'}
: ค่าต้องเป็นสตริงที่มีค่าเป็น <string1>
หรือ <string2>
ชื่อของประเภท string
จะถูกระบุโดยนัยเมื่อคุณใช้ไวยากรณ์นี้ สิ่งนี้เลียนแบบ enum:
REGISTER_OP("EnumExample")
.Attr("e: {'apple', 'orange'}");
{<type1>, <type2>}
: ค่าเป็น type
type และต้องเป็นหนึ่งใน <type1>
หรือ <type2>
โดยที่ <type1>
และ <type2>
ได้รับการสนับสนุน tf.DType
คุณไม่ได้ระบุว่าประเภทของ attr เป็น type
นี่เป็นนัยเมื่อคุณมีรายการประเภทใน {...}
ตัวอย่างเช่น ในกรณีนี้ attr t
เป็นประเภทที่ต้องเป็น int32
, float
หรือ a 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
รายการสามารถรวมกับรายการอื่น ๆ และประเภทเดียว op ต่อไปนี้อนุญาตให้ 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 type
ใช้ tf.DType
ความหลากหลาย
ประเภทความหลากหลาย
สำหรับ op ที่สามารถใช้ประเภทต่างๆ เป็นอินพุตหรือสร้างเอาต์พุตประเภทต่างๆ ได้ คุณสามารถระบุ 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 ที่ใช้เป็นประเภทของอินพุตหรือในประเภทของเอาต์พุต 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
tensor แล้ว 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 ให้กับ 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
เพื่อระบุความยาวของรายการ
นี้สามารถสร้าง ประเภท polymorphic ได้เช่นกัน ในตัวอย่างถัดไป อินพุตคือรายการของเทนเซอร์ (ที่มีความยาว "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");
ไวยากรณ์เดียวกันนี้ใช้ได้กับ attrs "list(type)"
:
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 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 ที่มี typetype
ตัวอย่างแรก op นี้ยอมรับรายการของเทนเซอร์int32
:REGISTER_OP("Int32SequenceExample") .Attr("NumTensors: int") .Input("in: NumTensors * int32")
ในขณะที่ op นี้ยอมรับรายการเทนเซอร์ประเภทใดก็ได้ ตราบใดที่เหมือนกันทั้งหมด:
REGISTER_OP("SameTypeSequenceExample") .Attr("NumTensors: int") .Attr("T: type") .Input("in: NumTensors * T")
สำหรับการอ้างอิงถึงเทนเซอร์:
Ref(<type>)
โดยที่<type>
เป็นหนึ่งในประเภทก่อนหน้า
attr ใด ๆ ที่ใช้ในประเภทของอินพุตจะถูกอนุมาน ตามแบบแผน attrs ที่อนุมานเหล่านั้นใช้ชื่อตัวพิมพ์ใหญ่ (เช่น T
หรือ N
) มิฉะนั้น อินพุต เอาต์พุต และ attrs จะมีชื่อเหมือนกับพารามิเตอร์ของฟังก์ชัน (เช่น num_outputs
) สำหรับรายละเอียดเพิ่มเติม โปรดดู ส่วนก่อนหน้าเกี่ยวกับการตั้งชื่อ
สำหรับรายละเอียดเพิ่มเติม โปรดดูที่ tensorflow/core/framework/op_def_builder.h
ความเข้ากันได้ย้อนหลัง
สมมติว่าคุณได้เขียน op แบบกำหนดเองที่สวยงามและแบ่งปันกับผู้อื่น ดังนั้นคุณจึงมีลูกค้าที่พึงพอใจโดยใช้การดำเนินการของคุณ อย่างไรก็ตาม คุณต้องการเปลี่ยนแปลง op ไม่ทางใดก็ทางหนึ่ง
โดยทั่วไป การเปลี่ยนแปลงข้อมูลจำเพาะที่ตรวจสอบแล้วที่มีอยู่จะต้องเข้ากันได้กับข้อมูลย้อนหลัง: การเปลี่ยนแปลงข้อมูลจำเพาะของ op จะต้องไม่ทำลายบัฟเฟอร์โปรโตคอล GraphDef
ที่ต่อเนื่องกันซึ่งสร้างจากข้อมูลจำเพาะที่เก่ากว่า มีการอธิบายรายละเอียดเกี่ยวกับความเข้ากันได้ของ GraphDef
ที่นี่
มีหลายวิธีในการรักษาความเข้ากันได้แบบย้อนกลับ
Attrs ใหม่ใดๆ ที่เพิ่มลงในการดำเนินการจะต้องมีการกำหนดค่าเริ่มต้น และด้วยค่าเริ่มต้นนั้น op จะต้องมีลักษณะการทำงานเดิม หากต้องการเปลี่ยนการดำเนินการจาก non 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 ของคุณชนกับ op ใดๆ ที่อาจรวมอยู่ใน TensorFlow เวอร์ชันอนาคต
วางแผนล่วงหน้า! พยายามคาดการณ์การใช้งานในอนาคตสำหรับ op การเปลี่ยนแปลงลายเซ็นบางอย่างไม่สามารถทำได้ด้วยวิธีที่เข้ากันได้ (เช่น การทำรายการประเภทเดียวกันให้เป็นรายการประเภทต่างๆ)
รายการการเปลี่ยนแปลงที่ปลอดภัยและไม่ปลอดภัยทั้งหมดสามารถพบได้ใน tensorflow/core/framework/op_compatibility_test.cc
หากคุณไม่สามารถเปลี่ยนแปลงการดำเนินการที่เข้ากันได้แบบย้อนกลับ ให้สร้างการดำเนินการใหม่ด้วยชื่อใหม่ที่มีความหมายใหม่
นอกจากนี้ โปรดทราบว่าแม้ว่าการเปลี่ยนแปลงเหล่านี้จะรักษาความเข้ากันได้ของ GraphDef
ไว้ได้ แต่โค้ด Python ที่สร้างขึ้นอาจเปลี่ยนแปลงในลักษณะที่เข้ากันไม่ได้กับผู้โทรเก่า Python API อาจคงความเข้ากันได้โดยการเปลี่ยนแปลงอย่างระมัดระวังใน Python wrapper ที่เขียนด้วยมือ โดยคงลายเซ็นเก่าไว้ ยกเว้นอาจเพิ่มอาร์กิวเมนต์ทางเลือกใหม่ต่อท้าย การเปลี่ยนแปลงที่เข้ากันไม่ได้โดยทั่วไปอาจทำได้ก็ต่อเมื่อ 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
และโค้ดที่ใช้ร่วมกันคือคลาส templated ที่กำหนดไว้ใน 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 ใหม่จาก tensorop.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 เท่านั้น ไม่สามารถใช้ข้อมูลเทนเซอร์ได้ ดังนั้น การคำนวณทั้งหมดจะต้องดำเนินการโดยใช้ tensorflow ops อื่น เพื่อให้รันในเวลาการดำเนินการของกราฟ
ฟังก์ชันรูปร่างใน 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)
มีฟังก์ชันรูปร่างทั่วไปจำนวนหนึ่งที่ใช้กับ 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)
มีรูปร่างที่มีมิติเดียวพอดี (หรือหากไม่ทราบรูปร่างอินพุต รูปร่างเอาต์พุตจะเป็นเวกเตอร์ที่มีมิติเดียวที่ไม่รู้จัก)
หาก 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 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
สำหรับ op ของคุณ โปรดดูตัวอย่าง tensorflow/custom-op คู่มือนี้แสดงวิธีสร้าง ops แบบกำหนดเองจากแพ็คเกจ TensorFlow pip แทนที่จะสร้าง TensorFlow จากแหล่งที่มา