एक ऑप बनाएं

यदि आप एक ऐसा ऑप बनाना चाहते हैं जो मौजूदा टेन्सरफ्लो लाइब्रेरी द्वारा कवर नहीं किया गया है, तो हम अनुशंसा करते हैं कि आप पहले ऑप को मौजूदा पायथन ऑप्स या फ़ंक्शंस की संरचना के रूप में पायथन में लिखने का प्रयास करें। यदि यह संभव नहीं है, तो आप एक कस्टम C++ ऑप बना सकते हैं। ऐसे कई कारण हैं जिनकी वजह से आप एक कस्टम C++ ऑप बनाना चाहेंगे:

  • अपने ऑपरेशन को मौजूदा ऑप्स की संरचना के रूप में व्यक्त करना आसान या संभव नहीं है।
  • अपने ऑपरेशन को मौजूदा आदिमों की संरचना के रूप में व्यक्त करना कुशल नहीं है।
  • आप आदिमों की एक रचना को हाथ से फ़्यूज़ करना चाहते हैं जिसे भविष्य के कंपाइलर को फ़्यूज़ करना मुश्किल लगेगा।

उदाहरण के लिए, कल्पना करें कि आप "मैक्सपूल" ऑपरेटर के समान "मीडियन पूलिंग" जैसा कुछ लागू करना चाहते हैं, लेकिन अधिकतम मानों के बजाय स्लाइडिंग विंडो पर मीडियन की गणना करना चाहते हैं। संचालन की संरचना का उपयोग करके ऐसा करना संभव हो सकता है (उदाहरण के लिए, ExtractImagePatches और TopK का उपयोग करके), लेकिन यह मूल ऑपरेशन के रूप में प्रदर्शन- या मेमोरी-कुशल नहीं हो सकता है जहां आप एकल, फ़्यूज्ड ऑपरेशन में कुछ और अधिक चतुर कर सकते हैं। हमेशा की तरह, आम तौर पर सबसे पहले ऑपरेटर संरचना का उपयोग करके आप जो चाहते हैं उसे व्यक्त करने का प्रयास करना उचित होता है, केवल एक नया ऑपरेशन जोड़ने का चयन करना यदि वह कठिन या अक्षम साबित होता है।

अपने कस्टम ऑप को शामिल करने के लिए आपको यह करना होगा:

  1. नए ऑप को C++ फ़ाइल में पंजीकृत करें। ऑप पंजीकरण ऑप की कार्यक्षमता के लिए एक इंटरफ़ेस (विनिर्देश) को परिभाषित करता है, जो ऑप के कार्यान्वयन से स्वतंत्र है। उदाहरण के लिए, ऑप पंजीकरण ऑप के नाम और ऑप के इनपुट और आउटपुट को परिभाषित करता है। यह आकार फ़ंक्शन को भी परिभाषित करता है जिसका उपयोग टेंसर आकार अनुमान के लिए किया जाता है।
  2. C++ में ऑप को कार्यान्वित करें। एक ऑप के कार्यान्वयन को कर्नेल के रूप में जाना जाता है, और यह चरण 1 में आपके द्वारा पंजीकृत विनिर्देश का ठोस कार्यान्वयन है। विभिन्न इनपुट / आउटपुट प्रकार या आर्किटेक्चर (उदाहरण के लिए, सीपीयू, जीपीयू) के लिए कई कर्नेल हो सकते हैं।
  3. एक पायथन रैपर बनाएं (वैकल्पिक)। यह रैपर सार्वजनिक एपीआई है जिसका उपयोग पायथन में ऑप बनाने के लिए किया जाता है। ऑप पंजीकरण से एक डिफ़ॉल्ट रैपर उत्पन्न होता है, जिसे सीधे उपयोग किया जा सकता है या इसमें जोड़ा जा सकता है।
  4. ऑप (वैकल्पिक) के लिए ग्रेडिएंट की गणना करने के लिए एक फ़ंक्शन लिखें।
  5. ऑप का परीक्षण करें. हम आमतौर पर सुविधा के लिए इसे पायथन में करते हैं, लेकिन आप C++ में भी ऑप का परीक्षण कर सकते हैं। यदि आप ग्रेडिएंट्स को परिभाषित करते हैं, तो आप उन्हें पायथन tf.test.compute_gradient_error से सत्यापित कर सकते हैं। एक उदाहरण के रूप में relu_op_test.py देखें जो Relu-जैसे ऑपरेटरों और उनके ग्रेडिएंट्स के आगे के कार्यों का परीक्षण करता है।

आवश्यक शर्तें

ऑप इंटरफ़ेस को परिभाषित करें

