Hari Komunitas ML adalah 9 November! Bergabung dengan kami untuk update dari TensorFlow, JAX, dan lebih Pelajari lebih lanjut

Buat op

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

  • Tidak mudah atau mungkin untuk mengekspresikan operasi Anda sebagai komposisi ops yang ada.
  • Tidaklah efisien untuk mengekspresikan operasi Anda sebagai komposisi primitif yang sudah ada.
  • Anda ingin menggabungkan komposisi primitif yang akan sulit dilakukan oleh compiler di masa mendatang.

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

Untuk memasukkan operasi kustom Anda, Anda harus:

  1. Daftarkan operasi baru di file C ++. Registrasi op menentukan antarmuka (spesifikasi) untuk fungsionalitas operasi, yang tidak bergantung pada implementasi operasi. Misalnya, registrasi op mendefinisikan nama op serta masukan dan keluaran op. Ini juga mendefinisikan fungsi bentuk yang digunakan untuk inferensi bentuk tensor.
  2. Implementasikan op di C ++. Implementasi operasi dikenal sebagai kernel, dan ini adalah implementasi konkret dari spesifikasi yang Anda daftarkan di Langkah 1. Mungkin ada beberapa kernel untuk jenis atau arsitektur input / output yang berbeda (misalnya, CPU, GPU).
  3. Buat pembungkus Python (opsional). Wrapper ini adalah API publik yang digunakan untuk membuat op dengan Python. Pembungkus default dihasilkan dari pendaftaran op, yang dapat digunakan secara langsung atau ditambahkan ke.
  4. Tulis fungsi untuk menghitung gradien untuk op (opsional).
  5. Uji op. Kami biasanya melakukan ini dengan Python untuk kenyamanan, tetapi Anda juga dapat menguji op dalam 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 maju operator mirip Relu dan gradiennya.

Prasyarat

Tentukan antarmuka operasi

Anda menentukan antarmuka operasi dengan mendaftarkannya ke sistem TensorFlow. Dalam registrasi, Anda menentukan nama op Anda, inputnya (tipe dan nama) dan outputnya (tipe dan nama), serta docstrings dan atribut apa pun yang mungkin dibutuhkan op.

Untuk melihat cara kerjanya, misalkan Anda ingin membuat op yang menggunakan tensor int32 s dan mengeluarkan salinan tensor, dengan semua kecuali elemen pertama disetel ke nol. Untuk melakukan ini, buat file bernama zero_out.cc . Kemudian tambahkan panggilan ke makro REGISTER_OP yang menentukan 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();
    });

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

Implementasikan kernel untuk op

Setelah Anda menentukan antarmuka, berikan satu atau beberapa implementasi op. Untuk membuat salah satu kernel ini, buat kelas yang memperluas OpKernel dan menimpa metode Compute . Metode Compute menyediakan satu argumen context tipe OpKernelContext* , dari mana Anda dapat mengakses hal-hal yang berguna seperti tensor input dan output.

Tambahkan kernel Anda ke file yang Anda buat di atas. Kernel 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 dengan sistem TensorFlow. Dalam registrasi, Anda menentukan batasan berbeda untuk menjalankan kernel ini. Misalnya, Anda mungkin memiliki satu kernel yang dibuat untuk CPU, dan yang terpisah untuk GPU.

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

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

Kernel CPU multi-utas

Untuk menulis kernel CPU multi-utas, fungsi Shard di work_sharder.h dapat digunakan. Fungsi ini membagi fungsi komputasi di seluruh utas 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 dan kode peluncurannya.

Terkadang implementasi OpKernel adalah umum antara CPU dan GPU kernel, seperti memeriksa input dan mengalokasikan output. Dalam hal ini, penerapan yang disarankan adalah:

  1. Tentukan template OpKernel pada Perangkat dan tipe primitif tensor.
  2. Untuk melakukan penghitungan output yang sebenarnya, fungsi Compute memanggil struct functor dengan template.
  3. Spesialisasi dari functor untuk CPUDevice didefinisikan dalam file yang sama, tetapi spesialisasi untuk GPUDevice didefinisikan dalam file .cu.cc, karena akan dikompilasi dengan compiler CUDA.

