یک op ایجاد کنید

اگر می خواهید یک op ایجاد کنید که توسط کتابخانه موجود TensorFlow پوشش داده نشود، توصیه می کنیم ابتدا op را در پایتون به عنوان ترکیبی از عملیات یا توابع موجود پایتون بنویسید. اگر این امکان وجود ندارد، می توانید یک عملیات C++ سفارشی ایجاد کنید. دلایل متعددی وجود دارد که ممکن است بخواهید یک عملیات سفارشی C++ ایجاد کنید:

  • بیان عملیات خود به عنوان ترکیبی از عملیات موجود آسان یا ممکن نیست.
  • این کارآمد نیست که عملکرد خود را به عنوان ترکیبی از موارد اولیه موجود بیان کنید.
  • شما می‌خواهید ترکیبی از ابتدایی‌ها را با دست ترکیب کنید که برای یک کامپایلر آینده، ادغام آن دشوار خواهد بود.

به عنوان مثال، تصور کنید می‌خواهید چیزی مانند «تجمع متوسط»، شبیه به عملگر «MaxPool» را پیاده‌سازی کنید، اما به جای مقادیر حداکثر، میانه‌ها را روی پنجره‌های کشویی محاسبه کنید. انجام این کار با استفاده از ترکیبی از عملیات ممکن است امکان پذیر باشد (مثلاً با استفاده از ExtractImagePatches و TopK)، اما ممکن است به اندازه یک عملیات بومی که در آن می توانید کار هوشمندانه تری را در یک عملیات واحد و ترکیبی انجام دهید، از نظر عملکرد یا حافظه کارآمد نباشد. مثل همیشه، معمولاً ابتدا ارزش آن را دارد که با استفاده از ترکیب عملگر، آنچه را که می خواهید بیان کنید، فقط در صورتی که دشوار یا ناکارآمد باشد، یک عملیات جدید را انتخاب کنید.

برای ادغام عملیات سفارشی خود باید:

  1. عملیات جدید را در یک فایل C++ ثبت کنید. ثبت عملیات یک رابط (مشخصات) برای عملکرد عملیات تعریف می کند که مستقل از اجرای عملیات است. به عنوان مثال، ثبت عملیات نام عملیات و ورودی ها و خروجی های عملیات را تعریف می کند. همچنین تابع شکلی را که برای استنتاج شکل تانسور استفاده می شود، تعریف می کند.
  2. عملیات را در C++ پیاده سازی کنید. پیاده‌سازی یک عملیات به عنوان هسته شناخته می‌شود، و اجرای دقیق مشخصاتی است که در مرحله 1 ثبت کرده‌اید. می‌تواند چندین هسته برای انواع ورودی/خروجی یا معماری‌های مختلف (به عنوان مثال، CPU، GPU) وجود داشته باشد.
  3. یک پوشش پایتون (اختیاری) ایجاد کنید. این wrapper API عمومی است که برای ایجاد op در پایتون استفاده می شود. یک پوشش پیش‌فرض از ثبت عملیات تولید می‌شود که می‌تواند مستقیماً استفاده شود یا به آن اضافه شود.
  4. یک تابع برای محاسبه گرادیان برای عملیات بنویسید (اختیاری).
  5. عملیات را تست کنید. ما معمولاً این کار را در پایتون برای راحتی انجام می دهیم، اما می توانید عملیات را در C++ نیز آزمایش کنید. اگر گرادیان ها را تعریف می کنید، می توانید آنها را با Python tf.test.compute_gradient_error تأیید کنید. relu_op_test.py به عنوان مثالی ببینید که توابع رو به جلو عملگرهای Relu مانند و گرادیان آنها را آزمایش می کند.

پیش نیازها

رابط op را تعریف کنید

شما رابط یک op را با ثبت آن در سیستم TensorFlow تعریف می کنید. در ثبت نام، نام عملیات خود، ورودی‌های آن (انواع و نام‌ها) و خروجی‌ها (انواع و نام‌ها)، و همچنین رشته‌های اسناد و هر نوع attr که ممکن است عملیات مورد نیاز باشد را مشخص می‌کنید.

برای اینکه ببینید چگونه این کار می کند، فرض کنید می خواهید عملیاتی ایجاد کنید که یک تانسور int32 s می گیرد و یک کپی از تانسور را خروجی می دهد و همه عنصر به جز اولین عنصر روی صفر تنظیم شده است. برای این کار فایلی با نام zero_out.cc ایجاد کنید. سپس یک تماس به ماکرو REGISTER_OP اضافه کنید که رابط عملیات شما را تعریف می کند:

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

using namespace tensorflow;

REGISTER_OP("ZeroOut")
    .Input("to_zero: int32")
    .Output("zeroed: int32")
    .SetShapeFn([](::tensorflow::shape_inference::InferenceContext* c) {
      c->set_output(0, c->input(0));
      return Status::OK();
    });

این عملیات ZeroOut یک تانسور to_zero از اعداد صحیح 32 بیتی را به عنوان ورودی می گیرد و یک تانسور zeroed از اعداد صحیح 32 بیتی را خروجی می دهد. op همچنین از یک تابع شکل برای اطمینان از اینکه تانسور خروجی همان شکل تانسور ورودی است استفاده می کند. به عنوان مثال، اگر ورودی یک تانسور شکل [10، 20] باشد، این تابع شکل مشخص می کند که شکل خروجی نیز [10،20] باشد.

هسته را برای عملیات پیاده سازی کنید

پس از اینکه رابط را تعریف کردید، یک یا چند پیاده سازی از عملیات را ارائه دهید. برای ایجاد یکی از این هسته ها، کلاسی ایجاد کنید که OpKernel گسترش دهد و متد Compute را لغو کند. متد Compute یک آرگومان context از نوع OpKernelContext* را ارائه می دهد که از آن می توانید به موارد مفیدی مانند تانسورهای ورودی و خروجی دسترسی داشته باشید.

هسته خود را به فایلی که در بالا ایجاد کردید اضافه کنید. هسته ممکن است چیزی شبیه به این باشد:

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

using namespace tensorflow;

class ZeroOutOp : public OpKernel {
 public:
  explicit ZeroOutOp(OpKernelConstruction* context) : OpKernel(context) {}

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

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

    // Set all but the first element of the output tensor to 0.
    const int N = input.size();
    for (int i = 1; i < N; i++) {
      output_flat(i) = 0;
    }

    // Preserve the first input value if possible.
    if (N > 0) output_flat(0) = input(0);
  }
};

پس از پیاده سازی هسته خود، آن را در سیستم TensorFlow ثبت می کنید. در ثبت نام، شما محدودیت های مختلفی را مشخص می کنید که این هسته تحت آن اجرا می شود. برای مثال، ممکن است یک هسته برای پردازنده‌های مرکزی و یک هسته جداگانه برای پردازنده‌های گرافیکی داشته باشید.

برای انجام این کار برای عملیات ZeroOut ، موارد زیر را به zero_out.cc اضافه کنید:

REGISTER_KERNEL_BUILDER(Name("ZeroOut").Device(DEVICE_CPU), ZeroOutOp);

هسته های CPU چند رشته ای