आप किसी ऑप के इंटरफ़ेस को TensorFlow सिस्टम के साथ पंजीकृत करके परिभाषित करते हैं। पंजीकरण में, आप अपने ऑप का नाम, उसके इनपुट (प्रकार और नाम) और आउटपुट (प्रकार और नाम), साथ ही डॉकस्ट्रिंग और ऑप के लिए आवश्यक किसी भी एटर्स को निर्दिष्ट करते हैं।

यह देखने के लिए कि यह कैसे काम करता है, मान लीजिए कि आप एक ऐसा ऑप बनाना चाहते हैं जो int32 s का टेंसर लेता है और टेंसर की एक प्रति आउटपुट करता है, जिसमें पहले तत्व को छोड़कर सभी को शून्य पर सेट किया जाता है। ऐसा करने के लिए, zero_out.cc नाम की एक फ़ाइल बनाएं। फिर REGISTER_OP मैक्रो में एक कॉल जोड़ें जो आपके ऑप के लिए इंटरफ़ेस को परिभाषित करता है:

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

using namespace tensorflow;

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

यह ZeroOut ऑप इनपुट के रूप में 32-बिट पूर्णांकों में से एक टेंसर to_zero लेता है, और 32-बिट पूर्णांकों का एक zeroed टेंसर आउटपुट करता है। यह सुनिश्चित करने के लिए कि आउटपुट टेंसर इनपुट टेंसर के समान आकार का है, ऑप एक आकार फ़ंक्शन का भी उपयोग करता है। उदाहरण के लिए, यदि इनपुट आकार का टेंसर है [10, 20], तो यह आकार फ़ंक्शन निर्दिष्ट करता है कि आउटपुट आकार भी [10, 20] है।

ऑप के लिए कर्नेल लागू करें

इंटरफ़ेस को परिभाषित करने के बाद, ऑप का एक या अधिक कार्यान्वयन प्रदान करें। इनमें से एक कर्नेल बनाने के लिए, एक क्लास बनाएं जो OpKernel विस्तार करता है और Compute विधि को ओवरराइड करता है। Compute विधि OpKernelContext* प्रकार का एक context तर्क प्रदान करती है, जिससे आप इनपुट और आउटपुट टेंसर जैसी उपयोगी चीजों तक पहुंच सकते हैं।

ऊपर बनाई गई फ़ाइल में अपना कर्नेल जोड़ें। कर्नेल कुछ इस तरह दिख सकता है:

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

मल्टी-थ्रेडेड सीपीयू कर्नेल

मल्टी-थ्रेडेड सीपीयू कर्नेल लिखने के लिए, work_sharder.h में शार्ड फ़ंक्शन का उपयोग किया जा सकता है। यह फ़ंक्शन इंट्रा-ऑप थ्रेडिंग के लिए उपयोग किए जाने के लिए कॉन्फ़िगर किए गए थ्रेड्स में एक गणना फ़ंक्शन को शार्प करता है ( config.proto में intra_op_parallelism_threads देखें)।

जीपीयू कर्नेल

एक GPU कर्नेल को दो भागों में कार्यान्वित किया जाता है: OpKernel और CUDA कर्नेल और इसका लॉन्च कोड।

कभी-कभी ओपकर्नेल कार्यान्वयन सीपीयू और जीपीयू कर्नेल के बीच आम होता है, जैसे इनपुट का निरीक्षण करना और आउटपुट आवंटित करना। उस स्थिति में, एक सुझाया गया कार्यान्वयन यह है:

  1. डिवाइस पर टेम्प्लेट किए गए OpKernel और टेंसर के आदिम प्रकार को परिभाषित करें।
  2. आउटपुट की वास्तविक गणना करने के लिए, कंप्यूट फ़ंक्शन एक टेम्प्लेट फ़ंक्टर स्ट्रक्चर को कॉल करता है।
  3. CPUDevice के लिए उस फ़ैक्टर की विशेषज्ञता को उसी फ़ाइल में परिभाषित किया गया है, लेकिन GPUDevice के लिए विशेषज्ञता को .cu.cc फ़ाइल में परिभाषित किया गया है, क्योंकि इसे CUDA कंपाइलर के साथ संकलित किया जाएगा।

यहां एक उदाहरण कार्यान्वयन है.

// kernel_example.h
#ifndef KERNEL_EXAMPLE_H_
#define KERNEL_EXAMPLE_H_

#include <unsupported/Eigen/CXX11/Tensor>

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

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

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

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

using namespace tensorflow;

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

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

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

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

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

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

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

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

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

using namespace tensorflow;

using GPUDevice = Eigen::GpuDevice;

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

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

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

#endif  // GOOGLE_CUDA

ऑप लाइब्रेरी बनाएं

अपने सिस्टम कंपाइलर (TensorFlow बाइनरी इंस्टॉलेशन) का उपयोग करके ऑप संकलित करें

