إنشاء المرجع

إذا كنت ترغب في إنشاء عملية لا تغطيها مكتبة TensorFlow الحالية ، فننصحك أولاً بتجربة كتابة المرجع في Python كتكوين لعمليات أو وظائف Python الحالية. إذا لم يكن ذلك ممكنًا ، يمكنك إنشاء مرجع C ++ مخصص. هناك عدة أسباب وراء رغبتك في إنشاء عملية C ++ مخصصة:

  • ليس من السهل أو من الممكن التعبير عن عمليتك كتكوين للعمليات الحالية.
  • ليس من الفعال التعبير عن عمليتك كتركيبة من الأوليات الموجودة.
  • تريد دمج تركيبة من الأوليات التي سيجد المترجم المستقبلي صعوبة في دمجها يدويًا.

على سبيل المثال ، تخيل أنك تريد تنفيذ شيء مثل "متوسط ​​التجميع" ، على غرار عامل التشغيل "MaxPool" ، لكن حساب المتوسطات على النوافذ المنزلقة بدلاً من القيم القصوى. قد يكون القيام بذلك باستخدام تركيبة من العمليات ممكنًا (على سبيل المثال ، باستخدام ExtractImagePatches و TopK) ، ولكن قد لا يكون فعالاً في الأداء أو الذاكرة مثل العملية الأصلية حيث يمكنك القيام بشيء أكثر ذكاءً في عملية واحدة مدمجة. كما هو الحال دائمًا ، يجدر أولاً محاولة التعبير عما تريد باستخدام تكوين المشغل ، واختيار فقط إضافة عملية جديدة إذا ثبت أن ذلك صعب أو غير فعال.

لتضمين العملية المخصصة الخاصة بك ، ستحتاج إلى:

  1. قم بتسجيل المرجع الجديد في ملف C ++. يحدد تسجيل Op واجهة (مواصفات) لوظيفة المرجع ، والتي تكون مستقلة عن تنفيذ المرجع. على سبيل المثال ، يحدد تسجيل المرجع اسم المرجع ومدخلات ومخرجات المرجع. كما تحدد وظيفة الشكل المستخدمة لاستدلال شكل الموتر.
  2. قم بتنفيذ المرجع في C ++. يُعرف تطبيق op باسم kernel ، وهو التنفيذ الملموس للمواصفات التي سجلتها في الخطوة 1. يمكن أن يكون هناك العديد من النواة لأنواع مختلفة من الإدخال / الإخراج أو البنى (على سبيل المثال ، وحدات المعالجة المركزية ووحدات معالجة الرسومات).
  3. قم بإنشاء غلاف بايثون (اختياري). هذا الغلاف هو واجهة برمجة التطبيقات العامة المستخدمة لإنشاء المرجع في بايثون. يتم إنشاء غلاف افتراضي من تسجيل المرجع ، والذي يمكن استخدامه مباشرة أو الإضافة إليه.
  4. اكتب دالة لحساب تدرجات المرجع (اختياري).
  5. اختبار المرجع. عادة ما نقوم بذلك في Python للراحة ، ولكن يمكنك أيضًا اختبار المرجع في C ++. إذا قمت بتعريف التدرجات ، فيمكنك التحقق منها باستخدام Python tf.test.compute_gradient_error . راجع relu_op_test.py كمثال يختبر الوظائف الأمامية للعوامل المشابهة لـ Relu وتدرجاتها.

المتطلبات الأساسية

تحديد واجهة المرجع

يمكنك تحديد واجهة المرجع من خلال تسجيله في نظام TensorFlow. في التسجيل ، تحدد اسم المرجع الخاص بك ومدخلاته (الأنواع والأسماء) والمخرجات (الأنواع والأسماء) ، بالإضافة إلى سلاسل المستندات وأي سمات قد تتطلبها المرجع.

لمعرفة كيفية عمل ذلك ، افترض أنك ترغب في إنشاء عملية تأخذ موترًا من 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 هذه ZeroOut واحدًا إلى صفر من الأعداد الصحيحة 32 بت كمدخلات ، وتخرج zeroed to_zero من الأعداد الصحيحة 32 بت. يستخدم المرجع أيضًا وظيفة الشكل للتأكد من أن موتر الإخراج هو نفس شكل موتر الإدخال. على سبيل المثال ، إذا كان الإدخال موترًا للشكل [10 ، 20] ، فإن وظيفة الشكل هذه تحدد أن شكل الإخراج هو أيضًا [10 ، 20].