برای نوشتن یک هسته CPU چند رشته ای، می توان از تابع Shard در work_sharder.h استفاده کرد. این تابع یک تابع محاسباتی را در سراسر رشته‌های پیکربندی شده برای استفاده برای threading درون عملیاتی تقسیم می‌کند (به intra_op_parallelism_threads در config.proto مراجعه کنید).

هسته های GPU

یک هسته GPU در دو بخش پیاده سازی می شود: OpKernel و هسته CUDA و کد راه اندازی آن.

گاهی اوقات پیاده سازی OpKernel بین هسته CPU و GPU مشترک است، مانند بررسی ورودی ها و تخصیص خروجی ها. در آن صورت، پیاده سازی پیشنهادی به این صورت است:

  1. قالب OpKernel را روی Device و نوع اولیه تانسور را تعریف کنید.
  2. برای انجام محاسبات واقعی خروجی، تابع Compute یک ساختار تابع الگو را فراخوانی می کند.
  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

کتابخانه op را بسازید

اپ را با استفاده از کامپایلر سیستم خود کامپایل کنید (نصب باینری TensorFlow)

شما باید بتوانید zero_out.cc با یک کامپایلر C++ مانند g++ یا clang موجود در سیستم خود کامپایل کنید. بسته PIP باینری فایل‌های هدر و کتابخانه‌ای را که برای کامپایل کردن عملیات خود نیاز دارید در مکان‌هایی که مختص سیستم هستند نصب می‌کند. با این حال، کتابخانه پایتون TensorFlow تابع get_include را برای دریافت دایرکتوری هدر ارائه می‌کند، و دایرکتوری get_lib دارای یک شی به اشتراک گذاشته شده برای پیوند دادن است. در اینجا خروجی های این توابع در یک ماشین اوبونتو آمده است.

$ python
>>> import tensorflow as tf
>>> tf.sysconfig.get_include()
'/usr/local/lib/python3.6/site-packages/tensorflow/include'
>>> tf.sysconfig.get_lib()
'/usr/local/lib/python3.6/site-packages/tensorflow'

با فرض اینکه g++ را نصب کرده اید، در اینجا دنباله ای از دستوراتی است که می توانید برای کامپایل کردن op خود در یک کتابخانه پویا استفاده کنید.

TF_CFLAGS=( $(python -c 'import tensorflow as tf; print(" ".join(tf.sysconfig.get_compile_flags()))') )
TF_LFLAGS=( $(python -c 'import tensorflow as tf; print(" ".join(tf.sysconfig.get_link_flags()))') )
g++ -std=c++14 -shared zero_out.cc -o zero_out.so -fPIC ${TF_CFLAGS[@]} ${TF_LFLAGS[@]} -O2

در macOS، هنگام ساخت فایل .so ، پرچم اضافی "-undefined dynamic_lookup" مورد نیاز است.

نکته در مورد نسخه gcc >=5 : gcc از C++ ABI جدید از نسخه 5 استفاده می کند. TensorFlow 2.8 و نسخه‌های قبلی با gcc4 ساخته شده‌اند که از ABI قدیمی‌تر استفاده می‌کند. اگر از این نسخه‌های TensorFlow استفاده می‌کنید و می‌خواهید کتابخانه op خود را با gcc>=5 کامپایل کنید، -D_GLIBCXX_USE_CXX11_ABI=0 را به خط فرمان اضافه کنید تا کتابخانه با ABI قدیمی‌تر سازگار شود. بسته های TensorFlow 2.9+ به طور پیش فرض با ABI جدیدتر سازگار هستند.

عملیات را با استفاده از bazel (نصب منبع TensorFlow) کامپایل کنید

اگر منابع TensorFlow را نصب کرده اید، می توانید از سیستم ساخت TensorFlow برای کامپایل عملیات خود استفاده کنید. یک فایل BUILD را با قانون ساخت Bazel در دایرکتوری tensorflow/core/user_ops قرار دهید.

load("//tensorflow:tensorflow.bzl", "tf_custom_op_library")

tf_custom_op_library(
    name = "zero_out.so",
    srcs = ["zero_out.cc"],
)

دستور زیر را برای ساخت zero_out.so اجرا کنید.

$ bazel build --config opt //tensorflow/core/user_ops:zero_out.so

برای کامپایل کردن عملیات Example ، با هسته CUDA، باید از پارامتر gpu_srcs tf_custom_op_library استفاده کنید. یک فایل BUILD با قانون ساخت Bazel زیر را در یک پوشه جدید در دایرکتوری tensorflow/core/user_ops قرار دهید (به عنوان مثال "example_gpu").

load("//tensorflow:tensorflow.bzl", "tf_custom_op_library")

tf_custom_op_library(
    # kernel_example.cc  kernel_example.cu.cc  kernel_example.h
    name = "kernel_example.so",
    srcs = ["kernel_example.h", "kernel_example.cc"],
    gpu_srcs = ["kernel_example.cu.cc", "kernel_example.h"],
)

دستور زیر را برای ساخت kernel_example.so اجرا کنید.

$ bazel build --config opt //tensorflow/core/user_ops/example_gpu:kernel_example.so

از op در پایتون استفاده کنید

TensorFlow Python API تابع tf.load_op_library را برای بارگذاری کتابخانه پویا و ثبت عملیات با چارچوب TensorFlow فراهم می کند. load_op_library یک ماژول پایتون را برمی‌گرداند که شامل پوشه‌های پایتون برای op و هسته است. بنابراین، هنگامی که عملیات را ساختید، می‌توانید برای اجرای آن از پایتون کارهای زیر را انجام دهید:

import tensorflow as tf
zero_out_module = tf.load_op_library('./zero_out.so')
print(zero_out_module.zero_out([[1, 2], [3, 4]]).numpy())

# Prints
array([[1, 0], [0, 0]], dtype=int32)

به خاطر داشته باشید، تابع تولید شده یک نام snake_case داده می شود (برای مطابقت با PEP8 ). بنابراین، اگر عملیات شما ZeroOut در فایل های C++ نامیده شود، تابع پایتون zero_out نامیده می شود.

برای در دسترس قرار دادن op به عنوان یک تابع معمولی import -able از یک ماژول پایتون، شاید مفید باشد که فراخوانی load_op_library را در یک فایل منبع پایتون به صورت زیر داشته باشید:

import tensorflow as tf

zero_out_module = tf.load_op_library('./zero_out.so')
zero_out = zero_out_module.zero_out

بررسی کنید که عملیات کار می کند

یک راه خوب برای تأیید اینکه عملیات خود را با موفقیت اجرا کرده اید، نوشتن یک آزمایش برای آن است. فایل zero_out_op_test.py را با محتویات ایجاد کنید:

import tensorflow as tf

class ZeroOutTest(tf.test.TestCase):
  def testZeroOut(self):
    zero_out_module = tf.load_op_library('./zero_out.so')
    with self.test_session():
      result = zero_out_module.zero_out([5, 4, 3, 2, 1])
      self.assertAllEqual(result.eval(), [5, 0, 0, 0, 0])

if __name__ == "__main__":
  tf.test.main()

سپس تست خود را اجرا کنید (با فرض اینکه تنسورفلو را نصب کرده اید):

$ python zero_out_op_test.py

ویژگی های پیشرفته را در عملیات خود بسازید