आपको अपने सिस्टम पर उपलब्ध g++ या clang जैसे C++ कंपाइलर के साथ zero_out.cc संकलित करने में सक्षम होना चाहिए। बाइनरी पीआईपी पैकेज हेडर फ़ाइलों और लाइब्रेरी को स्थापित करता है जिनकी आपको सिस्टम विशिष्ट स्थानों पर अपने ऑप को संकलित करने के लिए आवश्यकता होती है। हालाँकि, 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++ स्थापित कर लिया है, तो यहां उन आदेशों का क्रम दिया गया है जिनका उपयोग आप अपने ऑप को एक गतिशील लाइब्रेरी में संकलित करने के लिए कर सकते हैं।

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 फ़ाइल बनाते समय अतिरिक्त ध्वज "-अनिर्धारित डायनेमिक_लुकअप" की आवश्यकता होती है।

gcc संस्करण >=5 पर ध्यान दें: जीसीसी संस्करण 5 से नए सी++ एबीआई का उपयोग करता है। TensorFlow 2.8 और इससे पहले का संस्करण gcc4 के साथ बनाया गया था जो पुराने ABI का उपयोग करता है। यदि आप TensorFlow के इन संस्करणों का उपयोग कर रहे हैं और अपनी op लाइब्रेरी को gcc>=5 के साथ संकलित करने का प्रयास कर रहे हैं, तो लाइब्रेरी को पुराने ABI के साथ संगत बनाने के लिए कमांड लाइन में -D_GLIBCXX_USE_CXX11_ABI=0 जोड़ें। TensorFlow 2.9+ पैकेज डिफ़ॉल्ट रूप से नए ABI के साथ संगत हैं।

बेज़ेल का उपयोग करके ऑप संकलित करें (TensorFlow स्रोत स्थापना)

यदि आपके पास TensorFlow स्रोत स्थापित हैं, तो आप अपने ऑप को संकलित करने के लिए TensorFlow के बिल्ड सिस्टम का उपयोग कर सकते हैं। tensorflow/core/user_ops निर्देशिका में निम्नलिखित बेज़ेल बिल्ड नियम के साथ एक BUILD फ़ाइल रखें।

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 कर्नेल के साथ, आपको tf_custom_op_library के gpu_srcs पैरामीटर का उपयोग करने की आवश्यकता है। निम्नलिखित बेज़ेल बिल्ड नियम के साथ एक BUILD फ़ाइल को tensorflow/core/user_ops निर्देशिका (उदाहरण के लिए "example_gpu") के अंदर एक नए फ़ोल्डर में रखें।

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

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

kernel_example.so बनाने के लिए निम्न कमांड चलाएँ।

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

पायथन में ऑप का प्रयोग करें

TensorFlow Python API डायनेमिक लाइब्रेरी को लोड करने और TensorFlow फ्रेमवर्क के साथ ऑप को पंजीकृत करने के लिए tf.load_op_library फ़ंक्शन प्रदान करता है। load_op_library एक पायथन मॉड्यूल लौटाता है जिसमें ऑप और कर्नेल के लिए पायथन रैपर शामिल होते हैं। इस प्रकार, एक बार जब आप ऑप बना लें, तो आप इसे पायथन से चलाने के लिए निम्नलिखित कार्य कर सकते हैं:

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)

ध्यान रखें, जेनरेट किए गए फ़ंक्शन को एक snap_case नाम दिया जाएगा ( PEP8 का अनुपालन करने के लिए)। इसलिए, यदि आपके ऑप को C++ फ़ाइलों में ZeroOut नाम दिया गया है, तो Python फ़ंक्शन को 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

सत्यापित करें कि ऑप काम करता है

यह सत्यापित करने का एक अच्छा तरीका है कि आपने अपना ऑपरेशन सफलतापूर्वक लागू कर दिया है, इसके लिए एक परीक्षण लिखना है। सामग्री के साथ 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

ऑप्स में एटीआर हो सकते हैं, जिनके मान तब सेट होते हैं जब ऑप को ग्राफ़ में जोड़ा जाता है। इनका उपयोग ऑप को कॉन्फ़िगर करने के लिए किया जाता है, और उनके मूल्यों को कर्नेल कार्यान्वयन के भीतर और ऑप पंजीकरण में इनपुट और आउटपुट के प्रकारों तक पहुँचा जा सकता है। जब संभव हो तो एटीआर के बजाय इनपुट का उपयोग करना पसंद करें, क्योंकि इनपुट अधिक लचीले होते हैं। ऐसा इसलिए है क्योंकि एटीआर स्थिरांक हैं और इन्हें ग्राफ़ निर्माण के समय परिभाषित किया जाना चाहिए। इसके विपरीत, इनपुट टेंसर होते हैं जिनके मान गतिशील हो सकते हैं; यानी, इनपुट हर चरण को बदल सकते हैं, फ़ीड का उपयोग करके सेट किए जा सकते हैं, आदि। एटीआर का उपयोग उन चीजों के लिए किया जाता है जो इनपुट के साथ नहीं किया जा सकता है: कोई भी कॉन्फ़िगरेशन जो हस्ताक्षर को प्रभावित करता है (इनपुट या आउटपुट की संख्या या प्रकार) या जो ' यह चरण-दर-चरण बदलता है।

