Buat operasi

Jika Anda ingin membuat operasi yang tidak tercakup dalam pustaka TensorFlow yang ada, sebaiknya coba tulis operasi tersebut dengan Python terlebih dahulu sebagai komposisi operasi atau fungsi Python yang ada. Jika itu tidak memungkinkan, Anda dapat membuat operasi C++ khusus. Ada beberapa alasan mengapa Anda mungkin ingin membuat operasi C++ khusus:

  • Tidak mudah atau tidak mungkin untuk mengekspresikan operasi Anda sebagai komposisi operasi yang ada.
  • Tidaklah efisien untuk mengekspresikan operasi Anda sebagai komposisi primitif yang ada.
  • Anda ingin menggabungkan komposisi primitif yang sulit digabungkan oleh kompiler masa depan.

Misalnya, bayangkan Anda ingin menerapkan sesuatu seperti "penggabungan median", mirip dengan operator "MaxPool", tetapi menghitung median melalui jendela geser, bukan nilai maksimum. Melakukan hal ini dengan menggunakan komposisi operasi mungkin dapat dilakukan (misalnya, menggunakan ExtractImagePatches dan TopK), namun mungkin tidak seefisien kinerja atau memori seperti operasi asli di mana Anda dapat melakukan sesuatu yang lebih pintar dalam satu operasi yang digabungkan. Seperti biasa, pertama-tama ada baiknya mencoba mengungkapkan apa yang Anda inginkan menggunakan komposisi operator, hanya memilih untuk menambahkan operasi baru jika itu terbukti sulit atau tidak efisien.

Untuk memasukkan operasi khusus, Anda harus:

  1. Daftarkan operasi baru dalam file C++. Registrasi op mendefinisikan antarmuka (spesifikasi) untuk fungsionalitas operasi, yang tidak bergantung pada implementasi operasi. Misalnya, registrasi op mendefinisikan nama op serta input dan output op. Ini juga mendefinisikan fungsi bentuk yang digunakan untuk inferensi bentuk tensor.
  2. Implementasikan operasi di C++. Implementasi sebuah operasi dikenal sebagai kernel, dan ini merupakan implementasi konkrit dari spesifikasi yang Anda daftarkan pada Langkah 1. Mungkin ada beberapa kernel untuk tipe atau arsitektur input/output yang berbeda (misalnya, CPU, GPU).
  3. Buat pembungkus Python (opsional). Pembungkus ini adalah API publik yang digunakan untuk membuat operasi dengan Python. Pembungkus default dihasilkan dari registrasi op, yang dapat digunakan secara langsung atau ditambahkan.
  4. Tulis fungsi untuk menghitung gradien untuk op (opsional).
  5. Uji operasinya. Kami biasanya melakukan ini dengan Python untuk kenyamanan, tetapi Anda juga dapat menguji operasinya di C++. Jika Anda mendefinisikan gradien, Anda dapat memverifikasinya dengan Python tf.test.compute_gradient_error . Lihat relu_op_test.py sebagai contoh yang menguji fungsi penerusan operator mirip Relu dan gradiennya.

Prasyarat

Tentukan antarmuka operasi

Anda menentukan antarmuka operasi dengan mendaftarkannya ke sistem TensorFlow. Dalam registrasi, Anda menentukan nama operasi Anda, inputnya (tipe dan nama) dan outputnya (tipe dan nama), serta docstring dan attr apa pun yang mungkin diperlukan oleh operasi tersebut.

Untuk melihat cara kerjanya, misalkan Anda ingin membuat operasi yang menggunakan tensor int32 s dan mengeluarkan salinan tensor tersebut, dengan semua kecuali elemen pertama disetel ke nol. Untuk melakukannya, buat file bernama zero_out.cc . Kemudian tambahkan panggilan ke makro REGISTER_OP yang mendefinisikan antarmuka untuk operasi Anda:

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

Operasi ZeroOut ini mengambil satu tensor to_zero dari bilangan bulat 32-bit sebagai masukan, dan mengeluarkan tensor zeroed dari bilangan bulat 32-bit. Operasi ini juga menggunakan fungsi bentuk untuk memastikan bahwa tensor keluaran memiliki bentuk yang sama dengan tensor masukan. Misalnya, jika masukannya adalah tensor berbentuk [10, 20], maka fungsi bentuk ini menetapkan bahwa bentuk keluarannya juga [10, 20].

Implementasikan kernel untuk operasi

Setelah Anda menentukan antarmuka, berikan satu atau lebih implementasi operasi. Untuk membuat salah satu kernel ini, buatlah kelas yang memperluas OpKernel dan mengganti metode Compute . Metode Compute menyediakan satu argumen context bertipe OpKernelContext* , yang darinya Anda dapat mengakses hal-hal berguna seperti tensor input dan output.

