Mevcut TensorFlow kitaplığı tarafından kapsanmayan bir işlem oluşturmak istiyorsanız öncelikle işlemi Python'da mevcut Python işlemlerinin veya işlevlerinin bir bileşimi olarak yazmayı denemenizi öneririz. Bu mümkün değilse özel bir C++ işlemi oluşturabilirsiniz. Özel bir C++ işlemi oluşturmak istemenizin birkaç nedeni vardır:
- Operasyonunuzu mevcut operasyonların bir bileşimi olarak ifade etmek kolay ve mümkün değil.
- Operasyonunuzu mevcut ilkellerin bir bileşimi olarak ifade etmek verimli değil.
- Gelecekteki bir derleyicinin birleştirmede zorlanacağı bir ilkel kompozisyonunu elle birleştirmek istiyorsunuz.
Örneğin, "MaxPool" operatörüne benzer şekilde "medyan havuzu" gibi bir şey uygulamak, ancak medyanları maksimum değerler yerine kayan pencereler üzerinden hesaplamak istediğinizi düşünün. Bunu bir dizi işlem kullanarak yapmak mümkün olabilir (örneğin, ExtractImagePatches ve TopK kullanarak), ancak tek bir birleştirilmiş işlemde daha akıllı bir şey yapabileceğiniz yerel bir işlem kadar performans veya bellek açısından verimli olmayabilir. Her zaman olduğu gibi, genellikle öncelikle operatör kompozisyonunu kullanarak ne istediğinizi ifade etmeye çalışmak, ancak bunun zor veya verimsiz olduğu ortaya çıkarsa yeni bir işlem eklemeyi seçmek faydalı olacaktır.
Özel operasyonunuzu dahil etmek için şunları yapmanız gerekir:
- Yeni operasyonu bir C++ dosyasına kaydedin. Op kaydı, operasyonun işlevselliği için, operasyonun uygulanmasından bağımsız olan bir arayüz (şartname) tanımlar. Örneğin op kaydı, op'un adını ve op'un giriş ve çıkışlarını tanımlar. Ayrıca tensör şekli çıkarımı için kullanılan şekil fonksiyonunu da tanımlar.
- Op'u C++'da uygulayın. Bir op'un uygulanması çekirdek olarak bilinir ve Adım 1'de 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ısı oluşturun (isteğe bağlı). Bu sarmalayıcı, Python'da op'u oluşturmak için kullanılan genel API'dir. Operasyon kaydından, doğrudan kullanılabilen veya eklenebilen bir varsayılan sarmalayıcı oluşturulur.
- Op için gradyanları hesaplayacak bir fonksiyon yazın (isteğe bağlı).
- Operasyonu test edin. Kolaylık sağlamak için bunu genellikle Python'da yaparız, ancak işlemi C++'da da test edebilirsiniz. Degradeleri tanımlarsanız bunları Python
tf.test.compute_gradient_error
ile doğrulayabilirsiniz. Relu benzeri operatörlerin ileri işlevlerini ve bunların gradyanlarını test eden bir örnek olarakrelu_op_test.py
bakın.
Önkoşullar
- C++'a biraz aşinalık.
- TensorFlow ikili dosyasını kurmuş olmalı veya TensorFlow kaynağını indirmiş olmalı ve oluşturabilmelidir.
Operasyon arayüzünü tanımlayın
Bir operasyonun arayüzünü TensorFlow sistemine kaydederek tanımlarsınız. Kayıt sırasında, operasyonunuzun adını, girişlerini (türleri ve adları) ve çıktılarını (türleri ve adları), ayrıca belge dizilerini ve operasyonun gerektirebileceği tüm öznitelikleri 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 dışındaki tüm öğelerin sıfıra ayarlandığı bir işlem oluşturmak istediğinizi varsayalım. Bunu yapmak için zero_out.cc
adında bir dosya oluşturun. Ardından, operasyonunuzun arayüzünü 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, giriş olarak 32 bitlik tam sayıların bir tensörünü to_zero
alır ve 32 bitlik tam sayılardan oluşan zeroed
bir tensör çıkışı sağlar. Op ayrıca çıkış tensörünün giriş tensörüyle aynı şekilde olmasını sağlamak için bir şekil fonksiyonu kullanır. Örneğin, eğer girdi [10, 20] şeklinde bir tensör ise, bu şekil fonksiyonu çıktı şeklinin de [10, 20] olduğunu belirtir.
Operasyon için çekirdeği uygulayın
Arayüzü tanımladıktan sonra operasyonun 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 yararlı şeylere erişebileceğiniz OpKernelContext*
türünde bir context
argümanı sağlar.
Yukarıda oluşturduğunuz dosyaya çekirdeğinizi ekleyin. Çekirdek şuna benzeyebilir:
#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ı belirtirsiniz. Örneğin, CPU'lar için yapılmış bir çekirdeğiniz ve GPU'lar için ayrı bir çekirdeğiniz olabilir.
ZeroOut
işlemi için bunu yapmak üzere, aşağıdakini zero_out.cc
dosyasına 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
Shard işlevi kullanılabilir. Bu işlev, intra-op iş parçacığı için kullanılmak üzere yapılandırılmış iş parçacıkları boyunca bir hesaplama işlevini parçalar (bkz. config.proto
intra_op_parallelism_threads).
GPU çekirdekleri
GPU çekirdeği iki parça halinde uygulanır: OpKernel ve CUDA çekirdeği ve başlatma kodu.
Bazen OpKernel uygulaması, girişlerin incelenmesi ve çıkışların tahsis edilmesi gibi konularda CPU ve GPU çekirdeği arasında ortaktır. Bu durumda önerilen uygulama şu şekildedir:
- Cihazda şablonlanan OpKernel'i ve tensörün temel tipini tanımlayın.
- Çıktının gerçek hesaplamasını yapmak için Compute işlevi şablonlu bir işlev yapısını çağırır.
- Bu işlevin CPUDevice için 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
Operasyon kitaplığını oluşturun
Sistem derleyicinizi kullanarak operasyonu derleyin (TensorFlow ikili kurulumu)
zero_out.cc
sisteminizde bulunan g++
veya clang
gibi bir C++
derleyicisiyle derleyebilmelisiniz. İkili PIP paketi, operasyonunuzu sisteme özel konumlarda derlemek için ihtiyaç duyduğunuz başlık dosyalarını ve kitaplığı yükler. Ancak TensorFlow python kütüphanesi, 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 bu fonksiyonların bir Ubuntu makinesindeki çı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'
g++
kurulu olduğunu varsayarsak, operasyonunuzu dinamik bir kitaplıkta derlemek için kullanabileceğiniz komutların sırası aşağıda verilmiştir.
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ını oluştururken ek olarak "-unDefinitiondynamic_lookup" bayrağı gerekir.
gcc
sürümü>=5
hakkında not: gcc, sürüm5
beri yeni C++ ABI'yi kullanıyor. TensorFlow 2.8 ve önceki sürümler, 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.
Operasyonu bazel kullanarak derleyin (TensorFlow kaynak kurulumu)
TensorFlow kaynaklarınız kuruluysa operasyonunuzu derlemek için TensorFlow'un derleme sisteminden yararlanabilirsiniz. tensorflow/core/user_ops
dizinine aşağıdaki Bazel derleme kuralını içeren 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
derlemek 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 derleme kuralını içeren bir BUILD dosyasını tensorflow/core/user_ops
dizini içindeki yeni bir klasöre yerleştirin (örn. "example_gpu").
load("//tensorflow:tensorflow.bzl", "tf_custom_op_library")
tf_custom_op_library(
# kernel_example.cc kernel_example.cu.cc kernel_example.h
name = "kernel_example.so",
srcs = ["kernel_example.h", "kernel_example.cc"],
gpu_srcs = ["kernel_example.cu.cc", "kernel_example.h"],
)
kernel_example.so
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 op'u TensorFlow çerçevesine kaydetmek için tf.load_op_library
işlevini sağlar. load_op_library
op ve çekirdek için Python sarmalayıcılarını içeren bir Python modülünü döndürür. Böylece, operasyonu oluşturduktan sonra 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)
Oluşturulan fonksiyona bir yılan_durumu adı verileceğini unutmayın ( PEP8'e uymak için). Dolayısıyla, eğer operasyonunuz C++ dosyalarında ZeroOut
olarak adlandırılmışsa, python işlevine zero_out
adı verilecektir.
Op'un bir Python modülünden import
normal bir işlev olarak kullanılabilir olmasını sağlamak için, bir Python kaynak dosyasında load_op_library
çağrısının aşağıdaki gibi olması faydalı olabilir:
import tensorflow as tf
zero_out_module = tf.load_op_library('./zero_out.so')
zero_out = zero_out_module.zero_out
Operasyonun çalıştığını doğrulayın
Operasyonunuzu başarıyla uyguladığınızı doğrulamanın iyi bir yolu, bunun için bir test yazmaktır. İçeriğiyle 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
Operasyonunuza gelişmiş özellikler ekleyin
Artık temel (ve biraz kısıtlı) bir operasyon ve uygulamanın nasıl oluşturulacağını bildiğinize göre, operasyonunuza eklemek için genellikle ihtiyaç duyacağınız daha karmaşık şeylerden bazılarına bakacağız. Bu şunları içerir:
- Koşullu kontroller ve doğrulama
- İşlem kaydı
- GPU desteği
- Python'da degradeyi uygulama
- C++'da şekil fonksiyonları
Koşullu kontroller ve doğrulama
Yukarıdaki örnekte op'un herhangi bir şekle sahip bir tensöre uygulandığı varsayılmış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, girişin bir vektör olduğunu ileri sürer ve 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
(bkz.tensorflow/core/framework/op_kernel.h
). - Durum. Örneğin,
tensorflow/core/framework/tensor_shape.h
dosyasında 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ü (çoğunluklaInvalidArgument
, ancak türlerin listesine bakın) hem de bir mesajı vardır. Hata oluşturmaya yönelik işlevlertensorflow/core/platform/errors.h
dosyasında bulunabilir.
Alternatif olarak, bir işlevden döndürülen bir Status
nesnesinin hata olup olmadığını test etmek istiyorsanız ve öyleyse onu geri döndürün, OP_REQUIRES_OK
kullanın. Bu makroların her ikisi de hata durumunda işlevden geri döner.
İşlem kaydı
Öznitelikler
Op'lar, op bir grafiğe eklendiğinde değerleri ayarlanan attr'lere sahip olabilir. Bunlar op'u yapılandırmak için kullanılır ve değerlerine hem çekirdek uygulamasından hem de op kaydındaki giriş ve çıkış türlerinden erişilebilir. Girişler daha esnek olduğundan mümkün olduğunda attr yerine giriş kullanmayı tercih edin. Bunun nedeni özniteliklerin sabit olması ve grafik oluşturma sırasında tanımlanması gerekmesidir. Bunun tersine girdiler, değerleri dinamik olabilen Tensörlerdir; yani, girişler her adımı değiştirebilir, bir besleme vb. kullanılarak ayarlanabilir. Attr'ler, girişlerle yapılamayan şeyler için kullanılır: imzayı etkileyen (giriş veya çıkışların sayısı veya türü) veya ' adım adım değişmez.
Op'u kaydettiğinizde, formun bir özelliğini bekleyen Attr
yöntemini kullanarak adını ve türünü belirterek bir attr tanımlarsınız:
<name>: <attr-type-expr>
burada <name>
bir harfle başlar ve alfasayısal karakterlerden ve alt çizgilerden oluşabilir ve <attr-type-expr>
aşağıda açıklanan biçimde bir tür ifadesidir.
Örneğin, ZeroOut
işleminin yalnızca 0'ıncı öğe yerine kullanıcı tarafından belirtilen 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_;
};
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_);
}
Öznitelik türleri
Bir öznitelikte aşağıdaki türler desteklenir:
-
string
: Herhangi bir bayt dizisi (UTF8 olması gerekli değildir). -
int
: İmzalı bir tamsayı. -
float
: Kayan noktalı sayı. -
bool
: Doğru veya yanlış. -
type
:DataType
(ref olmayan) değerlerinden biri. -
shape
: BirTensorShapeProto
. -
list(<type>)
:<type>
listesi; burada<type>
yukarıdaki türlerden biridir.list(list(<type>))
öğesinin geçersiz olduğunu unutmayın.
Ayrıca bkz.: op_def_builder.cc:FinalizeAttr
kesin bir liste için.
Varsayılan değerler ve kısıtlamalar
Özniteliklerin varsayılan değerleri olabilir ve bazı öznitelik türlerinin kısıtlamaları olabilir. Kısıtlamalarla 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ı ( string
) ima edilir. Bu bir numaralandırmayı taklit eder:
REGISTER_OP("EnumExample")
.Attr("e: {'apple', 'orange'}");
{<type1>, <type2>}
: Değer type type
ve <type1>
veya <type2>
öğelerinden biri olmalıdır; burada <type1>
ve <type2>
tf.DType
desteklenir. Attr türünün type
olduğunu belirtmezsiniz. Bu {...}
içinde türlerin bir listesine sahip olduğunuzda 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ı olan türtype
. -
realnumbertype
: Karmaşık türleri olmayannumbertype
gibi. -
quantizedtype
:numbertype
gibi ancak yalnızca nicelenmiş sayı türleri.
Bunların izin verdiği belirli tür listeleri tensorflow/core/framework/types.h
dosyasındaki işlevler ( NumberTypes()
gibi) tarafından tanımlanır. Bu örnekte attr t
sayısal türlerden biri olmalıdır:
REGISTER_OP("NumberType")
.Attr("t: numbertype");
Bu operasyon için:
tf.number_type(t=tf.int32) # Valid
tf.number_type(t=tf.bool) # Invalid
Listeler diğer listeler ve tek tiplerle birleştirilebilir. Aşağıdaki op, attr t
sayısal türlerden herhangi biri veya bool türü olmasını sağlar:
REGISTER_OP("NumberOrBooleanType")
.Attr("t: {numbertype, bool}");
Bu operasyon 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, değeri <n>
den büyük veya ona eşit olan bir int olmalıdır; burada <n>
bir doğal sayıdır. Örneğin, aşağıdaki işlem kaydı, attr a
en az 2
değerinde bir değere sahip 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ünün listesi. Örneğin, aşağıdaki işlem kaydı, attr a
bir tür listesi olduğunu ( int32
veya float
) ve bunlardan en az 3 tanesinin olması gerektiğini belirtir:
REGISTER_OP("TypeListExample")
.Attr("a: list({int32, float}) >= 3");
Bir öznitelik için varsayılan bir değer ayarlamak (oluşturulan kodda bunu isteğe bağlı kılmak) için, sonuna aşağıdaki gibi = <default>
ekleyin:
REGISTER_OP("AttrDefaultExample")
.Attr("i: int = 0");
Ek olarak, hem kısıtlama hem de varsayılan değer belirtilebilir:
REGISTER_OP("AttrConstraintAndDefaultExample")
.Attr("i: int >= 1 = 1");
Varsayılan değerin desteklenen sözdizimi, elde edilen GraphDef tanımının proto gösteriminde kullanılacaktır.
Aşağıda tüm türler için bir varsayılanın nasıl belirleneceğine ilişkin örnekler 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 tip type
değerlerinin tf.DType
kullandığını unutmayın.
Polimorfizm
Tür polimorfizmi
Farklı türleri girdi olarak alabilen veya farklı çıktı türleri üretebilen op'lar için, op kaydındaki girdi veya çıktı türünde bir attr belirtebilirsiniz. Genellikle desteklenen her tür için bir OpKernel
kaydedersiniz.
Örneğin, ZeroOut
işleminin int32
ek olarak float
üzerinde de çalışmasını istiyorsanız, işlem 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şin türünün float
veya int32
olması gerektiğini ve her ikisinin de türü T
olduğundan çıkışının aynı türde olacağını belirtiyor.
Adlandırma
Girişlere, çıkışlara ve özniteliklere genellikle yılan_durumu adları verilmelidir. Bunun tek istisnası, girdi türü olarak veya çıktı türünde kullanılan özniteliklerdir. Bu öznitelikler, op grafiğe eklendiğinde çıkarılabilir ve bu nedenle op'un 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ü aktarılırsa, T
otomatik olarak int32
ayarlanır (yani aslında DT_INT32
). Çıkarılan bu özniteliklere Büyük Harfli veya CamelCase adları verilir.
Bunu, çıktı türünü belirleyen attr türüne sahip bir op ile 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ının, oluşturulan Python'da olduğu gibi çıktı türünü belirtmesi gerekir:
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`.
"""
Tür 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 op'a bir ö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, double
deyin:
REGISTER_OP("ZeroOut")
.Attr("T: {float, double, int32}")
.Input("to_zero: T")
.Output("zeroed: T");
Yukarıdaki gibi yedek kodla başka bir OpKernel
yazmak yerine, genellikle bunun yerine bir C++ şablonu kullanabileceksiniz. Aşırı yükleme başına hâlâ 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>);
Birkaç aşırı yüklemeniz varsa kaydı bir makroya yerleştirebilirsiniz.
#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 kaydettirdiğ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, op'lar değişken sayıda tensör tüketebilir veya üretebilir.
Bir sonraki örnekte, attr T
türlerin bir listesini tutar ve hem in
hem de out
türü olarak kullanılır. Giriş ve çıkış, bu türdeki tensörlerin listesidir (ve her ikisi de T
türüne sahip olduğundan, çıkıştaki tensörlerin sayısı ve türleri girişle 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, giriş, float
ve double
tensörlerin bir listesidir. Op, örneğin giriş türlerini (float, double, float)
kabul eder ve bu durumda çıkış türü 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
attr N
kullanır.
Bu aynı zamanda polimorfik tipte de yapılabilir. Bir sonraki örnekte, giriş aynı (ancak belirtilmemiş) tipteki ( "T"
"N"
olan) bir listesidir ve çıkış, eşleşen tipte 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. Bu varsayılanı , karşılık gelen attr üzerinde bir ">="
kısıtlaması kullanarak değiştirebilirsiniz. Bir sonraki örnekte giriş, en az 2 int32
tensörden oluşan bir listedir:
REGISTER_OP("MinLengthIntListExample")
.Attr("N: int >= 2")
.Input("in: N * int32")
.Output("out: int32");
Aynı sözdizimi "list(type)"
öznitelikleriyle çalışır:
REGISTER_OP("MinimumLengthPolymorphicListExample")
.Attr("T: list(type) >= 3")
.Input("in: T")
.Output("out: T");
Girişler ve çıkışlar
Yukarıdakileri özetlemek gerekirse, bir op kaydının birden fazla girişi ve çıkışı olabilir:
REGISTER_OP("MultipleInsAndOuts")
.Input("y: int32")
.Input("z: float")
.Output("a: string")
.Output("b: int32");
Her giriş veya çıkış spesifikasyonu şu biçimdedir:
<name>: <io-type-expr>
<name>
bir harfle başlar ve alfasayısal 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>
, türtype
veyalist(type)
(olası bir tür kısıtlamasıyla birlikte) sahip bir Özniteliğin 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");
list(type)
türünde bir özniteliğe referans vermek, bir tensör dizisini kabul etmenize olanak tanır.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");
Çıkış
out
tensörlerin sayısı ve türlerinin,in
tensörlerle aynı olduğuna dikkat edin, çünkü her ikisi deT
tipindedir.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
olabilir veya typetype
sahip bir özniteliğin adı olabilir. İlkinin bir örneği olarak, bu operasyonint32
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 her tür tensörün 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 anlaşılacaktır. Kural olarak, bu türetilen öznitelikler büyük adlar kullanır ( T
veya N
gibi). Aksi halde girişler, çıkışlar ve öznitelikler, işlev parametreleri gibi adlara sahiptir (örn. num_outputs
). Daha fazla ayrıntı için adlandırmayla ilgili önceki bölüme bakın.
Daha fazla ayrıntı için tensorflow/core/framework/op_def_builder.h
bakın.
Geriye dönük uyumluluk
Güzel, özel bir operasyon yazdığınızı ve bunu başkalarıyla paylaştığınızı varsayalım, böylece operasyonunuzu kullanan mutlu müşterileriniz olur. Ancak operasyonda bir şekilde değişiklik yapmak istiyorsunuz.
Genel olarak, mevcut, teslim edilen spesifikasyonlarda yapılan değişiklikler geriye dönük olarak uyumlu olmalıdır: bir operasyonun spesifikasyonunun değiştirilmesi, eski spesifikasyonlardan oluşturulan önceki serileştirilmiş GraphDef
protokolü arabelleklerini bozmamalıdır. GraphDef
uyumluluğunun detayları burada anlatılmıştır .
Geriye dönük uyumluluğu korumanın birkaç yolu vardır.
Bir işleme eklenen tüm yeni özniteliklerin tanımlanmış varsayılan değerleri olmalıdır ve bu varsayılan değerle op, orijinal davranışa sahip olmalıdır. Bir işlemi polimorfik olmayandan polimorfik olarak değiştirmek için, orijinal imzayı varsayılan olarak korumak amacıyla yeni attr türüne varsayılan bir değer vermelisiniz . Örneğin, eğer operasyonunuz:
REGISTER_OP("MyGeneralUnaryOp") .Input("in: float") .Output("out: float");
aşağıdakileri kullanarak geriye dönük uyumlu bir şekilde polimorfik hale getirebilirsiniz:
REGISTER_OP("MyGeneralUnaryOp") .Input("in: T") .Output("out: T") .Attr("T: numerictype = DT_FLOAT");
Bir attr üzerinde daha az kısıtlayıcı bir kısıtlamayı güvenli bir şekilde yapabilirsiniz. Örneğin,
{int32, int64}
tan{int32, int64, float}
a geçiş yapabilir veyatype
. Veya{"apple", "orange"}
yerine{"apple", "banana", "orange"}
veyastring
değiştirebilirsiniz.Liste türünün varsayılanı eski imzayla eşleştiği sürece tekli 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.
Oluşturduğunuz tüm yeni operasyonlara, operasyon adlarının önüne projenize özel bir şey ekleyerek ad alanı verin. Bu, operasyonunuzun TensorFlow'un gelecek sürümlerinde bulunabilecek herhangi bir operasyonla çarpışmasını önler.
İleriyi planlayın! Operasyonun gelecekteki kullanımlarını tahmin etmeye çalışın. Bazı imza değişiklikleri uyumlu bir şekilde yapılamaz (örneğin, aynı türden bir listeyi farklı türlerden oluşan 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 olarak uyumlu hale getiremiyorsanız, yeni anlambilime sahip yeni bir adla yeni bir işlem oluşturun.
Ayrıca, bu değişiklikler GraphDef
uyumluluğunu koruyabilirken, oluşturulan Python kodunun eski arayanlarla uyumlu olmayacak şekilde değişebileceğini de unutmayın. Python API'si, elle yazılmış bir Python sarmalayıcısındaki dikkatli değişikliklerle, eski imzayı koruyarak, muhtemelen sona yeni isteğe bağlı argümanlar ekleyerek 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ı çekirdekleri farklı türler için kaydedebildiğiniz gibi, birini CPU için, diğerini GPU için kaydedebilirsiniz. tensorflow/core/kernels/
dosyasında GPU desteğine sahip birkaç çekirdek örneği vardır. Bazı çekirdeklerin bir .cc
dosyasında bir CPU sürümü, _gpu.cu.cc
ile biten bir dosyada bir GPU sürümü ve bir .h
dosyasında ortak olarak paylaşılan bazı kodlar olduğuna dikkat edin.
Örneğin, tf.pad
tensorflow/core/kernels/pad_op.cc
dosyasında GPU çekirdeği dışında her şeye sahiptir. GPU çekirdeği tensorflow/core/kernels/pad_op_gpu.cu.cc
dosyasındadır ve paylaşılan kod tensorflow/core/kernels/pad_op.h
dosyasında 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 olanak tanır ve GPU uygulamasını yalnızca GPU derleyicisi tarafından derlenebilmesi için ayrı bir dosyaya koyar.
Unutulmaması gereken bir nokta, pad
GPU çekirdek sürümü kullanıldığında bile CPU belleğindeki "paddings"
girişine ihtiyaç duymasıdır. Girişlerin veya çıkış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ğini kullanan bir örnek için cuda_op_kernel.cu.cc adresine bakın. tf_custom_op_library
CUDA çekirdeklerini ( *.cu.cc
dosyaları) içeren kaynak dosyaların listesinin belirtilebileceği gpu_srcs
bağımsız değişkenini kabul eder. TensorFlow'un ikili kurulumuyla kullanmak için CUDA çekirdeklerinin NVIDIA'nın nvcc
derleyicisiyle 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 komut dizisi aşağıda verilmiştir:
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
Python'da her zamanki gibi tf.load_op_library
işlevi kullanılarak yüklenebilir.
CUDA kitaplıklarınız /usr/local/lib64
dizininde kurulu 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
dizininde kuruluysa -L /usr/local/cuda-8.0/lib64/
ekleyin.
Python'da degradeyi uygulama
Bir işlem grafiği verildiğinde TensorFlow, mevcut işlemlere göre gradyanları temsil eden yeni işlemler eklemek için otomatik farklılaşmayı (geriye yayılım) kullanır. Otomatik farklılaşmanın yeni operasyonlar için çalışmasını sağlamak amacıyla, operasyonların çıkışlarına göre degradeler verildiğinde, operasyonların girişlerine göre degradeleri hesaplayan bir degrade fonksiyonunu kaydetmeniz gerekir.
Matematiksel olarak, eğer bir operasyon hesaplanıyorsa \(y = f(x)\) kayıtlı degrade işlemi degradeleri dönüştürür \(\partial L/ \partial y\) kayıp \(L\) göre\(y\) degradelere \(\partial L/ \partial x\) göre \(x\) zincir kuralı aracılığıyla:
\[\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, dolayısıyla 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
Degrade işlevlerini tf.RegisterGradient
kaydetmeyle ilgili ayrıntılar:
Tek çıkışlı bir op için, gradyan işlevi bir
tf.Operation
,op
ve birtf.Tensor
grad
alacak veop.inputs[i]
,op.outputs[i]
vegrad
tensörlerinden yeni op'lar oluşturacaktır. Herhangi bir öznitelik hakkında bilgitf.Operation.get_attr
aracılığıyla bulunabilir.Op'un birden fazla çıkışı varsa, degrade işlevi
op
vegrads
alır; buradagrads
her çıktıya göre degradelerin bir listesidir. Degrade fonksiyonunun sonucu, her girişe göre degradeleri temsil edenTensor
nesnelerinin bir listesi olmalıdır.Dizin olarak kullanılan tamsayı girişleri gibi bazı girişler için iyi tanımlanmış bir degrade yoksa, karşılık gelen döndürülen degrade
None
olmalıdır. Örneğin, kayan nokta tensörüx
ve tamsayı indeksii
alan bir işlem için, gradyan işlevireturn [x_grad, None]
.Operasyon için anlamlı bir degrade yoksa, genellikle herhangi bir degrade kaydetmeniz gerekmez ve operasyonun degradesine hiçbir zaman ihtiyaç duyulmadığı sürece sorun olmaz. Bazı durumlarda, bir op'un iyi tanımlanmış bir gradyanı yoktur ancak gradyanın hesaplanmasında yer alabilir. Burada sıfırları otomatik olarak geriye doğru yaymak için
ops.NotDifferentiable
kullanabilirsiniz.
Gradyan fonksiyonu çağrıldığında, tensör verilerinin kendisinin değil, yalnızca ops'un veri akış grafiğinin mevcut olduğunu unutmayın. Bu nedenle, tüm hesaplamaların, grafik yürütme zamanında çalıştırılabilmesi için diğer tensorflow işlemleri kullanılarak gerçekleştirilmesi gerekir.
Kodu daha okunabilir, hata ayıklanabilir, bakımı kolay ve veri doğrulama yoluyla daha sağlam hale getirmek için bir işlem türü için özel degradeyi kaydederken tür ipuçları ekleyin. Örneğin, bir fonksiyonda parametre olarak bir op
alırken, degrade fonksiyonunun parametre türü olarak tf.Operation
alacağını belirtin.
C++'da şekil fonksiyonları
TensorFlow API, grafiği çalıştırmaya gerek kalmadan tensörlerin şekilleri hakkında bilgi sağlayan "şekil çıkarımı" adı verilen bir özelliğe sahiptir. Şekil çıkarımı, C++ REGISTER_OP
bildiriminde her bir işlem türü için kaydedilen "şekil işlevleri" tarafından desteklenir ve iki rol gerçekleştirir: grafik oluşturma sırasında girdilerin şekillerinin uyumlu olduğunu iddia etmek ve çıktılar için şekilleri belirlemek.
Şekil işlevleri, shape_inference::InferenceContext
sınıfındaki işlemler olarak tanımlanır. Örneğin, ZeroOut'un şekil fonksiyonunda:
.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 bildirir. Yukarıdaki örnekte olduğu gibi çıktı indeksine göre seçiliyorsa set_output
ikinci parametresi bir ShapeHandle
nesnesi olmalıdır. Varsayılan yapıcısıyla boş bir ShapeHandle
nesnesi oluşturabilirsiniz. idx
indeksli bir giriş için ShapeHandle
nesnesi c->input(idx)
ile elde edilebilir.
Birçok işlem için geçerli olan, common_shape_fns.h dosyasında bulunabilen ve aşağıdaki şekilde kullanılabilen shape_inference::UnchangedShape
gibi 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 fonksiyonu aynı zamanda bir girdinin şeklini de kısıtlayabilir. ZeroOut
vektör şekli kısıtlamasına sahip 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)
girdi şeklinin tam olarak tek boyutlu bir şekle sahip olduğunu doğrular (veya girdi şekli bilinmiyorsa, çıktı şekli, bilinmeyen boyutlu bir vektör olacaktır).
Operasyonunuz birden fazla girişle polimorfikse , kontrol edilecek şekil 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ı belirten niteliklere erişin, (op'un özniteliklerine 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ının herhangi bir girdi için eksik şekil bilgisine karşı dayanıklı olması gerekir. InferenceContext
Merge
yöntemi, çağıranın, iki şeklin biri veya her ikisi de tam bilgiye sahip olmasa bile iki şeklin aynı olduğunu iddia etmesine olanak tanır. Şekil fonksiyonları, tüm temel TensorFlow işlemleri için tanımlanmıştır ve birçok farklı kullanım örneği sağlar.
InferenceContext
sınıfı, şekil işlevi işlemlerini 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; InferenceContext::Add
ve InferenceContext::Multiply
kullanarak bir çıktı boyutunun iki giriş boyutunun toplamı / ürünü olduğunu belirtebilirsiniz. Belirleyebileceğiniz çeşitli şekil işlemlerinin tümü için InferenceContext
sınıfına bakın. Aşağıdaki örnek, ilk çıktının şeklini (n, 3) olarak ayarlar; burada ilk girdi, (n, ...) şeklindedir.
.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 fonksiyonunuz varsa, çeşitli girdi şekli birleşimlerinin beklenen çıktı şekli birleşimlerini ürettiğini doğrulamak için bir test eklemeyi düşünmelisiniz. Bu testlerin nasıl yazılacağına dair örnekleri bazı temel operasyon testlerimizde görebilirsiniz. ( INFER_OK
ve INFER_ERROR
sözdizimi biraz şifrelidir, ancak testlerde giriş ve çıkış şekli spesifikasyonlarını temsil etmede kompakt olmaya çalışın. Şimdilik, şekil dizisi spesifikasyonu hakkında fikir edinmek için bu testlerdeki çevredeki yorumlara bakın).
Özel operasyonunuz için bir pip paketi oluşturun
Operasyonunuz için bir pip
paketi oluşturmak için tensorflow/custom-op örneğine bakın. Bu kılavuz, TensorFlow'u kaynaktan oluşturmak yerine TensorFlow pip paketinden nasıl özel operasyonlar oluşturulacağını gösterir.