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

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

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

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

برای درج عملکرد سفارشی خود باید:

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

پیش نیازها

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

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

برای دیدن چگونگی عملکرد ، فرض کنید می خواهید یک op ایجاد کنید که یک tensor از int32 s را بگیرد و یک کپی از tensor را خارج کند ، در حالی که همه عناصر به غیر از اولین صفر است. برای این کار ، فایلی با نام 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] باشد.

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

پس از تعریف رابط ، یک یا چند پیاده سازی از op را ارائه دهید. برای ایجاد یکی از این هسته ها ، یک کلاس ایجاد کنید که OpKernel گسترش 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 چند رشته ای ، می توان از تابع Shard در work_sharder.h استفاده کرد. این تابع یک تابع محاسبه را در میان رشته های پیکربندی شده برای استفاده در رشته درون اپلیکیشن خرد می کند (به موضوعات داخل_اپ_پارللیسم در config.proto ).

هسته های GPU

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

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

  1. OpKernel که روی دستگاه و نوع ابتدایی تنسور را تعریف کنید ، تعریف کنید.
  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 را بسازید

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

شما باید قادر به کامپایل شود zero_out.cc با C++ کامپایلر مانند g++ و یا clang موجود بر روی سیستم شما. بسته دودویی PIP ، پرونده های سرآیند و کتابخانه ای را که برای تهیه کامپوننت خود در مکان های خاص سیستم نصب می کنید ، نصب می کند. با این حال ، کتابخانه پایتون get_include تابع 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++11 -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 . بسته های pip دودویی موجود در وب سایت gcc4 با gcc4 ساخته شده است که از ABI قدیمی تر استفاده می کند. اگر کتابخانه op خود را با gcc>=5 کامپایل می کنید ، -D_GLIBCXX_USE_CXX11_ABI=0 به خط فرمان اضافه کنید تا کتابخانه با abi قدیمی سازگار شود.

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

اگر منابع TensorFlow را نصب کرده اید ، می توانید از سیستم ساخت TensorFlow برای کامپایل عملیات خود استفاده کنید. یک فایل BUILD با زیر قانون ساخت tensorflow/core/user_ops در شاخه 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 را با قانون زیر ساخت tensorflow/core/user_ops در یک پوشه جدید در داخل فهرست 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 دستور زیر را 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 یک ماژول Python را برمی گرداند که شامل بسته بندی های Python برای op و هسته است. بنابراین ، پس از ایجاد op ، می توانید برای اجرای آن از 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)

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

برای در دسترس قرار دادن اپ به عنوان یک عملکرد منظم که از یک ماژول پایتون قابل import کردن است ، ممکن است تماس load_op_library در یک فایل منبع پایتون به شرح زیر مفید باشد:

import tensorflow as tf

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

تأیید کنید که op کار می کند

یک روش خوب برای تأیید اینکه شما با موفقیت عمل خود را اجرا کرده اید ، نوشتن آزمایشی برای آن است. فایل 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

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

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

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

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

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

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

این ادعا می کند که ورودی یک بردار است و در صورت عدم تنظیم وضعیت InvalidArgument برمی گردد. ماکرو OP_REQUIRES سه استدلال را در بر می گیرد:

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

ثبت نام

عطاران

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

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

<name>: <attr-type-expr>

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

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

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

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

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

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

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

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

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

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

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

انواع Attr

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

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

برای دیدن لیست op_def_builder.cc:FinalizeAttr : op_def_builder.cc:FinalizeAttr نیز مراجعه کنید.

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

Attrs ممکن است مقادیر پیش فرض داشته باشد و برخی از انواع Attrs نیز می توانند محدودیت داشته باشند. برای تعریف یک 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 . شما مشخص نمی کنید که نوع گیرنده type باشد. این وقتی ضمنی است که لیستی از انواع آن را در {...} . به عنوان مثال ، در این حالت attr t نوعی است که باید int32 ، float یا bool :

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

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

  • numbertype : نوع type محدود به انواع عددی (غیر رشته ای و غیر بول) است.
  • 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> یک عدد طبیعی است. به عنوان مثال ، ثبت نام op زیر مشخص می کند که Attr a باید مقداری داشته باشد که حداقل 2 :

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

list(<type>) >= <n> : لیستی از نوع <type> که طول آن بزرگتر یا برابر با <n> باشد. به عنوان مثال ، ثبت نام op زیر مشخص می کند که 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 استفاده میtf.DType .

پلی مورفیسم

چندشکلی را تایپ کنید

برای ops هایی که می توانند انواع مختلفی را به عنوان ورودی در نظر بگیرند یا انواع مختلفی از خروجی را تولید کنند ، می توانید در ثبت ورودی ، یک Attr را در نوع ورودی یا خروجی مشخص کنید. به طور معمول می توانید OpKernel برای هر نوع پشتیبانی ثبت کنید.

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

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

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