اکنون که می‌دانید چگونه یک عملیات اساسی (و تا حدودی محدود) بسازید و پیاده‌سازی کنید، ما به برخی از موارد پیچیده‌تر که معمولاً باید در عملیات خود بسازید، نگاه می‌کنیم. این شامل:

بررسی های مشروط و اعتبارسنجی

در مثال بالا فرض شد که عملیات به یک تانسور با هر شکلی اعمال می شود. اگر فقط برای بردارها اعمال شود چه؟ این به معنای افزودن یک چک به اجرای OpKernel بالا است.

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

    OP_REQUIRES(context, TensorShapeUtils::IsVector(input_tensor.shape()),
                errors::InvalidArgument("ZeroOut expects a 1-D vector."));
    // ...
  }

این نشان می‌دهد که ورودی یک بردار است و اگر اینطور نباشد، وضعیت InvalidArgument را تنظیم می‌کند. ماکرو OP_REQUIRES سه آرگومان می گیرد:

  • context ، که می تواند یک نشانگر OpKernelContext یا OpKernelConstruction باشد (به tensorflow/core/framework/op_kernel.h مراجعه کنید)، برای متد SetStatus() آن.
  • شرایط. برای مثال، توابعی برای اعتبارسنجی شکل یک تانسور در tensorflow/core/framework/tensor_shape.h وجود دارد.
  • خود خطا که با یک شی Status نشان داده می شود، به tensorflow/core/platform/status.h مراجعه کنید. یک Status هم نوع دارد (اغلب InvalidArgument ، اما لیست انواع را ببینید) و هم یک پیام. توابع برای ساخت یک خطا ممکن است در tensorflow/core/platform/errors.h یافت شود.

از طرف دیگر، اگر می‌خواهید آزمایش کنید که آیا یک شی Status که از یک تابع بازگردانده شده است خطا است یا خیر، و اگر چنین است آن را برگردانید، از OP_REQUIRES_OK استفاده کنید. هر دوی این ماکروها از تابع در خطا برمی گردند.

ثبت نام عملیات

Attrs

Ops می تواند دارای attrs باشد که مقادیر آنها زمانی که op به یک نمودار اضافه می شود تنظیم می شود. اینها برای پیکربندی op استفاده می‌شوند و مقادیر آنها هم در پیاده‌سازی هسته و هم در انواع ورودی‌ها و خروجی‌ها در ثبت عملیات قابل دسترسی است. در صورت امکان از ورودی به جای attr استفاده کنید، زیرا ورودی ها انعطاف پذیرتر هستند. این به این دلیل است که attr ها ثابت هستند و باید در زمان ساخت گراف تعریف شوند. در مقابل، ورودی ها تانسورهایی هستند که مقادیر آنها می تواند پویا باشد. یعنی ورودی‌ها می‌توانند در هر مرحله تغییر کنند، با استفاده از فید تنظیم شوند، و غیره. Attrها برای کارهایی استفاده می‌شوند که با ورودی‌ها نمی‌توان انجام داد: هر پیکربندی که بر امضا تأثیر می‌گذارد (تعداد یا نوع ورودی‌ها یا خروجی‌ها) یا می‌تواند. گام به گام تغییر کند.

زمانی که op را ثبت می کنید، یک attr را با تعیین نام و نوع آن با استفاده از متد Attr تعریف می کنید، که انتظار مشخصات فرم را دارد:

<name>: <attr-type-expr>

که در آن <name> با یک حرف شروع می شود و می تواند از نویسه های الفبایی عددی و زیرخط تشکیل شده باشد، و <attr-type-expr> یک نوع عبارت از شکلی است که در زیر توضیح داده شده است .

به عنوان مثال، اگر می‌خواهید عملیات ZeroOut یک نمایه مشخص شده توسط کاربر را حفظ کند، به جای عنصر 0، می‌توانید عملیات را به این صورت ثبت کنید:

REGISTER_OP("ZeroOut")
    .Attr("preserve_index: int")
    .Input("to_zero: int32")
    .Output("zeroed: int32");

(توجه داشته باشید که مجموعه انواع ویژگی با tf.DType که برای ورودی ها و خروجی ها استفاده می شود متفاوت است.)

سپس هسته شما می تواند از طریق پارامتر context به این attr در سازنده خود دسترسی داشته باشد:

class ZeroOutOp : public OpKernel {
 public:
  explicit ZeroOutOp(OpKernelConstruction* context) : OpKernel(context) {
    // Get the index of the value to preserve
    OP_REQUIRES_OK(context,
                   context->GetAttr("preserve_index", &preserve_index_));
    // Check that preserve_index is positive
    OP_REQUIRES(context, preserve_index_ >= 0,
                errors::InvalidArgument("Need preserve_index >= 0, got ",
                                        preserve_index_));
  }
  void Compute(OpKernelContext* context) override {
    // ...
  }
 private:
  int preserve_index_;
};

که سپس می تواند در روش Compute استفاده شود:

  void Compute(OpKernelContext* context) override {
    // ...

    // We're using saved attr to validate potentially dynamic input
    // So we check that preserve_index is in range
    OP_REQUIRES(context, preserve_index_ < input.dimension(0),
                errors::InvalidArgument("preserve_index out of range"));

    // Set all the elements of the output tensor to 0
    const int N = input.size();
    for (int i = 0; i < N; i++) {
      output_flat(i) = 0;
    }

    // Preserve the requested input value
    output_flat(preserve_index_) = input(preserve_index_);
  }

انواع Attr

انواع زیر در attr پشتیبانی می شوند:

  • string : هر دنباله ای از بایت (لازم نیست UTF8 باشد).
  • int : یک عدد صحیح امضا شده.
  • float : یک عدد ممیز شناور.
  • bool : درست یا غلط.
  • type : یکی از مقادیر (غیر رفرنس) DataType .
  • shape : یک TensorShapeProto .
  • list(<type>) : لیستی از <type> که در آن <type> یکی از انواع بالا است. توجه داشته باشید که list(list(<type>)) نامعتبر است.

همچنین برای فهرست قطعی به: op_def_builder.cc:FinalizeAttr مراجعه کنید.

مقادیر و محدودیت های پیش فرض

Attr ها ممکن است مقادیر پیش فرض داشته باشند و برخی از انواع attr ها می توانند محدودیت هایی داشته باشند. برای تعریف attr با محدودیت ها، می توانید از <attr-type-expr> های زیر استفاده کنید:

{'<string1>', '<string2>'} : مقدار باید رشته ای باشد که دارای مقدار <string1> یا <string2> باشد. هنگامی که از این نحو استفاده می کنید، نام نوع، string ، مشخص می شود. این یک enum را شبیه سازی می کند:

REGISTER_OP("EnumExample")
    .Attr("e: {'apple', 'orange'}");

{<type1>, <type2>} : مقدار از type است و باید یکی از <type1> یا <type2> باشد، جایی که <type1> و <type2> tf.DType پشتیبانی می‌شوند. شما مشخص نمی کنید که نوع attr type باشد. هنگامی که شما لیستی از انواع در {...} دارید، این امر به معنایی است. برای مثال، در این مورد attr t نوعی است که باید int32 ، float یا bool باشد:

REGISTER_OP("RestrictedTypeExample")
    .Attr("t: {int32, float, bool}");

میانبرهایی برای محدودیت های نوع رایج وجود دارد:

  • numbertype : نوع type محدود به انواع عددی (غیر رشته ای و غیر bool).
  • realnumbertype : مانند numbertype بدون انواع پیچیده.
  • quantizedtype : مانند numbertype اما فقط انواع quantized اعداد.

لیست های خاصی از انواع مجاز توسط این توابع (مانند NumberTypes() ) در tensorflow/core/framework/types.h تعریف می شوند. در این مثال attr t باید یکی از انواع عددی باشد:

REGISTER_OP("NumberType")
    .Attr("t: numbertype");

برای این عملیات:

tf.number_type(t=tf.int32)  # Valid
tf.number_type(t=tf.bool)   # Invalid

لیست ها را می توان با لیست های دیگر و انواع تک ترکیب کرد. عملیات زیر به attr t اجازه می دهد تا هر یک از انواع عددی یا نوع bool باشد:

REGISTER_OP("NumberOrBooleanType")
    .Attr("t: {numbertype, bool}");

برای این عملیات:

tf.number_or_boolean_type(t=tf.int32)  # Valid
tf.number_or_boolean_type(t=tf.bool)   # Valid
tf.number_or_boolean_type(t=tf.string) # Invalid

int >= <n> : مقدار باید یک int باشد که مقدار آن بزرگتر یا مساوی <n> باشد، که در آن <n> یک عدد طبیعی است. به عنوان مثال، ثبت عملیات زیر مشخص می کند که attr a باید مقداری حداقل 2 داشته باشد:

REGISTER_OP("MinIntExample")
    .Attr("a: int >= 2");

list(<type>) >= <n> : لیستی از نوع <type> که طول آن بزرگتر یا مساوی <n> است. به عنوان مثال، ثبت عملیات زیر مشخص می کند که attr a لیستی از انواع (اعم از int32 یا float ) است و باید حداقل 3 مورد از آنها وجود داشته باشد:

REGISTER_OP("TypeListExample")
    .Attr("a: list({int32, float}) >= 3");

برای تنظیم یک مقدار پیش‌فرض برای attr (که آن را در کد تولید شده اختیاری می‌کند)، = <default> به انتها اضافه کنید، مانند:

REGISTER_OP("AttrDefaultExample")
    .Attr("i: int = 0");

علاوه بر این، هم یک محدودیت و هم یک مقدار پیش‌فرض می‌تواند مشخص شود:

REGISTER_OP("AttrConstraintAndDefaultExample")
    .Attr("i: int >= 1 = 1");

نحو پشتیبانی شده از مقدار پیش فرض همان چیزی است که در نمایش اولیه تعریف GraphDef حاصل استفاده می شود.

در اینجا مثال هایی برای نحوه تعیین یک پیش فرض برای همه انواع آورده شده است:

REGISTER_OP("AttrDefaultExampleForAllTypes")
   .Attr("s: string = 'foo'")
   .Attr("i: int = 0")
   .Attr("f: float = 1.0")
   .Attr("b: bool = true")
   .Attr("ty: type = DT_INT32")
   .Attr("sh: shape = { dim { size: 1 } dim { size: 2 } }")
   .Attr("te: tensor = { dtype: DT_INT32 int_val: 5 }")
   .Attr("l_empty: list(int) = []")
   .Attr("l_int: list(int) = [2, 3, 5, 7]");

به ویژه توجه داشته باشید که مقادیر type از tf.DType استفاده می کنند.

پلی مورفیسم

پلی مورفیسم نوع

برای عملیات هایی که می توانند انواع مختلفی را به عنوان ورودی دریافت کنند یا انواع خروجی های مختلفی تولید کنند، می توانید یک attr را در یک نوع ورودی یا خروجی در ثبت عملیات مشخص کنید. معمولاً برای هر نوع پشتیبانی شده یک OpKernel ثبت می کنید.

به عنوان مثال، اگر می خواهید که ZeroOut op علاوه بر int32 روی float s نیز کار کند، ثبت عملیات شما ممکن است به شکل زیر باشد:

REGISTER_OP("ZeroOut")
    .Attr("T: {float, int32}")
    .Input("to_zero: T")
    .Output("zeroed: T");

ثبت عملیات شما اکنون مشخص می‌کند که نوع ورودی باید float یا int32 باشد و خروجی آن از نوع T باشد، زیرا هر دو دارای نوع T هستند.

نامگذاری

ورودی ها، خروجی ها و attr ها عموماً باید نام snake_case داده شوند. تنها استثنا، attr ها هستند که به عنوان نوع ورودی یا در نوع خروجی استفاده می شوند. زمانی که op به نمودار اضافه می شود، می توان آن attr ها را استنباط کرد و بنابراین در تابع op ظاهر نمی شوند. به عنوان مثال، آخرین تعریف ZeroOut یک تابع پایتون را ایجاد می کند که به شکل زیر است:

def zero_out(to_zero, name=None):
  """...
  Args:
    to_zero: A `Tensor`. Must be one of the following types:
        `float32`, `int32`.
    name: A name for the operation (optional).

  Returns:
    A `Tensor`. Has the same type as `to_zero`.
  """

اگر to_zero از یک تانسور int32 عبور داده شود، T به طور خودکار روی int32 تنظیم می شود (خوب، در واقع DT_INT32 ). به آن عبارات استنباط‌شده نام‌های بزرگ یا CamelCase داده می‌شود.

این را با یک op مقایسه کنید که دارای یک نوع attr است که نوع خروجی را تعیین می کند:

REGISTER_OP("StringToNumber")
    .Input("string_tensor: string")
    .Output("output: out_type")
    .Attr("out_type: {float, int32} = DT_FLOAT");
    .Doc(R"doc(
Converts each string in the input Tensor to the specified numeric type.
)doc");

در این مورد، کاربر باید نوع خروجی را مانند پایتون تولید شده مشخص کند:

def string_to_number(string_tensor, out_type=None, name=None):
  """Converts each string in the input Tensor to the specified numeric type.

  Args:
    string_tensor: A `Tensor` of type `string`.
    out_type: An optional `tf.DType` from: `tf.float32, tf.int32`.
      Defaults to `tf.float32`.
    name: A name for the operation (optional).

  Returns:
    A `Tensor` of type `out_type`.
  """
نمونه چندشکلی نوع
#include "tensorflow/core/framework/op_kernel.h"

class ZeroOutInt32Op : public OpKernel {
  // as before
};

class ZeroOutFloatOp : public OpKernel {
 public:
  explicit ZeroOutFloatOp(OpKernelConstruction* context)
      : OpKernel(context) {}

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

    // Create an output tensor
    Tensor* output = NULL;
    OP_REQUIRES_OK(context,
                   context->allocate_output(0, input_tensor.shape(), &output));
    auto output_flat = output->template flat<float>();

    // Set all the elements of the output tensor to 0
    const int N = input.size();
    for (int i = 0; i < N; i++) {
      output_flat(i) = 0;
    }

    // Preserve the first input value
    if (N > 0) output_flat(0) = input(0);
  }
};