تنفيذ النواة للمرجع

بعد تحديد الواجهة ، قم بتوفير واحد أو أكثر من تطبيقات المرجع. لإنشاء أحد هذه النواة ، قم بإنشاء فئة تقوم بتوسيع OpKernel وتتجاوز طريقة Compute . توفر طريقة الحساب وسيطة context واحدة من النوع Compute 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 ، مثل فحص المدخلات وتخصيص المخرجات. في هذه الحالة ، يكون التنفيذ المقترح هو:

  1. حدد OpKernel على شكل قالب على الجهاز والنوع البدائي للموتر.
  2. للقيام بالحساب الفعلي للمخرجات ، تستدعي الدالة Compute بنية functor نموذجية.
  3. يتم تحديد تخصص ذلك الممر للجهاز CPUDevice في نفس الملف ، ولكن يتم تحديد التخصص لجهاز GPUDevice في ملف .cu.cc ، حيث سيتم تجميعه باستخدام برنامج التحويل البرمجي CUDA.

هنا مثال على التنفيذ.

// kernel_example.h
#ifndef KERNEL_EXAMPLE_H_
#define KERNEL_EXAMPLE_H_

#include <unsupported/Eigen/CXX11/Tensor>

template <typename Device, typename T>
struct ExampleFunctor {
  void operator()(const Device& d, int size, const T* in, T* out);
};

#if GOOGLE_CUDA
// Partially specialize functor for GpuDevice.
template <typename T>
struct ExampleFunctor<Eigen::GpuDevice, T> {
  void operator()(const Eigen::GpuDevice& d, int size, const T* in, T* out);
};
#endif

#endif KERNEL_EXAMPLE_H_
// kernel_example.cc
#include "kernel_example.h"

#include "tensorflow/core/framework/op.h"
#include "tensorflow/core/framework/shape_inference.h"
#include "tensorflow/core/framework/op_kernel.h"

using namespace tensorflow;

using CPUDevice = Eigen::ThreadPoolDevice;
using GPUDevice = Eigen::GpuDevice;

REGISTER_OP("Example")
    .Attr("T: numbertype")
    .Input("input: T")
    .Output("input_times_two: T")
    .SetShapeFn([](::tensorflow::shape_inference::InferenceContext* c) {
      c->set_output(0, c->input(0));
      return Status::OK();
    });

// CPU specialization of actual computation.
template <typename T>
struct ExampleFunctor<CPUDevice, T> {
  void operator()(const CPUDevice& d, int size, const T* in, T* out) {
    for (int i = 0; i < size; ++i) {
      out[i] = 2 * in[i];
    }
  }
};

// OpKernel definition.
// template parameter <T> is the datatype of the tensors.
template <typename Device, typename T>
class ExampleOp : public OpKernel {
 public:
  explicit ExampleOp(OpKernelConstruction* context) : OpKernel(context) {}

  void Compute(OpKernelContext* context) override {
    // Grab the input tensor
    const Tensor& input_tensor = context->input(0);

    // Create an output tensor
    Tensor* output_tensor = NULL;
    OP_REQUIRES_OK(context, context->allocate_output(0, input_tensor.shape(),
                                                     &output_tensor));

    // Do the computation.
    OP_REQUIRES(context, input_tensor.NumElements() <= tensorflow::kint32max,
                errors::InvalidArgument("Too many elements in tensor"));
    ExampleFunctor<Device, T>()(
        context->eigen_device<Device>(),
        static_cast<int>(input_tensor.NumElements()),
        input_tensor.flat<T>().data(),
        output_tensor->flat<T>().data());
  }
};

// Register the CPU kernels.
#define REGISTER_CPU(T)                                          \
  REGISTER_KERNEL_BUILDER(                                       \
      Name("Example").Device(DEVICE_CPU).TypeConstraint<T>("T"), \
      ExampleOp<CPUDevice, T>);
REGISTER_CPU(float);
REGISTER_CPU(int32);