Tambahkan kernel Anda ke file yang Anda buat di atas. Kernelnya mungkin terlihat seperti ini:

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

Setelah mengimplementasikan kernel, Anda mendaftarkannya ke sistem TensorFlow. Dalam registrasi, Anda menentukan batasan berbeda di mana kernel ini akan dijalankan. Misalnya, Anda mungkin memiliki satu kernel yang dibuat untuk CPU, dan satu kernel terpisah untuk GPU.

Untuk melakukan ini pada operasi ZeroOut , tambahkan kode berikut ke zero_out.cc :

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

Kernel CPU multi-utas

Untuk menulis kernel CPU multi-thread, fungsi Shard di work_sharder.h dapat digunakan. Fungsi ini membagi fungsi komputasi di seluruh thread yang dikonfigurasi untuk digunakan untuk threading intra-op (lihat intra_op_parallelism_threads di config.proto ).

Kernel GPU

Kernel GPU diimplementasikan dalam dua bagian: OpKernel dan kernel CUDA serta kode peluncurannya.

Terkadang implementasi OpKernel bersifat umum antara kernel CPU dan GPU, seperti pemeriksaan masukan dan pengalokasian keluaran. Dalam hal ini, penerapan yang disarankan adalah:

  1. Tentukan templat OpKernel pada Perangkat dan tipe tensor primitif.
  2. Untuk melakukan penghitungan keluaran sebenarnya, fungsi Komputasi memanggil struktur fungsi templat.
  3. Spesialisasi fungsi tersebut untuk CPUDevice ditentukan dalam file yang sama, namun spesialisasi untuk GPUDevice ditentukan dalam file .cu.cc, karena akan dikompilasi dengan kompiler CUDA.

Berikut adalah contoh penerapannya.

// 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

Bangun perpustakaan operasi

Kompilasi operasi menggunakan kompiler sistem Anda (instalasi biner TensorFlow)

Anda harus dapat mengkompilasi zero_out.cc dengan kompiler C++ seperti g++ atau clang yang tersedia di sistem Anda. Paket biner PIP menginstal file header dan pustaka yang Anda perlukan untuk mengkompilasi operasi Anda di lokasi yang spesifik untuk sistem. Namun, pustaka python TensorFlow menyediakan fungsi get_include untuk mendapatkan direktori header, dan direktori get_lib memiliki objek bersama untuk ditautkan. Berikut adalah output dari fungsi-fungsi ini pada mesin Ubuntu.

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

Dengan asumsi Anda telah menginstal g++ , berikut adalah urutan perintah yang dapat Anda gunakan untuk mengkompilasi operasi Anda ke dalam perpustakaan dinamis.

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

Di macOS, tanda tambahan "-undefinisi Dynamic_lookup" diperlukan saat membuat file .so .

Catatan tentang versi gcc >=5 : gcc menggunakan C++ ABI baru sejak versi 5 . TensorFlow 2.8 dan versi sebelumnya dibuat dengan gcc4 yang menggunakan ABI lama. Jika Anda menggunakan versi TensorFlow ini dan mencoba mengkompilasi perpustakaan operasi Anda dengan gcc>=5 , tambahkan -D_GLIBCXX_USE_CXX11_ABI=0 ke baris perintah untuk membuat perpustakaan kompatibel dengan ABI yang lebih lama. Paket TensorFlow 2.9+ kompatibel dengan ABI yang lebih baru secara default.

Kompilasi operasi menggunakan bazel (instalasi sumber TensorFlow)

Jika Anda telah menginstal sumber TensorFlow, Anda dapat menggunakan sistem build TensorFlow untuk mengkompilasi operasi Anda. Tempatkan file BUILD dengan aturan build Bazel berikut di direktori tensorflow/core/user_ops .

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

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

Jalankan perintah berikut untuk membangun zero_out.so .

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

Untuk mengkompilasi operasi Example , dengan Kernel CUDA, Anda perlu menggunakan parameter gpu_srcs dari tf_custom_op_library . Tempatkan file BUILD dengan aturan build Bazel berikut di folder baru di dalam direktori tensorflow/core/user_ops (misalnya "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"],
)

Jalankan perintah berikut untuk membangun kernel_example.so .

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

Gunakan operasi dengan Python

TensorFlow Python API menyediakan fungsi tf.load_op_library untuk memuat perpustakaan dinamis dan mendaftarkan operasi dengan kerangka TensorFlow. load_op_library mengembalikan modul Python yang berisi pembungkus Python untuk operasi dan kernel. Jadi, setelah Anda membuat operasinya, Anda dapat melakukan hal berikut untuk menjalankannya dari 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)