// Note that TypeConstraint<int32>("T") means that attr "T" (defined
// in the op registration above) must be "int32" to use this template
// instantiation.
REGISTER_KERNEL_BUILDER(
    Name("ZeroOut")
    .Device(DEVICE_CPU)
    .TypeConstraint<int32>("T"),
    ZeroOutInt32Op);
REGISTER_KERNEL_BUILDER(
    Name("ZeroOut")
    .Device(DEVICE_CPU)
    .TypeConstraint<float>("T"),
    ZeroOutFloatOp);

برای حفظ سازگاری با عقب ، باید یک مقدار پیش فرض را هنگام افزودن attr به یک عملیات موجود مشخص کنید:

REGISTER_OP("ZeroOut")
  .Attr("T: {float, int32} = DT_INT32")
  .Input("to_zero: T")
  .Output("zeroed: T")

فرض کنید می‌خواهید انواع بیشتری اضافه کنید، بگویید double :

REGISTER_OP("ZeroOut")
    .Attr("T: {float, double, int32}")
    .Input("to_zero: T")
    .Output("zeroed: T");

به جای نوشتن OpKernel دیگر با کد اضافی مانند بالا، اغلب می توانید به جای آن از یک الگوی C++ استفاده کنید. شما همچنان یک ثبت هسته ( REGISTER_KERNEL_BUILDER تماس) در هر بار اضافه خواهید داشت.

template <typename T>
class ZeroOutOp : public OpKernel {
 public:
  explicit ZeroOutOp(OpKernelConstruction* context) : OpKernel(context) {}

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

    // Create an output tensor
    Tensor* output = NULL;
    OP_REQUIRES_OK(context,
                   context->allocate_output(0, input_tensor.shape(), &output));
    auto output_flat = output->template flat<T>();

    // Set all the elements of the output tensor to 0
    const int N = input.size();
    for (int i = 0; i < N; i++) {
      output_flat(i) = 0;
    }

    // Preserve the first input value
    if (N > 0) output_flat(0) = input(0);
  }
};

// Note that TypeConstraint<int32>("T") means that attr "T" (defined
// in the op registration above) must be "int32" to use this template
// instantiation.
REGISTER_KERNEL_BUILDER(
    Name("ZeroOut")
    .Device(DEVICE_CPU)
    .TypeConstraint<int32>("T"),
    ZeroOutOp<int32>);
REGISTER_KERNEL_BUILDER(
    Name("ZeroOut")
    .Device(DEVICE_CPU)
    .TypeConstraint<float>("T"),
    ZeroOutOp<float>);
REGISTER_KERNEL_BUILDER(
    Name("ZeroOut")
    .Device(DEVICE_CPU)
    .TypeConstraint<double>("T"),
    ZeroOutOp<double>);

اگر بیش از دو بار اضافه بار دارید، می توانید ثبت نام را در یک ماکرو قرار دهید.

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

#define REGISTER_KERNEL(type)                                       \
  REGISTER_KERNEL_BUILDER(                                          \
      Name("ZeroOut").Device(DEVICE_CPU).TypeConstraint<type>("T"), \
      ZeroOutOp<type>)

REGISTER_KERNEL(int32);
REGISTER_KERNEL(float);
REGISTER_KERNEL(double);

#undef REGISTER_KERNEL

بسته به لیست انواعی که هسته را برای آنها ثبت می کنید، ممکن است بتوانید از یک ماکرو ارائه شده توسط tensorflow/core/framework/register_types.h استفاده کنید:

#include "tensorflow/core/framework/op_kernel.h"
#include "tensorflow/core/framework/register_types.h"

REGISTER_OP("ZeroOut")
    .Attr("T: realnumbertype")
    .Input("to_zero: T")
    .Output("zeroed: T");

template <typename T>
class ZeroOutOp : public OpKernel { ... };

#define REGISTER_KERNEL(type)                                       \
  REGISTER_KERNEL_BUILDER(                                          \
      Name("ZeroOut").Device(DEVICE_CPU).TypeConstraint<type>("T"), \
      ZeroOutOp<type>)

TF_CALL_REAL_NUMBER_TYPES(REGISTER_KERNEL);

#undef REGISTER_KERNEL
ورودی ها و خروجی ها را فهرست کنید

Ops علاوه بر توانایی پذیرش یا تولید انواع مختلف، می تواند تعداد متغیری از تانسورها را مصرف یا تولید کند.

در مثال بعدی، attr T فهرستی از انواع را در خود دارد و به عنوان نوع in و out استفاده می‌شود. ورودی و خروجی لیستی از تانسورهای آن نوع هستند (و تعداد و انواع تانسورها در خروجی با ورودی یکسان است، زیرا هر دو دارای نوع T هستند).

REGISTER_OP("PolymorphicListExample")
    .Attr("T: list(type)")
    .Input("in: T")
    .Output("out: T");

همچنین می‌توانید محدودیت‌هایی را در مورد انواعی که می‌توان در لیست مشخص کرد، اعمال کنید. در این مورد بعدی، ورودی فهرستی از تانسورهای float و double است. عملیات به عنوان مثال، انواع ورودی (float, double, float) را می پذیرد و در آن صورت نوع خروجی نیز خواهد بود (float, double, float) .

REGISTER_OP("ListTypeRestrictionExample")
    .Attr("T: list({float, double})")
    .Input("in: T")
    .Output("out: T");

اگر می‌خواهید همه تانسورهای یک لیست از یک نوع باشند، می‌توانید کاری مانند:

REGISTER_OP("IntListInputExample")
    .Attr("N: int")
    .Input("in: N * int32")
    .Output("out: int32");

این لیستی از تانسورهای int32 را می پذیرد و از یک int attr N برای تعیین طول لیست استفاده می کند.

این را می توان از نوع چند شکلی نیز ساخت. در مثال بعدی، ورودی لیستی از تانسورها (با طول "N" ) از نوع مشابه (اما نامشخص) ( "T" ) است، و خروجی یک تانسور منفرد از نوع منطبق است:

REGISTER_OP("SameListInputExample")
    .Attr("N: int")
    .Attr("T: type")
    .Input("in: N * T")
    .Output("out: T");

به طور پیش‌فرض، فهرست‌های تانسور دارای حداقل طول 1 هستند. می‌توانید این پیش‌فرض را با استفاده از یک محدودیت ">=" در attr مربوطه تغییر دهید. در این مثال بعدی، ورودی فهرستی از حداقل 2 تانسور int32 است:

REGISTER_OP("MinLengthIntListExample")
    .Attr("N: int >= 2")
    .Input("in: N * int32")
    .Output("out: int32");

همان نحو با attr های "list(type)" کار می کند:

REGISTER_OP("MinimumLengthPolymorphicListExample")
    .Attr("T: list(type) >= 3")
    .Input("in: T")
    .Output("out: T");

ورودی ها و خروجی ها

برای خلاصه کردن موارد فوق، یک ثبت عملیات می تواند چندین ورودی و خروجی داشته باشد:

REGISTER_OP("MultipleInsAndOuts")
    .Input("y: int32")
    .Input("z: float")
    .Output("a: string")
    .Output("b: int32");

هر مشخصات ورودی یا خروجی به این شکل است:

<name>: <io-type-expr>