जब आप ऑप पंजीकृत करते हैं तो आप Attr विधि का उपयोग करके उसका नाम और प्रकार निर्दिष्ट करके एक attr को परिभाषित करते हैं, जो फॉर्म की एक विशिष्टता की अपेक्षा करता है:

<name>: <attr-type-expr>

जहां <name> एक अक्षर से शुरू होता है और अल्फ़ान्यूमेरिक वर्णों और अंडरस्कोर से बना हो सकता है, और <attr-type-expr> नीचे वर्णित फॉर्म की एक प्रकार की अभिव्यक्ति है।

उदाहरण के लिए, यदि आप चाहते हैं कि ZeroOut ऑप केवल 0वें तत्व के बजाय उपयोगकर्ता-निर्दिष्ट इंडेक्स को संरक्षित करे, तो आप ऑप को इस प्रकार पंजीकृत कर सकते हैं:

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

(ध्यान दें कि विशेषता प्रकारों का सेट इनपुट और आउटपुट के लिए उपयोग किए जाने वाले tf.DType से भिन्न है।)

आपका कर्नेल इसके कंस्ट्रक्टर में context पैरामीटर के माध्यम से इस एटीआर तक पहुंच सकता है:

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

जिसे बाद में Compute विधि में उपयोग किया जा सकता है:

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

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

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

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

एटीआर प्रकार

निम्नलिखित प्रकार एक attr में समर्थित हैं:

  • string : बाइट्स का कोई भी क्रम (UTF8 होना आवश्यक नहीं है)।
  • int : एक हस्ताक्षरित पूर्णांक।
  • float : एक फ़्लोटिंग पॉइंट नंबर।
  • bool : सही या गलत.
  • type : DataType के (गैर-रेफरी) मानों में से एक।
  • shape : एक TensorShapeProto
  • list(<type>) : <type> की एक सूची, जहां <type> उपरोक्त प्रकारों में से एक है। ध्यान दें कि list(list(<type>)) अमान्य है।

यह भी देखें: निश्चित सूची के लिए op_def_builder.cc:FinalizeAttr

डिफ़ॉल्ट मान और बाधाएँ

Attrs में डिफ़ॉल्ट मान हो सकते हैं, और कुछ प्रकार के attrs में बाधाएँ हो सकती हैं। बाधाओं के साथ एक attr को परिभाषित करने के लिए, आप निम्नलिखित <attr-type-expr> s का उपयोग कर सकते हैं:

{'<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 संख्यात्मक (गैर-स्ट्रिंग और गैर-बूल) प्रकारों तक सीमित है।
  • realnumbertype : जटिल प्रकार के बिना numbertype की तरह।
  • quantizedtype : numbertype तरह लेकिन केवल परिमाणित संख्या प्रकार।

इनके द्वारा अनुमत प्रकारों की विशिष्ट सूचियों tensorflow/core/framework/types.h में फ़ंक्शंस (जैसे NumberTypes() ) द्वारा परिभाषित किया गया है। इस उदाहरण में attr t संख्यात्मक प्रकारों में से एक होना चाहिए:

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

इस ऑप के लिए:

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

सूचियों को अन्य सूचियों और एकल प्रकारों के साथ जोड़ा जा सकता है। निम्नलिखित ऑप एटीआर t किसी भी संख्यात्मक प्रकार या बूल प्रकार की अनुमति देता है:

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

इस ऑप के लिए:

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

int >= <n> : मान एक पूर्णांक होना चाहिए जिसका मान <n> से अधिक या उसके बराबर है, जहां <n> एक प्राकृतिक संख्या है। उदाहरण के लिए, निम्नलिखित ऑप पंजीकरण निर्दिष्ट करता है कि 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");

डिफ़ॉल्ट मान का समर्थित सिंटैक्स वह है जिसका उपयोग परिणामी ग्राफ़डेफ़ परिभाषा के प्रोटो प्रतिनिधित्व में किया जाएगा।

यहां सभी प्रकार के लिए डिफ़ॉल्ट निर्दिष्ट करने के उदाहरण दिए गए हैं:

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

विशेष रूप से ध्यान दें कि type के मान tf.DType का उपयोग करते हैं।

बहुरूपता

बहुरूपता टाइप करें

उन ऑप्स के लिए जो विभिन्न प्रकार के इनपुट ले सकते हैं या विभिन्न प्रकार के आउटपुट उत्पन्न कर सकते हैं, आप ऑप पंजीकरण में इनपुट या आउटपुट प्रकार में एक एटीआर निर्दिष्ट कर सकते हैं। आमतौर पर आप प्रत्येक समर्थित प्रकार के लिए एक OpKernel पंजीकृत करेंगे।

उदाहरण के लिए, यदि आप चाहते हैं कि ZeroOut ऑप int32 s के अलावा float s पर भी काम करे, तो आपका ऑप पंजीकरण इस तरह दिख सकता है:

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

आपका ऑप पंजीकरण अब निर्दिष्ट करता है कि इनपुट का प्रकार float , या int32 होना चाहिए, और इसका आउटपुट एक ही प्रकार का होगा, क्योंकि दोनों का प्रकार T है।

नामकरण

इनपुट, आउटपुट और एटीआरएस को आम तौर पर स्नेक_केस नाम दिया जाना चाहिए। एक अपवाद एटीआर है जिसका उपयोग इनपुट के प्रकार या आउटपुट के प्रकार में किया जाता है। जब ऑप को ग्राफ़ में जोड़ा जाता है तो उन अट्रैक्शन का अनुमान लगाया जा सकता है और इसलिए ऑप के फ़ंक्शन में दिखाई नहीं देते हैं। उदाहरण के लिए, ज़ीरोआउट की यह अंतिम परिभाषा एक पायथन फ़ंक्शन उत्पन्न करेगी जो इस तरह दिखती है:

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 )। उन अनुमानित attrs को बड़े अक्षरों में या CamelCase नाम दिए गए हैं।