Berikut ini contoh implementasinya.

// 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 op menggunakan kompilator sistem Anda (penginstalan biner TensorFlow)

Anda harus dapat mengompilasi zero_out.cc dengan kompiler C++ seperti g++ atau clang tersedia di sistem Anda. Paket PIP biner menginstal file header dan pustaka yang Anda perlukan untuk mengompilasi 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 keluaran 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 mengompilasi op Anda menjadi pustaka 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++11 -shared zero_out.cc -o zero_out.so -fPIC ${TF_CFLAGS[@]} ${TF_LFLAGS[@]} -O2

Di macOS, tanda tambahan "-undefined dynamic_lookup" diperlukan saat membuat file .so .

Catatan tentang gcc versi >=5 : gcc menggunakan C ++ ABI baru sejak versi 5 . Paket pip biner yang tersedia di situs TensorFlow dibuat dengan gcc4 yang menggunakan ABI lama. Jika Anda mengkompilasi pustaka op Anda dengan gcc>=5 , tambahkan -D_GLIBCXX_USE_CXX11_ABI=0 ke baris perintah untuk membuat pustaka tersebut kompatibel dengan abi yang lebih lama.

Kompilasi op menggunakan bazel (penginstalan sumber TensorFlow)

Jika Anda telah menginstal sumber TensorFlow, Anda dapat menggunakan sistem build TensorFlow untuk mengompilasi operasi Anda. Tempatkan file BUILD dengan aturan build Bazel berikut di 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 menyusun Example operasi, dengan CUDA Kernel, Anda perlu menggunakan gpu_srcs parameter tf_custom_op_library . Tempatkan file BUILD dengan aturan build Bazel berikut di folder baru di dalam 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 op dengan Python

TensorFlow Python API menyediakan fungsi tf.load_op_library untuk memuat library dinamis dan mendaftarkan operasi dengan framework TensorFlow. load_op_library mengembalikan modul Python yang berisi pembungkus Python untuk operasi dan kernel. Jadi, setelah Anda membangun op, 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 (sesuai dengan PEP8 ). Jadi, jika op Anda bernama ZeroOut di file C ++, fungsi python akan dipanggil zero_out .

Untuk membuat op tersedia sebagai fungsi biasa 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 berfungsi

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

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 yang lebih rumit yang biasanya Anda perlukan untuk membangun operasi Anda. Ini termasuk:

Pemeriksaan dan validasi bersyarat

Contoh di atas mengasumsikan bahwa op diterapkan pada tensor dalam bentuk apa pun. Bagaimana jika itu 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 input adalah vektor, dan mengembalikan setelah menyetel status InvalidArgument jika tidak. Makro OP_REQUIRES mengambil tiga argumen:

Alternatifnya, jika Anda ingin menguji apakah objek Status dikembalikan dari beberapa fungsi adalah kesalahan, dan jika mengembalikannya, gunakan OP_REQUIRES_OK . Kedua makro ini kembali dari fungsi saat terjadi kesalahan.

Pendaftaran op

Attrs

Operasi dapat memiliki attrs, yang nilainya ditetapkan saat op ditambahkan ke grafik. Ini digunakan untuk mengkonfigurasi op, dan nilainya dapat diakses baik di dalam implementasi kernel dan dalam jenis masukan dan keluaran dalam pendaftaran op. Lebih suka menggunakan masukan daripada attr bila memungkinkan, karena masukan lebih fleksibel. Ini karena attrs adalah konstanta dan harus ditentukan pada waktu pembuatan grafik. Sebaliknya, input adalah Tensor yang nilainya bisa dinamis; artinya, masukan dapat mengubah setiap langkah, disetel menggunakan umpan, dll. Attr digunakan untuk hal-hal yang tidak dapat dilakukan dengan masukan: konfigurasi apa pun yang memengaruhi tanda tangan (jumlah atau jenis masukan atau keluaran) atau yang dapat ' t berubah dari langkah ke langkah.

Anda mendefinisikan sebuah attr ketika Anda mendaftarkan op, dengan menentukan nama dan tipenya menggunakan metode Attr , yang mengharapkan spesifikasi dari form:

<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 formulir yang dijelaskan di bawah ini .

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

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

(Perhatikan bahwa kumpulan tipe atribut berbeda daritf.DType 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 sebuah attr:

  • string : Semua urutan byte (tidak harus UTF8).
  • int : Integer bertanda tangan.
  • 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 definitif.

Nilai dan batasan default

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

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

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

{<type1>, <type2>} : Nilainya adalah tipe type , dan harus salah satu dari <type1> atau <type2> , di mana <type1> dan <type2> didukung tf.DType . Anda tidak menentukan bahwa tipe dari attr adalah type . Ini tersirat ketika Anda memiliki daftar tipe di {...} . Misalnya, dalam kasus 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 : Jenis type dibatasi untuk jenis numerik (non-string dan non-bool).
  • realnumbertype : Seperti numbertype tanpa tipe kompleks.
  • quantizedtype : Seperti numbertype tetapi hanya jenis nomor 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. Opsi 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> : Nilainya harus berupa int yang nilainya lebih besar dari atau sama dengan <n> , di mana <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 dari atau sama dengan <n> . Misalnya, registrasi op berikut menetapkan bahwa attr a adalah daftar tipe (baik int32 atau float ), dan setidaknya harus ada 3 di antaranya:

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

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

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 adalah contoh cara menentukan default untuk semua jenis:

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 menggunakantf.DType .

Polimorfisme

Ketik polimorfisme

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

Misalnya, jika Anda ingin op ZeroOut bekerja pada float selain int32 , pendaftaran op Anda mungkin terlihat seperti:

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

Registrasi op Anda sekarang menentukan bahwa tipe input harus float , atau int32 , dan outputnya akan memiliki tipe yang sama, karena keduanya memiliki tipe T

Penamaan

Input, output, dan attrs umumnya harus diberi nama snake_case. Satu-satunya pengecualian adalah attrs yang digunakan sebagai tipe input atau tipe output. Atr tersebut dapat disimpulkan saat op ditambahkan ke grafik dan karenanya tidak muncul di fungsi op. Misalnya, definisi ZeroOut terakhir 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 dilewatkan tensor int32 , maka T secara otomatis disetel ke int32 (sebenarnya DT_INT32 ). Atribut 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 kasus 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 mundur , 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, katakanlah double :

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

Alih-alih menulis OpKernel lain dengan kode redundan seperti di atas, seringkali Anda dapat menggunakan template C ++ sebagai gantinya. Anda masih memiliki satu pendaftaran kernel (panggilan REGISTER_KERNEL_BUILDER ) untuk setiap 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 meletakkan pendaftaran di 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 jenis tempat Anda mendaftarkan kernel, 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
Buat daftar masukan dan keluaran

Selain dapat menerima atau menghasilkan tipe yang berbeda, ops dapat menggunakan atau menghasilkan tensor dalam jumlah variabel.

Pada contoh berikutnya, attr T memegang daftar jenis, dan digunakan sebagai jenis dari kedua input in dan output out . Input dan output adalah daftar tensor jenis itu (dan jumlah serta jenis tensor dalam output sama dengan input, karena keduanya memiliki tipe 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, inputnya adalah daftar tensor float dan double . Operasi menerima, misalnya, jenis masukan (float, double, float) dan dalam hal ini jenis keluaran 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 jenis 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 bisa dibuat menjadi polimorfik juga. Dalam contoh berikutnya, masukannya adalah daftar tensor (dengan panjang "N" ) dengan jenis yang sama (tetapi tidak ditentukan) ( "T" ), dan keluarannya adalah tensor tunggal dengan jenis 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 terkait . Dalam contoh berikut ini, inputnya adalah daftar setidaknya 2 tensor int32 :

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

Sintaks yang sama bekerja 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, pendaftaran operasi dapat memiliki banyak input dan output:

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

Setiap spesifikasi masukan atau keluaran 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 input yang didukung (misalnya float , int32 , string ). Ini menentukan tensor tunggal dari tipe yang diberikan.

    Lihattf.DType .

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

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

    Mengacu pada attr dari list(type) tipe list(type) memungkinkan Anda untuk menerima urutan 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 jenis tensor pada keluaran out sama dengan pada masukan in , karena keduanya berjenis T

  • Untuk urutan tensor dengan tipe yang sama: <number> * <type> , di mana <number> adalah nama Attr dengan tipe int . <type> bisa berupatf.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 op ini menerima daftar tensor jenis apa saja, asalkan semuanya sama:

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

Atribut apa pun yang digunakan dalam jenis masukan akan disimpulkan. Menurut konvensi, atribut yang disimpulkan menggunakan nama kapital (seperti T atau N ). Jika tidak, input, output, dan attrs memiliki nama seperti parameter fungsi (misalnya num_outputs ). Untuk lebih jelasnya, lihat bagian sebelumnya tentang penamaan .

Untuk lebih jelasnya, lihat tensorflow/core/framework/op_def_builder.h .

Kompatibilitas mundur

Anggaplah Anda telah menulis operasi kustom yang bagus dan membagikannya dengan orang lain, sehingga Anda memiliki pelanggan yang senang menggunakan operasi Anda. Namun, Anda ingin melakukan perubahan pada op dalam beberapa cara.

Secara umum, perubahan pada spesifikasi yang sudah ada dan check-in harus kompatibel ke belakang: mengubah spesifikasi op tidak boleh merusak buffer protokol GraphDef serial sebelumnya yang dibuat dari spesifikasi yang lebih lama. Detail kompatibilitas GraphDef dijelaskan di sini .

Ada beberapa cara untuk menjaga 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 tidak polimorfik menjadi polimorfik, Anda harus memberikan nilai default ke attr tipe 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 menjadi polimorfik dengan cara yang kompatibel dengan versi sebelumnya menggunakan:

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

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

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

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

  6. Rencanakan ke depan! Cobalah untuk mengantisipasi penggunaan op. Beberapa perubahan tanda tangan tidak dapat dilakukan dengan cara yang kompatibel (misalnya, membuat daftar jenis yang sama ke dalam daftar jenis yang berbeda-beda).

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

Perhatikan juga bahwa meskipun perubahan ini dapat mempertahankan kompatibilitas GraphDef , kode Python yang dihasilkan dapat berubah dengan cara yang tidak kompatibel dengan pemanggil lama. Python API dapat tetap kompatibel dengan perubahan hati-hati dalam pembungkus Python yang ditulis tangan, dengan mempertahankan tanda tangan lama kecuali mungkin menambahkan argumen opsional baru di akhir. Perubahan yang umumnya tidak kompatibel hanya dapat dilakukan saat TensorFlow mengubah versi utama, dan harus sesuai dengan semantik versi GraphDef .

Dukungan GPU

Anda dapat mengimplementasikan OpKernels yang berbeda dan mendaftarkan satu untuk CPU dan yang lainnya untuk GPU, sama seperti Anda dapat mendaftarkan kernel untuk jenis yang berbeda . Ada beberapa contoh kernel dengan dukungan GPU di tensorflow/core/kernels/ . Perhatikan bahwa beberapa kernel memiliki versi CPU dalam file .cc , versi GPU dalam file yang diakhiri dengan _gpu.cu.cc , dan beberapa kode yang digunakan bersama 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 bersama adalah kelas template yang ditentukan di tensorflow/core/kernels/pad_op.h . Kami mengatur kode dengan cara ini karena dua alasan: ini memungkinkan Anda untuk berbagi kode umum di antara implementasi CPU dan GPU, dan 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 membutuhkan input "paddings" di memori CPU. Untuk menandai bahwa input atau output disimpan di CPU, tambahkan panggilan HostMemory() ke pendaftaran 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 op. tf_custom_op_library menerima argumen gpu_srcs di mana daftar file sumber yang berisi kernel CUDA (file *.cu.cc ) dapat ditentukan. Untuk digunakan dengan penginstalan biner TensorFlow, kernel CUDA harus dikompilasi dengan kompiler 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 perpustakaan yang dapat dimuat secara dinamis:

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

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

cuda_op_kernel.so dihasilkan di atas dapat dimuat seperti biasa dengan Python, menggunakan fungsi tf.load_op_library .

Perhatikan bahwa jika pustaka 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. Untuk membuat diferensiasi otomatis bekerja untuk operasi baru, Anda harus mendaftarkan fungsi gradien yang menghitung gradien sehubungan dengan masukan operasi yang diberikan gradien sehubungan dengan keluaran operasi.

Secara matematis, jika sebuah op menghitung \(y = f(x)\) op gradien terdaftar mengubah gradien \(\partial L/ \partial y\) kerugian \(L\) sehubungan dengan \(y\) menjadi gradien \(\partial L/ \partial x\) sehubungan dengan \(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 sehubungan dengan masukan adalah tensor "satu panas" yang jarang. 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 dan membangun operasi baru dari tensor op.inputs[i] , op.outputs[i] , dan grad . Informasi tentang atribut 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 yang berkaitan dengan setiap keluaran. Hasil dari fungsi gradien harus berupa daftar objek Tensor mewakili gradien terkait dengan setiap masukan.

  • Jika tidak ada gradien yang terdefinisi dengan baik untuk beberapa masukan, seperti untuk masukan bilangan bulat yang digunakan sebagai indeks, gradien yang dikembalikan harus berupa None . Misalnya, untuk operasi yang mengambil tensor titik mengambang 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, sebuah operasi tidak memiliki gradien yang terdefinisi dengan baik tetapi dapat terlibat dalam penghitungan gradien. Di sini Anda dapat menggunakan ops.NotDifferentiable untuk menyebarkan angka nol secara otomatis ke belakang.

Perhatikan bahwa pada saat fungsi gradien dipanggil, hanya grafik aliran data operasi yang tersedia, bukan data tensor itu sendiri. Jadi, semua komputasi harus dilakukan menggunakan operasi tensorflow lain, untuk dijalankan pada waktu eksekusi grafik.

Fungsi bentuk di C ++

TensorFlow API memiliki fitur yang disebut "inferensi bentuk" yang memberikan informasi tentang bentuk tensor tanpa harus menjalankan grafik. Inferensi bentuk didukung oleh "fungsi bentuk" yang terdaftar untuk setiap jenis operasi di deklarasi C ++ REGISTER_OP , dan menjalankan dua peran: menegaskan bahwa bentuk masukan kompatibel selama pembuatan grafik, dan menentukan bentuk untuk 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)); mendeklarasikan bahwa bentuk keluaran pertama harus disetel ke bentuk masukan pertama. Jika output dipilih oleh indeksnya seperti pada contoh di atas, parameter kedua dari 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 untuk 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) memiliki bentuk dengan tepat satu dimensi (atau jika bentuk masukan tidak diketahui, bentuk keluaran akan menjadi vektor dengan satu dimensi yang tidak diketahui).