جایی که <name> با یک حرف شروع می شود و می تواند از کاراکترهای الفبایی عددی و زیرخط تشکیل شده باشد. <io-type-expr> یکی از عبارات نوع زیر است:

  • <type> ، که در آن <type> یک نوع ورودی پشتیبانی شده است (به عنوان مثال float ، int32 ، string ). این یک تانسور منفرد از نوع داده شده را مشخص می کند.

    tf.DType ببینید.

    REGISTER_OP("BuiltInTypesExample")
        .Input("integers: int32")
        .Input("complex_numbers: complex64");
    
  • <attr-type> ، که در آن <attr-type> نام یک Attr با نوع type یا list(type) است (با محدودیت نوع احتمالی). این نحو به عملیات چند شکلی اجازه می دهد.

    REGISTER_OP("PolymorphicSingleInput")
        .Attr("T: type")
        .Input("in: T");
    
    REGISTER_OP("RestrictedPolymorphicSingleInput")
        .Attr("T: {int32, int64}")
        .Input("in: T");
    

    ارجاع به attr از نوع list(type) به شما امکان می دهد دنباله ای از تانسورها را بپذیرید.

    REGISTER_OP("ArbitraryTensorSequenceExample")
        .Attr("T: list(type)")
        .Input("in: T")
        .Output("out: T");
    
    REGISTER_OP("RestrictedTensorSequenceExample")
        .Attr("T: list({int32, int64})")
        .Input("in: T")
        .Output("out: T");
    

    توجه داشته باشید که تعداد و انواع تانسورها در out مانند ورودی in است، زیرا هر دو از نوع T هستند.

  • برای دنباله ای از تانسورها با همان نوع: <number> * <type> ، که در آن <number> نام Attr با نوع int است. <type> می تواند یک tf.DType یا نام یک attr با type نوع باشد. به عنوان نمونه اول، این عملیات فهرستی از تانسورهای int32 را می پذیرد:

    REGISTER_OP("Int32SequenceExample")
        .Attr("NumTensors: int")
        .Input("in: NumTensors * int32")
    

    در حالی که این عملیات فهرستی از تانسورها از هر نوع را می پذیرد، تا زمانی که همه آنها یکسان باشند:

    REGISTER_OP("SameTypeSequenceExample")
        .Attr("NumTensors: int")
        .Attr("T: type")
        .Input("in: NumTensors * T")
    
  • برای ارجاع به تانسور: Ref(<type>) که در آن <type> یکی از انواع قبلی است.

هر attr استفاده شده در نوع ورودی استنباط می شود. طبق قرارداد آن عطف های استنباط شده از نام های بزرگ (مانند T یا N ) استفاده می کنند. در غیر این صورت ورودی ها، خروجی ها و attr ها دارای نام هایی مانند پارامترهای تابع هستند (به عنوان مثال num_outputs ). برای جزئیات بیشتر، بخش قبلی در مورد نامگذاری را ببینید.

برای جزئیات بیشتر، به tensorflow/core/framework/op_def_builder.h مراجعه کنید.

سازگاری با عقب

بیایید فرض کنیم شما یک عملیات خوب و سفارشی نوشته‌اید و آن را با دیگران به اشتراک گذاشته‌اید، بنابراین مشتریان خوشحالی خواهید داشت که از عملیات خود استفاده می‌کنند. با این حال، شما می خواهید به طریقی تغییراتی در عملیات اعمال کنید.

به طور کلی، تغییرات در مشخصات موجود و بررسی شده باید با عقب سازگار باشد: تغییر مشخصات یک عملیات نباید بافرهای پروتکل سریال GraphDef قبلی ساخته شده از مشخصات قدیمی را خراب کند. جزئیات سازگاری GraphDef در اینجا توضیح داده شده است.

چندین راه برای حفظ سازگاری با عقب وجود دارد.

  1. هر attr جدید اضافه شده به یک عملیات باید دارای مقادیر پیش‌فرض تعریف شده باشد و با آن مقدار پیش‌فرض، op باید رفتار اصلی را داشته باشد. برای تغییر یک عملیات از غیر چند شکلی به چند شکلی، باید یک مقدار پیش فرض به نوع جدید attr بدهید تا امضای اصلی به طور پیش فرض حفظ شود. به عنوان مثال، اگر عملیات شما این بود:

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

    شما می توانید آن را به روشی سازگار با عقب با استفاده از موارد زیر چند شکلی کنید:

    REGISTER_OP("MyGeneralUnaryOp")
        .Input("in: T")
        .Output("out: T")
        .Attr("T: numerictype = DT_FLOAT");
    
  2. شما می توانید با خیال راحت محدودیتی را برای attr کمتر محدود کنید. به عنوان مثال، می توانید از {int32, int64} به {int32, int64, float} تغییر دهید یا type . یا ممکن است از {"apple", "orange"} به {"apple", "banana", "orange"} یا string تغییر دهید.

  3. تا زمانی که پیش‌فرض نوع فهرست با امضای قدیمی مطابقت داشته باشد، می‌توانید ورودی‌ها/خروجی‌ها را به ورودی/خروجی فهرست تغییر دهید.

  4. اگر به طور پیش فرض خالی باشد، می توانید یک ورودی/خروجی لیست جدید اضافه کنید.

  5. هر عملیات جدیدی را که ایجاد می کنید، با پیشوند نام های عملیاتی با چیزی منحصر به فرد برای پروژه خود، فضای نامی ایجاد کنید. این از برخورد عملیات شما با هر عملیاتی که ممکن است در نسخه‌های بعدی TensorFlow گنجانده شود، جلوگیری می‌کند.

  6. از پیش برنامه ریزی! سعی کنید کاربردهای آتی این عملیات را پیش بینی کنید. برخی از تغییرات امضا را نمی توان به روشی سازگار انجام داد (به عنوان مثال، ایجاد یک لیست از همان نوع به لیستی از انواع مختلف).

لیست کامل تغییرات ایمن و ناایمن را می توان در tensorflow/core/framework/op_compatibility_test.cc یافت. اگر نمی توانید تغییر خود را به یک عملیات سازگار با معکوس کنید، یک عملیات جدید با نام جدید با معنای جدید ایجاد کنید.

همچنین توجه داشته باشید که اگرچه این تغییرات می تواند سازگاری GraphDef را حفظ کند، کد پایتون تولید شده ممکن است به گونه ای تغییر کند که با تماس گیرندگان قدیمی سازگار نباشد. API پایتون را می‌توان با تغییرات دقیق در بسته‌بندی دست‌نویس پایتون، با حفظ امضای قدیمی به جز افزودن آرگومان‌های اختیاری جدید به انتها، سازگار نگه داشت. معمولاً تغییرات ناسازگار ممکن است تنها زمانی ایجاد شوند که TensorFlow نسخه‌های اصلی را تغییر دهد و باید با معنایی نسخه GraphDef مطابقت داشته باشد.

پشتیبانی از پردازنده گرافیکی

