نشكرك على متابعة Google I / O. عرض جميع الجلسات عند الطلب مشاهدة عند الطلب

إنشاء المرجع

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

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

  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 الثنائية بتثبيت ملفات الرأس والمكتبة التي تحتاجها لتجميع مرجعك في مواقع خاصة بالنظام. ومع ذلك ، توفر مكتبة 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 هنا .

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

  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 متوافقة من خلال تغييرات دقيقة في غلاف 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 tensors op.inputs[i] ، op.outputs[i] ، and 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 لإدخال مع معرف 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 من المصدر.