Mevcut TensorFlow kitaplığının kapsamadığı bir işlem oluşturmak istiyorsanız, önce işlemi mevcut Python işlemlerinin veya işlevlerinin bir bileşimi olarak Python'da yazmayı denemenizi öneririz. Bu mümkün değilse, özel bir C++ işlemi oluşturabilirsiniz. Özel bir C++ işlemi oluşturmak isteyebileceğiniz birkaç neden vardır:
- Operasyonunuzu mevcut operasyonların bir bileşimi olarak ifade etmek kolay veya mümkün değil.
- İşleminizi mevcut ilkellerin bir bileşimi olarak ifade etmek verimli değildir.
- Gelecekteki bir derleyicinin kaynaştırmayı zor bulacağı bir ilkel bileşim bileşimini elle kaynaştırmak istiyorsunuz.
Örneğin, "MaxPool" operatörüne benzer bir "medyan havuzlama" gibi bir şey uygulamak istediğinizi, ancak maksimum değerler yerine kayan pencereler üzerinden medyanları hesapladığınızı hayal edin. Bunu bir işlemler bileşimi kullanarak yapmak mümkün olabilir (örneğin, ExtractImagePatches ve TopK kullanarak), ancak tek bir birleşik işlemde daha akıllıca bir şey yapabileceğiniz yerel bir işlem kadar performans veya bellek açısından verimli olmayabilir. Her zaman olduğu gibi, genellikle ilk önce operatör kompozisyonunu kullanarak ne istediğinizi ifade etmeye çalışmak, ancak bunun zor veya verimsiz olduğu ortaya çıkarsa yeni bir operasyon eklemeyi seçmek değerdir.
Özel operasyonunuzu dahil etmek için yapmanız gerekenler:
- Yeni işlemi bir C++ dosyasına kaydedin. İşlem kaydı, işlemin uygulanmasından bağımsız olan, işlemin işlevselliği için bir arabirim (özellik) tanımlar. Örneğin, op kaydı, op'un adını ve op'un girdi ve çıktılarını tanımlar. Ayrıca, tensör şekil çıkarımı için kullanılan şekil fonksiyonunu da tanımlar.
- İşlemi C++'da uygulayın. Bir işlemin uygulanması, çekirdek olarak bilinir ve 1. Adımda kaydettiğiniz spesifikasyonun somut uygulamasıdır. Farklı giriş/çıkış türleri veya mimarileri (örneğin, CPU'lar, GPU'lar) için birden fazla çekirdek olabilir.
- Bir Python sarmalayıcı oluşturun (isteğe bağlı). Bu sarmalayıcı, Python'da işlemi oluşturmak için kullanılan genel API'dir. İşlem kaydından, doğrudan kullanılabilen veya eklenebilen varsayılan bir sarıcı oluşturulur.
- İşlem için gradyanları hesaplamak üzere bir işlev yazın (isteğe bağlı).
- İşlemi test edin. Bunu genellikle kolaylık sağlamak için Python'da yaparız, ancak işlemi C++'da da test edebilirsiniz. Degradeler tanımlarsanız, bunları Python
tf.test.compute_gradient_error
ile doğrulayabilirsiniz. Relu benzeri işleçlerin ileri işlevlerini ve gradyanlarını test eden bir örnek olarakrelu_op_test.py
bakın.
Önkoşullar
- C++ ile biraz aşinalık.
- TensorFlow ikili dosyasını kurmuş olmalı veya TensorFlow kaynağını indirmiş ve onu oluşturabilmelidir.
İşlem arayüzünü tanımlayın
Bir operasyonun arayüzünü TensorFlow sistemine kaydederek tanımlarsınız. Kayıtta, operasyonunuzun adını, girdilerini (türleri ve adları) ve çıktılarını (türleri ve adları), ayrıca docstringleri ve operasyonun gerektirebileceği tüm nitelikleri belirtirsiniz.
Bunun nasıl çalıştığını görmek için, int32
s tensörünü alan ve tensörün bir kopyasını çıkaran, ilk öğe hariç tümü sıfıra ayarlanmış bir işlem oluşturmak istediğinizi varsayalım. Bunu yapmak için, zero_out.cc
adlı bir dosya oluşturun. Ardından, operasyonunuz için arabirimi tanımlayan REGISTER_OP
makrosuna bir çağrı ekleyin:
#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();
});
Bu ZeroOut
işlemi, 32-bit tamsayılardan to_zero
bir tensörü girdi olarak alır ve 32-bit tamsayıların zeroed
bir tensörünü çıkarır. İşlem, çıkış tensörünün giriş tensörü ile aynı şekilde olmasını sağlamak için bir şekil işlevi de kullanır. Örneğin, girdi [10, 20] şeklindeki bir tensör ise, bu şekil fonksiyonu çıktı şeklinin de [10, 20] olduğunu belirtir.
İşlem için çekirdeği uygulayın
Arayüzü tanımladıktan sonra, işlemin bir veya daha fazla uygulamasını sağlayın. Bu çekirdeklerden birini oluşturmak için OpKernel
genişleten ve Compute
yöntemini geçersiz kılan bir sınıf oluşturun. Compute
yöntemi, giriş ve çıkış tensörleri gibi faydalı şeylere erişebileceğiniz OpKernelContext*
türünde bir context
argümanı sağlar.
Çekirdeğinizi yukarıda oluşturduğunuz dosyaya ekleyin. Çekirdek şöyle görünebilir:
#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);
}
};
Çekirdeğinizi uyguladıktan sonra, onu TensorFlow sistemine kaydedersiniz. Kayıt sırasında, bu çekirdeğin çalışacağı farklı kısıtlamalar belirlersiniz. Örneğin, CPU'lar için yapılmış bir çekirdeğiniz ve GPU'lar için ayrı bir çekirdeğiniz olabilir.
Bunu ZeroOut
işlemi için yapmak üzere, zero_out.cc
dosyasına şunu ekleyin:
REGISTER_KERNEL_BUILDER(Name("ZeroOut").Device(DEVICE_CPU), ZeroOutOp);
Çok iş parçacıklı CPU çekirdekleri
Çok iş parçacıklı bir CPU çekirdeği yazmak için work_sharder.h
içindeki Shard işlevi kullanılabilir. Bu işlev, işlem içi iş parçacığı için kullanılmak üzere yapılandırılmış iş parçacıkları boyunca bir hesaplama işlevi parçalar (bkz. config.proto
içindeki intra_op_parallelism_threads).
GPU çekirdekleri
Bir GPU çekirdeği iki kısımda uygulanır: OpKernel ve CUDA çekirdeği ve başlatma kodu.
Bazen OpKernel uygulaması, girdilerin incelenmesi ve çıktıların tahsis edilmesi gibi bir CPU ve GPU çekirdeği arasında ortaktır. Bu durumda, önerilen bir uygulama şudur:
- Cihazda şablonlanmış OpKernel'i ve tensörün ilkel tipini tanımlayın.
- Çıktının asıl hesaplamasını yapmak için Compute işlevi şablonlu bir functor yapısını çağırır.
- CPUDevice için bu functor'ın uzmanlığı aynı dosyada tanımlanır, ancak GPUDevice için uzmanlık, CUDA derleyicisi ile derleneceği için bir .cu.cc dosyasında tanımlanır.
İşte örnek bir uygulama.
// 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
İşlem kitaplığını oluşturun
Sistem derleyicinizi kullanarak işlemi derleyin (TensorFlow ikili kurulum)
zero_out.cc
sisteminizde bulunan g++
veya clang
gibi bir C++
derleyicisi ile derleyebilmeniz gerekir. İkili PIP paketi, işlemlerinizi sisteme özel konumlarda derlemek için ihtiyaç duyduğunuz başlık dosyalarını ve kitaplığı kurar. Ancak, TensorFlow python kitaplığı, başlık dizinini almak için get_include
işlevini sağlar ve get_lib
dizini, bağlantı kurulacak paylaşılan bir nesneye sahiptir. İşte bir Ubuntu makinesinde bu işlevlerin çıktıları.
$ 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'
Bilgisayarınızda g++
kurulu olduğunu varsayarsak, operasyonunuzu dinamik bir kitaplıkta derlemek için kullanabileceğiniz komut dizisi aşağıdadır.
TF_CFLAGS=( $(python -c 'import tensorflow as tf; print(" ".join(tf.sysconfig.get_compile_flags()))') )
TF_LFLAGS=( $(python -c 'import tensorflow as tf; print(" ".join(tf.sysconfig.get_link_flags()))') )
g++ -std=c++14 -shared zero_out.cc -o zero_out.so -fPIC ${TF_CFLAGS[@]} ${TF_LFLAGS[@]} -O2
macOS'ta, .so
dosyası oluşturulurken "-undefined dynamic_lookup" ek bayrağı gerekir.
gcc
sürümü>=5
ile ilgili not: gcc, sürüm5
beri yeni C++ ABI'yi kullanır. TensorFlow 2.8 ve öncesi, eski ABI'yi kullanangcc4
ile oluşturulmuştur. TensorFlow'un bu sürümlerini kullanıyorsanız ve op kitaplığınızıgcc>=5
ile derlemeye çalışıyorsanız, kitaplığı eski ABI ile uyumlu hale getirmek için komut satırına-D_GLIBCXX_USE_CXX11_ABI=0
ekleyin. TensorFlow 2.9+ paketleri, varsayılan olarak daha yeni ABI ile uyumludur.
Bazel kullanarak işlemi derleyin (TensorFlow kaynak kurulumu)
Yüklü TensorFlow kaynaklarınız varsa, işlemlerinizi derlemek için TensorFlow'un oluşturma sisteminden yararlanabilirsiniz. tensorflow/core/user_ops
dizinine aşağıdaki Bazel oluşturma kuralına sahip bir BUILD dosyası yerleştirin.
load("//tensorflow:tensorflow.bzl", "tf_custom_op_library")
tf_custom_op_library(
name = "zero_out.so",
srcs = ["zero_out.cc"],
)
zero_out.so
oluşturmak için aşağıdaki komutu çalıştırın.
$ bazel build --config opt //tensorflow/core/user_ops:zero_out.so
Example
işlemi CUDA Çekirdeği ile derlemek için tf_custom_op_library
gpu_srcs
parametresini kullanmanız gerekir. Aşağıdaki Bazel oluşturma kuralına sahip bir BUILD dosyasını tensorflow/core/user_ops
dizini (örn. "example_gpu") içindeki yeni bir klasöre yerleştirin.
load("//tensorflow:tensorflow.bzl", "tf_custom_op_library")
tf_custom_op_library(
# kernel_example.cc kernel_example.cu.cc kernel_example.h
name = "kernel_example.so",
srcs = ["kernel_example.h", "kernel_example.cc"],
gpu_srcs = ["kernel_example.cu.cc", "kernel_example.h"],
)
kernel_example.so
oluşturmak için aşağıdaki komutu çalıştırın.
$ bazel build --config opt //tensorflow/core/user_ops/example_gpu:kernel_example.so
Python'da op'u kullanın
TensorFlow Python API, dinamik kitaplığı yüklemek ve işlemi TensorFlow çerçevesine kaydetmek için tf.load_op_library
işlevini sağlar. load_op_library
işlem ve çekirdek için Python sarmalayıcılarını içeren bir Python modülü döndürür. Böylece, işlemi bir kez oluşturduğunuzda, onu Python'dan çalıştırmak için aşağıdakileri yapabilirsiniz:
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)
Unutmayın, oluşturulan işleve bir snake_case adı verilecektir ( PEP8 ile uyumlu olması için). Bu nedenle, C++ dosyalarında işleminizin adı ZeroOut
ise, python işlevinin adı zero_out
olacaktır.
İşlemi bir Python modülünden import
normal bir işlev olarak kullanılabilir hale getirmek için, load_op_library
çağrısının bir Python kaynak dosyasında şu şekilde olması yararlı olabilir:
import tensorflow as tf
zero_out_module = tf.load_op_library('./zero_out.so')
zero_out = zero_out_module.zero_out
İşlemin çalıştığını doğrulayın
İşleminizi başarıyla uyguladığınızı doğrulamanın iyi bir yolu, bunun için bir test yazmaktır. Şu içerikle birlikte zero_out_op_test.py
dosyasını oluşturun:
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()
Ardından testinizi çalıştırın (tensorflow'un kurulu olduğunu varsayarak):
$ python zero_out_op_test.py
Operasyonunuzda gelişmiş özellikler oluşturun
Artık temel (ve biraz kısıtlı) bir operasyon ve uygulamayı nasıl oluşturacağınızı bildiğinize göre, operasyonunuzda genellikle ihtiyaç duyacağınız daha karmaşık şeylerden bazılarına bakacağız. Bu içerir:
- Koşullu kontroller ve doğrulama
- operasyon kaydı
- GPU desteği
- Gradyanı Python'da uygulama
- C++'da şekil fonksiyonları
Koşullu kontroller ve doğrulama
Yukarıdaki örnek, işlemin herhangi bir şekle sahip bir tensöre uygulandığını varsaymıştır. Ya sadece vektörlere uygulansaydı? Bu, yukarıdaki OpKernel uygulamasına bir kontrol eklemek anlamına gelir.
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."));
// ...
}
Bu, girdinin bir vektör olduğunu iddia eder ve öyle değilse InvalidArgument
durumunu ayarlayarak geri döner. OP_REQUIRES
makrosu üç bağımsız değişken alır:
-
SetStatus()
yöntemi için birOpKernelContext
veyaOpKernelConstruction
işaretçisi olabilencontext
(bkztensorflow/core/framework/op_kernel.h
). - Kondisyon. Örneğin,
tensorflow/core/framework/tensor_shape.h
bir tensörün şeklini doğrulamak için işlevler vardır. - Bir
Status
nesnesi tarafından temsil edilen hatanın kendisi, bkztensorflow/core/platform/status.h
. BirStatus
hem bir türü (genellikleInvalidArgument
, ancak türlerin listesine bakın) hem de bir mesajı vardır. Hata oluşturma işlevleritensorflow/core/platform/errors.h
içinde bulunabilir.
Alternatif olarak, bir işlevden döndürülen Status
nesnesinin bir hata olup olmadığını test etmek ve öyleyse onu döndürmek istiyorsanız, OP_REQUIRES_OK
kullanın. Bu makroların her ikisi de hata durumunda işlevden döner.
operasyon kaydı
Öznitelikler
İşlemler, bir grafiğe işlem eklendiğinde değerleri ayarlanan özniteliklere sahip olabilir. Bunlar, işlemi yapılandırmak için kullanılır ve değerlerine hem çekirdek uygulamasından hem de işlem kaydındaki giriş ve çıkış türlerinden erişilebilir. Girdiler daha esnek olduğundan, mümkün olduğunda bir öznitelik yerine bir girdi kullanmayı tercih edin. Bunun nedeni, özniteliklerin sabit olmasıdır ve grafik oluşturma zamanında tanımlanmalıdır. Bunun aksine girdiler, değerleri dinamik olabilen Tensörlerdir; yani, girişler her adımı değiştirebilir, bir besleme vb. t adımdan adıma değişir.
İşlemi kaydettiğinizde, formun bir özelliğini bekleyen Attr
yöntemini kullanarak adını ve türünü belirterek bir öznitelik tanımlarsınız:
<name>: <attr-type-expr>
burada <name>
bir harfle başlar ve alfanümerik karakterlerden ve alt çizgilerden oluşabilir ve <attr-type-expr>
aşağıda açıklanan formun bir tür ifadesidir.
Örneğin, ZeroOut
işleminin yalnızca 0'ıncı öğe yerine kullanıcı tanımlı bir dizini korumasını istiyorsanız, işlemi şu şekilde kaydedebilirsiniz:
REGISTER_OP("ZeroOut")
.Attr("preserve_index: int")
.Input("to_zero: int32")
.Output("zeroed: int32");
( Öznitelik türleri kümesinin, girişler ve çıkışlar için kullanılan tf.DType
farklı olduğunu unutmayın.)
Çekirdeğiniz daha sonra bu özniteliğe yapıcısında context
parametresi aracılığıyla erişebilir:
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_;
};
bu daha sonra Compute
yönteminde kullanılabilir:
void Compute(OpKernelContext* context) override {
// ...
// We're using saved attr to validate potentially dynamic input
// So we check that preserve_index is in range
OP_REQUIRES(context, preserve_index_ < input.dimension(0),
errors::InvalidArgument("preserve_index out of range"));
// Set all the elements of the output tensor to 0
const int N = input.size();
for (int i = 0; i < N; i++) {
output_flat(i) = 0;
}
// Preserve the requested input value
output_flat(preserve_index_) = input(preserve_index_);
}
Attr türleri
Bir öznitelikte aşağıdaki türler desteklenir:
-
string
: Herhangi bir bayt dizisi (UTF8 olması gerekli değildir). -
int
: İşaretli bir tamsayı. -
float
: Bir kayan noktalı sayı. -
bool
: Doğru veya yanlış. -
type
:DataType
öğesinin (başvuru olmayan) değerlerinden biri. -
shape
: BirTensorShapeProto
. -
list(<type>)
:<type>
listesi, burada<type>
yukarıdaki türlerden biridir.list(list(<type>))
geçersiz olduğuna dikkat edin.
Kesin bir liste için ayrıca bkz. op_def_builder.cc:FinalizeAttr
.
Varsayılan değerler ve kısıtlamalar
Öznitelikler varsayılan değerlere sahip olabilir ve bazı öznitelik türlerinin kısıtlamaları olabilir. Kısıtlı bir öznitelik tanımlamak için aşağıdaki <attr-type-expr>
s'yi kullanabilirsiniz:
{'<string1>', '<string2>'}
: Değer, <string1>
veya <string2>
değerine sahip bir dize olmalıdır. Bu sözdizimini kullandığınızda türün adı olan string
ima edilir. Bu bir numaralandırma öykünür:
REGISTER_OP("EnumExample")
.Attr("e: {'apple', 'orange'}");
{<type1>, <type2>}
: Değer type type
ve <type1>
veya <type2>
türünden biri olmalıdır, burada <type1>
ve <type2>
desteklenir tf.DType
. Attr türünün type
olduğunu belirtmezsiniz. Bu, {...}
içinde bir tür listeniz olduğunda ima edilir. Örneğin, bu durumda attr t
int32
, float
veya bool
olması gereken bir türdür:
REGISTER_OP("RestrictedTypeExample")
.Attr("t: {int32, float, bool}");
Yaygın tür kısıtlamaları için kısayollar vardır:
-
numbertype
: Sayısal (dize olmayan ve bool olmayan) türlerle sınırlıtype
. -
realnumbertype
: Karmaşık türleri olmayannumbertype
gibi. -
quantizedtype
:numbertype
gibi, ancak yalnızca nicelenmiş sayı türleri.
Bunlar tarafından izin verilen belirli tür listeleri tensorflow/core/framework/types.h
içindeki işlevler ( NumberTypes()
gibi) tarafından tanımlanır. Bu örnekte, öznitelik t
sayısal türlerden biri olmalıdır:
REGISTER_OP("NumberType")
.Attr("t: numbertype");
Bu işlem için:
tf.number_type(t=tf.int32) # Valid
tf.number_type(t=tf.bool) # Invalid
Listeler, diğer listeler ve tek türlerle birleştirilebilir. Aşağıdaki işlem, attr t
sayısal türlerden herhangi biri veya bool türü olmasına izin verir:
REGISTER_OP("NumberOrBooleanType")
.Attr("t: {numbertype, bool}");
Bu işlem için:
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>
: Değer, <n>
doğal sayı olmak üzere <n>
değerinden büyük veya ona eşit olan bir int olmalıdır. Örneğin, aşağıdaki işlem kaydı, özniteliğin a
değerinin en az 2
olması gerektiğini belirtir:
REGISTER_OP("MinIntExample")
.Attr("a: int >= 2");
list(<type>) >= <n>
: Uzunluğu <n>
den büyük veya ona eşit olan <type>
türünde bir liste. Örneğin, aşağıdaki op kaydı, a
özniteliğinin bir tür listesi ( int32
veya float
) olduğunu ve bunlardan en az 3 tane olması gerektiğini belirtir:
REGISTER_OP("TypeListExample")
.Attr("a: list({int32, float}) >= 3");
Bir öznitelik için varsayılan bir değer ayarlamak için (oluşturulan kodda isteğe bağlı hale getirir), aşağıdaki gibi sonuna = <default>
ekleyin:
REGISTER_OP("AttrDefaultExample")
.Attr("i: int = 0");
Ek olarak, hem bir kısıtlama hem de bir varsayılan değer belirtilebilir:
REGISTER_OP("AttrConstraintAndDefaultExample")
.Attr("i: int >= 1 = 1");
Varsayılan değerin desteklenen sözdizimi, ortaya çıkan GraphDef tanımının proto gösteriminde kullanılacak olandır.
Tüm türler için bir varsayılanın nasıl belirleneceğine ilişkin örnekler aşağıda verilmiştir:
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]");
Özellikle type type
değerlerinin tf.DType
kullandığına dikkat edin.
polimorfizm
Tip polimorfizmi
Farklı türleri girdi olarak alabilen veya farklı çıktı türleri üretebilen işlemler için, işlem kaydında bir girdi veya çıktı türünde bir öznitelik belirtebilirsiniz. Genellikle desteklenen her tür için bir OpKernel
kaydedersiniz.
Örneğin, ZeroOut
op'unun int32
s'ye ek olarak float
s üzerinde çalışmasını isterseniz, op kaydınız şöyle görünebilir:
REGISTER_OP("ZeroOut")
.Attr("T: {float, int32}")
.Input("to_zero: T")
.Output("zeroed: T");
İşlem kaydınız artık giriş türünün float
veya int32
olması gerektiğini ve her ikisinin de türü T
olduğundan çıktısının aynı tür olacağını belirtir.
Adlandırma
Girişler, çıkışlar ve özniteliklere genellikle yılan_durum adları verilmelidir. Tek istisna, bir girdi türü veya bir çıktı türü olarak kullanılan özniteliklerdir. Bu nitelikler, işlem grafiğe eklendiğinde çıkarılabilir ve bu nedenle işlemin işlevinde görünmez. Örneğin, ZeroOut'un bu son tanımı, şuna benzeyen bir Python işlevi üretecektir:
def zero_out(to_zero, name=None):
"""...
Args:
to_zero: A `Tensor`. Must be one of the following types:
`float32`, `int32`.
name: A name for the operation (optional).
Returns:
A `Tensor`. Has the same type as `to_zero`.
"""
to_zero
bir int32
tensörü iletilirse, T
otomatik olarak int32
ayarlanır (aslında DT_INT32
). Bu çıkarsanan özniteliklere Büyük Harf veya CamelCase adları verilir.
Bunu, çıktı türünü belirleyen bir tür özniteliği olan bir işlemle karşılaştırın:
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");
Bu durumda, kullanıcı, oluşturulan Python'da olduğu gibi çıktı türünü belirtmelidir:
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`.
"""
Tip polimorfizmi örneği
#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);
Geriye dönük uyumluluğu korumak için, mevcut bir işleme öznitelik eklerken varsayılan bir değer belirtmelisiniz:
REGISTER_OP("ZeroOut")
.Attr("T: {float, int32} = DT_INT32")
.Input("to_zero: T")
.Output("zeroed: T")
Diyelim ki daha fazla tür eklemek istiyorsunuz, diyelim ki double
:
REGISTER_OP("ZeroOut")
.Attr("T: {float, double, int32}")
.Input("to_zero: T")
.Output("zeroed: T");
Yukarıdaki gibi gereksiz kod içeren başka bir OpKernel
yazmak yerine, bunun yerine genellikle bir C++ şablonu kullanabileceksiniz. Her aşırı yük için hala bir çekirdek kaydınız ( REGISTER_KERNEL_BUILDER
çağrısı) olacaktır.
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>);
Birden fazla aşırı yüklemeniz varsa, kaydı bir makroya koyabilirsiniz.
#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
Çekirdeği kaydettiğiniz türlerin listesine bağlı olarak, tensorflow/core/framework/register_types.h
tarafından sağlanan bir makroyu kullanabilirsiniz:
#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
Girişleri ve çıkışları listeleyin
Farklı türleri kabul edebilmenin veya üretebilmenin yanı sıra, operasyonlar değişken sayıda tensör tüketebilir veya üretebilir.
Bir sonraki örnekte, öznitelik T
, türlerin bir listesini tutar ve hem in
hem de out
türü olarak kullanılır. Girdi ve çıktı, o türdeki tensörlerin listeleridir (ve çıktıdaki tensörlerin sayısı ve türleri, her ikisi de T
türüne sahip olduğundan, girdiyle aynıdır).
REGISTER_OP("PolymorphicListExample")
.Attr("T: list(type)")
.Input("in: T")
.Output("out: T");
Listede hangi türlerin belirtilebileceğine de kısıtlamalar getirebilirsiniz. Bu sonraki durumda, girdi bir float
ve double
tensör listesidir. İşlem, örneğin giriş tiplerini (float, double, float)
kabul eder ve bu durumda çıktı tipi de (float, double, float)
olur.
REGISTER_OP("ListTypeRestrictionExample")
.Attr("T: list({float, double})")
.Input("in: T")
.Output("out: T");
Bir listedeki tüm tensörlerin aynı türde olmasını istiyorsanız, şöyle bir şey yapabilirsiniz:
REGISTER_OP("IntListInputExample")
.Attr("N: int")
.Input("in: N * int32")
.Output("out: int32");
Bu, int32
tensörlerinin bir listesini kabul eder ve listenin uzunluğunu belirtmek için bir int
öznitelik N
kullanır.
Bu tip polimorfik de yapılabilir. Bir sonraki örnekte girdi, aynı (ancak belirtilmemiş) türdeki ( "T"
) tensörlerin ( "N"
uzunluğunda) bir listesidir ve çıktı, eşleşen türde tek bir tensördür:
REGISTER_OP("SameListInputExample")
.Attr("N: int")
.Attr("T: type")
.Input("in: N * T")
.Output("out: T");
Varsayılan olarak, tensör listelerinin minimum uzunluğu 1'dir . İlgili öznitelikte bir ">="
kısıtlaması kullanarak bu varsayılanı değiştirebilirsiniz. Sonraki örnekte, girdi en az 2 int32
tensör listesidir:
REGISTER_OP("MinLengthIntListExample")
.Attr("N: int >= 2")
.Input("in: N * int32")
.Output("out: int32");
Aynı sözdizimi "list(type)"
öznitelikleri ile çalışır:
REGISTER_OP("MinimumLengthPolymorphicListExample")
.Attr("T: list(type) >= 3")
.Input("in: T")
.Output("out: T");
Girdiler ve çıktılar
Yukarıdakileri özetlemek gerekirse, bir işlem kaydının birden çok girdisi ve çıktısı olabilir:
REGISTER_OP("MultipleInsAndOuts")
.Input("y: int32")
.Input("z: float")
.Output("a: string")
.Output("b: int32");
Her giriş veya çıkış özelliği şu şekildedir:
<name>: <io-type-expr>
burada <name>
bir harfle başlar ve alfanümerik karakterlerden ve alt çizgilerden oluşabilir. <io-type-expr>
, aşağıdaki tür ifadelerinden biridir:
<type>
, burada<type>
desteklenen bir giriş türüdür (örn.float
,int32
,string
). Bu, verilen türden tek bir tensörü belirtir.tf.DType
bakın.REGISTER_OP("BuiltInTypesExample") .Input("integers: int32") .Input("complex_numbers: complex64");
<attr-type>
, burada<attr-type>
,type
veyalist(type)
(olası bir tür kısıtlamasıyla) içeren bir Attr'nin adıdır. Bu sözdizimi polimorfik işlemlere izin verir.REGISTER_OP("PolymorphicSingleInput") .Attr("T: type") .Input("in: T"); REGISTER_OP("RestrictedPolymorphicSingleInput") .Attr("T: {int32, int64}") .Input("in: T");
Bir tür
list(type)
özniteliğine başvurmak, bir tensör dizisini kabul etmenize izin verir.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");
Her ikisi de
T
tipinde olduğundan, outputout
tensörlerin sayısı ve tiplerinin inin
aynı olduğuna dikkat edin.Aynı türdeki tensör dizisi için:
<number> * <type>
, burada<number>
int
türündeki bir Attr'nin adıdır.<type>
, birtf.DType
veya typetype
bir özniteliğin adı olabilir. İlkinin bir örneği olarak, bu işlemint32
tensörlerinin bir listesini kabul eder:REGISTER_OP("Int32SequenceExample") .Attr("NumTensors: int") .Input("in: NumTensors * int32")
Oysa bu işlem, hepsi aynı olduğu sürece herhangi bir tensör listesini kabul eder:
REGISTER_OP("SameTypeSequenceExample") .Attr("NumTensors: int") .Attr("T: type") .Input("in: NumTensors * T")
Bir tensöre referans için:
Ref(<type>)
, burada<type>
önceki türlerden biridir.
Bir giriş türünde kullanılan herhangi bir öznitelik çıkarsanacaktır. Geleneksel olarak, bu türetilmiş öznitelikler büyük adlar kullanır ( T
veya N
gibi). Aksi takdirde girdiler, çıktılar ve öznitelikler işlev parametreleri gibi adlara sahiptir (örn. num_outputs
). Daha fazla ayrıntı için, adlandırma ile ilgili önceki bölüme bakın.
Daha fazla ayrıntı için bkz. tensorflow/core/framework/op_def_builder.h
.
Geriye dönük uyumluluk
Diyelim ki güzel, özel bir operasyon yazdınız ve başkalarıyla paylaştınız, böylece operasyonunuzu kullanan mutlu müşterileriniz oldu. Ancak, operasyonda bir şekilde değişiklik yapmak istiyorsunuz.
Genel olarak, mevcut, teslim edilmiş özelliklerde yapılan değişiklikler geriye dönük olarak uyumlu olmalıdır: bir işlemin belirtiminin değiştirilmesi, daha eski özelliklerden oluşturulan önceki seri hale getirilmiş GraphDef
protokolü arabelleklerini bozmamalıdır. GraphDef
uyumluluğunun ayrıntıları burada açıklanmıştır .
Geriye dönük uyumluluğu korumanın birkaç yolu vardır.
Bir işleme eklenen herhangi bir yeni öznitelik tanımlanmış varsayılan değerlere sahip olmalıdır ve bu varsayılan değerle, işlem orijinal davranışa sahip olmalıdır. Polimorfik olmayan bir işlemi polimorfik olarak değiştirmek için, orijinal imzayı varsayılan olarak korumak için yeni attr tipine varsayılan bir değer vermelisiniz . Örneğin, işleminiz şuysa:
REGISTER_OP("MyGeneralUnaryOp") .Input("in: float") .Output("out: float");
aşağıdakileri kullanarak geriye dönük uyumlu bir şekilde polimorfik yapabilirsiniz:
REGISTER_OP("MyGeneralUnaryOp") .Input("in: T") .Output("out: T") .Attr("T: numerictype = DT_FLOAT");
Daha az kısıtlayıcı bir öznitelik üzerinde güvenli bir şekilde kısıtlama yapabilirsiniz. Örneğin,
{int32, int64}
ı{int32, int64, float}
olarak değiştirebilir veyatype
değiştirebilirsiniz. Veya{"apple", "orange"}
dan{"apple", "banana", "orange"}
veyastring
değiştirebilirsiniz.Liste tipi için varsayılan eski imzayla eşleştiği sürece, tek girişleri / çıkışları liste girişleri / çıkışları olarak değiştirebilirsiniz.
Varsayılan olarak boşsa, yeni bir liste girişi / çıkışı ekleyebilirsiniz.
İşlem adlarının önüne projenize özel bir şey ekleyerek, oluşturduğunuz tüm yeni işlemleri ad alanı yapın. Bu, operasyonunuzun TensorFlow'un gelecekteki sürümlerinde yer alabilecek herhangi bir operasyonla çakışmasını önler.
Önceden planlamak! Operasyon için gelecekteki kullanımları tahmin etmeye çalışın. Bazı imza değişiklikleri uyumlu bir şekilde yapılamaz (örneğin, aynı türdeki bir listeyi farklı türlerdeki bir listeye dönüştürmek).
Güvenli ve güvenli olmayan değişikliklerin tam listesi tensorflow/core/framework/op_compatibility_test.cc
adresinde bulunabilir. Bir işlemde yaptığınız değişikliği geriye dönük uyumlu hale getiremezseniz, yeni semantikle yeni bir adla yeni bir işlem oluşturun.
Ayrıca, bu değişikliklerin GraphDef
uyumluluğunu koruyabilmesine rağmen, oluşturulan Python kodunun eski arayanlarla uyumlu olmayan bir şekilde değişebileceğini unutmayın. Python API, muhtemelen sona yeni isteğe bağlı bağımsız değişkenler eklemek dışında eski imzayı koruyarak, elle yazılmış bir Python sarmalayıcısında dikkatli değişiklikler yapılarak uyumlu tutulabilir. Genel olarak uyumsuz değişiklikler, yalnızca TensorFlow ana sürümleri değiştirdiğinde yapılabilir ve GraphDef
sürüm semantiğine uygun olmalıdır.
GPU desteği
Farklı OpKernel'leri uygulayabilir ve tıpkı farklı türler için çekirdekleri kaydedebildiğiniz gibi CPU ve diğerini GPU için kaydedebilirsiniz. tensorflow/core/kernels/
içinde GPU destekli birkaç çekirdek örneği vardır. Bazı çekirdeklerin bir .cc
dosyasında bir CPU sürümüne, _gpu.cu.cc
ile biten bir dosyada bir GPU sürümüne ve bir .h
dosyasında ortak olarak paylaşılan bazı kodlara sahip olduğuna dikkat edin.
Örneğin, tf.pad
, tensorflow/core/kernels/pad_op.cc
içindeki GPU çekirdeği dışında her şeye sahiptir. GPU çekirdeği tensorflow/core/kernels/pad_op_gpu.cu.cc
içindedir ve paylaşılan kod tensorflow/core/kernels/pad_op.h
içinde tanımlanan şablonlu bir sınıftır. Kodu iki nedenden dolayı bu şekilde düzenliyoruz: CPU ve GPU uygulamaları arasında ortak kod paylaşmanıza izin veriyor ve GPU uygulamasını yalnızca GPU derleyicisi tarafından derlenebilmesi için ayrı bir dosyaya koyuyor.
Unutulmaması gereken bir nokta, pad
GPU çekirdek sürümü kullanıldığında bile, yine de CPU belleğinde "paddings"
girdisine ihtiyaç duyar. Girdilerin veya çıktıların CPU'da tutulduğunu işaretlemek için, çekirdek kaydına bir HostMemory()
çağrısı ekleyin, örneğin:
#define REGISTER_GPU_KERNEL(T) \
REGISTER_KERNEL_BUILDER(Name("Pad") \
.Device(DEVICE_GPU) \
.TypeConstraint<T>("T") \
.HostMemory("paddings"), \
PadOp<GPUDevice, T>)
GPU aygıtı için çekirdeğin derlenmesi
Bir işlemi uygulamak için CUDA çekirdeği kullanan bir örnek için cuda_op_kernel.cu.cc'ye bakın. tf_custom_op_library
, CUDA çekirdeklerini ( *.cu.cc
dosyaları) içeren kaynak dosyaların listesinin belirtilebildiği bir gpu_srcs
bağımsız değişkenini kabul eder. TensorFlow'un ikili kurulumuyla kullanım için, CUDA çekirdeklerinin NVIDIA'nın nvcc
derleyicisi ile derlenmesi gerekir. cuda_op_kernel.cu.cc ve cuda_op_kernel.cc'yi dinamik olarak yüklenebilir tek bir kitaplıkta derlemek için kullanabileceğiniz komutların sırası şöyledir:
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[@]}
Yukarıda üretilen cuda_op_kernel.so
, tf.load_op_library
işlevi kullanılarak Python'da her zamanki gibi yüklenebilir.
CUDA kitaplıklarınız /usr/local/lib64
içinde yüklü değilse, yukarıdaki ikinci (g++) komutta yolu açıkça belirtmeniz gerekeceğini unutmayın. Örneğin, CUDA'nız /usr/local/cuda-8.0
içinde kuruluysa -L /usr/local/cuda-8.0/lib64/
ekleyin.
Gradyanı Python'da uygulama
Bir operasyon grafiği verildiğinde, TensorFlow, mevcut operasyonlara göre gradyanları temsil eden yeni operasyonlar eklemek için otomatik farklılaşmayı (geriye yayılım) kullanır. Otomatik farklılaştırmanın yeni operasyonlarda çalışmasını sağlamak için, operasyonların çıktılarına göre gradyanlar verildiğinde operasyonların girişlerine göre gradyanları hesaplayan bir gradyan fonksiyonunu kaydetmeniz gerekir.
Matematiksel olarak, bir op \(y = f(x)\) hesaplarsa kayıtlı gradyan op, l10n-yer tutucu4'e göre \(\partial L/ \partial y\) \(L\) kayıp\(y\) gradyanlarını zincir kuralı aracılığıyla \(\partial L/ \partial x\) göre \(x\) gradyanlarına dönüştürür:
\[\frac{\partial L}{\partial x} = \frac{\partial L}{\partial y} \frac{\partial y}{\partial x} = \frac{\partial L}{\partial y} \frac{\partial f}{\partial x}.\]
ZeroOut
durumunda, girişteki yalnızca bir giriş çıkışı etkiler, bu nedenle girişe göre gradyan seyrek bir "tek sıcak" tensördür. Bu şu şekilde ifade edilir:
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
Gradyan işlevlerini tf.RegisterGradient
ile kaydetmeyle ilgili ayrıntılar:
Tek çıkışlı bir işlem için, gradyan işlevi bir
tf.Operation
,op
ve birtf.Tensor
grad
alır veop.inputs[i]
,op.outputs[i]
vegrad
tensörlerinden yeni işlemler oluşturur. Herhangi bir öznitelik hakkında bilgitf.Operation.get_attr
aracılığıyla bulunabilir.İşlemin birden çok çıktısı varsa, gradyan işlevi
op
vegrads
alır; buradagrads
, her çıktıya göre degradelerin bir listesidir. Gradyan fonksiyonunun sonucu, her girişe göre gradyanları temsil edenTensor
nesnelerinin bir listesi olmalıdır.İndeks olarak kullanılan tamsayı girişleri gibi bazı girdiler için iyi tanımlanmış bir gradyan yoksa, karşılık gelen döndürülen gradyan
None
olmalıdır. Örneğin,x
kayan nokta tensörü vei
tamsayı dizini alan bir işlem için, gradyan işlevireturn [x_grad, None]
.İşlem için anlamlı bir gradyan yoksa, genellikle herhangi bir gradyan kaydetmeniz gerekmez ve operasyonun gradyanına hiçbir zaman ihtiyaç duyulmadığı sürece sorun olmaz. Bazı durumlarda, bir işlemin iyi tanımlanmış bir gradyanı yoktur ancak gradyanın hesaplanmasına dahil edilebilir. Burada sıfırları otomatik olarak geriye doğru yaymak için
ops.NotDifferentiable
kullanabilirsiniz.
Gradyan işlevi çağrıldığında, tensör verilerinin kendisinin değil, yalnızca işlemlerin veri akış grafiğinin mevcut olduğunu unutmayın. Bu nedenle, tüm hesaplamalar, grafik yürütme zamanında çalıştırılmak üzere diğer tensorflow işlemleri kullanılarak gerçekleştirilmelidir.
Veri doğrulama yoluyla kodu daha okunabilir, hata ayıklanabilir, bakımı kolay ve daha sağlam hale getirmek için bir işlem türü için özel gradyanı kaydederken tür ipuçları ekleyin. Örneğin, bir işlevde parametre olarak bir op
alırken, gradyan işlevinin parametre türü olarak bir tf.Operation
alacağını belirtin.
C++'da şekil fonksiyonları
TensorFlow API, grafiği yürütmek zorunda kalmadan tensörlerin şekilleri hakkında bilgi sağlayan "şekil çıkarımı" adlı bir özelliğe sahiptir. Şekil çıkarımı, C++ REGISTER_OP
bildiriminde her işlem türü için kaydedilen ve iki rolü yerine getiren "şekil işlevleri" tarafından desteklenir: grafik oluşturma sırasında girdilerin şekillerinin uyumlu olduğunu iddia etmek ve çıktılar için şekilleri belirtmek.
Şekil işlevleri, shape_inference::InferenceContext
sınıfındaki işlemler olarak tanımlanır. Örneğin, ZeroOut için şekil işlevinde:
.SetShapeFn([](::tensorflow::shape_inference::InferenceContext* c) {
c->set_output(0, c->input(0));
return Status::OK();
});
c->set_output(0, c->input(0));
ilk çıktının şeklinin ilk girdinin şekline ayarlanması gerektiğini beyan eder. Çıktı, yukarıdaki örnekte olduğu gibi dizinine göre seçilirse, set_output
ikinci parametresi bir ShapeHandle
nesnesi olmalıdır. Varsayılan yapıcısı ile boş bir ShapeHandle
nesnesi oluşturabilirsiniz. idx
dizinli bir girdi için ShapeHandle
nesnesi c->input(idx)
ile elde edilebilir.
Common_shape_fns.h'de bulunabilen ve aşağıdaki şekilde kullanılabilen shape_inference::UnchangedShape
gibi birçok operasyon için geçerli olan bir dizi ortak şekil işlevi vardır:
REGISTER_OP("ZeroOut")
.Input("to_zero: int32")
.Output("zeroed: int32")
.SetShapeFn(::tensorflow::shape_inference::UnchangedShape);
Bir şekil işlevi, bir girdinin şeklini de kısıtlayabilir. ZeroOut
vektör şekli kısıtlaması olan sürümü için şekil işlevi aşağıdaki gibi olacaktır:
.SetShapeFn([](::tensorflow::shape_inference::InferenceContext* c) {
::tensorflow::shape_inference::ShapeHandle input;
TF_RETURN_IF_ERROR(c->WithRank(c->input(0), 1, &input));
c->set_output(0, input);
return Status::OK();
});
WithRank
çağrısı c->input(0)
giriş şeklinin tam olarak tek boyutlu bir şekle sahip olduğunu doğrular (veya giriş şekli bilinmiyorsa, çıkış şekli tek boyutu bilinmeyen bir vektör olacaktır).
İşleminiz birden çok girişle çok biçimliyse , kontrol edilecek şekillerin sayısını belirlemek için InferenceContext
üyelerini ve şekillerin tümünün uyumlu olduğunu doğrulamak için Merge
kullanabilirsiniz (alternatif olarak, InferenceContext::GetAttr
ile uzunlukları gösteren özniteliklere erişin, operasyonun özelliklerine erişim sağlar).
.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();
});
Şekil çıkarımı isteğe bağlı bir özellik olduğundan ve tensörlerin şekilleri dinamik olarak değişebileceğinden, şekil fonksiyonları, girdilerin herhangi biri için eksik şekil bilgilerine karşı dayanıklı olmalıdır. InferenceContext
Merge
yöntemi, arayanın iki şeklin aynı olduğunu iddia etmesine izin verir, bunlardan biri veya her ikisi de tam bilgiye sahip olmasa bile. Şekil işlevleri, tüm temel TensorFlow işlemleri için tanımlanır ve birçok farklı kullanım örneği sağlar.
InferenceContext
sınıfı, şekil işlevi manipülasyonlarını tanımlamak için kullanılabilecek bir dizi işleve sahiptir. Örneğin, InferenceContext::Dim
ve InferenceContext::WithValue
kullanarak belirli bir boyutun çok özel bir değere sahip olduğunu doğrulayabilirsiniz; bir çıktı boyutunun, InferenceContext::Add
ve InferenceContext::Multiply
kullanarak iki girdi boyutunun toplamı / ürünü olduğunu belirtebilirsiniz. Belirleyebileceğiniz tüm çeşitli şekil işlemleri için InferenceContext
sınıfına bakın. Aşağıdaki örnek, ilk çıktının şeklini (n, 3) olarak ayarlar, burada ilk girdinin şekli (n, ...)
.SetShapeFn([](::tensorflow::shape_inference::InferenceContext* c) {
c->set_output(0, c->Matrix(c->Dim(c->input(0), 0), 3));
return Status::OK();
});
Karmaşık bir şekil işleviniz varsa, çeşitli girdi şekli kombinasyonlarının beklenen çıktı şekli kombinasyonlarını ürettiğini doğrulamak için bir test eklemeyi düşünmelisiniz. Bazı core ops testlerimizde bu testlerin nasıl yazılacağına dair örnekler görebilirsiniz. ( INFER_OK
ve INFER_ERROR
sözdizimi biraz gizemlidir, ancak testlerde girdi ve çıktı şekli belirtimlerini temsil ederken kompakt olmaya çalışın. Şimdilik, şekil dizisi belirtimi hakkında bir fikir edinmek için bu testlerdeki çevreleyen yorumlara bakın).
Özel operasyonunuz için bir pip paketi oluşturun
İşleminiz için bir pip
paketi oluşturmak üzere tensorflow/özel işlem örneğine bakın. Bu kılavuz, TensorFlow'u kaynaktan oluşturmak yerine TensorFlow pip paketinden özel operasyonların nasıl oluşturulacağını gösterir.