इसकी तुलना उस ऑप से करें जिसमें एक प्रकार 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);

बैकवर्ड संगतता को संरक्षित करने के लिए, आपको किसी मौजूदा ऑप में एटीआर जोड़ते समय एक डिफ़ॉल्ट मान निर्दिष्ट करना चाहिए:

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
इनपुट और आउटपुट की सूची बनाएं

विभिन्न प्रकारों को स्वीकार करने या उत्पादन करने में सक्षम होने के अलावा, ऑप्स विभिन्न प्रकार के टेंसरों का उपभोग या उत्पादन कर सकते हैं।

अगले उदाहरण में, 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 का उपयोग करता है।

इसे बहुरूपी प्रकार का भी बनाया जा सकता है। अगले उदाहरण में, इनपुट समान (लेकिन अनिर्दिष्ट) प्रकार ( "T" ) के टेंसर (लंबाई "N" के साथ) की एक सूची है, और आउटपुट मिलान प्रकार का एकल टेंसर है:

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)" attrs के साथ काम करता है:

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

इनपुट और आउटपुट

उपरोक्त को सारांशित करने के लिए, एक ऑप पंजीकरण में कई इनपुट और आउटपुट हो सकते हैं:

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

प्रत्येक इनपुट या आउटपुट विशिष्टता इस प्रकार है:

<name>: <io-type-expr>

जहां <name> एक अक्षर से शुरू होता है और अल्फ़ान्यूमेरिक वर्णों और अंडरस्कोर से बना हो सकता है। <io-type-expr> निम्न प्रकार के भावों में से एक है:

  • <type> > , जहां <type> एक समर्थित इनपुट प्रकार है (जैसे float , int32 , string )। यह दिए गए प्रकार का एकल टेंसर निर्दिष्ट करता है।

    tf.DType देखें।

    REGISTER_OP("BuiltInTypesExample")
        .Input("integers: int32")
        .Input("complex_numbers: complex64");
    
  • <attr-type> , जहां <attr-type> प्रकार type या list(type) (संभावित प्रकार प्रतिबंध के साथ) के साथ एक Attr का नाम है। यह सिंटैक्स बहुरूपी ऑप्स की अनुमति देता है।

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

    प्रकार list(type) के एटीआर को संदर्भित करने से आप टेंसरों के अनुक्रम को स्वीकार कर सकते हैं।

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

    ध्यान दें कि आउटपुट out में टेंसरों की संख्या और प्रकार इनपुट in समान हैं, क्योंकि दोनों प्रकार T के हैं।

  • समान प्रकार वाले टेंसरों के अनुक्रम के लिए: <number> * <type> , जहां <number> int प्रकार वाले Attr का नाम है। <type> या तो tf.DType हो सकता है, या type type वाले attr का नाम हो सकता है। पहले उदाहरण के रूप में, यह ऑप 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 ) का उपयोग करते हैं। अन्यथा इनपुट, आउटपुट और एटीआरएस में फ़ंक्शन पैरामीटर (उदाहरण के लिए num_outputs ) जैसे नाम होते हैं। अधिक विवरण के लिए, नामकरण पर पिछला अनुभाग देखें।

अधिक विवरण के लिए, tensorflow/core/framework/op_def_builder.h देखें।

पिछेड़ी संगतता