// Register the GPU kernels.
#ifdef GOOGLE_CUDA
#define REGISTER_GPU(T)                                          \
  /* Declare explicit instantiations in kernel_example.cu.cc. */ \
  extern template class ExampleFunctor<GPUDevice, T>;            \
  REGISTER_KERNEL_BUILDER(                                       \
      Name("Example").Device(DEVICE_GPU).TypeConstraint<T>("T"), \
      ExampleOp<GPUDevice, T>);
REGISTER_GPU(float);
REGISTER_GPU(int32);
#endif  // GOOGLE_CUDA
// kernel_example.cu.cc
#ifdef GOOGLE_CUDA
#define EIGEN_USE_GPU
#include "kernel_example.h"
#include "tensorflow/core/util/gpu_kernel_helper.h"

using namespace tensorflow;

using GPUDevice = Eigen::GpuDevice;

// Define the CUDA kernel.
template <typename T>
__global__ void ExampleCudaKernel(const int size, const T* in, T* out) {
  for (int i = blockIdx.x * blockDim.x + threadIdx.x; i < size;
       i += blockDim.x * gridDim.x) {
    out[i] = 2 * __ldg(in + i);
  }
}

// Define the GPU implementation that launches the CUDA kernel.
template <typename T>
void ExampleFunctor<GPUDevice, T>::operator()(
    const GPUDevice& d, int size, const T* in, T* out) {
  // Launch the cuda kernel.
  //
  // See core/util/gpu_kernel_helper.h for example of computing
  // block count and thread_per_block count.
  int block_count = 1024;
  int thread_per_block = 20;
  ExampleCudaKernel<T>
      <<<block_count, thread_per_block, 0, d.stream()>>>(size, in, out);
}

// Explicitly instantiate functors for the types of OpKernels registered.
template struct ExampleFunctor<GPUDevice, float>;
template struct ExampleFunctor<GPUDevice, int32>;

#endif  // GOOGLE_CUDA

بناء مكتبة المرجع

قم بتجميع المرجع باستخدام مترجم النظام الخاص بك (تثبيت TensorFlow الثنائي)

يجب أن تكون قادرًا على ترجمة zero_out.cc باستخدام مترجم C++ مثل g++ أو clang المتاح على نظامك. تقوم حزمة PIP الثنائية بتثبيت ملفات الرأس والمكتبة التي تحتاجها لتجميع مرجعك في مواقع خاصة بالنظام. ومع ذلك ، توفر مكتبة get_include 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 باستخدام gcc4 الذي يستخدم ABI الأقدم. إذا جمعت مكتبة op الخاصة بك باستخدام gcc>=5 ، فأضف -D_GLIBCXX_USE_CXX11_ABI=0 إلى سطر الأوامر لجعل المكتبة متوافقة مع 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 ثلاث وسيطات:

بدلاً من ذلك ، إذا كنت تريد اختبار ما إذا كان كائن Status الذي تم إرجاعه من بعض الوظائف هو خطأ ، وإذا كان الأمر كذلك ، قم بإعادته ، استخدم OP_REQUIRES_OK . كل من وحدات الماكرو هذه ترجع من الدالة عند الخطأ.

تسجيل أب

أترس

يمكن أن تحتوي العمليات على Attrs ، والتي يتم تعيين قيمها عند إضافة المرجع إلى الرسم البياني. تُستخدم هذه لتكوين المرجع ، ويمكن الوصول إلى قيمها داخل تطبيق kernel وفي أنواع المدخلات والمخرجات في تسجيل المرجع. يفضل استخدام الإدخال بدلاً من attr عندما يكون ذلك ممكنًا ، لأن المدخلات أكثر مرونة. هذا لأن Attrs عبارة عن ثوابت ويجب تحديدها في وقت إنشاء الرسم البياني. في المقابل ، المدخلات هي Tensors التي يمكن أن تكون قيمها ديناميكية ؛ وهذا يعني أن المدخلات يمكن أن تتغير في كل خطوة ، وأن يتم ضبطها باستخدام موجز ، وما إلى ذلك. يتم استخدام Attrs للأشياء التي لا يمكن القيام بها مع المدخلات: أي تكوين يؤثر على التوقيع (عدد أو نوع المدخلات أو المخرجات) أو يمكن " ر التغيير من خطوة إلى خطوة.

