Operasyon oluştur

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 bileşimini 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:

  1. 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.
  2. Op'u C++'da uygulayın. Bir op'un uygulanması çekirdek olarak bilinir ve 1. Adımda kaydettiğiniz spesifikasyonun somut uygulamasıdır. Farklı giriş/çıkış türleri veya mimarileri (örneğin CPU'lar, GPU'lar) için birden fazla çekirdek olabilir.
  3. 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.
  4. Op için gradyanları hesaplayacak bir fonksiyon yazın (isteğe bağlı).
  5. 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 olarak relu_op_test.py bakın.

Önkoşullar

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 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:

  1. Cihazda şablonlanan OpKernel'i ve tensörün temel tipini tanımlayın.
  2. Çıktının gerçek hesaplamasını yapmak için Compute işlevi şablonlu bir işlev yapısını çağırır.
  3. 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 işlemi 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üm 5 beri yeni C++ ABI'yi kullanıyor. TensorFlow 2.8 ve önceki sürümleri, eski ABI'yi kullanan gcc4 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 genellikle dahil etmeniz gereken daha karmaşık şeylerden bazılarına bakacağız. Bu içerir:

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:

Alternatif olarak, bir işlevden döndürülen bir Status nesnesinin hata olup olmadığını test etmek istiyorsanız ve eğer ö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ının 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 : Bir TensorShapeProto .
  • list(<type>) : <type> listesi; burada <type> yukarıdaki türlerden biridir. list(list(<type>)) 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ür type .
  • realnumbertype : Karmaşık türleri olmayan numbertype 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 ( int32 veya float ) olduğunu 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ı hale getirmek) için sonuna = <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ılacak olandı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 yanı sıra float noktalar ü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ş 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" ) tensörlerin (uzunluğu "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");

Girdiler ve çıktı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ür type veya list(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 başvurmak, 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");
    

    out çıkışındaki tensörlerin sayısı ve türlerinin, giriş in tensörlerle aynı olduğuna dikkat edin, çünkü her ikisi de T tipindedir.

  • Aynı türdeki tensör dizisi için: <number> * <type> ; burada <number> , int türündeki bir Attr'ın adıdır. <type> bir tf.DType olabilir veya type type sahip bir özniteliğin adı olabilir. İlkinin bir örneği olarak, bu operasyon int32 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 başvuru için: Ref(<type>) , burada <type> önceki türlerden biridir.

Bir giriş türünde kullanılan herhangi bir öznitelik çıkarılacaktır. Kural olarak, bu türetilen öznitelikler büyük adlar kullanır ( T veya N gibi). Aksi takdirde 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.

  1. 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");
    
  2. 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 veya type . Veya {"apple", "orange"} yerine {"apple", "banana", "orange"} veya string değiştirebilirsiniz.

  3. 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.

  4. Varsayılan olarak boşsa yeni bir liste girişi/çıkışı ekleyebilirsiniz.

  5. 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.

  6. Ö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 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 verilen operasyonların girişlerine göre degradeleri hesaplayan bir degrade fonksiyonunu kaydetmeniz gerekir.

Matematiksel olarak, bir op \(y = f(x)\) hesaplarsa, kayıtlı gradyan op, l10n-yer tutucu4'e göre \(L\) \(\partial L/ \partial y\) \(y\) gradyanlarını, zincir kuralı aracılığıyla \(\partial L/ \partial x\) yer tutucu6'ya göre \(x\) gradyanlarına dönüştürür:

\[\frac{\partial L}{\partial x} = \frac{\partial L}{\partial y} \frac{\partial y}{\partial x} = \frac{\partial L}{\partial y} \frac{\partial f}{\partial x}.\]

ZeroOut durumunda, girişteki yalnızca bir giriş çıkışı etkiler, dolayısıyla girişe göre gradyan seyrek bir "bir 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 bir tf.Tensor grad alacak ve op.inputs[i] , op.outputs[i] ve grad tensörlerinden yeni op'lar oluşturacaktır. Herhangi bir öznitelik hakkında bilgi tf.Operation.get_attr aracılığıyla bulunabilir.

  • Op'un birden fazla çıkışı varsa, degrade işlevi op ve grads alır; burada grads her bir çıktıya göre degradelerin bir listesidir. Degrade fonksiyonunun sonucu, her girişe göre degradeleri temsil eden Tensor 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ı indeksi i alan bir işlem için, gradyan işlevi return [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 indeksine sahip 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 niteliklerine 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.