Mevcut TensorFlow kitaplığı tarafından kapsanmayan bir operasyon oluşturmak istiyorsanız, önce operasyonu Python'da mevcut Python operasyonlarının 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 olabilir:
- 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ğil.
- Gelecekteki bir derleyicinin birleştirmeyi zor bulacağı ilkellerin bir bileşimini elle birleştirmek istiyorsunuz.
Örneğin, "MaxPool" operatörüne benzer "medyan havuzu" gibi bir şey uygulamak istediğinizi, ancak medyanları maksimum değerler yerine kayan pencereler üzerinden hesaplamak istediğinizi hayal edin. Bunu bir işlem bileşimi kullanarak yapmak mümkün olabilir (örneğin, ExtractImagePatches ve TopK kullanarak), ancak tek bir kaynaşmış 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 işlem eklemeyi seçmek önemlidir.
Özel operasyonunuzu dahil etmek için yapmanız gerekenler:
- Yeni operasyonu bir C++ dosyasına kaydedin. Operasyon kaydı, operasyonun uygulamasından bağımsız olan, operasyonun işlevselliği için bir arayüz (şartname) tanımlar. Örneğin, işlem kaydı işlemin adını ve işlemin giriş ve çıkışlarını tanımlar. Ayrıca tensör şekil çıkarımı için kullanılan şekil fonksiyonunu da tanımlar.
- İşlemi C++ ile uygulayın. Bir işlemin uygulanması çekirdek olarak bilinir ve 1. Adımda kaydettiğiniz belirtimin somut uygulamasıdır. Farklı giriş/çıkış türleri veya mimariler (örneğin CPU'lar, GPU'lar) için birden çok çekirdek olabilir.
- Bir Python sarmalayıcı oluşturun (isteğe bağlı). Bu sarmalayıcı, Python'da op'u oluşturmak için kullanılan genel API'dir. Doğrudan kullanılabilen veya eklenebilen op kaydından varsayılan bir sarmalayıcı oluşturulur.
- İşlem için gradyanları hesaplamak için bir fonksiyon yazın (isteğe bağlı).
- İşlemi test edin. Bunu genellikle kolaylık olması için 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 fonksiyonlarını ve onların 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 source dosyasını indirmiş olmalı ve onu oluşturabilmelidir.
op arayüzünü tanımlayın
Bir operasyonun arayüzünü, onu TensorFlow sistemine kaydederek tanımlarsınız. Kayıtta, operasyonunuzun adını, girdilerini (türler ve adlar) ve çıktılarını (türler ve adlar) ve ayrıca belge dizilerini ve operasyonun gerektirebileceği tüm özellikleri belirtirsiniz.
Bunun nasıl çalıştığını görmek için, int32
s tensörünü alan ve ilk öğe hariç tümü sıfıra ayarlanmış olarak tensörün bir kopyasını çıkaran 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 arayüzü 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, girdi olarak 32-bit tamsayılardan bir tensör to_zero
alır ve 32-bit tamsayılardan zeroed
bir tensör çıkışı verir. İşlem ayrıca çıkış tensörünün giriş tensörü ile aynı şekilde olmasını sağlamak için bir şekil işlevi kullanır. Örneğin, giriş [10, 20] şeklinde bir tensör ise, bu şekil işlevi çıkış şeklinin de [10, 20] olduğunu belirtir.
İşlem için çekirdeği uygulayın
Arabirimi tanımladıktan sonra, op'un 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
bağımsız değişkeni 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ıtta, bu çekirdeğin altında ç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.
ZeroOut operasyonu için bunu yapmak için, ZeroOut
zero_out.cc
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, operasyon içi iş parçacığı oluşturma için kullanılmak üzere yapılandırılmış iş parçacıkları arasında bir hesaplama işlevini parçalar (bkz. config.proto
içindeki intra_op_parallelism_threads).
GPU çekirdekleri
Bir GPU çekirdeği iki parça halinde uygulanır: OpKernel ve CUDA çekirdeği ve başlatma kodu.
Bazen OpKernel uygulaması, girdileri denetleme ve çıktıları ayırma 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 türünü tanımlayın.
- Çıktının gerçek hesaplamasını yapmak için, Hesaplama işlevi şablonlu bir işlev yapısını çağırır.
- Bu işlevin CPUDevice için uzmanlaşması aynı dosyada tanımlanır, ancak GPUDevice için uzmanlaşma 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
-yer tutucu12 l10n-yer// 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
op kitaplığını oluşturun
Sistem derleyicinizi kullanarak işlemi derleyin (TensorFlow ikili kurulumu)
zero_out.cc
sisteminizde mevcut olan g++
veya clang
gibi bir C++
derleyicisi ile derleyebilmelisiniz. İkili PIP paketi, op'unuzu derlemek için ihtiyaç duyduğunuz başlık dosyalarını ve kitaplığı sisteme özel konumlarda kurar. Bununla birlikte, TensorFlow python kitaplığı, başlık dizinini almak için get_include
işlevini sağlar ve get_lib
dizininin bağlantı kurulacak paylaşılan bir nesnesi vardır. İş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'
g++
ın kurulu olduğunu varsayarsak, operasyonunuzu dinamik bir kitaplıkta derlemek için kullanabileceğiniz komutların sırası 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ğı gereklidir.
gcc
sürümü>=5
ile ilgili not: gcc, sürüm5
bu yana yeni C++ ABI'yi kullanır. TensorFlow web sitesinde bulunan ikili pip paketleri, eskigcc4
kullanan gcc4 ile oluşturulmuştur. Op kitaplığınızıgcc>=5
ile derlerseniz, kitaplığı eski abi ile uyumlu hale getirmek için komut satırına-D_GLIBCXX_USE_CXX11_ABI=0
ekleyin.
Operasyonu bazel kullanarak derleyin (TensorFlow kaynak kurulumu)
Yüklü TensorFlow kaynaklarınız varsa, operasyonunuzu derlemek için TensorFlow'un derleme sisteminden yararlanabilirsiniz. tensorflow/core/user_ops
dizinine aşağıdaki Bazel derleme 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 derleme kuralına sahip 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 işlemi 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ü döndürür. Böylece, op'u 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 işleve bir snake_case adı verileceğini unutmayın ( PEP8 ile uyumlu olması için). Bu nedenle, işleminiz C++ dosyalarında ZeroOut
olarak adlandırılmışsa, python işlevi zero_out
olarak adlandırılır.
Op'u bir Python modülünden import
normal bir işlev olarak kullanılabilir kılmak için, bir Python kaynak dosyasında load_op_library
çağrısının aşağıdaki gibi yapılması 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
Operasyonunuzu başarıyla uyguladığınızı doğrulamanın iyi bir yolu, bunun için bir test yazmaktır. zero_out_op_test.py
dosyasını içerikle birlikte 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 inşa etmek için 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 uygulayın
- C++'da şekil fonksiyonları
Koşullu kontroller ve doğrulama
Yukarıdaki örnek, işlemin herhangi bir şekle sahip bir tensöre uygulandığını varsayıyordu. Ya sadece vektörlere uygulandıysa? 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 değilse, InvalidArgument
durumunu ayarlayarak döner. OP_REQUIRES
makrosu üç bağımsız değişken alır:
-
SetStatus()
yöntemi içinOpKernelContext
veyaOpKernelConstruction
işaretçisi olabilencontext
(bkz.tensorflow/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 için bkz.tensorflow/core/platform/status.h
. BirStatus
hem bir türü (genellikleInvalidArgument
, ancak türlerin listesine bakın) hem de bir mesajı vardır. Hata oluşturmaya yönelik işlevlertensorflow/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 istiyorsanız ve hata veriyorsa onu döndürün, OP_REQUIRES_OK
kullanın. Bu makroların her ikisi de hata durumunda işlevden döner.
operasyon kaydı
Özellikler
Ops, op bir grafiğe eklendiğinde değerleri belirlenen özniteliklere sahip olabilir. Bunlar op'u yapılandırmak için kullanılır ve değerlerine hem çekirdek uygulamasında hem de op kaydındaki giriş ve çıkış türlerinde erişilebilir. Girişler daha esnek olduğundan, mümkün olduğunda attr yerine giriş kullanmayı tercih edin. Bunun nedeni, özniteliklerin sabit olmaları ve grafik oluşturma zamanında tanımlanmaları gerektiğidir. Buna karşılık girdiler, değerleri dinamik olabilen Tensörlerdir; yani, girdiler her adımı değiştirebilir, bir besleme vb. kullanılarak ayarlanabilir. Öznitelikler, girdilerle yapılamayacak şeyler için kullanılır: imzayı etkileyen (giriş veya çıktıların sayısı veya türü) veya " 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 attr tanımlarsınız:
<name>: <attr-type-expr>
<name>
bir harfle başlar ve alfasayısal karakterlerden ve alt çizgilerden oluşabilir ve <attr-type-expr>
aşağıda açıklanan formun bir tür ifadesidir.
Örneğin, ZeroOut
operasyonunun yalnızca 0. öğe yerine kullanıcı tarafından belirlenen bir dizini korumasını istiyorsanız, operasyonu ş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ğuna dikkat edin.)
Ç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_);
}
özellik türleri
Bir attr'de aşağıdaki türler desteklenir:
-
string
: Herhangi bir bayt dizisi (UTF8 olması gerekmez). -
int
: İşaretli bir tam sayı. -
float
: Kayan noktalı bir sayı. -
bool
: Doğru veya yanlış. -
type
:DataType
(başvuru olmayan) değerlerinden biri. -
shape
: BirTensorShapeProto
. -
list(<type>)
: <type<type>
listesi, burada<type>
yukarıdaki türlerden biridir.list(list(<type>))
öğesinin geçersiz olduğunu unutmayın.
Kesin bir liste için ayrıca bkz: op_def_builder.cc:FinalizeAttr
.
Varsayılan değerler ve kısıtlamalar
Attr'lerin varsayılan değerleri olabilir ve bazı attr türlerinin kısıtlamaları olabilir. Kısıtlamalı 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 <type1>
veya <type2>
type2> değerlerinden biri olmalıdır, burada <type1>
ve <type2>
tf.DType
desteklenir. 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ı türtype
. -
realnumbertype
: Karmaşık türleri olmayannumbertype
türü gibi. -
quantizedtype
: Sayı türü gibi ama yalnızcanumbertype
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 attr 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 tiplerle birleştirilebilir. Aşağıdaki işlem, attr t
sayısal türlerden veya bool türünden herhangi biri 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, değeri < <n>
> değerinden büyük veya buna eşit olan bir int olmalıdır, burada <n>
bir doğal sayıdır. Örneğin, aşağıdaki op kaydı, attr a
öğesinin en az 2
değerinde olması gerektiğini belirtir:
REGISTER_OP("MinIntExample")
.Attr("a: int >= 2");
list(<type>) >= <n>
: Uzunluğu <n>
değerinden büyük veya eşit olan <type>
türünde bir liste. Örneğin, aşağıdaki op kaydı, attr a
öğesinin 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 attr için varsayılan bir değer ayarlamak için (oluşturulan kodda bunu 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, elde edilen GraphDef tanımının proto temsilinde kullanılacak olan şeydir.
Tüm türler için bir varsayılanın nasıl belirleneceğine ilişkin örnekler:
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, tür type
değerlerinin tf.DType
kullandığını unutmayın.
polimorfizm
tip polimorfizmi
Farklı türleri girdi olarak alabilen veya farklı çıktı türleri üretebilen işlemler için, bir girdide bir attr veya op kaydında çıktı türü belirtebilirsiniz. Tipik olarak, desteklenen her tür için bir OpKernel
kaydedersiniz.
Örneğin, ZeroOut
int32
s'ye ek olarak float
s üzerinde çalışmasını istiyorsanız, 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 girdi türünün float
veya int32
olması gerektiğini ve her ikisinin de T
türüne sahip olduğundan çıktısının aynı tür olacağını belirtir.
adlandırma
Girişler, çıkışlar ve attr'lere genellikle snake_case isimleri verilmelidir. Bunun tek istisnası, bir girdinin türü olarak veya bir çıktının türünde kullanılan attr'lerdir. Bu özellikler, 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
olarak ayarlanır (aslında DT_INT32
). Bu çıkarsanan niteliklere Büyük Harfli veya CamelCase adları verilir.
Bunu, çıktı türünü belirleyen attr türüne sahip 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 operasyona attr 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 istediniz, double
diyelim:
REGISTER_OP("ZeroOut")
.Attr("T: {float, double, int32}")
.Input("to_zero: T")
.Output("zeroed: T");
Yukarıdaki gibi fazlalık kodla başka bir OpKernel
yazmak yerine, bunun yerine genellikle bir C++ şablonu kullanabileceksiniz. Aşırı yük başına yine de 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 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, operasyonlar değişken sayıda tensör tüketebilir veya üretebilir.
Sonraki örnekte, attr T
bir tür listesi tutar ve hem in
hem de out
türü olarak kullanılır. Giriş ve çıkış, bu tür tensörlerin listeleridir (ve çıkıştaki tensörlerin sayısı ve türleri, her ikisi de T
türüne sahip olduğundan, girişle aynıdır).
REGISTER_OP("PolymorphicListExample")
.Attr("T: list(type)")
.Input("in: T")
.Output("out: T");
Ayrıca listede hangi türlerin belirtilebileceği konusunda kısıtlamalar koyabilirsiniz. Bu sonraki durumda, girdi bir float
ve double
tensör listesidir. Op, örneğin giriş türlerini (float, double, float)
kabul eder ve bu durumda çıktı 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ürden 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 tip polimorfik de yapılabilir. Sonraki örnekte, girdi aynı (ancak belirtilmemiş) tipte ( "T"
) tensörlerin ( "N"
uzunluğunda) bir listesidir ve çıktı, 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. İlgili attr üzerinde bir ">="
kısıtlaması kullanarak bu varsayılanı değiştirebilirsiniz. Bu sonraki örnekte, girdi, en az 2 int32
tensörünün bir listesidir:
REGISTER_OP("MinLengthIntListExample")
.Attr("N: int >= 2")
.Input("in: N * int32")
.Output("out: int32");
Aynı sözdizimi "list(type)"
özellikleriyle ç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 op kaydının birden çok girişi ve çıkışı olabilir:
REGISTER_OP("MultipleInsAndOuts")
.Input("y: int32")
.Input("z: float")
.Output("a: string")
.Output("b: int32");
Her girdi veya çıktı özelliği şu şekildedir:
<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 (örneğin,float
,int32
,string
). Bu, verilen tipte tek bir tensörü belirtir.Bkz.
tf.DType
.REGISTER_OP("BuiltInTypesExample") .Input("integers: int32") .Input("complex_numbers: complex64");
<attr-type>
, burada<attr-type>
, typetype
veyalist(type)
(olası bir tür kısıtlaması ile) olan 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");
list(type)
türünde bir özniteliğe başvurmak, bir dizi tensörü 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");
Her ikisi de
T
türünde olduğundan, çıkışout
tensörlerin sayısı ve türlerinin giriştekiin
aynı olduğuna dikkat edin.Aynı türe sahip bir 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
ile bir attr 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 türden tensör listesini kabul eder:
REGISTER_OP("SameTypeSequenceExample") .Attr("NumTensors: int") .Attr("T: type") .Input("in: NumTensors * T")
Bir tensöre başvuru için:
Ref(<type>)
, burada<type>
önceki türlerden biridir.
Bir girdi türünde kullanılan herhangi bir öznitelik çıkarılacaktır. Kural olarak, bu çıkarsanan nitelikler büyük adlar kullanır ( T
veya N
gibi). Aksi takdirde girişler, çıkışlar ve attr'ler, 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 bkz. tensorflow/core/framework/op_def_builder.h
.
geriye dönük uyumluluk
Güzel, özel bir operasyon yazdığınızı ve 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 istersiniz.
Genel olarak, mevcut, iade edilmiş özelliklerde yapılan değişiklikler geriye dönük uyumlu olmalıdır: bir işlemin belirtiminin değiştirilmesi, eski belirtimlerden oluşturulan önceki serileştirilmiş 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 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 için yeni tür attr'ye varsayılan bir değer vermelisiniz . Örneğin, işleminiz şöyle olsaydı:
REGISTER_OP("MyGeneralUnaryOp") .Input("in: float") .Output("out: float");
şunu 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üvenle bir kısıtlama yapabilirsiniz. Örneğin,
{int32, int64}
yerine{int32, int64, float}
veyatype
olarak değiştirebilirsiniz. Veya{"apple", "orange"}
yerine{"apple", "banana", "orange"}
veyastring
olarak değiştirebilirsiniz.Liste türü 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ş/çıkış ekleyebilirsiniz.
Operasyon adlarını projenize özgü bir şeyle önekleyerek, oluşturduğunuz tüm yeni operasyonları adlandırın. Bu, operasyonunuzun gelecekteki TensorFlow sürümlerinde yer alabilecek operasyonlarla çakışmasını önler.
Önceden planlamak! 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
içinde bulunabilir. Geriye dönük uyumlu bir işlemde yaptığınız değişikliği yapamıyorsanız, yeni semantik ile 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'si, muhtemelen sona yeni isteğe bağlı argümanlar eklemek dışında, eski imzayı koruyarak, elle yazılmış bir Python sarmalayıcısında dikkatli değişikliklerle uyumlu tutulabilir. Genellikle 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'ler uygulayabilir ve birini CPU için, diğerini GPU için kaydedebilirsiniz, tıpkı farklı türler için çekirdek kaydedebildiğiniz gibi. 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, bir dosyada _gpu.cu.cc
ile biten 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
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 kodu 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 şey, pad
GPU çekirdek sürümü kullanıldığında bile, CPU belleğinde "paddings"
girişine ihtiyaç duyar. 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ğ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 belirtilebileceğ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ı:
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 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 içinde kuruluysa -L /usr/local/cuda-8.0/lib64/
/usr/local/cuda-8.0
.
Gradyanı Python'da uygulayın
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ı (geri yayılım) kullanır. Yeni operasyonlar için otomatik farklılaşmanın çalışmasını sağlamak için, operasyonların çıktılarına göre degradeler verilen operasyon girişlerine göre degradeleri hesaplayan bir degrade işlevi kaydetmeniz gerekir.
Matematiksel olarak, eğer bir op \(y = f(x)\) hesaplarsa, kayıtlı gradyan op, \(\partial L/ \partial y\) göre kayıp \(L\) gradyanlarını zincir kuralı yoluyla \(x\) göre \(\partial L/ \partial x\)\(y\) 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 op 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 özellik hakkında bilgitf.Operation.get_attr
aracılığıyla bulunabilir.İşlemin birden fazla çıktısı varsa, gradyan işlevi
op
vegrads
alır; buradagrads
, her bir çıktıya göre gradyanların bir listesidir. Gradyan fonksiyonunun sonucu, her bir girdiye göre gradyanları temsil edenTensor
nesnelerinin bir listesi olmalıdır.Endeks olarak kullanılan tamsayı girdileri gibi bazı girdiler için iyi tanımlanmış bir degrade yoksa, karşılık gelen döndürülen degrade
None
olmalıdır. Örneğin, bir kayan nokta tensörüx
ve bir tamsayı indeksii
alan bir op için, gradyan işlevireturn [x_grad, None]
.Operasyon için anlamlı bir gradyan yoksa, genellikle herhangi bir gradyan kaydetmeniz gerekmeyecektir ve op'un gradyanına hiçbir zaman ihtiyaç duyulmadığı sürece, iyi olacaksınız. Bazı durumlarda, bir işlemin 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 işlevi çağrıldığında, tensör verilerinin kendisinin değil, yalnızca operasyonların 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.
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 op 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 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 bildirir. Çıktı, yukarıdaki örnekte olduğu gibi dizini tarafından seçilirse, 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.
Common_shape_fns.h içinde bulunabilen ve aşağıdaki gibi kullanılabilen shape_inference::UnchangedShape
gibi birçok işlem 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 sınırlayabilir. Vektör şekli kısıtlamalı ZeroOut
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)
şeklinin tam olarak tek boyutlu bir şekle sahip olduğunu doğrular (veya girdi şekli bilinmiyorsa, çıktı şekli tek bilinmeyen boyutlu bir vektör olacaktır).
İşleminiz birden çok girişe sahip polimorfikse, kontrol edilecek şekillerin sayısını belirlemek için InferenceContext
üyelerini ve şekillerin hepsinin uyumlu olduğunu doğrulamak için Merge
üyelerini kullanabilirsiniz (alternatif olarak, InferenceContext::GetAttr
ile uzunlukları gösteren erişim nitelikleri, bu, op'un ö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şebildiğinden, şekil fonksiyonları herhangi bir girdi için eksik şekil bilgisine karşı sağlam olmalıdır. InferenceContext
Merge
yöntemi, arayan kişinin, ikisinden biri veya her ikisi de tam bilgiye sahip olmasa bile, iki şeklin aynı olduğunu iddia etmesine olanak tanır. Şekil işlevleri, tüm temel TensorFlow işlemleri için tanımlanmıştır ve birçok farklı kullanım örneği sunar.
InferenceContext
sınıfı, şekil işlevi manipülasyonlarını tanımlamak için kullanılabilecek bir dizi işleve sahiptir. Örneğin, InferenceContext InferenceContext::Dim
ve InferenceContext::WithValue
kullanarak belirli bir boyutun çok özel bir değeri olduğunu doğrulayabilirsiniz; InferenceContext::Add
ve InferenceContext::Multiply
kullanarak bir çıktı boyutunun iki giriş boyutunun toplamı / ürünü olduğunu belirtebilirsiniz. Belirtebileceğiniz çeşitli şekil manipülasyonlarının 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 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 fonksiyonunuz varsa, çeşitli girdi şekli kombinasyonlarının beklenen çıktı şekli kombinasyonlarını ü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 özelliklerini temsil etmede kompakt olmaya çalışın. Şimdilik, şekil dizisi belirtimi hakkında bir fikir edinmek için bu testlerde çevreleyen yorumlara bakın).
Özel operasyonunuz için bir pip paketi oluşturun
Operasyonunuz için bir pip
paketi oluşturmak için tensorflow/özel operasyon örneğine bakın. Bu kılavuz, kaynaktan TensorFlow oluşturmak yerine TensorFlow pip paketinden özel operasyonların nasıl oluşturulacağını gösterir.