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:
- 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.
- 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).
- 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.
- Tulis fungsi untuk menghitung gradien untuk op (opsional).
- 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
. Lihatrelu_op_test.py
sebagai contoh yang menguji fungsi penerusan operator mirip Relu dan gradiennya.
Prasyarat
- Beberapa keakraban dengan C++.
- Harus sudah menginstal biner TensorFlow , atau harus sudah mendownload sumber TensorFlow , dan dapat membuatnya.
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:
- Tentukan templat OpKernel pada Perangkat dan tipe tensor primitif.
- Untuk melakukan penghitungan keluaran sebenarnya, fungsi Komputasi memanggil struktur fungsi templat.
- 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 versi5
. TensorFlow 2.8 dan versi sebelumnya dibuat dengangcc4
yang menggunakan ABI lama. Jika Anda menggunakan versi TensorFlow ini dan mencoba mengkompilasi perpustakaan operasi Anda dengangcc>=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
- Pendaftaran
- dukungan GPU
- Terapkan gradien dengan Python
- Fungsi bentuk di C++
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:
-
context
, yang dapat berupa penunjukOpKernelContext
atauOpKernelConstruction
(lihattensorflow/core/framework/op_kernel.h
), untuk metodeSetStatus()
-nya. - Kondisinya. Misalnya, ada fungsi untuk memvalidasi bentuk tensor di
tensorflow/core/framework/tensor_shape.h
- Kesalahan itu sendiri, yang diwakili oleh objek
Status
, lihattensorflow/core/platform/status.h
.Status
memiliki tipe (seringkaliInvalidArgument
, tetapi lihat daftar tipe) dan pesan. Fungsi untuk membuat kesalahan dapat ditemukan ditensorflow/core/platform/errors.h
.
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) dariDataType
. -
shape
:TensorShapeProto
. -
list(<type>)
: Daftar<type>
, di mana<type>
adalah salah satu tipe di atas. Perhatikan bahwalist(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
: Sepertinumbertype
tanpa tipe kompleks. -
quantizedtype
: Sepertinumbertype
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 (misalnyafloat
,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 dengantype
tipe ataulist(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 inputin
, karena keduanya bertipeT
.Untuk rangkaian tensor dengan tipe yang sama:
<number> * <type>
, dengan<number>
adalah nama Attr dengan tipeint
.<type>
dapat berupatf.DType
, atau nama attr dengan tipetype
. Sebagai contoh yang pertama, operasi ini menerima daftar tensorint32
: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.
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");
Anda dapat dengan aman membuat batasan pada attr menjadi tidak terlalu ketat. Misalnya, Anda dapat mengubah dari
{int32, int64}
menjadi{int32, int64, float}
atautype
. Atau Anda dapat mengubah dari{"apple", "orange"}
menjadi{"apple", "banana", "orange"}
ataustring
.Anda dapat mengubah input/output tunggal menjadi input/output daftar, selama default untuk tipe daftar cocok dengan tanda tangan lama.
Anda dapat menambahkan daftar input/output baru, jika defaultnya kosong.
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.
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
, dantf.Tensor
grad
serta membuat operasi baru dari tensorop.inputs[i]
,op.outputs[i]
, dangrad
. Informasi tentang attr apa pun dapat ditemukan melaluitf.Operation.get_attr
.Jika op memiliki beberapa keluaran, fungsi gradien akan mengambil
op
dangrads
, di managrads
adalah daftar gradien terhadap setiap keluaran. Hasil dari fungsi gradien harus berupa daftar objekTensor
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 pointx
dan indeks bilangan bulati
, fungsi gradien akanreturn [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.