आइए मान लें कि आपने एक अच्छा, कस्टम ऑप लिखा है और इसे दूसरों के साथ साझा किया है, ताकि आपके पास आपके ऑपरेशन का उपयोग करके खुश ग्राहक हों। हालाँकि, आप किसी तरह से ऑप में बदलाव करना चाहेंगे।

सामान्य तौर पर, मौजूदा, चेक-इन विनिर्देशों में परिवर्तन पश्चगामी-संगत होना चाहिए: किसी ऑप के विनिर्देश को बदलने से पुराने विनिर्देशों से निर्मित पूर्व क्रमबद्ध GraphDef प्रोटोकॉल बफ़र्स नहीं टूटना चाहिए। GraphDef संगतता का विवरण यहां वर्णित है।

पश्च-संगतता को संरक्षित करने के कई तरीके हैं।

  1. किसी ऑपरेशन में जोड़े गए किसी भी नए एटीआर में डिफ़ॉल्ट मान परिभाषित होना चाहिए, और उस डिफ़ॉल्ट मान के साथ ऑप का मूल व्यवहार होना चाहिए। किसी ऑपरेशन को गैर बहुरूपी से बहुरूपी में बदलने के लिए, आपको मूल हस्ताक्षर को डिफ़ॉल्ट रूप से संरक्षित करने के लिए नए प्रकार के 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. आपके द्वारा बनाए गए किसी भी नए ऑप्स को नेमस्पेस, आपके प्रोजेक्ट के लिए कुछ अद्वितीय ऑप नामों के पहले जोड़कर। यह आपके ऑप को किसी भी ऑप से टकराने से बचाता है जिसे TensorFlow के भविष्य के संस्करणों में शामिल किया जा सकता है।

  6. आगे की योजना! ऑप के भविष्य के उपयोग का अनुमान लगाने का प्रयास करें। कुछ हस्ताक्षर परिवर्तन संगत तरीके से नहीं किए जा सकते (उदाहरण के लिए, एक ही प्रकार की सूची को अलग-अलग प्रकारों की सूची में बनाना)।

सुरक्षित और असुरक्षित परिवर्तनों की पूरी सूची tensorflow/core/framework/op_compatibility_test.cc में पाई जा सकती है। यदि आप किसी ऑपरेशन में अपना परिवर्तन बैकवर्ड संगत नहीं कर सकते हैं, तो नए शब्दार्थ के साथ एक नए नाम के साथ एक नया ऑपरेशन बनाएं।

यह भी ध्यान दें कि हालांकि ये परिवर्तन GraphDef संगतता को बनाए रख सकते हैं, जेनरेट किया गया पायथन कोड इस तरह से बदल सकता है जो पुराने कॉलर्स के साथ संगत नहीं है। पाइथॉन एपीआई को हाथ से लिखे पाइथॉन रैपर में सावधानीपूर्वक परिवर्तन करके, पुराने हस्ताक्षर को छोड़कर, संभवतः अंत में नए वैकल्पिक तर्क जोड़कर संगत रखा जा सकता है। आम तौर पर असंगत परिवर्तन केवल तभी किए जा सकते हैं जब TensorFlow प्रमुख संस्करणों को बदलता है, और GraphDef संस्करण शब्दार्थ के अनुरूप होना चाहिए।

जीपीयू समर्थन

आप विभिन्न OpKernels को कार्यान्वित कर सकते हैं और एक को CPU के लिए और दूसरे को GPU के लिए पंजीकृत कर सकते हैं, जैसे आप विभिन्न प्रकारों के लिए कर्नेल को पंजीकृत कर सकते हैं। tensorflow/core/kernels/ में जीपीयू समर्थन के साथ कर्नेल के कई उदाहरण हैं। ध्यान दें कि कुछ कर्नेल में .cc फ़ाइल में एक CPU संस्करण होता है, _gpu.cu.cc पर समाप्त होने वाली फ़ाइल में एक GPU संस्करण होता है, और .h फ़ाइल में कुछ कोड समान रूप से साझा किए जाते हैं।

उदाहरण के लिए, tf.pad में tensorflow/core/kernels/pad_op.cc में GPU कर्नेल के अलावा सब कुछ है। GPU कर्नेल tensorflow/core/kernels/pad_op_gpu.cu.cc में है, और साझा कोड tensorflow/core/kernels/pad_op.h में परिभाषित एक टेम्पलेट क्लास है। हम दो कारणों से कोड को इस तरह व्यवस्थित करते हैं: यह आपको सीपीयू और जीपीयू कार्यान्वयन के बीच सामान्य कोड साझा करने की अनुमति देता है, और यह जीपीयू कार्यान्वयन को एक अलग फ़ाइल में रखता है ताकि इसे केवल जीपीयू कंपाइलर द्वारा संकलित किया जा सके।