می‌توانید OpKernel‌های مختلفی را پیاده‌سازی کنید و یکی را برای CPU و دیگری را برای GPU ثبت کنید، همانطور که می‌توانید کرنل‌ها را برای انواع مختلف ثبت کنید . چندین نمونه از هسته هایی با پشتیبانی از GPU در tensorflow/core/kernels/ وجود دارد. توجه داشته باشید که برخی از کرنل ها دارای یک نسخه CPU در یک فایل .cc ، یک نسخه GPU در یک فایل با پایان _gpu.cu.cc و برخی کدهای مشترک در یک فایل .h هستند.

به عنوان مثال، tf.pad همه چیز دارد به جز هسته GPU در tensorflow/core/kernels/pad_op.cc . هسته GPU در tensorflow/core/kernels/pad_op_gpu.cu.cc است و کد مشترک یک کلاس قالبی است که در tensorflow/core/kernels/pad_op.h تعریف شده است. ما کد را به دو دلیل به این ترتیب سازماندهی می کنیم: به شما امکان می دهد کدهای مشترک را بین پیاده سازی های CPU و GPU به اشتراک بگذارید و اجرای GPU را در یک فایل جداگانه قرار می دهد تا فقط توسط کامپایلر GPU کامپایل شود.

نکته ای که باید به آن توجه داشت، حتی زمانی که از نسخه هسته GPU pad استفاده می شود، همچنان به ورودی "paddings" خود در حافظه CPU نیاز دارد. برای مشخص کردن اینکه ورودی ها یا خروجی ها روی CPU نگهداری می شوند، یک فراخوانی HostMemory() به ثبت هسته اضافه کنید، به عنوان مثال:

#define REGISTER_GPU_KERNEL(T)                         \
  REGISTER_KERNEL_BUILDER(Name("Pad")                  \
                              .Device(DEVICE_GPU)      \
                              .TypeConstraint<T>("T")  \
                              .HostMemory("paddings"), \
                          PadOp<GPUDevice, T>)

کامپایل کردن هسته برای دستگاه GPU

برای مثالی که از هسته CUDA برای پیاده سازی یک op استفاده می کند، به cuda_op_kernel.cu.cc نگاه کنید. tf_custom_op_library یک آرگومان gpu_srcs را می‌پذیرد که در آن فهرست فایل‌های منبع حاوی هسته‌های CUDA (فایل‌های *.cu.cc ) را می‌توان مشخص کرد. برای استفاده با نصب باینری TensorFlow، هسته های CUDA باید با کامپایلر nvcc انویدیا کامپایل شوند. در اینجا دنباله ای از دستورات است که می توانید از آنها برای کامپایل cuda_op_kernel.cu.cc و cuda_op_kernel.cc در یک کتابخانه واحد به صورت پویا استفاده کنید:

nvcc -std=c++14 -c -o cuda_op_kernel.cu.o cuda_op_kernel.cu.cc \
  ${TF_CFLAGS[@]} -D GOOGLE_CUDA=1 -x cu -Xcompiler -fPIC

g++ -std=c++14 -shared -o cuda_op_kernel.so cuda_op_kernel.cc \
  cuda_op_kernel.cu.o ${TF_CFLAGS[@]} -fPIC -lcudart ${TF_LFLAGS[@]}

cuda_op_kernel.so تولید شده در بالا می تواند طبق معمول در پایتون با استفاده از تابع tf.load_op_library بارگذاری شود.

توجه داشته باشید که اگر کتابخانه های CUDA شما در /usr/local/lib64 نصب نشده باشند، باید مسیر را به صراحت در دستور دوم (g++) بالا مشخص کنید. به عنوان مثال، اگر CUDA شما در /usr/local/cuda-8.0 نصب شده است، -L /usr/local/cuda-8.0/lib64/ را اضافه کنید.

گرادیان را در پایتون پیاده کنید

با توجه به نموداری از عملیات، TensorFlow از تمایز خودکار (پس انتشار) برای اضافه کردن عملیات جدید که نشان دهنده گرادیان ها با توجه به عملیات های موجود است، استفاده می کند. برای اینکه تمایز خودکار برای عملیات‌های جدید کار کند، باید یک تابع گرادیان ثبت کنید که گرادیان‌ها را با توجه به ورودی‌های عملیات، گرادیان‌های داده‌شده با توجه به خروجی‌های عملیات محاسبه می‌کند.

از نظر ریاضی، اگر یک عملیات \(y = f(x)\) محاسبه کند، عملیات گرادیان ثبت شده، گرادیان های \(\partial L/ \partial y\) از دست دادن \(L\) با توجه به\(y\) به گرادیان های \(\partial L/ \partial x\) با توجه به قانون \(x\) تبدیل می کند:

\[\frac{\partial L}{\partial x} = \frac{\partial L}{\partial y} \frac{\partial y}{\partial x} = \frac{\partial L}{\partial y} \frac{\partial f}{\partial x}.\]

در مورد ZeroOut ، تنها یک ورودی در ورودی بر خروجی تأثیر می‌گذارد، بنابراین گرادیان نسبت به ورودی یک تانسور «یک داغ» پراکنده است. این به صورت زیر بیان می شود:

from tensorflow.python.framework import ops
from tensorflow.python.ops import array_ops
from tensorflow.python.ops import sparse_ops

@ops.RegisterGradient("ZeroOut")
def _zero_out_grad(op, grad):
  """The gradients for `zero_out`.

  Args:
    op: The `zero_out` `Operation` that we are differentiating, which we can use
      to find the inputs and outputs of the original op.
    grad: Gradient with respect to the output of the `zero_out` op.

  Returns:
    Gradients with respect to the input of `zero_out`.
  """
  to_zero = op.inputs[0]
  shape = array_ops.shape(to_zero)
  index = array_ops.zeros_like(shape)
  first_grad = array_ops.reshape(grad, [-1])[0]
  to_zero_grad = sparse_ops.sparse_to_dense([index], shape, first_grad, 0)
  return [to_zero_grad]  # List of one Tensor, since we have one input

جزئیات ثبت توابع گرادیان با tf.RegisterGradient :

  • برای یک عملیات با یک خروجی، تابع گرادیان یک tf.Operation ، op ، و یک grad tf.Tensor را می گیرد و از تانسورهای op.inputs[i] ، op.outputs[i] و grad عملیات جدیدی می سازد. اطلاعات مربوط به هر attr را می توان از طریق tf.Operation.get_attr یافت.

  • اگر عملیات چند خروجی داشته باشد، تابع گرادیان op و grads می گیرد، جایی که grads لیستی از گرادیان ها با توجه به هر خروجی است. نتیجه تابع گرادیان باید لیستی از اشیاء Tensor باشد که گرادیان ها را با توجه به هر ورودی نشان می دهد.

  • اگر برای برخی از ورودی‌ها، گرادیان کاملاً تعریف‌شده‌ای وجود نداشته باشد، مانند ورودی‌های عدد صحیح که به‌عنوان شاخص استفاده می‌شوند، گرادیان بازگشتی مربوطه باید None باشد. به عنوان مثال، برای عملیاتی که یک تانسور ممیز شناور x و یک اندیس صحیح i را می گیرد، تابع گرادیان return [x_grad, None] .

  • اگر اصلاً گرادیان معنی‌داری برای op وجود نداشته باشد، اغلب مجبور نخواهید بود که هیچ گرادیانی را ثبت کنید، و تا زمانی که گرادیان عملیات هرگز مورد نیاز نباشد، خوب خواهید بود. در برخی موارد، یک عملیات شیب مشخصی ندارد اما می تواند در محاسبه گرادیان دخالت داشته باشد. در اینجا می توانید از ops.NotDifferentiable برای انتشار خودکار صفرها به عقب استفاده کنید.