أنت تحدد سمة عند تسجيل المرجع ، من خلال تحديد اسمها ونوعها باستخدام طريقة 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 المستخدم للمدخلات والمخرجات.)

يمكن لـ kernel بعد ذلك الوصول إلى هذا 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 : إحدى قيم (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

تسمية

يجب إعطاء أسماء المدخلات والمخرجات والأترس بشكل عام. الاستثناء الوحيد هو 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 على قائمة بالأنواع ، ويستخدم كنوع لكل out المدخلات in والمخرجات. المدخلات والمخرجات عبارة عن قوائم من الموترات من هذا النوع (وعدد وأنواع الموترات في المخرجات هي نفس المدخلات ، لأن كلاهما لهما النوع 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 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");

تعمل نفس الصيغة مع Attrs "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");
    

    تسمح لك الإشارة إلى 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 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 هنا .

هناك عدة طرق للمحافظة على التوافق مع الإصدارات السابقة.

  1. يجب تحديد قيم افتراضية لأي سمة جديدة مضافة إلى عملية ، وبهذه القيمة الافتراضية يجب أن يكون للمرجع السلوك الأصلي. لتغيير عملية من غير متعددة الأشكال إلى متعددة الأشكال ، يجب عليك إعطاء قيمة افتراضية إلى سمة النوع الجديد للحفاظ على التوقيع الأصلي افتراضيًا. على سبيل المثال ، إذا كانت عمليتك:

    REGISTER_OP("MyGeneralUnaryOp")
        .Input("in: float")
        .Output("out: float");
    

    يمكنك جعله متعدد الأشكال بطريقة متوافقة مع الإصدارات السابقة باستخدام:

    REGISTER_OP("MyGeneralUnaryOp")
        .Input("in: T")
        .Output("out: T")
        .Attr("T: numerictype = DT_FLOAT");
    
  2. يمكنك بأمان جعل القيد على سمة أقل تقييدًا. على سبيل المثال ، يمكنك التغيير من {int32, int64} إلى {int32, int64, float} أو type . أو يمكنك التغيير من {"apple", "orange"} إلى {"apple", "banana", "orange"} أو string .

  3. يمكنك تغيير المدخلات / المخرجات الفردية إلى مدخلات / مخرجات قائمة ، طالما أن الافتراضي لنوع القائمة يطابق التوقيع القديم.

  4. يمكنك إضافة إدخال / إخراج قائمة جديدة ، إذا كانت فارغة.

  5. Namespace أي عمليات جديدة تقوم بإنشائها ، عن طريق وضع بادئة لأسماء المرجع بشيء فريد لمشروعك. يؤدي هذا إلى تجنب اصطدام مرجعك بأي عمليات قد يتم تضمينها في الإصدارات المستقبلية من TensorFlow.

  6. خطط مسبقا! حاول توقع الاستخدامات المستقبلية للمرجع. لا يمكن إجراء بعض تغييرات التوقيع بطريقة متوافقة (على سبيل المثال ، عمل قائمة من نفس النوع في قائمة أنواع مختلفة).

يمكن العثور على القائمة الكاملة للتغييرات الآمنة وغير الآمنة في tensorflow/core/framework/op_compatibility_test.cc . إذا لم تتمكن من إجراء التغيير على عملية متوافقة مع الإصدارات السابقة ، فقم بإنشاء عملية جديدة باسم جديد باستخدام الدلالات الجديدة.

لاحظ أيضًا أنه على الرغم من أن هذه التغييرات يمكن أن تحافظ على توافق GraphDef ، فقد يتغير كود Python الذي تم إنشاؤه بطريقة لا تتوافق مع المتصلين القدامى. قد تظل Python API متوافقة من خلال تغييرات دقيقة في غلاف 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() لتسجيل النواة ، على سبيل المثال:

#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 إنشاء عمليات تشغيل جديدة من op.inputs[i] ، op.outputs[i] ، و grad . يمكن العثور على معلومات حول أي 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 لإدخال مع معرف الفهرس بواسطة c->input(idx) 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 من المصدر.