Perlu diingat, fungsi yang dihasilkan akan diberi nama Snake_case (untuk mematuhi PEP8 ). Jadi, jika operasi Anda diberi nama ZeroOut di file C++, fungsi python akan dipanggil zero_out .

Untuk membuat operasi tersedia sebagai fungsi reguler yang dapat import dari modul Python, mungkin berguna untuk memiliki panggilan load_op_library dalam file sumber Python sebagai berikut:

import tensorflow as tf

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

Verifikasi bahwa operasi berhasil

Cara yang baik untuk memverifikasi bahwa Anda telah berhasil mengimplementasikan operasi Anda adalah dengan menulis tes untuk operasi tersebut. Buat file zero_out_op_test.py dengan isi:

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()

Kemudian jalankan pengujian Anda (dengan asumsi Anda telah menginstal tensorflow):

$ python zero_out_op_test.py

Bangun fitur-fitur canggih ke dalam operasi Anda

Sekarang setelah Anda mengetahui cara membuat operasi dan implementasi dasar (dan agak terbatas), kita akan melihat beberapa hal rumit yang biasanya perlu Anda masukkan ke dalam operasi Anda. Ini termasuk:

Pemeriksaan dan validasi bersyarat

Contoh di atas mengasumsikan bahwa op diterapkan pada tensor dalam bentuk apa pun. Bagaimana jika ini hanya diterapkan pada vektor? Itu berarti menambahkan tanda centang pada implementasi OpKernel di atas.

  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."));
    // ...
  }

Ini menegaskan bahwa inputnya adalah vektor, dan kembali setelah menetapkan status InvalidArgument jika bukan. Makro OP_REQUIRES membutuhkan tiga argumen:

Alternatifnya, jika Anda ingin menguji apakah objek Status yang dikembalikan dari beberapa fungsi merupakan kesalahan, dan jika demikian, kembalikan, gunakan OP_REQUIRES_OK . Kedua makro ini kembali dari fungsi jika terjadi kesalahan.

Pendaftaran

Attr

Ops dapat memiliki attrs, yang nilainya ditetapkan ketika op ditambahkan ke grafik. Ini digunakan untuk mengkonfigurasi operasi, dan nilainya dapat diakses baik dalam implementasi kernel maupun dalam jenis input dan output dalam registrasi operasi. Lebih suka menggunakan input daripada attr bila memungkinkan, karena input lebih fleksibel. Hal ini karena attrs adalah konstanta dan harus didefinisikan pada waktu pembuatan grafik. Sebaliknya, masukan adalah Tensor yang nilainya bisa dinamis; yaitu, input dapat berubah setiap langkah, diatur menggunakan feed, dll. Attr digunakan untuk hal-hal yang tidak dapat dilakukan dengan input: konfigurasi apa pun yang memengaruhi tanda tangan (jumlah atau jenis input atau output) atau yang dapat' tidak berubah dari langkah ke langkah.

Anda mendefinisikan attr saat mendaftarkan op, dengan menentukan nama dan tipenya menggunakan metode Attr , yang mengharapkan spesifikasi formulir:

<name>: <attr-type-expr>

di mana <name> dimulai dengan huruf dan dapat terdiri dari karakter alfanumerik dan garis bawah, dan <attr-type-expr> adalah ekspresi tipe dari bentuk yang dijelaskan di bawah ini .

Misalnya, jika Anda ingin operasi ZeroOut mempertahankan indeks yang ditentukan pengguna, alih-alih hanya elemen ke-0, Anda dapat mendaftarkan operasi seperti ini:

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

(Perhatikan bahwa kumpulan tipe atribut berbeda dari tf.DType yang digunakan untuk input dan output.)

Kernel Anda kemudian dapat mengakses attr ini di konstruktornya melalui parameter 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_;
};

yang kemudian dapat digunakan dalam metode 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_);
  }

Jenis attr

Jenis berikut ini didukung dalam attr:

  • string : Urutan byte apa pun (tidak harus UTF8).
  • int : Bilangan bulat bertanda.
  • float : Angka floating point.
  • bool : Benar atau salah.
  • type : Salah satu nilai (non-ref) dari DataType .
  • shape : TensorShapeProto .
  • list(<type>) : Daftar <type> , di mana <type> adalah salah satu tipe di atas. Perhatikan bahwa list(list(<type>)) tidak valid.

Lihat juga: op_def_builder.cc:FinalizeAttr untuk daftar pasti.

Nilai dan batasan default

