إذا كنت ترغب في إنشاء عملية لا تغطيها مكتبة TensorFlow الحالية ، فننصحك أولاً بتجربة كتابة المرجع في Python كتكوين من عمليات أو وظائف Python الحالية. إذا لم يكن ذلك ممكنًا ، فيمكنك إنشاء مرجع C ++ مخصص. هناك عدة أسباب وراء رغبتك في إنشاء مرجع C ++ مخصص:
- ليس من السهل أو من الممكن التعبير عن عمليتك كتكوين للعمليات الحالية.
- ليس من الفعال التعبير عن عمليتك كتركيبة من الأوليات الموجودة.
- تريد دمج تركيبة من الأوليات التي سيجد المترجم المستقبلي صعوبة في دمجها يدويًا.
على سبيل المثال ، تخيل أنك تريد تنفيذ شيء مثل "متوسط التجميع" ، على غرار عامل التشغيل "MaxPool" ، لكن حساب المتوسطات على النوافذ المنزلقة بدلاً من القيم القصوى. قد يكون القيام بذلك باستخدام تركيبة العمليات ممكنًا (على سبيل المثال ، باستخدام ExtractImagePatches و TopK) ، ولكن قد لا يكون فعالاً في الأداء أو الذاكرة مثل العملية الأصلية حيث يمكنك القيام بشيء أكثر ذكاءً في عملية واحدة مدمجة. كما هو الحال دائمًا ، يجدر أولاً محاولة التعبير عما تريد باستخدام تركيبة المشغل ، واختيار فقط إضافة عملية جديدة إذا ثبت أن ذلك صعب أو غير فعال.
لتضمين العملية المخصصة الخاصة بك ، ستحتاج إلى:
- قم بتسجيل المرجع الجديد في ملف C ++. يحدد تسجيل Op واجهة (مواصفات) لوظيفة المرجع ، والتي تكون مستقلة عن تنفيذ المرجع. على سبيل المثال ، يحدد تسجيل المرجع اسم المرجع ومدخلات ومخرجات المرجع. كما تحدد وظيفة الشكل المستخدمة لاستدلال شكل الموتر.
- قم بتنفيذ المرجع في C ++. يُعرف تطبيق op باسم kernel ، وهو التنفيذ الملموس للمواصفات التي سجلتها في الخطوة 1. يمكن أن يكون هناك العديد من النواة لأنواع مختلفة من الإدخال / الإخراج أو البنى (على سبيل المثال ، وحدات المعالجة المركزية ووحدات معالجة الرسومات).
- قم بإنشاء غلاف بايثون (اختياري). هذا الغلاف هو واجهة برمجة التطبيقات العامة المستخدمة لإنشاء المرجع في بايثون. يتم إنشاء غلاف افتراضي من تسجيل المرجع ، والذي يمكن استخدامه مباشرة أو الإضافة إليه.
- اكتب دالة لحساب تدرجات المرجع (اختياري).
- اختبار المرجع. عادة ما نقوم بذلك في Python للراحة ، ولكن يمكنك أيضًا اختبار المرجع في C ++. إذا قمت بتعريف التدرجات ، فيمكنك التحقق منها باستخدام Python
tf.test.compute_gradient_error
. راجعrelu_op_test.py
كمثال يختبر الوظائف الأمامية للعوامل المشابهة لـ Relu وتدرجاتها.
المتطلبات الأساسية
- بعض الإلمام بـ C ++.
- يجب أن تكون قد قمت بتثبيت TensorFlow الثنائي ، أو يجب أن تكون قد قمت بتنزيل مصدر TensorFlow ، وأن تكون قادرًا على بنائه.
تحديد واجهة المرجع
يمكنك تحديد واجهة المرجع من خلال تسجيله في نظام TensorFlow. في التسجيل ، تحدد اسم المرجع الخاص بك ومدخلاته (الأنواع والأسماء) والمخرجات (الأنواع والأسماء) ، بالإضافة إلى سلاسل المستندات وأي سمات قد تتطلبها المرجع.
لمعرفة كيفية عمل ذلك ، افترض أنك ترغب في إنشاء op يأخذ موترًا من 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 بت. يستخدم المرجع أيضًا وظيفة الشكل للتأكد من أن موتر الإخراج هو نفس شكل موتر الإدخال. على سبيل المثال ، إذا كان الإدخال موترًا للشكل [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);
}
};
بعد تطبيق kernel الخاص بك ، تقوم بتسجيله في نظام TensorFlow. في التسجيل ، تحدد قيودًا مختلفة يعمل تحتها هذا النواة. على سبيل المثال ، قد يكون لديك نواة واحدة مخصصة لوحدات المعالجة المركزية ونواة منفصلة لوحدات معالجة الرسومات.
للقيام بذلك لـ ZeroOut
op ، أضف ما يلي إلى zero_out.cc
:
REGISTER_KERNEL_BUILDER(Name("ZeroOut").Device(DEVICE_CPU), ZeroOutOp);
نواة وحدة المعالجة المركزية متعددة الخيوط
لكتابة نواة وحدة المعالجة المركزية متعددة الخيوط ، يمكن استخدام وظيفة Shard في work_sharder.h
. تقوم هذه الوظيفة بتقسيم وظيفة حسابية عبر الخيوط التي تم تكوينها لاستخدامها في الترابط الداخلي (انظر intra_op_parallelism_threads في config.proto
).
نواة GPU
يتم تنفيذ نواة GPU في جزأين: OpKernel و CUDA kernel ورمز التشغيل الخاص به.
في بعض الأحيان يكون تطبيق OpKernel شائعًا بين CPU و GPU kernel ، مثل فحص المدخلات وتخصيص المخرجات. في هذه الحالة ، يكون التنفيذ المقترح هو:
- حدد OpKernel على شكل قالب على الجهاز والنوع البدائي للموتر.
- للقيام بالحساب الفعلي للمخرجات ، تستدعي الدالة Compute بنية 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
بناء مكتبة المرجع
قم بتجميع المرجع باستخدام مترجم النظام الخاص بك (تثبيت TensorFlow الثنائي)
يجب أن تكون قادرًا على ترجمة zero_out.cc
باستخدام مترجم C++
مثل g++
أو clang
المتاح على نظامك. تقوم حزمة PIP الثنائية بتثبيت ملفات الرأس والمكتبة التي تحتاجها لتجميع مرجعك في مواقع خاصة بالنظام. ومع ذلك ، توفر مكتبة TensorFlow python وظيفة 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++
مثبتًا ، فإليك تسلسل الأوامر التي يمكنك استخدامها لتجميع مرجعك في مكتبة ديناميكية.
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
: يستخدم مجلس التعاون الخليجي لغة C ++ ABI الجديدة منذ الإصدار5
. تم تصميم TensorFlow 2.8 والإصدارات الأقدم باستخدامgcc4
الذي يستخدم ABI الأقدم. إذا كنت تستخدم هذه الإصدارات من TensorFlow وتحاول تجميع مكتبة المرجع الخاصة بك مع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 Kernel ، تحتاج إلى استخدام المعلمة gpu_srcs
الخاصة بـ tf_custom_op_library
. ضع ملف BUILD مع قاعدة بناء Bazel التالية في مجلد جديد داخل دليل tensorflow/core/user_ops
(على سبيل المثال ، "example_gpu").
load("//tensorflow:tensorflow.bzl", "tf_custom_op_library")
tf_custom_op_library(
# kernel_example.cc kernel_example.cu.cc kernel_example.h
name = "kernel_example.so",
srcs = ["kernel_example.h", "kernel_example.cc"],
gpu_srcs = ["kernel_example.cu.cc", "kernel_example.h"],
)
قم بتشغيل الأمر التالي لإنشاء kernel_example.so
.
$ bazel build --config opt //tensorflow/core/user_ops/example_gpu:kernel_example.so
استخدم المرجع في بايثون
توفر TensorFlow Python API وظيفة tf.load_op_library
لتحميل المكتبة الديناميكية وتسجيل المرجع مع إطار عمل TensorFlow. يُرجع load_op_library
وحدة Python التي تحتوي على أغلفة Python لـ op و kernel. وهكذا ، بمجرد إنشاء المرجع ، يمكنك القيام بما يلي لتشغيله من 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 ). لذلك ، إذا تم تسمية المرجع الخاص بك بـ ZeroOut
في ملفات C ++ ، فسيتم استدعاء دالة python zero_out
.
لجعل المرجع متاحًا كدالة عادية قابلة 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
تحقق من أن المرجع يعمل
هناك طريقة جيدة للتحقق من أنك قد نفذت عمليتك بنجاح وهي أن تكتب اختبارًا لها. قم بإنشاء ملف 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
قم ببناء ميزات متقدمة في عملياتك
الآن بعد أن عرفت كيفية إنشاء عملية أساسية (ومقيدة إلى حد ما) وتنفيذها ، سننظر في بعض الأشياء الأكثر تعقيدًا التي ستحتاج عادةً إلى تضمينها في عمليتك. هذا يتضمن:
الفحوصات والمصادقة المشروطة
افترض المثال أعلاه أن المرجع ينطبق على موتر من أي شكل. ماذا لو تم تطبيقه على النواقل فقط؟ هذا يعني إضافة تحقق إلى تطبيق 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 ، والتي يتم تعيين قيمها عند إضافة المرجع إلى الرسم البياني. تُستخدم هذه لتكوين المرجع ، ويمكن الوصول إلى قيمها داخل تطبيق kernel وفي أنواع المدخلات والمخرجات في تسجيل المرجع. يفضل استخدام الإدخال بدلاً من attr عندما يكون ذلك ممكنًا ، لأن المدخلات أكثر مرونة. هذا لأن Attrs عبارة عن ثوابت ويجب تحديدها في وقت إنشاء الرسم البياني. في المقابل ، المدخلات هي Tensors التي يمكن أن تكون قيمها ديناميكية ؛ وهذا يعني أن المدخلات يمكن أن تتغير في كل خطوة ، ويتم تعيينها باستخدام موجز ، وما إلى ذلك. يتم استخدام Attrs للأشياء التي لا يمكن إجراؤها باستخدام المدخلات: أي تكوين يؤثر على التوقيع (عدد أو نوع المدخلات أو المخرجات) أو يمكن " ر التغيير من خطوة إلى خطوة.
يمكنك تحديد 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
:
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
الأنواع التالية مدعومة في سمة:
-
string
: أي تسلسل من البايتات (لا يشترط أن يكون UTF8). -
int
: عدد صحيح موقّع. -
float
: رقم فاصلة عائمة. -
bool
: صح أم خطأ. -
type
: إحدى قيم (non-ref) الخاصة بـDataType
. -
shape
:TensorShapeProto
. -
list(<type>)
: قائمة<type>
، حيث<type>
هو أحد الأنواع المذكورة أعلاه. لاحظ أنlist(list(<type>))
غير صالحة.
راجع أيضًا: op_def_builder.cc:FinalizeAttr
للحصول على قائمة نهائية.
القيم والقيود الافتراضية
قد يكون لدى Attrs قيم افتراضية ، ويمكن أن يكون لبعض أنواع Attrs قيود. لتعريف سمة مع قيود ، يمكنك استخدام <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
. لا تحدد أن نوع السمة هو type
. هذا يعني عندما يكون لديك قائمة الأنواع في {...}
. على سبيل المثال ، في هذه الحالة ، يكون attr t
نوعًا يجب أن يكون int32
أو float
أو bool
:
REGISTER_OP("RestrictedTypeExample")
.Attr("t: {int32, float, bool}");
توجد اختصارات لقيود النوع الشائعة:
-
numbertype
: نوعtype
يقتصر على الأنواع الرقمية (غير سلسلة وغير منطقية). -
realnumbertype
: مثلnumbertype
بدون أنواع معقدة. -
quantizedtype
: مثلnumbertype
ولكن فقط أنواع الأرقام الكمية.
يتم تحديد القوائم المحددة للأنواع المسموح بها من خلال الوظائف (مثل NumberTypes()
) في tensorflow/core/framework/types.h
. في هذا المثال ، يجب أن يكون Attr t
أحد الأنواع الرقمية:
REGISTER_OP("NumberType")
.Attr("t: numbertype");
لهذا المرجع:
tf.number_type(t=tf.int32) # Valid
tf.number_type(t=tf.bool) # Invalid
يمكن دمج القوائم مع قوائم وأنواع فردية أخرى. يسمح المرجع التالي بأن يكون Attr t
أيًا من الأنواع الرقمية أو النوع المنطقي:
REGISTER_OP("NumberOrBooleanType")
.Attr("t: {numbertype, bool}");
لهذا المرجع:
tf.number_or_boolean_type(t=tf.int32) # Valid
tf.number_or_boolean_type(t=tf.bool) # Valid
tf.number_or_boolean_type(t=tf.string) # Invalid
int >= <n>
: يجب أن تكون القيمة int التي تكون قيمتها أكبر من أو تساوي <n>
، حيث <n>
هي رقم طبيعي. على سبيل المثال ، يحدد تسجيل المرجع التالي أن Attr a
يجب أن يكون له قيمة 2
على الأقل:
REGISTER_OP("MinIntExample")
.Attr("a: int >= 2");
list(<type>) >= <n>
: قائمة بالنوع <type>
يكون طولها أكبر من أو يساوي <n>
. على سبيل المثال ، يحدد تسجيل المرجع التالي أن Attr a
عبارة عن قائمة من الأنواع (إما int32
أو float
) ، وأنه يجب أن يكون هناك 3 منها على الأقل:
REGISTER_OP("TypeListExample")
.Attr("a: list({int32, float}) >= 3");
لتعيين قيمة افتراضية للسمة (مما يجعلها اختيارية في الكود الذي تم إنشاؤه) ، أضف = <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
.
تعدد الأشكال
اكتب تعدد الأشكال
بالنسبة إلى العمليات التي يمكن أن تأخذ أنواعًا مختلفة كمدخلات أو تنتج أنواعًا مختلفة من المخرجات ، يمكنك تحديد سمة في نوع الإدخال أو الإخراج في تسجيل المرجع. عادةً ما تقوم بتسجيل OpKernel
لكل نوع مدعوم.
على سبيل المثال ، إذا كنت ترغب في أن يعمل ZeroOut
op على float
s بالإضافة إلى int32
s ، فقد يبدو تسجيل op الخاص بك كما يلي:
REGISTER_OP("ZeroOut")
.Attr("T: {float, int32}")
.Input("to_zero: T")
.Output("zeroed: T");
يحدد تسجيل المرجع الخاص بك الآن أن نوع الإدخال يجب أن يكون float
، أو int32
، وأن ناتجه سيكون من نفس النوع ، لأن كلاهما له النوع T
تسمية
المدخلات والمخرجات والأترس بشكل عام يجب أن تعطى أسماء snake_case. الاستثناء الوحيد هو attrs التي تُستخدم كنوع للإدخال أو في نوع الإخراج. يمكن استنتاج تلك attrs عند إضافة المرجع إلى الرسم البياني وبالتالي لا تظهر في وظيفة المرجع. على سبيل المثال ، سيؤدي هذا التعريف الأخير لـ 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.
قارن هذا مع المرجع الذي يحتوي على سمة النوع التي تحدد نوع الإخراج:
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);
للحفاظ على التوافق مع الإصدارات السابقة ، يجب تحديد قيمة افتراضية عند إضافة سمة إلى مرجع موجود:
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 ++ بدلاً من ذلك. سيظل لديك تسجيل kernel واحد (استدعاء 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
سرد المدخلات والمخرجات
بالإضافة إلى القدرة على قبول أو إنتاج أنواع مختلفة ، يمكن للعمليات أن تستهلك أو تنتج عددًا متغيرًا من الموترات.
في المثال التالي ، يحتوي 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");
هذا يقبل قائمة tensors int32
، ويستخدم N
int
لتحديد طول القائمة.
يمكن جعل هذا النوع متعدد الأشكال أيضًا. في المثال التالي ، الإدخال عبارة عن قائمة من الموترات (بطول "N"
) من نفس النوع (ولكن غير محدد) ( "T"
) ، والإخراج عبارة عن موتر واحد من النوع المطابق:
REGISTER_OP("SameListInputExample")
.Attr("N: int")
.Attr("T: type")
.Input("in: N * T")
.Output("out: T");
بشكل افتراضي ، يكون الحد الأدنى للطول لقوائم الموتر هو 1. يمكنك تغيير هذا الإعداد الافتراضي باستخدام قيد ">="
على السمات المقابلة . في هذا المثال التالي ، الإدخال عبارة عن قائمة 2 على الأقل من موتر int32
:
REGISTER_OP("MinLengthIntListExample")
.Attr("N: int >= 2")
.Input("in: N * int32")
.Output("out: int32");
نفس الصيغة تعمل مع "list(type)"
attrs:
REGISTER_OP("MinimumLengthPolymorphicListExample")
.Attr("T: list(type) >= 3")
.Input("in: T")
.Output("out: T");
مدخلات ومخرجات
لتلخيص ما سبق ، يمكن أن يحتوي تسجيل المرجع على مدخلات ومخرجات متعددة:
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");
تسمح لك الإشارة إلى
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
، أو اسم سمة من نوع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>
هو أحد الأنواع السابقة.
سيتم استنتاج أي سمة مستخدمة في نوع الإدخال. وفقًا للاتفاقية ، يستخدم هؤلاء الموظفون المستنتجون أسماء كبيرة (مثل T
أو N
). بخلاف ذلك ، يكون للمدخلات والمخرجات والأترس أسماء مثل معلمات الوظيفة (على سبيل المثال ، num_outputs
). لمزيد من التفاصيل ، راجع القسم السابق حول التسمية .
لمزيد من التفاصيل ، راجع tensorflow/core/framework/op_def_builder.h
.
التوافق الوراء
لنفترض أنك كتبت عملية لطيفة ومخصصة وشاركتها مع الآخرين ، لذلك لديك عملاء سعداء باستخدام عمليتك. ومع ذلك ، قد ترغب في إجراء تغييرات على المرجع بطريقة ما.
بشكل عام ، يجب أن تكون التغييرات على المواصفات الحالية التي تم التحقق منها متوافقة مع الإصدارات السابقة: يجب ألا يؤدي تغيير مواصفات المرجع إلى كسر المخازن المؤقتة لبروتوكول GraphDef
المتسلسلة السابقة التي تم إنشاؤها من المواصفات القديمة. يتم وصف تفاصيل التوافق GraphDef
هنا .
هناك عدة طرق للمحافظة على التوافق مع الإصدارات السابقة.
يجب تحديد قيم افتراضية لأي سمة جديدة مضافة إلى عملية ، وبهذه القيمة الافتراضية يجب أن يكون للمرجع السلوك الأصلي. لتغيير عملية من غير متعددة الأشكال إلى متعددة الأشكال ، يجب عليك إعطاء قيمة افتراضية إلى سمة النوع الجديد للحفاظ على التوقيع الأصلي افتراضيًا. على سبيل المثال ، إذا كانت العملية الخاصة بك:
REGISTER_OP("MyGeneralUnaryOp") .Input("in: float") .Output("out: float");
يمكنك جعله متعدد الأشكال بطريقة متوافقة مع الإصدارات السابقة باستخدام:
REGISTER_OP("MyGeneralUnaryOp") .Input("in: T") .Output("out: T") .Attr("T: numerictype = DT_FLOAT");
يمكنك بأمان جعل القيد على سمة أقل تقييدًا. على سبيل المثال ، يمكنك التغيير من
{int32, int64}
إلى{int32, int64, float}
أوtype
. أو يمكنك التغيير من{"apple", "orange"}
إلى{"apple", "banana", "orange"}
أوstring
.يمكنك تغيير المدخلات / المخرجات الفردية إلى مدخلات / مخرجات قائمة ، طالما أن الافتراضي لنوع القائمة يطابق التوقيع القديم.
يمكنك إضافة إدخال / إخراج قائمة جديدة ، إذا كانت فارغة.
Namespace أي عمليات جديدة تقوم بإنشائها ، عن طريق وضع بادئة لأسماء المرجع بشيء فريد لمشروعك. يؤدي هذا إلى تجنب اصطدام مرجعك بأي عمليات قد يتم تضمينها في الإصدارات المستقبلية من TensorFlow.
خطط مسبقا! حاول توقع الاستخدامات المستقبلية للمرجع. لا يمكن إجراء بعض تغييرات التوقيع بطريقة متوافقة (على سبيل المثال ، عمل قائمة من نفس النوع في قائمة أنواع مختلفة).
يمكن العثور على القائمة الكاملة للتغييرات الآمنة وغير الآمنة في tensorflow/core/framework/op_compatibility_test.cc
. إذا لم تتمكن من إجراء التغيير على عملية متوافقة مع الإصدارات السابقة ، فقم بإنشاء عملية جديدة باسم جديد باستخدام الدلالات الجديدة.
لاحظ أيضًا أنه على الرغم من أن هذه التغييرات يمكن أن تحافظ على توافق GraphDef
، فقد يتغير كود Python الذي تم إنشاؤه بطريقة لا تتوافق مع المتصلين القدامى. قد تظل واجهة برمجة تطبيقات Python متوافقة من خلال تغييرات دقيقة في غلاف Python المكتوب بخط اليد ، عن طريق الاحتفاظ بالتوقيع القديم باستثناء إمكانية إضافة وسيطات اختيارية جديدة إلى النهاية. يمكن إجراء التغييرات غير المتوافقة بشكل عام فقط عندما يغير TensorFlow الإصدارات الرئيسية ، ويجب أن يتوافق مع دلالات إصدار GraphDef
.
دعم GPU
يمكنك تنفيذ OpKernels مختلفة وتسجيل واحدة لوحدة المعالجة المركزية وأخرى لوحدة معالجة الرسومات ، تمامًا كما يمكنك تسجيل نواة لأنواع مختلفة . هناك العديد من الأمثلة على النواة التي تدعم 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 kernel pad
، فإنه لا يزال بحاجة إلى إدخال "paddings"
في ذاكرة وحدة المعالجة المركزية. لتحديد أن المدخلات أو المخرجات محفوظة على وحدة المعالجة المركزية ، أضف استدعاء HostMemory()
لتسجيل kernel ، على سبيل المثال:
#define REGISTER_GPU_KERNEL(T) \
REGISTER_KERNEL_BUILDER(Name("Pad") \
.Device(DEVICE_GPU) \
.TypeConstraint<T>("T") \
.HostMemory("paddings"), \
PadOp<GPUDevice, T>)
تجميع النواة لجهاز GPU
انظر إلى cuda_op_kernel.cu.cc للحصول على مثال يستخدم نواة CUDA لتنفيذ المرجع. تقبل tf_custom_op_library
وسيطة gpu_srcs
حيث يمكن تحديد قائمة ملفات المصدر التي تحتوي على نواة CUDA (ملفات *.cu.cc
). للاستخدام مع التثبيت الثنائي لـ TensorFlow ، يجب تجميع نواة CUDA مع مترجم NVIDIA nvcc
. إليك تسلسل الأوامر التي يمكنك استخدامها لترجمة cuda_op_kernel.cu.cc و cuda_op_kernel.cc في مكتبة واحدة قابلة للتحميل ديناميكيًا:
nvcc -std=c++14 -c -o cuda_op_kernel.cu.o cuda_op_kernel.cu.cc \
${TF_CFLAGS[@]} -D GOOGLE_CUDA=1 -x cu -Xcompiler -fPIC
g++ -std=c++14 -shared -o cuda_op_kernel.so cuda_op_kernel.cc \
cuda_op_kernel.cu.o ${TF_CFLAGS[@]} -fPIC -lcudart ${TF_LFLAGS[@]}
يمكن تحميل cuda_op_kernel.so
الذي تم إنتاجه أعلاه كالمعتاد في Python ، باستخدام وظيفة tf.load_op_library
.
لاحظ أنه إذا لم يتم تثبيت مكتبات CUDA في /usr/local/lib64
، فستحتاج إلى تحديد المسار صراحةً في الأمر الثاني (g ++) أعلاه. على سبيل المثال ، أضف -L /usr/local/cuda-8.0/lib64/
إذا تم تثبيت CUDA في /usr/local/cuda-8.0
.
طبِّق التدرج اللوني في بايثون
بالنظر إلى رسم بياني للعمليات ، يستخدم TensorFlow التمايز التلقائي (backpropagation) لإضافة عمليات جديدة تمثل التدرجات فيما يتعلق بالعمليات الحالية. لجعل التمايز التلقائي يعمل مع عمليات التشغيل الجديدة ، يجب عليك تسجيل دالة التدرج اللوني التي تحسب التدرجات فيما يتعلق بمدخلات العمليات المعطاة التدرجات فيما يتعلق بمخرجات العمليات.
رياضياً ، إذا قام المرجع بحساب \(y = f(x)\) ، فإن التدرج اللوني المسجل يحول التدرجات \(\partial L/ \partial y\) من الخسارة \(L\) فيما يتعلق بـ\(y\) إلى التدرجات \(\partial L/ \partial x\) فيما يتعلق \(x\) عبر قاعدة السلسلة:
\[\frac{\partial L}{\partial x} = \frac{\partial L}{\partial y} \frac{\partial y}{\partial x} = \frac{\partial L}{\partial y} \frac{\partial f}{\partial x}.\]
في حالة ZeroOut
، يؤثر إدخال واحد فقط في الإخراج ، وبالتالي فإن التدرج فيما يتعلق بالمدخلات هو موتر "واحد ساخن" متفرق. يتم التعبير عن هذا على النحو التالي:
from tensorflow.python.framework import ops
from tensorflow.python.ops import array_ops
from tensorflow.python.ops import sparse_ops
@ops.RegisterGradient("ZeroOut")
def _zero_out_grad(op, grad):
"""The gradients for `zero_out`.
Args:
op: The `zero_out` `Operation` that we are differentiating, which we can use
to find the inputs and outputs of the original op.
grad: Gradient with respect to the output of the `zero_out` op.
Returns:
Gradients with respect to the input of `zero_out`.
"""
to_zero = op.inputs[0]
shape = array_ops.shape(to_zero)
index = array_ops.zeros_like(shape)
first_grad = array_ops.reshape(grad, [-1])[0]
to_zero_grad = sparse_ops.sparse_to_dense([index], shape, first_grad, 0)
return [to_zero_grad] # List of one Tensor, since we have one input
تفاصيل حول تسجيل وظائف التدرج مع tf.RegisterGradient
:
بالنسبة لعملية إخراج واحدة ، فإن دالة التدرج اللوني تأخذ
tf.Operation
،op
، وtf.Tensor
grad
and builds new ops out of the tensorsop.inputs[i]
،op.outputs[i]
، andgrad
. يمكن العثور على معلومات حول أي Attrs عبرtf.Operation.get_attr
.إذا كان المرجع يحتوي على مخرجات متعددة ، فستأخذ وظيفة التدرج
op
وgrads
، حيث تمثلgrads
قائمة من التدرجات فيما يتعلق بكل ناتج. يجب أن تكون نتيجة دالة التدرج قائمة بكائناتTensor
تمثل التدرجات اللونية فيما يتعلق بكل إدخال.إذا لم يكن هناك تدرج محدد جيدًا لبعض المدخلات ، مثل مدخلات الأعداد الصحيحة المستخدمة كمؤشرات ، فيجب أن يكون التدرج المقابل المرتجع
None
. على سبيل المثال ، بالنسبة لعملية أخذ موتر الفاصلة العائمةx
وفهرس عدد صحيحi
، فإن دالة التدرج اللونيreturn [x_grad, None]
.إذا لم يكن هناك تدرج لوني ذي معنى لـ op على الإطلاق ، فغالبًا ما لن تضطر إلى تسجيل أي تدرج ، وطالما أن تدرج المرجع غير مطلوب أبدًا ، فستكون بخير. في بعض الحالات ، لا يحتوي المرجع على تدرج لوني محدد جيدًا ولكن يمكن أن يشارك في حساب التدرج اللوني. هنا يمكنك استخدام
ops.NotDifferentiable
لنشر الأصفار بشكل عكسي تلقائيًا.
لاحظ أنه في الوقت الذي يتم استدعاء وظيفة التدرج اللوني ، يتوفر الرسم البياني لتدفق البيانات للعمليات فقط ، وليس بيانات الموتر نفسها. وبالتالي ، يجب إجراء جميع الحسابات باستخدام عمليات tensorflow أخرى ، ليتم تشغيلها في وقت تنفيذ الرسم البياني.
وظائف الشكل في C ++
تحتوي واجهة برمجة تطبيقات TensorFlow على ميزة تسمى "استدلال الشكل" توفر معلومات حول أشكال الموترات دون الحاجة إلى تنفيذ الرسم البياني. يتم دعم استنتاج الشكل من خلال "وظائف الشكل" التي يتم تسجيلها لكل نوع 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)
له شكل ذو بُعد واحد بالضبط (أو إذا كان شكل الإدخال غير معروف ، فسيكون شكل الإخراج متجهًا ببعد واحد غير معروف).
إذا كان المرجع الخاص بك متعدد الأشكال مع مدخلات متعددة ، فيمكنك استخدام أعضاء InferenceContext
لتحديد عدد الأشكال المراد فحصها ، Merge
للتحقق من أن الأشكال كلها متوافقة (بدلاً من ذلك ، سمات الوصول التي تشير إلى الأطوال ، باستخدام InferenceContext::GetAttr
، الذي يوفر الوصول إلى سمات المرجع).
.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
and InferenceContext::WithValue
؛ يمكنك تحديد أن بُعد الإخراج هو مجموع / ناتج بعدين من المدخلات باستخدام InferenceContext::Add
and 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 بدلاً من إنشاء TensorFlow من المصدر.