توجه داشته باشید که در زمانی که تابع گرادیان فراخوانی می شود، فقط نمودار جریان داده عملیات در دسترس است، نه خود داده تانسور. بنابراین، تمام محاسبات باید با استفاده از سایر عملیات tensorflow انجام شوند تا در زمان اجرای نمودار اجرا شوند.

هنگام ثبت گرادیان سفارشی برای نوع op، نکات نوع اضافه کنید تا کد خواناتر، قابل اشکال زدایی، نگهداری آسان تر و از طریق اعتبارسنجی داده ها قوی تر شود. به عنوان مثال، هنگام در نظر گرفتن یک op به عنوان پارامتر در یک تابع، مشخص کنید که تابع گرادیان یک tf.Operation را به عنوان نوع پارامتر خود می گیرد.

توابع شکل در C++

API TensorFlow دارای ویژگی به نام «استنتاج شکل» است که اطلاعاتی در مورد اشکال تانسورها بدون نیاز به اجرای نمودار ارائه می دهد. استنتاج شکل توسط "توابع شکل" که برای هر نوع عملیات در اعلان C++ REGISTER_OP ثبت شده است، پشتیبانی می‌شود و دو نقش را انجام می‌دهد: بیان اینکه اشکال ورودی‌ها در طول ساخت نمودار با هم سازگار هستند، و مشخص کردن اشکال برای خروجی‌ها.

توابع شکل به عنوان عملیات روی کلاس shape_inference::InferenceContext تعریف می شوند. به عنوان مثال، در تابع شکل برای ZeroOut:

    .SetShapeFn([](::tensorflow::shape_inference::InferenceContext* c) {
      c->set_output(0, c->input(0));
      return Status::OK();
    });

c->set_output(0, c->input(0)); اعلام می کند که شکل اولین خروجی باید به شکل اولین ورودی تنظیم شود. اگر خروجی با شاخص خود مانند مثال بالا انتخاب شود، دومین پارامتر set_output باید یک شی ShapeHandle باشد. شما می توانید یک شی ShapeHandle خالی توسط سازنده پیش فرض آن ایجاد کنید. شی ShapeHandle برای ورودی با شاخص idx را می توان با c->input(idx) به دست آورد.

تعدادی توابع شکل رایج وجود دارد که برای بسیاری از عملیات ها اعمال می شود، مانند shape_inference::UnchangedShape که در common_shape_fns.h یافت می شود و به صورت زیر استفاده می شود:

REGISTER_OP("ZeroOut")
    .Input("to_zero: int32")
    .Output("zeroed: int32")
    .SetShapeFn(::tensorflow::shape_inference::UnchangedShape);

یک تابع شکل همچنین می تواند شکل یک ورودی را محدود کند. برای نسخه ZeroOut با محدودیت شکل برداری ، تابع شکل به صورت زیر خواهد بود:

    .SetShapeFn([](::tensorflow::shape_inference::InferenceContext* c) {
      ::tensorflow::shape_inference::ShapeHandle input;
      TF_RETURN_IF_ERROR(c->WithRank(c->input(0), 1, &input));
      c->set_output(0, input);
      return Status::OK();
    });

فراخوانی WithRank تأیید می کند که شکل ورودی c->input(0) دارای شکلی با یک بعد است (یا اگر شکل ورودی ناشناخته باشد، شکل خروجی یک بردار با یک بعد مجهول خواهد بود).

اگر عملیات شما چند شکلی با ورودی های متعدد است، می توانید از اعضای InferenceContext برای تعیین تعداد اشکال برای بررسی، و Merge برای تأیید صحت سازگاری اشکال استفاده کنید (به طور متناوب، به ویژگی هایی دسترسی پیدا کنید که طول ها را نشان می دهند، با InferenceContext::GetAttr ، که دسترسی به ویژگی های op را فراهم می کند).

    .SetShapeFn([](::tensorflow::shape_inference::InferenceContext* c) {
      ::tensorflow::shape_inference::ShapeHandle input;
      ::tensorflow::shape_inference::ShapeHandle output;
      for (size_t i = 0; i < c->num_inputs(); ++i) {
        TF_RETURN_IF_ERROR(c->WithRank(c->input(i), 2, &input));
        TF_RETURN_IF_ERROR(c->Merge(output, input, &output));
      }
      c->set_output(0, output);
      return Status::OK();
    });

از آنجایی که استنتاج شکل یک ویژگی اختیاری است و شکل تانسورها ممکن است به صورت دینامیکی متفاوت باشد، توابع شکل باید نسبت به اطلاعات شکل ناقص برای هر یک از ورودی ها قوی باشند. متد Merge در InferenceContext به تماس گیرنده اجازه می دهد تا ادعا کند که دو شکل یکسان هستند، حتی اگر هر کدام یا هر دو اطلاعات کاملی نداشته باشند. توابع شکل برای تمام عملیات‌های اصلی TensorFlow تعریف شده‌اند و مثال‌های استفاده متفاوتی را ارائه می‌کنند.

کلاس InferenceContext دارای تعدادی توابع است که می توان از آنها برای تعریف دستکاری تابع شکل استفاده کرد. برای مثال، با استفاده از InferenceContext::Dim و InferenceContext::WithValue می توانید تأیید کنید که یک بعد خاص دارای یک مقدار بسیار خاص است. با استفاده از InferenceContext::Add و InferenceContext::Multiply می توانید مشخص کنید که یک بعد خروجی حاصل جمع / حاصلضرب دو بعد ورودی است. کلاس InferenceContext را برای همه دستکاری‌های شکلی که می‌توانید مشخص کنید، ببینید. مثال زیر شکل اولین خروجی را بر روی (n، 3) قرار می دهد، که در آن ورودی اول دارای شکل (n، ...) است.

.SetShapeFn([](::tensorflow::shape_inference::InferenceContext* c) {
    c->set_output(0, c->Matrix(c->Dim(c->input(0), 0), 3));
    return Status::OK();
});

اگر تابع شکل پیچیده ای دارید، باید آزمایشی اضافه کنید تا تأیید کنید که ترکیبات شکل ورودی مختلف، ترکیبات شکل خروجی مورد انتظار را تولید می کنند. شما می توانید نمونه هایی از نحوه نوشتن این تست ها را در برخی از تست های عملیات اصلی ما مشاهده کنید. (سینتکس INFER_OK و INFER_ERROR کمی مرموز است، اما سعی کنید در نمایش مشخصات شکل ورودی و خروجی در تست ها فشرده باشید. در حال حاضر، نظرات اطراف را در آن تست ها ببینید تا متوجه مشخصات رشته شکل شوید).

یک بسته پیپ برای عملیات سفارشی خود بسازید

برای ساختن یک بسته pip برای عملیات خود، به مثال tensorflow/custom-op مراجعه کنید. این راهنما نشان می دهد که چگونه به جای ساخت TensorFlow از منبع، عملیات سفارشی را از بسته Pip TensorFlow بسازید.