एक बात का ध्यान रखें, जब pad के जीपीयू कर्नेल संस्करण का उपयोग किया जाता है, तब भी इसे सीपीयू मेमोरी में इसके "paddings" इनपुट की आवश्यकता होती है। यह चिह्नित करने के लिए कि इनपुट या आउटपुट सीपीयू पर रखे गए हैं, कर्नेल पंजीकरण में एक HostMemory() कॉल जोड़ें, उदाहरण के लिए:

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

GPU डिवाइस के लिए कर्नेल संकलित करना

एक उदाहरण के लिए cuda_op_kernel.cu.cc देखें जो एक ऑप को लागू करने के लिए CUDA कर्नेल का उपयोग करता है। tf_custom_op_library एक gpu_srcs तर्क स्वीकार करता है जिसमें CUDA कर्नेल ( *.cu.cc फ़ाइलें) वाली स्रोत फ़ाइलों की सूची निर्दिष्ट की जा सकती है। TensorFlow की बाइनरी स्थापना के साथ उपयोग के लिए, CUDA कर्नेल को NVIDIA के nvcc कंपाइलर के साथ संकलित करना होगा। यहां उन आदेशों का क्रम दिया गया है जिनका उपयोग आप cuda_op_kernel.cu.cc और cuda_op_kernel.cc को एक गतिशील रूप से लोड करने योग्य लाइब्रेरी में संकलित करने के लिए कर सकते हैं:

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

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

ऊपर निर्मित cuda_op_kernel.so tf.load_op_library फ़ंक्शन का उपयोग करके हमेशा की तरह Python में लोड किया जा सकता है।

ध्यान दें कि यदि आपकी CUDA लाइब्रेरीज़ /usr/local/lib64 में स्थापित नहीं हैं, तो आपको ऊपर दिए गए दूसरे (g++) कमांड में स्पष्ट रूप से पथ निर्दिष्ट करना होगा। उदाहरण के लिए, यदि आपका CUDA /usr/local/cuda-8.0 में स्थापित है, तो -L /usr/local/cuda-8.0/lib64/ जोड़ें।

पायथन में ग्रेडिएंट लागू करें

ऑप्स के एक ग्राफ को देखते हुए, TensorFlow मौजूदा ऑप्स के संबंध में ग्रेडिएंट का प्रतिनिधित्व करने वाले नए ऑप्स को जोड़ने के लिए स्वचालित विभेदन (बैकप्रॉपैगेशन) का उपयोग करता है। नए ऑप्स के लिए स्वचालित विभेदन कार्य करने के लिए, आपको एक ग्रेडिएंट फ़ंक्शन पंजीकृत करना होगा जो ऑप्स के आउटपुट के संबंध में दिए गए ग्रेडिएंट्स के इनपुट के संबंध में ग्रेडिएंट्स की गणना करता है।

गणितीय रूप से, यदि कोई ओपी \(y = f(x)\) गणना करता है तो पंजीकृत ग्रेडिएंट ओपी श्रृंखला नियम के माध्यम से एल10एन \(\partial L/ \partial y\) प्लेसहोल्डर4 के संबंध में हानि \(L\) के ग्रेडिएंट\(y\) को \(\partial L/ \partial x\) \(x\) में परिवर्तित करता है:

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

ZeroOut के मामले में, इनपुट में केवल एक प्रविष्टि आउटपुट को प्रभावित करती है, इसलिए इनपुट के संबंध में ग्रेडिएंट एक विरल "एक हॉट" टेंसर है। इसे इस प्रकार व्यक्त किया गया है:

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

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

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

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

tf.RegisterGradient के साथ ग्रेडिएंट फ़ंक्शंस को पंजीकृत करने के बारे में विवरण:

  • एक आउटपुट वाले ऑप के लिए, ग्रेडिएंट फ़ंक्शन एक tf.Operation , op , और एक tf.Tensor grad लेगा और टेंसर op.inputs[i] , op.outputs[i] , और grad से नए ऑप बनाएगा। किसी भी attrs के बारे में जानकारी tf.Operation.get_attr के माध्यम से पाई जा सकती है।

  • यदि ऑप में कई आउटपुट हैं, तो ग्रेडिएंट फ़ंक्शन op और grads लेगा, जहां grads प्रत्येक आउटपुट के संबंध में ग्रेडिएंट की एक सूची है। ग्रेडिएंट फ़ंक्शन का परिणाम प्रत्येक इनपुट के संबंध में ग्रेडिएंट का प्रतिनिधित्व करने वाले Tensor ऑब्जेक्ट की एक सूची होनी चाहिए।

  • यदि कुछ इनपुट के लिए कोई अच्छी तरह से परिभाषित ग्रेडिएंट नहीं है, जैसे कि सूचकांक के रूप में उपयोग किए जाने वाले पूर्णांक इनपुट के लिए, तो संबंधित लौटाया गया ग्रेडिएंट None होना चाहिए। उदाहरण के लिए, एक फ्लोटिंग पॉइंट टेंसर x और एक पूर्णांक इंडेक्स i लेने वाले ऑप के लिए, ग्रेडिएंट फ़ंक्शन return [x_grad, None]

  • यदि ऑप के लिए कोई सार्थक ग्रेडिएंट नहीं है, तो आपको अक्सर किसी ग्रेडिएंट को पंजीकृत नहीं करना पड़ेगा, और जब तक ऑप के ग्रेडिएंट की कभी आवश्यकता नहीं होती, तब तक आप ठीक रहेंगे। कुछ मामलों में, एक ऑप में कोई अच्छी तरह से परिभाषित ग्रेडिएंट नहीं होता है, लेकिन ग्रेडिएंट की गणना में इसे शामिल किया जा सकता है। यहां आप शून्य को स्वचालित रूप से पीछे की ओर प्रसारित करने के लिए ops.NotDifferentiable उपयोग कर सकते हैं।

