اگر می خواهید یک op ایجاد کنید که توسط کتابخانه موجود TensorFlow پوشش داده نشود، توصیه می کنیم ابتدا op را در پایتون به عنوان ترکیبی از عملیات یا توابع موجود پایتون بنویسید. اگر این امکان وجود ندارد، می توانید یک عملیات C++ سفارشی ایجاد کنید. دلایل متعددی وجود دارد که ممکن است بخواهید یک عملیات سفارشی C++ ایجاد کنید:
- بیان عملیات خود به عنوان ترکیبی از عملیات موجود آسان یا ممکن نیست.
- این کارآمد نیست که عملکرد خود را به عنوان ترکیبی از موارد اولیه موجود بیان کنید.
- شما میخواهید ترکیبی از ابتداییها را با دست ترکیب کنید که برای یک کامپایلر آینده، ادغام آن دشوار خواهد بود.
به عنوان مثال، تصور کنید میخواهید چیزی مانند «تجمع متوسط»، شبیه به عملگر «MaxPool» را پیادهسازی کنید، اما به جای مقادیر حداکثر، میانهها را روی پنجرههای کشویی محاسبه کنید. انجام این کار با استفاده از ترکیبی از عملیات ممکن است امکان پذیر باشد (مثلاً با استفاده از ExtractImagePatches و TopK)، اما ممکن است به اندازه یک عملیات بومی که در آن می توانید کار هوشمندانه تری را در یک عملیات واحد و ترکیبی انجام دهید، از نظر عملکرد یا حافظه کارآمد نباشد. مثل همیشه، معمولاً ابتدا ارزش آن را دارد که با استفاده از ترکیب عملگر، آنچه را که می خواهید بیان کنید، فقط در صورتی که دشوار یا ناکارآمد باشد، یک عملیات جدید را انتخاب کنید.
برای ادغام عملیات سفارشی خود باید:
- عملیات جدید را در یک فایل C++ ثبت کنید. ثبت عملیات یک رابط (مشخصات) برای عملکرد عملیات تعریف می کند که مستقل از اجرای عملیات است. به عنوان مثال، ثبت عملیات نام عملیات و ورودی ها و خروجی های عملیات را تعریف می کند. همچنین تابع شکلی را که برای استنتاج شکل تانسور استفاده می شود، تعریف می کند.
- عملیات را در C++ پیاده سازی کنید. پیادهسازی یک عملیات به عنوان هسته شناخته میشود، و اجرای دقیق مشخصاتی است که در مرحله 1 ثبت کردهاید. میتواند چندین هسته برای انواع ورودی/خروجی یا معماریهای مختلف (به عنوان مثال، CPU، GPU) وجود داشته باشد.
- یک پوشش پایتون (اختیاری) ایجاد کنید. این wrapper API عمومی است که برای ایجاد op در پایتون استفاده می شود. یک پوشش پیشفرض از ثبت عملیات تولید میشود که میتواند مستقیماً استفاده شود یا به آن اضافه شود.
- یک تابع برای محاسبه گرادیان برای عملیات بنویسید (اختیاری).
- عملیات را تست کنید. ما معمولاً این کار را در پایتون برای راحتی انجام می دهیم، اما می توانید عملیات را در C++ نیز آزمایش کنید. اگر گرادیان ها را تعریف می کنید، می توانید آنها را با Python
tf.test.compute_gradient_error
تأیید کنید.relu_op_test.py
به عنوان مثالی ببینید که توابع رو به جلو عملگرهای Relu مانند و گرادیان آنها را آزمایش می کند.
پیش نیازها
- آشنایی با C++
- باید باینری TensorFlow را نصب کرده باشید یا منبع TensorFlow را دانلود کرده باشید و بتوانید آن را بسازید.
رابط op را تعریف کنید
شما رابط یک op را با ثبت آن در سیستم TensorFlow تعریف می کنید. در ثبت نام، نام عملیات خود، ورودیهای آن (انواع و نامها) و خروجیها (انواع و نامها)، و همچنین رشتههای اسناد و هر نوع attr که ممکن است عملیات مورد نیاز باشد را مشخص میکنید.
برای اینکه ببینید چگونه این کار می کند، فرض کنید می خواهید عملیاتی ایجاد کنید که یک تانسور int32
s می گیرد و یک کپی از تانسور را خروجی می دهد و همه عنصر به جز اولین عنصر روی صفر تنظیم شده است. برای این کار فایلی با نام zero_out.cc
ایجاد کنید. سپس یک تماس به ماکرو REGISTER_OP
اضافه کنید که رابط عملیات شما را تعریف می کند:
#include "tensorflow/core/framework/op.h"
#include "tensorflow/core/framework/shape_inference.h"
using namespace tensorflow;
REGISTER_OP("ZeroOut")
.Input("to_zero: int32")
.Output("zeroed: int32")
.SetShapeFn([](::tensorflow::shape_inference::InferenceContext* c) {
c->set_output(0, c->input(0));
return Status::OK();
});
این عملیات ZeroOut
یک تانسور to_zero
از اعداد صحیح 32 بیتی را به عنوان ورودی می گیرد و یک تانسور zeroed
از اعداد صحیح 32 بیتی را خروجی می دهد. op همچنین از یک تابع شکل برای اطمینان از اینکه تانسور خروجی همان شکل تانسور ورودی است استفاده می کند. به عنوان مثال، اگر ورودی یک تانسور شکل [10، 20] باشد، این تابع شکل مشخص می کند که شکل خروجی نیز [10،20] باشد.
هسته را برای عملیات پیاده سازی کنید
پس از اینکه رابط را تعریف کردید، یک یا چند پیاده سازی از عملیات را ارائه دهید. برای ایجاد یکی از این هسته ها، کلاسی ایجاد کنید که 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 ثبت می کنید. در ثبت نام، شما محدودیت های مختلفی را مشخص می کنید که این هسته تحت آن اجرا می شود. برای مثال، ممکن است یک هسته برای پردازندههای مرکزی و یک هسته جداگانه برای پردازندههای گرافیکی داشته باشید.
برای انجام این کار برای عملیات ZeroOut
، موارد زیر را به zero_out.cc
اضافه کنید:
REGISTER_KERNEL_BUILDER(Name("ZeroOut").Device(DEVICE_CPU), ZeroOutOp);
هسته های CPU چند رشته ای
برای نوشتن یک هسته CPU چند رشته ای، می توان از تابع Shard در work_sharder.h
استفاده کرد. این تابع یک تابع محاسباتی را در سراسر رشتههای پیکربندی شده برای استفاده برای threading درون عملیاتی تقسیم میکند (به intra_op_parallelism_threads در config.proto
مراجعه کنید).
هسته های GPU
یک هسته GPU در دو بخش پیاده سازی می شود: OpKernel و هسته CUDA و کد راه اندازی آن.
گاهی اوقات پیاده سازی OpKernel بین هسته CPU و GPU مشترک است، مانند بررسی ورودی ها و تخصیص خروجی ها. در آن صورت، پیاده سازی پیشنهادی به این صورت است:
- قالب OpKernel را روی Device و نوع اولیه تانسور را تعریف کنید.
- برای انجام محاسبات واقعی خروجی، تابع Compute یک ساختار تابع الگو را فراخوانی می کند.
- تخصص آن تابع برای CPUDevice در همان فایل تعریف شده است، اما تخصص برای GPUDevice در یک فایل cu.cc تعریف شده است، زیرا با کامپایلر CUDA کامپایل خواهد شد.
در اینجا یک نمونه پیاده سازی است.
// kernel_example.h
#ifndef KERNEL_EXAMPLE_H_
#define KERNEL_EXAMPLE_H_
#include <unsupported/Eigen/CXX11/Tensor>
template <typename Device, typename T>
struct ExampleFunctor {
void operator()(const Device& d, int size, const T* in, T* out);
};
#if GOOGLE_CUDA
// Partially specialize functor for GpuDevice.
template <typename T>
struct ExampleFunctor<Eigen::GpuDevice, T> {
void operator()(const Eigen::GpuDevice& d, int size, const T* in, T* out);
};
#endif
#endif KERNEL_EXAMPLE_H_
// kernel_example.cc
#include "kernel_example.h"
#include "tensorflow/core/framework/op.h"
#include "tensorflow/core/framework/shape_inference.h"
#include "tensorflow/core/framework/op_kernel.h"
using namespace tensorflow;
using CPUDevice = Eigen::ThreadPoolDevice;
using GPUDevice = Eigen::GpuDevice;
REGISTER_OP("Example")
.Attr("T: numbertype")
.Input("input: T")
.Output("input_times_two: T")
.SetShapeFn([](::tensorflow::shape_inference::InferenceContext* c) {
c->set_output(0, c->input(0));
return Status::OK();
});
// CPU specialization of actual computation.
template <typename T>
struct ExampleFunctor<CPUDevice, T> {
void operator()(const CPUDevice& d, int size, const T* in, T* out) {
for (int i = 0; i < size; ++i) {
out[i] = 2 * in[i];
}
}
};
// OpKernel definition.
// template parameter <T> is the datatype of the tensors.
template <typename Device, typename T>
class ExampleOp : public OpKernel {
public:
explicit ExampleOp(OpKernelConstruction* context) : OpKernel(context) {}
void Compute(OpKernelContext* context) override {
// Grab the input tensor
const Tensor& input_tensor = context->input(0);
// Create an output tensor
Tensor* output_tensor = NULL;
OP_REQUIRES_OK(context, context->allocate_output(0, input_tensor.shape(),
&output_tensor));
// Do the computation.
OP_REQUIRES(context, input_tensor.NumElements() <= tensorflow::kint32max,
errors::InvalidArgument("Too many elements in tensor"));
ExampleFunctor<Device, T>()(
context->eigen_device<Device>(),
static_cast<int>(input_tensor.NumElements()),
input_tensor.flat<T>().data(),
output_tensor->flat<T>().data());
}
};
// Register the CPU kernels.
#define REGISTER_CPU(T) \
REGISTER_KERNEL_BUILDER( \
Name("Example").Device(DEVICE_CPU).TypeConstraint<T>("T"), \
ExampleOp<CPUDevice, T>);
REGISTER_CPU(float);
REGISTER_CPU(int32);
// Register the GPU kernels.
#ifdef GOOGLE_CUDA
#define REGISTER_GPU(T) \
/* Declare explicit instantiations in kernel_example.cu.cc. */ \
extern template class ExampleFunctor<GPUDevice, T>; \
REGISTER_KERNEL_BUILDER( \
Name("Example").Device(DEVICE_GPU).TypeConstraint<T>("T"), \
ExampleOp<GPUDevice, T>);
REGISTER_GPU(float);
REGISTER_GPU(int32);
#endif // GOOGLE_CUDA
// kernel_example.cu.cc
#ifdef GOOGLE_CUDA
#define EIGEN_USE_GPU
#include "kernel_example.h"
#include "tensorflow/core/util/gpu_kernel_helper.h"
using namespace tensorflow;
using GPUDevice = Eigen::GpuDevice;
// Define the CUDA kernel.
template <typename T>
__global__ void ExampleCudaKernel(const int size, const T* in, T* out) {
for (int i = blockIdx.x * blockDim.x + threadIdx.x; i < size;
i += blockDim.x * gridDim.x) {
out[i] = 2 * __ldg(in + i);
}
}
// Define the GPU implementation that launches the CUDA kernel.
template <typename T>
void ExampleFunctor<GPUDevice, T>::operator()(
const GPUDevice& d, int size, const T* in, T* out) {
// Launch the cuda kernel.
//
// See core/util/gpu_kernel_helper.h for example of computing
// block count and thread_per_block count.
int block_count = 1024;
int thread_per_block = 20;
ExampleCudaKernel<T>
<<<block_count, thread_per_block, 0, d.stream()>>>(size, in, out);
}
// Explicitly instantiate functors for the types of OpKernels registered.
template struct ExampleFunctor<GPUDevice, float>;
template struct ExampleFunctor<GPUDevice, int32>;
#endif // GOOGLE_CUDA
کتابخانه op را بسازید
اپ را با استفاده از کامپایلر سیستم خود کامپایل کنید (نصب باینری TensorFlow)
شما باید بتوانید zero_out.cc
با یک کامپایلر C++
مانند g++
یا clang
موجود در سیستم خود کامپایل کنید. بسته PIP باینری فایلهای هدر و کتابخانهای را که برای کامپایل کردن عملیات خود نیاز دارید در مکانهایی که مختص سیستم هستند نصب میکند. با این حال، کتابخانه پایتون TensorFlow تابع get_include
را برای دریافت دایرکتوری هدر ارائه میکند، و دایرکتوری get_lib
دارای یک شی به اشتراک گذاشته شده برای پیوند دادن است. در اینجا خروجی های این توابع در یک ماشین اوبونتو آمده است.
$ 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، هنگام ساخت فایل .so
، پرچم اضافی "-undefined dynamic_lookup" مورد نیاز است.
نکته در مورد نسخه
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 جدیدتر سازگار هستند.
عملیات را با استفاده از bazel (نصب منبع TensorFlow) کامپایل کنید
اگر منابع TensorFlow را نصب کرده اید، می توانید از سیستم ساخت TensorFlow برای کامپایل عملیات خود استفاده کنید. یک فایل 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 در پایتون استفاده کنید
TensorFlow Python API تابع tf.load_op_library
را برای بارگذاری کتابخانه پویا و ثبت عملیات با چارچوب TensorFlow فراهم می کند. load_op_library
یک ماژول پایتون را برمیگرداند که شامل پوشههای پایتون برای op و هسته است. بنابراین، هنگامی که عملیات را ساختید، میتوانید برای اجرای آن از پایتون کارهای زیر را انجام دهید:
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 ). بنابراین، اگر عملیات شما ZeroOut
در فایل های C++ نامیده شود، تابع پایتون zero_out
نامیده می شود.
برای در دسترس قرار دادن op به عنوان یک تابع معمولی import
-able از یک ماژول پایتون، شاید مفید باشد که فراخوانی load_op_library
در یک فایل منبع پایتون به صورت زیر داشته باشید:
import tensorflow as tf
zero_out_module = tf.load_op_library('./zero_out.so')
zero_out = zero_out_module.zero_out
بررسی کنید که عملیات کار می کند
یک راه خوب برای تأیید اینکه عملیات خود را با موفقیت اجرا کرده اید، نوشتن یک آزمایش برای آن است. فایل 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
ویژگی های پیشرفته را در عملیات خود بسازید
اکنون که میدانید چگونه یک عملیات اساسی (و تا حدودی محدود) بسازید و پیادهسازی کنید، ما به برخی از موارد پیچیدهتر که معمولاً باید در عملیات خود بسازید، نگاه میکنیم. این شامل:
- بررسی های مشروط و اعتبارسنجی
- ثبت نام عملیات
- پشتیبانی از پردازنده گرافیکی
- گرادیان را در پایتون پیاده کنید
- توابع شکل در C++
بررسی های مشروط و اعتبارسنجی
در مثال بالا فرض شد که عملیات به یک تانسور با هر شکلی اعمال می شود. اگر فقط برای بردارها اعمال شود چه؟ این به معنای افزودن یک چک به اجرای 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
استفاده کنید. هر دوی این ماکروها از تابع در خطا برمی گردند.
ثبت نام عملیات
Attrs
Ops می تواند دارای attrs باشد که مقادیر آنها زمانی که op به یک نمودار اضافه می شود تنظیم می شود. اینها برای پیکربندی op استفاده میشوند و مقادیر آنها هم در پیادهسازی هسته و هم در انواع ورودیها و خروجیها در ثبت عملیات قابل دسترسی است. در صورت امکان از ورودی به جای attr استفاده کنید، زیرا ورودی ها انعطاف پذیرتر هستند. این به این دلیل است که attr ها ثابت هستند و باید در زمان ساخت گراف تعریف شوند. در مقابل، ورودی ها تانسورهایی هستند که مقادیر آنها می تواند پویا باشد. یعنی ورودیها میتوانند در هر مرحله تغییر کنند، با استفاده از فید تنظیم شوند، و غیره. Attrها برای کارهایی استفاده میشوند که با ورودیها نمیتوان انجام داد: هر پیکربندی که بر امضا تأثیر میگذارد (تعداد یا نوع ورودیها یا خروجیها) یا میتواند. گام به گام تغییر کند.
زمانی که op را ثبت می کنید، یک attr را با تعیین نام و نوع آن با استفاده از متد Attr
تعریف می کنید، که انتظار مشخصات فرم را دارد:
<name>: <attr-type-expr>
که در آن <name>
با یک حرف شروع می شود و می تواند از نویسه های الفبایی عددی و زیرخط تشکیل شده باشد، و <attr-type-expr>
یک نوع عبارت از شکلی است که در زیر توضیح داده شده است .
به عنوان مثال، اگر میخواهید عملیات ZeroOut
یک نمایه مشخص شده توسط کاربر را حفظ کند، به جای عنصر 0، میتوانید عملیات را به این صورت ثبت کنید:
REGISTER_OP("ZeroOut")
.Attr("preserve_index: int")
.Input("to_zero: int32")
.Output("zeroed: int32");
(توجه داشته باشید که مجموعه انواع ویژگی با tf.DType
که برای ورودی ها و خروجی ها استفاده می شود متفاوت است.)
سپس هسته شما می تواند از طریق پارامتر context
به این attr در سازنده خود دسترسی داشته باشد:
class ZeroOutOp : public OpKernel {
public:
explicit ZeroOutOp(OpKernelConstruction* context) : OpKernel(context) {
// Get the index of the value to preserve
OP_REQUIRES_OK(context,
context->GetAttr("preserve_index", &preserve_index_));
// Check that preserve_index is positive
OP_REQUIRES(context, preserve_index_ >= 0,
errors::InvalidArgument("Need preserve_index >= 0, got ",
preserve_index_));
}
void Compute(OpKernelContext* context) override {
// ...
}
private:
int preserve_index_;
};
که سپس می تواند در روش Compute
استفاده شود:
void Compute(OpKernelContext* context) override {
// ...
// We're using saved attr to validate potentially dynamic input
// So we check that preserve_index is in range
OP_REQUIRES(context, preserve_index_ < input.dimension(0),
errors::InvalidArgument("preserve_index out of range"));
// Set all the elements of the output tensor to 0
const int N = input.size();
for (int i = 0; i < N; i++) {
output_flat(i) = 0;
}
// Preserve the requested input value
output_flat(preserve_index_) = input(preserve_index_);
}
انواع Attr
انواع زیر در attr پشتیبانی می شوند:
-
string
: هر دنباله ای از بایت (لازم نیست UTF8 باشد). -
int
: یک عدد صحیح امضا شده. -
float
: یک عدد ممیز شناور. -
bool
: درست یا غلط. -
type
: یکی از مقادیر (غیر رفرنس)DataType
. -
shape
: یکTensorShapeProto
. -
list(<type>)
: لیستی از<type>
که در آن<type>
یکی از انواع بالا است. توجه داشته باشید کهlist(list(<type>))
نامعتبر است.
همچنین برای فهرست قطعی به: op_def_builder.cc:FinalizeAttr
مراجعه کنید.
مقادیر و محدودیت های پیش فرض
Attr ها ممکن است مقادیر پیش فرض داشته باشند و برخی از انواع attr ها می توانند محدودیت هایی داشته باشند. برای تعریف attr با محدودیت ها، می توانید از <attr-type-expr>
های زیر استفاده کنید:
{'<string1>', '<string2>'}
: مقدار باید رشته ای باشد که دارای مقدار <string1>
یا <string2>
باشد. هنگامی که از این نحو استفاده می کنید، نام نوع، string
، مشخص می شود. این یک enum را شبیه سازی می کند:
REGISTER_OP("EnumExample")
.Attr("e: {'apple', 'orange'}");
{<type1>, <type2>}
: مقدار از type
است و باید یکی از <type1>
یا <type2>
باشد، جایی که <type1>
و <type2>
tf.DType
پشتیبانی میشوند. شما مشخص نمی کنید که نوع attr type
باشد. هنگامی که شما لیستی از انواع در {...}
دارید، این امر به معنایی است. برای مثال، در این مورد attr t
نوعی است که باید int32
، float
یا bool
باشد:
REGISTER_OP("RestrictedTypeExample")
.Attr("t: {int32, float, bool}");
میانبرهایی برای محدودیت های نوع رایج وجود دارد:
-
numbertype
: نوعtype
محدود به انواع عددی (غیر رشته ای و غیر bool). -
realnumbertype
: مانندnumbertype
بدون انواع پیچیده. -
quantizedtype
: مانندnumbertype
اما فقط انواع quantized اعداد.
لیست های خاصی از انواع مجاز توسط این توابع (مانند 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
اجازه می دهد تا هر یک از انواع عددی یا نوع bool باشد:
REGISTER_OP("NumberOrBooleanType")
.Attr("t: {numbertype, bool}");
برای این عملیات:
tf.number_or_boolean_type(t=tf.int32) # Valid
tf.number_or_boolean_type(t=tf.bool) # Valid
tf.number_or_boolean_type(t=tf.string) # Invalid
int >= <n>
: مقدار باید یک int باشد که مقدار آن بزرگتر یا مساوی <n>
باشد، که در آن <n>
یک عدد طبیعی است. به عنوان مثال، ثبت عملیات زیر مشخص می کند که attr a
باید مقداری حداقل 2
داشته باشد:
REGISTER_OP("MinIntExample")
.Attr("a: int >= 2");
list(<type>) >= <n>
: لیستی از نوع <type>
که طول آن بزرگتر یا مساوی <n>
است. به عنوان مثال، ثبت عملیات زیر مشخص می کند که attr a
لیستی از انواع (اعم از int32
یا float
) است و باید حداقل 3 مورد از آنها وجود داشته باشد:
REGISTER_OP("TypeListExample")
.Attr("a: list({int32, float}) >= 3");
برای تنظیم یک مقدار پیشفرض برای attr (که آن را در کد تولید شده اختیاری میکند)، = <default>
را به انتها اضافه کنید، مانند:
REGISTER_OP("AttrDefaultExample")
.Attr("i: int = 0");
علاوه بر این، هم یک محدودیت و هم یک مقدار پیشفرض میتواند مشخص شود:
REGISTER_OP("AttrConstraintAndDefaultExample")
.Attr("i: int >= 1 = 1");
نحو پشتیبانی شده از مقدار پیش فرض همان چیزی است که در نمایش اولیه تعریف GraphDef حاصل استفاده می شود.
در اینجا مثال هایی برای نحوه تعیین یک پیش فرض برای همه انواع آورده شده است:
REGISTER_OP("AttrDefaultExampleForAllTypes")
.Attr("s: string = 'foo'")
.Attr("i: int = 0")
.Attr("f: float = 1.0")
.Attr("b: bool = true")
.Attr("ty: type = DT_INT32")
.Attr("sh: shape = { dim { size: 1 } dim { size: 2 } }")
.Attr("te: tensor = { dtype: DT_INT32 int_val: 5 }")
.Attr("l_empty: list(int) = []")
.Attr("l_int: list(int) = [2, 3, 5, 7]");
به ویژه توجه داشته باشید که مقادیر type
از tf.DType
استفاده می کنند.
چند شکلی
پلی مورفیسم نوع
برای عملیات هایی که می توانند انواع مختلفی را به عنوان ورودی دریافت کنند یا انواع خروجی های مختلفی تولید کنند، می توانید یک attr را در یک نوع ورودی یا خروجی در ثبت عملیات مشخص کنید. معمولاً برای هر نوع پشتیبانی شده یک OpKernel
ثبت می کنید.
به عنوان مثال، اگر می خواهید که ZeroOut
op علاوه بر int32
روی float
s نیز کار کند، ثبت عملیات شما ممکن است به شکل زیر باشد:
REGISTER_OP("ZeroOut")
.Attr("T: {float, int32}")
.Input("to_zero: T")
.Output("zeroed: T");
ثبت عملیات شما اکنون مشخص میکند که نوع ورودی باید float
یا int32
باشد و خروجی آن از نوع T باشد، زیرا هر دو دارای نوع T
هستند.
نامگذاری
ورودی ها، خروجی ها و attr ها عموماً باید نام snake_case داده شوند. تنها استثنا، attr ها هستند که به عنوان نوع ورودی یا در نوع خروجی استفاده می شوند. زمانی که op به نمودار اضافه می شود، می توان آن attr ها را استنباط کرد و بنابراین در تابع op ظاهر نمی شوند. به عنوان مثال، آخرین تعریف ZeroOut یک تابع پایتون را ایجاد می کند که به شکل زیر است:
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");
در این مورد، کاربر باید نوع خروجی را مانند پایتون تولید شده مشخص کند:
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 به یک عملیات موجود مشخص کنید:
REGISTER_OP("ZeroOut")
.Attr("T: {float, int32} = DT_INT32")
.Input("to_zero: T")
.Output("zeroed: T")
فرض کنید میخواهید انواع بیشتری اضافه کنید، بگویید double
:
REGISTER_OP("ZeroOut")
.Attr("T: {float, double, int32}")
.Input("to_zero: T")
.Output("zeroed: T");
به جای نوشتن OpKernel
دیگر با کد اضافی مانند بالا، اغلب می توانید به جای آن از یک الگوی C++ استفاده کنید. شما همچنان یک ثبت هسته ( REGISTER_KERNEL_BUILDER
تماس) در هر بار اضافه خواهید داشت.
template <typename T>
class ZeroOutOp : public OpKernel {
public:
explicit ZeroOutOp(OpKernelConstruction* context) : OpKernel(context) {}
void Compute(OpKernelContext* context) override {
// Grab the input tensor
const Tensor& input_tensor = context->input(0);
auto input = input_tensor.flat<T>();
// Create an output tensor
Tensor* output = NULL;
OP_REQUIRES_OK(context,
context->allocate_output(0, input_tensor.shape(), &output));
auto output_flat = output->template flat<T>();
// Set all the elements of the output tensor to 0
const int N = input.size();
for (int i = 0; i < N; i++) {
output_flat(i) = 0;
}
// Preserve the first input value
if (N > 0) output_flat(0) = input(0);
}
};
// Note that TypeConstraint<int32>("T") means that attr "T" (defined
// in the op registration above) must be "int32" to use this template
// instantiation.
REGISTER_KERNEL_BUILDER(
Name("ZeroOut")
.Device(DEVICE_CPU)
.TypeConstraint<int32>("T"),
ZeroOutOp<int32>);
REGISTER_KERNEL_BUILDER(
Name("ZeroOut")
.Device(DEVICE_CPU)
.TypeConstraint<float>("T"),
ZeroOutOp<float>);
REGISTER_KERNEL_BUILDER(
Name("ZeroOut")
.Device(DEVICE_CPU)
.TypeConstraint<double>("T"),
ZeroOutOp<double>);
اگر بیش از دو بار اضافه بار دارید، می توانید ثبت نام را در یک ماکرو قرار دهید.
#include "tensorflow/core/framework/op_kernel.h"
#define REGISTER_KERNEL(type) \
REGISTER_KERNEL_BUILDER( \
Name("ZeroOut").Device(DEVICE_CPU).TypeConstraint<type>("T"), \
ZeroOutOp<type>)
REGISTER_KERNEL(int32);
REGISTER_KERNEL(float);
REGISTER_KERNEL(double);
#undef REGISTER_KERNEL
بسته به لیست انواعی که هسته را برای آنها ثبت می کنید، ممکن است بتوانید از یک ماکرو ارائه شده توسط tensorflow/core/framework/register_types.h
استفاده کنید:
#include "tensorflow/core/framework/op_kernel.h"
#include "tensorflow/core/framework/register_types.h"
REGISTER_OP("ZeroOut")
.Attr("T: realnumbertype")
.Input("to_zero: T")
.Output("zeroed: T");
template <typename T>
class ZeroOutOp : public OpKernel { ... };
#define REGISTER_KERNEL(type) \
REGISTER_KERNEL_BUILDER( \
Name("ZeroOut").Device(DEVICE_CPU).TypeConstraint<type>("T"), \
ZeroOutOp<type>)
TF_CALL_REAL_NUMBER_TYPES(REGISTER_KERNEL);
#undef REGISTER_KERNEL
ورودی ها و خروجی ها را فهرست کنید
Ops علاوه بر توانایی پذیرش یا تولید انواع مختلف، می تواند تعداد متغیری از تانسورها را مصرف یا تولید کند.
در مثال بعدی، attr T
فهرستی از انواع را در خود دارد و به عنوان نوع in
و out
استفاده میشود. ورودی و خروجی لیستی از تانسورهای آن نوع هستند (و تعداد و انواع تانسورها در خروجی با ورودی یکسان است، زیرا هر دو دارای نوع T
هستند).
REGISTER_OP("PolymorphicListExample")
.Attr("T: list(type)")
.Input("in: T")
.Output("out: T");
همچنین میتوانید محدودیتهایی را در مورد انواعی که میتوان در لیست مشخص کرد، اعمال کنید. در این مورد بعدی، ورودی فهرستی از تانسورهای float
و double
است. عملیات به عنوان مثال، انواع ورودی (float, double, float)
را می پذیرد و در آن صورت نوع خروجی نیز خواهد بود (float, double, float)
.
REGISTER_OP("ListTypeRestrictionExample")
.Attr("T: list({float, double})")
.Input("in: T")
.Output("out: T");
اگر میخواهید همه تانسورهای یک لیست از یک نوع باشند، میتوانید کاری مانند:
REGISTER_OP("IntListInputExample")
.Attr("N: int")
.Input("in: N * int32")
.Output("out: int32");
این لیستی از تانسورهای int32
را می پذیرد و از یک int
attr N
برای تعیین طول لیست استفاده می کند.
این را می توان از نوع چند شکلی نیز ساخت. در مثال بعدی، ورودی لیستی از تانسورها (با طول "N"
) از نوع مشابه (اما نامشخص) ( "T"
) است، و خروجی یک تانسور منفرد از نوع منطبق است:
REGISTER_OP("SameListInputExample")
.Attr("N: int")
.Attr("T: type")
.Input("in: N * T")
.Output("out: T");
به طور پیشفرض، فهرستهای تانسور دارای حداقل طول 1 هستند. میتوانید این پیشفرض را با استفاده از یک محدودیت ">="
در attr مربوطه تغییر دهید. در این مثال بعدی، ورودی فهرستی از حداقل 2 تانسور int32
است:
REGISTER_OP("MinLengthIntListExample")
.Attr("N: int >= 2")
.Input("in: N * int32")
.Output("out: int32");
همان نحو با attr های "list(type)"
کار می کند:
REGISTER_OP("MinimumLengthPolymorphicListExample")
.Attr("T: list(type) >= 3")
.Input("in: T")
.Output("out: T");
ورودی ها و خروجی ها
برای خلاصه کردن موارد فوق، یک ثبت عملیات می تواند چندین ورودی و خروجی داشته باشد:
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)
است (با محدودیت نوع احتمالی). این نحو به عملیات چند شکلی اجازه می دهد.REGISTER_OP("PolymorphicSingleInput") .Attr("T: type") .Input("in: T"); REGISTER_OP("RestrictedPolymorphicSingleInput") .Attr("T: {int32, int64}") .Input("in: T");
ارجاع به attr از نوع
list(type)
به شما امکان می دهد دنباله ای از تانسورها را بپذیرید.REGISTER_OP("ArbitraryTensorSequenceExample") .Attr("T: list(type)") .Input("in: T") .Output("out: T"); REGISTER_OP("RestrictedTensorSequenceExample") .Attr("T: list({int32, int64})") .Input("in: T") .Output("out: T");
توجه داشته باشید که تعداد و انواع تانسورها در خروجی
out
مانند ورودیin
است، زیرا هر دو از نوعT
هستند.برای دنباله ای از تانسورها با همان نوع:
<number> * <type>
، که در آن<number>
نام Attr با نوعint
است.<type>
می تواند یکtf.DType
یا نام یک attr با نوعtype
باشد. به عنوان نمونه اول، این عملیات فهرستی از تانسورهایint32
را می پذیرد:REGISTER_OP("Int32SequenceExample") .Attr("NumTensors: int") .Input("in: NumTensors * int32")
در حالی که این عملیات فهرستی از تانسورها از هر نوع را می پذیرد، تا زمانی که همه آنها یکسان باشند:
REGISTER_OP("SameTypeSequenceExample") .Attr("NumTensors: int") .Attr("T: type") .Input("in: NumTensors * T")
برای ارجاع به تانسور:
Ref(<type>)
که در آن<type>
یکی از انواع قبلی است.
هر attr استفاده شده در نوع ورودی استنباط می شود. طبق قرارداد آن عطف های استنباط شده از نام های بزرگ (مانند T
یا N
) استفاده می کنند. در غیر این صورت ورودی ها، خروجی ها و attr ها دارای نام هایی مانند پارامترهای تابع هستند (به عنوان مثال num_outputs
). برای جزئیات بیشتر، بخش قبلی در مورد نامگذاری را ببینید.
برای جزئیات بیشتر، به tensorflow/core/framework/op_def_builder.h
مراجعه کنید.
سازگاری با عقب
بیایید فرض کنیم شما یک عملیات خوب و سفارشی نوشتهاید و آن را با دیگران به اشتراک گذاشتهاید، بنابراین مشتریان خوشحالی خواهید داشت که از عملیات خود استفاده میکنند. با این حال، شما می خواهید به طریقی تغییراتی در عملیات اعمال کنید.
به طور کلی، تغییرات در مشخصات موجود و بررسی شده باید با عقب سازگار باشد: تغییر مشخصات یک عملیات نباید بافرهای پروتکل سریال GraphDef
قبلی ساخته شده از مشخصات قدیمی را خراب کند. جزئیات سازگاری GraphDef
در اینجا توضیح داده شده است.
چندین راه برای حفظ سازگاری با عقب وجود دارد.
هر attr جدید اضافه شده به یک عملیات باید دارای مقادیر پیشفرض تعریف شده باشد و با آن مقدار پیشفرض، op باید رفتار اصلی را داشته باشد. برای تغییر یک عملیات از غیر چند شکلی به چند شکلی، باید یک مقدار پیش فرض به نوع جدید attr بدهید تا امضای اصلی به طور پیش فرض حفظ شود. به عنوان مثال، اگر عملیات شما این بود:
REGISTER_OP("MyGeneralUnaryOp") .Input("in: float") .Output("out: float");
شما می توانید آن را به روشی سازگار با عقب با استفاده از موارد زیر چند شکلی کنید:
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
تغییر دهید.تا زمانی که پیشفرض نوع فهرست با امضای قدیمی مطابقت داشته باشد، میتوانید ورودیها/خروجیها را به ورودی/خروجی فهرست تغییر دهید.
اگر به طور پیش فرض خالی باشد، می توانید یک ورودی/خروجی لیست جدید اضافه کنید.
هر عملیات جدیدی را که ایجاد می کنید، با پیشوند نام های عملیاتی با چیزی منحصر به فرد برای پروژه خود، فضای نامی ایجاد کنید. این از برخورد عملیات شما با هر عملیاتی که ممکن است در نسخههای بعدی TensorFlow گنجانده شود، جلوگیری میکند.
از قبل برنامه ریزی کنید! سعی کنید کاربردهای آتی این عملیات را پیش بینی کنید. برخی از تغییرات امضا را نمی توان به روشی سازگار انجام داد (به عنوان مثال، ایجاد یک لیست از همان نوع به لیستی از انواع مختلف).
لیست کامل تغییرات ایمن و ناایمن را می توان در tensorflow/core/framework/op_compatibility_test.cc
یافت. اگر نمی توانید تغییر خود را به یک عملیات سازگار با معکوس کنید، یک عملیات جدید با نام جدید با معنای جدید ایجاد کنید.
همچنین توجه داشته باشید که اگرچه این تغییرات می تواند سازگاری GraphDef
حفظ کند، کد پایتون تولید شده ممکن است به گونه ای تغییر کند که با تماس گیرندگان قدیمی سازگار نباشد. API پایتون را میتوان با تغییرات دقیق در بستهبندی دستنویس پایتون، با حفظ امضای قدیمی به جز افزودن آرگومانهای اختیاری جدید به انتها، سازگار نگه داشت. معمولاً تغییرات ناسازگار ممکن است فقط زمانی ایجاد شوند که TensorFlow نسخههای اصلی را تغییر دهد و باید با معنایی نسخه GraphDef
مطابقت داشته باشد.
پشتیبانی از پردازنده گرافیکی
میتوانید OpKernelهای مختلفی را پیادهسازی کنید و یکی را برای CPU و دیگری را برای GPU ثبت کنید، همانطور که میتوانید کرنلها را برای انواع مختلف ثبت کنید . چندین نمونه از هستههای با پشتیبانی از GPU در tensorflow/core/kernels/
وجود دارد. توجه داشته باشید که برخی از کرنل ها دارای یک نسخه CPU در یک فایل .cc
، یک نسخه GPU در یک فایل با پایان _gpu.cu.cc
و برخی کدهای مشترک در یک فایل .h
هستند.
به عنوان مثال، tf.pad
همه چیز دارد به جز هسته GPU در tensorflow/core/kernels/pad_op.cc
. هسته GPU در tensorflow/core/kernels/pad_op_gpu.cu.cc
است و کد مشترک یک کلاس قالبی است که در tensorflow/core/kernels/pad_op.h
تعریف شده است. ما کد را به دو دلیل به این ترتیب سازماندهی می کنیم: به شما امکان می دهد کدهای مشترک را بین پیاده سازی های CPU و GPU به اشتراک بگذارید و اجرای GPU را در یک فایل جداگانه قرار می دهد تا فقط توسط کامپایلر GPU کامپایل شود.
نکته ای که باید به آن توجه داشت، حتی زمانی که از نسخه هسته GPU pad
استفاده می شود، همچنان به ورودی "paddings"
خود در حافظه CPU نیاز دارد. برای مشخص کردن اینکه ورودی ها یا خروجی ها روی CPU نگه داشته می شوند، یک فراخوانی HostMemory()
به ثبت هسته اضافه کنید، به عنوان مثال:
#define REGISTER_GPU_KERNEL(T) \
REGISTER_KERNEL_BUILDER(Name("Pad") \
.Device(DEVICE_GPU) \
.TypeConstraint<T>("T") \
.HostMemory("paddings"), \
PadOp<GPUDevice, T>)
کامپایل کردن هسته برای دستگاه GPU
برای مثالی که از هسته CUDA برای پیاده سازی یک op استفاده می کند، به cuda_op_kernel.cu.cc نگاه کنید. tf_custom_op_library
یک آرگومان gpu_srcs
را میپذیرد که در آن فهرست فایلهای منبع حاوی هستههای CUDA (فایلهای *.cu.cc
) را میتوان مشخص کرد. برای استفاده با نصب باینری TensorFlow، هسته های CUDA باید با کامپایلر nvcc
انویدیا کامپایل شوند. در اینجا دنباله ای از دستورات است که می توانید از آنها برای کامپایل cuda_op_kernel.cu.cc و cuda_op_kernel.cc در یک کتابخانه واحد به صورت پویا استفاده کنید:
nvcc -std=c++14 -c -o cuda_op_kernel.cu.o cuda_op_kernel.cu.cc \
${TF_CFLAGS[@]} -D GOOGLE_CUDA=1 -x cu -Xcompiler -fPIC
g++ -std=c++14 -shared -o cuda_op_kernel.so cuda_op_kernel.cc \
cuda_op_kernel.cu.o ${TF_CFLAGS[@]} -fPIC -lcudart ${TF_LFLAGS[@]}
cuda_op_kernel.so
تولید شده در بالا می تواند طبق معمول در پایتون با استفاده از تابع tf.load_op_library
بارگذاری شود.
توجه داشته باشید که اگر کتابخانه های CUDA شما در /usr/local/lib64
نصب نشده باشند، باید مسیر را به صراحت در دستور دوم (g++) بالا مشخص کنید. به عنوان مثال، اگر CUDA شما در /usr/local/cuda-8.0
نصب شده است، -L /usr/local/cuda-8.0/lib64/
را اضافه کنید.
گرادیان را در پایتون پیاده کنید
با توجه به نموداری از عملیات، TensorFlow از تمایز خودکار (پس انتشار) برای اضافه کردن عملیات جدید که نشان دهنده گرادیان ها با توجه به عملیات های موجود است، استفاده می کند. برای اینکه تمایز خودکار برای عملیاتهای جدید کار کند، باید یک تابع گرادیان ثبت کنید که گرادیانها را با توجه به ورودیهای عملیات، گرادیانهای دادهشده با توجه به خروجیهای عملیات محاسبه میکند.
از نظر ریاضی، اگر یک عملیات محاسبه کند \(y = f(x)\) عملیات گرادیان ثبت شده، گرادیان ها را تبدیل می کند \(\partial L/ \partial y\) از دست دادن \(L\) با توجه به\(y\) به گرادیان ها \(\partial L/ \partial x\) با توجه به \(x\) از طریق قانون زنجیره ای:
\[\frac{\partial L}{\partial x} = \frac{\partial L}{\partial y} \frac{\partial y}{\partial x} = \frac{\partial L}{\partial y} \frac{\partial f}{\partial x}.\]
در مورد ZeroOut
، تنها یک ورودی در ورودی بر خروجی تأثیر میگذارد، بنابراین گرادیان نسبت به ورودی یک تانسور «یک داغ» پراکنده است. این به صورت زیر بیان می شود:
from tensorflow.python.framework import ops
from tensorflow.python.ops import array_ops
from tensorflow.python.ops import sparse_ops
@ops.RegisterGradient("ZeroOut")
def _zero_out_grad(op, grad):
"""The gradients for `zero_out`.
Args:
op: The `zero_out` `Operation` that we are differentiating, which we can use
to find the inputs and outputs of the original op.
grad: Gradient with respect to the output of the `zero_out` op.
Returns:
Gradients with respect to the input of `zero_out`.
"""
to_zero = op.inputs[0]
shape = array_ops.shape(to_zero)
index = array_ops.zeros_like(shape)
first_grad = array_ops.reshape(grad, [-1])[0]
to_zero_grad = sparse_ops.sparse_to_dense([index], shape, first_grad, 0)
return [to_zero_grad] # List of one Tensor, since we have one input
جزئیات ثبت توابع گرادیان با tf.RegisterGradient
:
برای یک عملیات با یک خروجی، تابع گرادیان یک
tf.Operation
،op
، و یکgrad
tf.Tensor
را می گیرد و از تانسورهایop.inputs[i]
،op.outputs[i]
وgrad
عملیات جدیدی می سازد. اطلاعات مربوط به هر attr را می توان از طریقtf.Operation.get_attr
یافت.اگر عملیات چند خروجی داشته باشد، تابع گرادیان
op
وgrads
را می گیرد، جایی کهgrads
لیستی از گرادیان ها با توجه به هر خروجی است. نتیجه تابع گرادیان باید لیستی از اشیاءTensor
باشد که گرادیان ها را با توجه به هر ورودی نشان می دهد.اگر برای برخی از ورودیها، گرادیان کاملاً تعریفشدهای وجود نداشته باشد، مانند ورودیهای عدد صحیح که بهعنوان شاخص استفاده میشوند، گرادیان بازگشتی مربوطه باید
None
باشد. به عنوان مثال، برای عملیاتی که یک تانسور ممیز شناورx
و یک اندیس صحیحi
را می گیرد، تابع گرادیانreturn [x_grad, None]
.اگر اصلاً گرادیان معنیداری برای op وجود نداشته باشد، اغلب مجبور نخواهید بود که هیچ گرادیانی را ثبت کنید، و تا زمانی که گرادیان عملیات هرگز مورد نیاز نباشد، خوب خواهید بود. در برخی موارد، یک عملیات شیب مشخصی ندارد اما می تواند در محاسبه گرادیان دخالت داشته باشد. در اینجا می توانید از
ops.NotDifferentiable
برای انتشار خودکار صفرها به عقب استفاده کنید.
توجه داشته باشید که در زمانی که تابع گرادیان فراخوانی می شود، فقط نمودار جریان داده عملیات در دسترس است، نه خود داده تانسور. بنابراین، تمام محاسبات باید با استفاده از سایر عملیات tensorflow انجام شوند تا در زمان اجرای نمودار اجرا شوند.
هنگام ثبت گرادیان سفارشی برای نوع op، نکات نوع اضافه کنید تا کد خواناتر، قابل اشکال زدایی، نگهداری آسان تر و از طریق اعتبارسنجی داده ها قوی تر شود. به عنوان مثال، هنگام در نظر گرفتن یک op
به عنوان پارامتر در یک تابع، مشخص کنید که تابع گرادیان یک tf.Operation
را به عنوان نوع پارامتر خود می گیرد.
توابع شکل در C++
API TensorFlow دارای ویژگی به نام «استنتاج شکل» است که اطلاعاتی در مورد اشکال تانسورها بدون نیاز به اجرای نمودار ارائه می دهد. استنتاج شکل توسط "توابع شکل" که برای هر نوع عملیات در اعلان 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)
دارای شکلی با یک بعد است (یا اگر شکل ورودی ناشناخته باشد، شکل خروجی یک بردار با یک بعد مجهول خواهد بود).
اگر عملیات شما چند شکلی با ورودی های متعدد است، می توانید از اعضای 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
برای عملیات خود، به مثال tensorflow/custom-op مراجعه کنید. این راهنما نشان می دهد که چگونه به جای ساخت TensorFlow از منبع، عملیات سفارشی را از بسته Pip TensorFlow بسازید.