نامگذاری

به ورودی ها ، خروجی ها و علامت های تجدیدنظر باید به طور کلی مارک های موردی مار داده شود. یک استثنا ، نشانگرهایی است که به عنوان نوع ورودی یا در نوع خروجی استفاده می شود. وقتی op به نمودار اضافه می شود می توان آن علامت ها را استنباط کرد و بنابراین در عملکرد op ظاهر نمی شوند. به عنوان مثال ، این آخرین تعریف از 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 یک 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 به یک op موجود ، باید مقدار پیش فرض را تعیین کنید:

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");

همان نحو با نشانگرهای "list(type)" :

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

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

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

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) از نوع نوع 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 یا نام یکtf.DType با نوع type باشد. به عنوان مثالی از اولین مورد ، این op لیستی از 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 .

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

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

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

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

  1. هر علامت جدیدی که به یک عملیات اضافه می شود باید مقادیر پیش فرض تعریف شده داشته باشد و با آن مقدار پیش فرض 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. با خیال راحت می توانید محدودیتی را در مورد گیرنده ایجاد کنید که محدودیت کمتری داشته باشد. به عنوان مثال ، می توانید از {int32, int64} به {int32, int64, float} یا type . یا ممکن است از {"apple", "orange"} به {"apple", "banana", "orange"} یا string .

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

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

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

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

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

همچنین توجه داشته باشید که اگرچه این تغییرات می توانند سازگاری GraphDef حفظ GraphDef ، ممکن است کد پایتون تولید شده به گونه ای تغییر کند که با تماس گیرنده های قدیمی سازگار نباشد. با تغییر دقیق در یک بسته بندی دستی پایتون ، با حفظ امضای قدیمی ، به جز احتمال اضافه کردن آرگومان های اختیاری جدید ، ممکن است API پایتون سازگار باشد. به طور کلی تغییرات ناسازگار فقط وقتی ایجاد می شوند که GraphDef نسخه های اصلی را تغییر می دهد و باید با معناشناسی نسخه 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 . ما به دو دلیل کد را از این طریق سازماندهی می کنیم: این به شما امکان می دهد کد مشترکی را بین پیاده سازی های پردازنده و پردازنده گرافیکی به اشتراک بگذارید ، و اجرای 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 نگاه کنید. gpu_srcs آرگومان tf_custom_op_library را می پذیرد که در آن می توان لیستی از فایلهای منبع حاوی هسته های CUDA (پرونده های *.cu.cc ) را مشخص کرد. برای استفاده با نصب باینری TensorFlow ، هسته های CUDA باید با کامپایلر nvcc NVIDIA کامپایل شوند. در اینجا توالی دستوراتی وجود دارد که می توانید برای کامپایل cuda_op_kernel.cu.cc و cuda_op_kernel.cc در یک کتابخانه قابل بارگذاری پویا استفاده کنید:

nvcc -std=c++11 -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++11 -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 از تمایز اتوماتیک (backpropagation) برای افزودن عملیات جدید نمایانگر شیب ها با توجه به عملیات موجود استفاده می کند. برای ایجاد تمایز اتوماتیک برای ops های جدید ، باید یک تابع گرادیان ثبت کنید که شیب ها را با توجه به ورودی های ops با شیب های داده شده با توجه به خروجی ops محاسبه کند.

از نظر ریاضی ، اگر یک op \(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 :

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

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

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

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

توجه داشته باشید که در زمان فراخوانی تابع شیب ، فقط نمودار جریان داده ops در دسترس است ، نه خود داده tensor. بنابراین ، همه محاسبات باید با استفاده از سایر تنظیمات جریان تنسور انجام شود تا در زمان اجرای نمودار اجرا شود.

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

TensorFlow API دارای ویژگی ای به نام "استنباط شکل" است که بدون نیاز به اجرای نمودار ، اطلاعاتی را درباره اشکال تنسور ارائه می دهد. استنباط شکل توسط "توابع شکل" پشتیبانی می شود که برای هر نوع 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 برای ورودی با index 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) شکلی دقیقاً با یک بعد دارد (یا اگر شکل ورودی ناشناخته باشد ، شکل خروجی یک بردار با یک بعد ناشناخته خواهد بود).

اگر op شما چند شکل است و دارای چندین ورودی است ، می توانید از اعضای InferenceContext برای تعیین تعداد اشکال برای بررسی استفاده کنید و برای تأیید صحت سازگاری اشکال از Merge استفاده کنید ( InferenceContext::GetAttr ، ویژگی های دسترسی که طول ها را نشان می دهند ، با 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 درست کنید

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