Jika operasi Anda polimorfik dengan beberapa masukan , 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 panjang, 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 untuk informasi bentuk yang tidak lengkap untuk salah satu input. Metode Merge di InferenceContext memungkinkan pemanggil untuk menegaskan bahwa dua bentuk adalah sama, meskipun salah satu atau keduanya tidak memiliki informasi yang lengkap. Fungsi bentuk ditentukan untuk semua operasi TensorFlow inti dan memberikan banyak contoh penggunaan yang berbeda.

Kelas InferenceContext memiliki sejumlah fungsi yang dapat digunakan untuk menentukan manipulasi fungsi bentuk. Misalnya, Anda dapat memvalidasi bahwa dimensi tertentu memiliki nilai yang sangat spesifik menggunakan InferenceContext::Dim dan InferenceContext::WithValue ; Anda dapat menetapkan bahwa dimensi keluaran adalah jumlah / hasil kali dari dua dimensi masukan menggunakan InferenceContext::Add dan InferenceContext::Multiply . Lihat kelas InferenceContext untuk semua variasi manipulasi bentuk yang dapat Anda tentukan. Contoh berikut menetapkan bentuk keluaran pertama menjadi (n, 3), di mana 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 coba ringkas dalam merepresentasikan spesifikasi bentuk masukan dan keluaran dalam pengujian. Untuk saat ini, lihat komentar di sekitarnya dalam pengujian tersebut untuk mengetahui spesifikasi string bentuk).

Buat paket pip untuk operasi kustom Anda

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