ध्यान दें कि जिस समय ग्रेडिएंट फ़ंक्शन को कॉल किया जाता है, केवल ऑप्स का डेटा प्रवाह ग्राफ़ उपलब्ध होता है, टेंसर डेटा नहीं। इस प्रकार, सभी गणनाएँ ग्राफ़ निष्पादन समय पर चलाने के लिए अन्य टेंसरफ़्लो ऑप्स का उपयोग करके की जानी चाहिए।

कोड को अधिक पठनीय, डिबग करने योग्य, बनाए रखने में आसान और डेटा सत्यापन के माध्यम से अधिक मजबूत बनाने के लिए ऑप प्रकार के लिए कस्टम ग्रेडिएंट पंजीकृत करते समय प्रकार संकेत जोड़ें। उदाहरण के लिए, किसी फ़ंक्शन में पैरामीटर के रूप में op लेते समय, निर्दिष्ट करें कि ग्रेडिएंट फ़ंक्शन अपने पैरामीटर प्रकार के रूप में tf.Operation लेगा।

C++ में आकार कार्य करता है

TensorFlow API में "आकार अनुमान" नामक एक सुविधा है जो ग्राफ़ को निष्पादित किए बिना टेंसर के आकार के बारे में जानकारी प्रदान करती है। आकार का अनुमान "आकार कार्यों" द्वारा समर्थित है जो 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 ऑब्जेक्ट बना सकते हैं। इंडेक्स idx वाले इनपुट के लिए ShapeHandle ऑब्जेक्ट c->input(idx) द्वारा प्राप्त किया जा सकता है।

ऐसे कई सामान्य आकार फ़ंक्शन हैं जो कई ऑप्स पर लागू होते हैं, जैसे कि shape_inference::UnchangedShape जिसे आम_shape_fns.h में पाया जा सकता है और निम्नानुसार उपयोग किया जा सकता है:

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

एक आकार फ़ंक्शन किसी इनपुट के आकार को भी बाधित कर सकता है। वेक्टर आकार बाधा के साथ ZeroOut के संस्करण के लिए, आकार फ़ंक्शन इस प्रकार होगा:

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

WithRank कॉल पुष्टि करती है कि इनपुट आकार c->input(0) में बिल्कुल एक आयाम वाला एक आकार है (या यदि इनपुट आकार अज्ञात है, तो आउटपुट आकार एक अज्ञात आयाम वाला एक वेक्टर होगा)।

यदि आपका ऑप एकाधिक इनपुट के साथ बहुरूपी है, तो आप जाँचने के लिए आकृतियों की संख्या निर्धारित करने के लिए InferenceContext के सदस्यों का उपयोग कर सकते हैं, और यह सत्यापित करने के लिए Merge कि आकृतियाँ सभी संगत हैं (वैकल्पिक रूप से, InferenceContext::GetAttr के साथ लंबाई को इंगित करने वाली विशेषताओं तक पहुंचें, जो ऑप की विशेषताओं तक पहुंच प्रदान करता है)।

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

चूँकि आकार का अनुमान एक वैकल्पिक विशेषता है, और टेंसर के आकार गतिशील रूप से भिन्न हो सकते हैं, किसी भी इनपुट के लिए आकार की जानकारी अपूर्ण होने तक आकार फ़ंक्शन मजबूत होना चाहिए। InferenceContext में Merge विधि कॉल करने वाले को यह दावा करने की अनुमति देती है कि दो आकृतियाँ समान हैं, भले ही उनमें से एक या दोनों के पास पूरी जानकारी न हो। आकार फ़ंक्शन सभी मुख्य 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 बनाने के बजाय TensorFlow पिप पैकेज से कस्टम ऑप्स कैसे बनाया जाए।