Attr mungkin memiliki nilai default, dan beberapa jenis attr mungkin memiliki batasan. Untuk mendefinisikan attr dengan batasan, Anda dapat menggunakan <attr-type-expr> berikut:

{'<string1>', '<string2>'} : Nilai harus berupa string yang memiliki nilai <string1> atau <string2> . Nama tipenya, string , tersirat saat Anda menggunakan sintaksis ini. Ini mengemulasi enum:

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

{<type1>, <type2>} : Nilainya bertipe type , dan harus berupa salah satu dari <type1> atau <type2> , dengan <type1> dan <type2> didukung tf.DType . Anda tidak menentukan bahwa tipe attr adalah type . Ini tersirat ketika Anda memiliki daftar tipe di {...} . Misalnya, dalam hal ini attr t adalah tipe yang harus berupa int32 , float , atau bool :

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

Ada jalan pintas untuk batasan tipe umum:

  • numbertype : type tipe dibatasi pada tipe numerik (non-string dan non-bool).
  • realnumbertype : Seperti numbertype tanpa tipe kompleks.
  • quantizedtype : Seperti numbertype tetapi hanya tipe angka terkuantisasi.

Daftar tipe spesifik yang diizinkan oleh ini ditentukan oleh fungsi (seperti NumberTypes() ) di tensorflow/core/framework/types.h . Dalam contoh ini attr t harus berupa salah satu tipe numerik:

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

Untuk operasi ini:

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

Daftar dapat digabungkan dengan daftar lain dan tipe tunggal. Operasi berikut memungkinkan attr t menjadi salah satu tipe numerik, atau tipe bool:

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

Untuk operasi ini:

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> : Nilai harus berupa int yang nilainya lebih besar atau sama dengan <n> , dengan <n> adalah bilangan asli. Misalnya, registrasi op berikut menetapkan bahwa attr a harus memiliki nilai minimal 2 :

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

list(<type>) >= <n> : Daftar tipe <type> yang panjangnya lebih besar dari atau sama dengan <n> . Misalnya, registrasi operasi berikut menetapkan bahwa attr a adalah daftar tipe (baik int32 atau float ), dan setidaknya harus ada 3 tipe:

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

Untuk menetapkan nilai default untuk attr (menjadikannya opsional dalam kode yang dihasilkan), tambahkan = <default> di akhir, seperti pada:

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

Selain itu, batasan dan nilai default dapat ditentukan:

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

Sintaks yang didukung dari nilai default adalah apa yang akan digunakan dalam representasi proto dari definisi GraphDef yang dihasilkan.

Berikut ini contoh cara menentukan default untuk semua tipe:

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

Perhatikan secara khusus bahwa nilai tipe type menggunakan tf.DType .

Polimorfisme

Ketik polimorfisme

Untuk operasi yang dapat mengambil tipe berbeda sebagai input atau menghasilkan tipe output berbeda, Anda dapat menentukan attr pada tipe input atau output dalam registrasi operasi. Biasanya Anda kemudian akan mendaftarkan OpKernel untuk setiap jenis yang didukung.

Misalnya, jika Anda ingin operasi ZeroOut berfungsi pada float s selain int32 s, pendaftaran operasi Anda mungkin terlihat seperti:

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

Registrasi operasi Anda sekarang menetapkan bahwa tipe input harus float , atau int32 , dan outputnya akan bertipe sama, karena keduanya bertipe T .

Penamaan

Input, output, dan attr umumnya harus diberi nama ular_case. Satu-satunya pengecualian adalah attr yang digunakan sebagai tipe input atau tipe output. Attr tersebut dapat disimpulkan ketika op ditambahkan ke grafik sehingga tidak muncul dalam fungsi op. Misalnya, definisi terakhir dari ZeroOut ini akan menghasilkan fungsi Python yang terlihat seperti:

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`.
  """

Jika to_zero melewati tensor int32 , maka T secara otomatis disetel ke int32 (sebenarnya DT_INT32 ). Attr yang disimpulkan tersebut diberi nama Kapitalisasi atau CamelCase.

Bandingkan ini dengan operasi yang memiliki tipe attr yang menentukan tipe keluaran:

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

Dalam hal ini, pengguna harus menentukan tipe keluaran, seperti pada Python yang dihasilkan:

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`.
  """
Ketik contoh polimorfisme
#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);

Untuk menjaga kompatibilitas ke belakang , Anda harus menentukan nilai default saat menambahkan attr ke operasi yang ada:

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

Katakanlah Anda ingin menambahkan lebih banyak tipe, katakan double :

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

Daripada menulis OpKernel lain dengan kode berlebihan seperti di atas, sering kali Anda dapat menggunakan template C++. Anda masih akan memiliki satu registrasi kernel (panggilan REGISTER_KERNEL_BUILDER ) per kelebihan beban.

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

Jika Anda memiliki lebih dari beberapa kelebihan beban, Anda dapat memasukkan registrasi ke dalam makro.

#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

Bergantung pada daftar tipe kernel yang Anda daftarkan, Anda mungkin dapat menggunakan makro yang disediakan oleh 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
Daftar masukan dan keluaran

Selain dapat menerima atau memproduksi tipe yang berbeda, operasi dapat menggunakan atau menghasilkan sejumlah tensor yang bervariasi.

Pada contoh berikutnya, attr T menyimpan daftar tipe, dan digunakan sebagai tipe input in dan output out . Input dan output adalah daftar tensor tipe tersebut (dan jumlah serta tipe tensor pada output sama dengan input, karena keduanya bertipe T ).

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

Anda juga dapat membatasi jenis apa yang dapat ditentukan dalam daftar. Dalam kasus berikutnya, masukannya adalah daftar tensor float dan double . Operasi menerima, misalnya, tipe input (float, double, float) dan dalam hal ini tipe output juga akan menjadi (float, double, float) .

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

Jika Anda ingin semua tensor dalam daftar memiliki tipe yang sama, Anda dapat melakukan sesuatu seperti:

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

Ini menerima daftar tensor int32 , dan menggunakan int attr N untuk menentukan panjang daftar.

Ini juga bisa dibuat tipe polimorfik . Pada contoh berikutnya, inputnya adalah daftar tensor (dengan panjang "N" ) dengan tipe yang sama (tetapi tidak ditentukan) ( "T" ), dan outputnya adalah tensor tunggal dengan tipe yang cocok:

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

Secara default, daftar tensor memiliki panjang minimum 1. Anda dapat mengubah default tersebut menggunakan batasan ">=" pada attr yang sesuai . Dalam contoh berikutnya, inputnya adalah daftar minimal 2 tensor int32 :

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

Sintaks yang sama berfungsi dengan attrs "list(type)" :

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

Masukan dan keluaran

Untuk meringkas hal di atas, registrasi op dapat memiliki banyak input dan output:

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

Setiap spesifikasi input atau output berbentuk:

<name>: <io-type-expr>

di mana <name> dimulai dengan huruf dan dapat terdiri dari karakter alfanumerik dan garis bawah. <io-type-expr> adalah salah satu dari ekspresi tipe berikut:

  • <type> , di mana <type> adalah tipe masukan yang didukung (misalnya float , int32 , string ). Ini menentukan tensor tunggal dari tipe tertentu.

    Lihat tf.DType .

    REGISTER_OP("BuiltInTypesExample")
        .Input("integers: int32")
        .Input("complex_numbers: complex64");
    
  • <attr-type> , di mana <attr-type> adalah nama Attr dengan type tipe atau list(type) (dengan kemungkinan batasan tipe). Sintaks ini memungkinkan operasi polimorfik .

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

    Merujuk attr tipe list(type) memungkinkan Anda menerima rangkaian tensor.

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

    Perhatikan bahwa jumlah dan tipe tensor pada output out sama dengan input in , karena keduanya bertipe T .

  • Untuk rangkaian tensor dengan tipe yang sama: <number> * <type> , dengan <number> adalah nama Attr dengan tipe int . <type> dapat berupa tf.DType , atau nama attr dengan tipe type . Sebagai contoh yang pertama, operasi ini menerima daftar tensor int32 :

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

    Sedangkan operasi ini menerima daftar tensor jenis apa pun, asalkan semuanya sama:

    REGISTER_OP("SameTypeSequenceExample")
        .Attr("NumTensors: int")
        .Attr("T: type")
        .Input("in: NumTensors * T")
    
  • Untuk referensi ke tensor: Ref(<type>) , dengan <type> merupakan salah satu tipe sebelumnya.

Setiap attr yang digunakan dalam tipe input akan disimpulkan. Berdasarkan konvensi, attr yang disimpulkan tersebut menggunakan nama kapital (seperti T atau N ). Jika tidak, input, output, dan attr memiliki nama seperti parameter fungsi (misalnya num_outputs ). Untuk lebih jelasnya, lihat bagian sebelumnya tentang penamaan .

Untuk detail selengkapnya, lihat tensorflow/core/framework/op_def_builder.h .

Kompatibilitas mundur

Anggaplah Anda telah menulis operasi khusus yang bagus dan membaginya dengan orang lain, sehingga Anda memiliki pelanggan yang senang menggunakan operasi Anda. Namun, Anda ingin melakukan perubahan pada operasi dengan cara tertentu.

Secara umum, perubahan pada spesifikasi yang sudah ada dan diperiksa harus kompatibel dengan versi sebelumnya: mengubah spesifikasi operasi tidak boleh merusak buffer protokol GraphDef berseri sebelumnya yang dibuat dari spesifikasi lama. Detail kompatibilitas GraphDef dijelaskan di sini .

Ada beberapa cara untuk mempertahankan kompatibilitas ke belakang.

  1. Setiap attr baru yang ditambahkan ke operasi harus memiliki nilai default yang ditentukan, dan dengan nilai default tersebut, operasi harus memiliki perilaku asli. Untuk mengubah operasi dari bukan polimorfik menjadi polimorfik, Anda harus memberikan nilai default ke tipe attr baru untuk mempertahankan tanda tangan asli secara default. Misalnya, jika operasi Anda adalah:

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

    Anda dapat membuatnya polimorfik dengan cara yang kompatibel dengan menggunakan:

    REGISTER_OP("MyGeneralUnaryOp")
        .Input("in: T")
        .Output("out: T")
        .Attr("T: numerictype = DT_FLOAT");
    
  2. Anda dapat dengan aman membuat batasan pada attr menjadi tidak terlalu ketat. Misalnya, Anda dapat mengubah dari {int32, int64} menjadi {int32, int64, float} atau type . Atau Anda dapat mengubah dari {"apple", "orange"} menjadi {"apple", "banana", "orange"} atau string .

  3. Anda dapat mengubah input/output tunggal menjadi input/output daftar, selama default untuk tipe daftar cocok dengan tanda tangan lama.

  4. Anda dapat menambahkan daftar input/output baru, jika defaultnya kosong.

  5. Namespace setiap operasi baru yang Anda buat, dengan mengawali nama operasi dengan sesuatu yang unik untuk proyek Anda. Hal ini untuk menghindari operasi Anda bertabrakan dengan operasi apa pun yang mungkin disertakan dalam versi TensorFlow mendatang.

  6. Rencanakan ke depan! Cobalah untuk mengantisipasi penggunaan operasi di masa depan. Beberapa perubahan tanda tangan tidak dapat dilakukan dengan cara yang kompatibel (misalnya, membuat daftar dengan tipe yang sama menjadi daftar dengan tipe yang berbeda-beda).

Daftar lengkap perubahan yang aman dan tidak aman dapat ditemukan di tensorflow/core/framework/op_compatibility_test.cc . Jika Anda tidak dapat membuat perubahan pada operasi yang kompatibel, buatlah operasi baru dengan nama baru dengan semantik baru.

Perhatikan juga bahwa meskipun perubahan ini dapat mempertahankan kompatibilitas GraphDef , kode Python yang dihasilkan mungkin berubah sehingga tidak kompatibel dengan pemanggil lama. API Python dapat tetap kompatibel dengan perubahan yang cermat pada pembungkus Python yang ditulis tangan, dengan mempertahankan tanda tangan lama kecuali mungkin menambahkan argumen opsional baru di bagian akhir. Umumnya perubahan yang tidak kompatibel hanya dapat dilakukan ketika TensorFlow mengubah versi utama, dan harus sesuai dengan semantik versi GraphDef .

dukungan GPU

Anda dapat mengimplementasikan OpKernel yang berbeda dan mendaftarkan satu untuk CPU dan satu lagi untuk GPU, sama seperti Anda dapat mendaftarkan kernel untuk tipe yang berbeda . Ada beberapa contoh kernel dengan dukungan GPU di tensorflow/core/kernels/ . Perhatikan beberapa kernel memiliki versi CPU dalam file .cc , versi GPU dalam file yang diakhiri dengan _gpu.cu.cc , dan beberapa kode yang sama dalam file .h .

Misalnya, tf.pad memiliki segalanya kecuali kernel GPU di tensorflow/core/kernels/pad_op.cc . Kernel GPU ada di tensorflow/core/kernels/pad_op_gpu.cu.cc , dan kode yang dibagikan adalah kelas templat yang ditentukan dalam tensorflow/core/kernels/pad_op.h . Kami mengatur kode dengan cara ini karena dua alasan: ini memungkinkan Anda untuk berbagi kode umum antara implementasi CPU dan GPU, dan ini menempatkan implementasi GPU ke dalam file terpisah sehingga hanya dapat dikompilasi oleh kompiler GPU.

Satu hal yang perlu diperhatikan, meskipun pad versi kernel GPU digunakan, ia masih memerlukan input "paddings" di memori CPU. Untuk menandai bahwa input atau output disimpan di CPU, tambahkan panggilan HostMemory() ke registrasi kernel, misalnya:

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

Mengompilasi kernel untuk perangkat GPU

Lihat cuda_op_kernel.cu.cc untuk contoh yang menggunakan kernel CUDA untuk mengimplementasikan operasi. tf_custom_op_library menerima argumen gpu_srcs yang berisi daftar file sumber yang berisi kernel CUDA ( file *.cu.cc ) yang dapat ditentukan. Untuk digunakan dengan instalasi biner TensorFlow, kernel CUDA harus dikompilasi dengan compiler nvcc NVIDIA. Berikut adalah urutan perintah yang dapat Anda gunakan untuk mengkompilasi cuda_op_kernel.cu.cc dan cuda_op_kernel.cc ke dalam satu pustaka yang dapat dimuat secara dinamis:

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 yang dihasilkan di atas dapat dimuat seperti biasa dengan Python, menggunakan fungsi tf.load_op_library .

Perhatikan bahwa jika perpustakaan CUDA Anda tidak diinstal di /usr/local/lib64 , Anda harus menentukan jalur secara eksplisit pada perintah kedua (g++) di atas. Misalnya, tambahkan -L /usr/local/cuda-8.0/lib64/ jika CUDA Anda diinstal di /usr/local/cuda-8.0 .

Terapkan gradien dengan Python

Dengan adanya grafik operasi, TensorFlow menggunakan diferensiasi otomatis (propagasi mundur) untuk menambahkan operasi baru yang mewakili gradien sehubungan dengan operasi yang ada. Agar diferensiasi otomatis berfungsi pada operasi baru, Anda harus mendaftarkan fungsi gradien yang menghitung gradien sehubungan dengan masukan operasi dengan memberikan gradien sehubungan dengan keluaran operasi.

Secara matematis, jika suatu operasi menghitung \(y = f(x)\) operasi gradien terdaftar mengubah gradien \(\partial L/ \partial y\) kerugian \(L\) mengenai\(y\) menjadi gradien \(\partial L/ \partial x\) mengenai \(x\) melalui aturan rantai:

\[\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}.\]

Dalam kasus ZeroOut , hanya satu entri dalam masukan yang mempengaruhi keluaran, sehingga gradien terhadap masukan adalah tensor "one hot" yang jarang. Hal ini diungkapkan sebagai berikut:

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

Detail tentang mendaftarkan fungsi gradien dengan tf.RegisterGradient :

  • Untuk operasi dengan satu keluaran, fungsi gradien akan mengambil tf.Operation , op , dan tf.Tensor grad serta membuat operasi baru dari tensor op.inputs[i] , op.outputs[i] , dan grad . Informasi tentang attr apa pun dapat ditemukan melalui tf.Operation.get_attr .

  • Jika op memiliki beberapa keluaran, fungsi gradien akan mengambil op dan grads , di mana grads adalah daftar gradien terhadap setiap keluaran. Hasil dari fungsi gradien harus berupa daftar objek Tensor yang mewakili gradien terhadap setiap masukan.

  • Jika tidak ada gradien yang terdefinisi dengan baik untuk beberapa masukan, misalnya untuk masukan bilangan bulat yang digunakan sebagai indeks, gradien yang dikembalikan haruslah None . Misalnya, untuk operasi yang menggunakan tensor floating point x dan indeks bilangan bulat i , fungsi gradien akan return [x_grad, None] .

  • Jika tidak ada gradien yang berarti untuk operasi sama sekali, Anda sering kali tidak perlu mendaftarkan gradien apa pun, dan selama gradien operasi tidak diperlukan, Anda akan baik-baik saja. Dalam beberapa kasus, operasi tidak memiliki gradien yang terdefinisi dengan baik tetapi dapat dilibatkan dalam penghitungan gradien. Di sini Anda dapat menggunakan ops.NotDifferentiable untuk menyebarkan angka nol ke belakang secara otomatis.

Perhatikan bahwa pada saat fungsi gradien dipanggil, hanya grafik aliran data operasi yang tersedia, bukan data tensor itu sendiri. Oleh karena itu, semua komputasi harus dilakukan menggunakan operasi tensorflow lainnya, agar dapat dijalankan pada waktu eksekusi grafik.

Tambahkan petunjuk tipe saat mendaftarkan gradien khusus untuk tipe operasi agar kode lebih mudah dibaca, dapat di-debug, lebih mudah dipelihara, dan lebih kuat melalui validasi data. Misalnya, saat mengambil op sebagai parameter dalam suatu fungsi, tentukan bahwa fungsi gradien akan menggunakan tf.Operation sebagai tipe parameternya.

Fungsi bentuk di C++

TensorFlow API memiliki fitur yang disebut "inferensi bentuk" yang memberikan informasi tentang bentuk tensor tanpa harus mengeksekusi grafik. Inferensi bentuk didukung oleh "fungsi bentuk" yang didaftarkan untuk setiap tipe operasi dalam deklarasi C++ REGISTER_OP , dan menjalankan dua peran: menegaskan bahwa bentuk masukan kompatibel selama konstruksi grafik, dan menentukan bentuk keluaran.

Fungsi bentuk didefinisikan sebagai operasi pada kelas shape_inference::InferenceContext . Misalnya, dalam fungsi bentuk untuk ZeroOut:

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

c->set_output(0, c->input(0)); menyatakan bahwa bentuk keluaran pertama harus disetel ke bentuk masukan pertama. Jika output dipilih berdasarkan indeksnya seperti pada contoh di atas, parameter kedua set_output harus berupa objek ShapeHandle . Anda dapat membuat objek ShapeHandle kosong dengan konstruktor defaultnya. Objek ShapeHandle untuk input dengan indeks idx dapat diperoleh dengan c->input(idx) .

Ada sejumlah fungsi bentuk umum yang berlaku pada banyak operasi, seperti shape_inference::UnchangedShape yang dapat ditemukan di common_shape_fns.h dan digunakan sebagai berikut:

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

Fungsi bentuk juga dapat membatasi bentuk masukan. Untuk versi ZeroOut dengan batasan bentuk vektor , fungsi bentuknya adalah sebagai berikut:

    .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();
    });

Panggilan WithRank memvalidasi bahwa bentuk masukan c->input(0) mempunyai bentuk dengan tepat satu dimensi (atau jika bentuk masukan tidak diketahui, bentuk keluaran akan berupa vektor dengan satu dimensi yang tidak diketahui).

Jika operasi Anda polimorfik dengan beberapa inputs , Anda dapat menggunakan anggota InferenceContext untuk menentukan jumlah bentuk yang akan diperiksa, dan Merge untuk memvalidasi bahwa semua bentuk kompatibel (sebagai alternatif, akses atribut yang menunjukkan panjangnya, dengan InferenceContext::GetAttr , yang menyediakan akses ke atribut 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();
    });

Karena inferensi bentuk adalah fitur opsional, dan bentuk tensor dapat bervariasi secara dinamis, fungsi bentuk harus kuat terhadap informasi bentuk yang tidak lengkap untuk masukan apa pun. Metode Merge di InferenceContext memungkinkan pemanggil untuk menegaskan bahwa dua bentuk adalah sama, meskipun salah satu atau keduanya tidak memiliki informasi lengkap. Fungsi bentuk ditentukan untuk semua operasi inti TensorFlow dan memberikan banyak contoh penggunaan yang berbeda.

Kelas InferenceContext memiliki sejumlah fungsi yang dapat digunakan untuk mendefinisikan manipulasi fungsi bentuk. Misalnya, Anda dapat memvalidasi bahwa dimensi tertentu memiliki nilai yang sangat spesifik menggunakan InferenceContext::Dim dan InferenceContext::WithValue ; Anda dapat menentukan bahwa dimensi keluaran adalah jumlah/produk dari dua dimensi masukan menggunakan InferenceContext::Add dan InferenceContext::Multiply . Lihat kelas InferenceContext untuk semua berbagai manipulasi bentuk yang dapat Anda tentukan. Contoh berikut mengatur bentuk keluaran pertama menjadi (n, 3), dimana masukan pertama berbentuk (n, ...)

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

Jika Anda memiliki fungsi bentuk yang rumit, Anda harus mempertimbangkan untuk menambahkan pengujian untuk memvalidasi bahwa berbagai kombinasi bentuk masukan menghasilkan kombinasi bentuk keluaran yang diharapkan. Anda dapat melihat contoh cara menulis pengujian ini di beberapa pengujian operasi inti kami. (Sintaks INFER_OK dan INFER_ERROR agak samar, tetapi cobalah untuk kompak dalam merepresentasikan spesifikasi bentuk masukan dan keluaran dalam pengujian. Untuk saat ini, lihat komentar di sekitar pengujian tersebut untuk memahami spesifikasi string bentuk).

Buat paket pip untuk operasi khusus Anda

Untuk membuat paket pip untuk operasi Anda, lihat contoh tensorflow/operasi khusus . Panduan ini menunjukkan cara membuat operasi kustom dari paket pip TensorFlow alih-alih membuat TensorFlow dari sumber.