Operasyon oluştur

Mevcut TensorFlow kitaplığının kapsamadığı bir işlem oluşturmak istiyorsanız, önce işlemi mevcut Python işlemlerinin veya işlevlerinin bir bileşimi olarak Python'da yazmayı denemenizi öneririz. Bu mümkün değilse, özel bir C++ işlemi oluşturabilirsiniz. Özel bir C++ işlemi oluşturmak isteyebileceğiniz birkaç neden vardır:

  • Operasyonunuzu mevcut operasyonların bir bileşimi olarak ifade etmek kolay veya mümkün değil.
  • İşleminizi mevcut ilkellerin bir bileşimi olarak ifade etmek verimli değildir.
  • Gelecekteki bir derleyicinin kaynaştırmayı zor bulacağı bir ilkel bileşim bileşimini elle kaynaştırmak istiyorsunuz.

Örneğin, "MaxPool" operatörüne benzer bir "medyan havuzlama" gibi bir şey uygulamak istediğinizi, ancak maksimum değerler yerine kayan pencereler üzerinden medyanları hesapladığınızı hayal edin. Bunu bir işlemler bileşimi kullanarak yapmak mümkün olabilir (örneğin, ExtractImagePatches ve TopK kullanarak), ancak tek bir birleşik işlemde daha akıllıca bir şey yapabileceğiniz yerel bir işlem kadar performans veya bellek açısından verimli olmayabilir. Her zaman olduğu gibi, genellikle ilk önce operatör kompozisyonunu kullanarak ne istediğinizi ifade etmeye çalışmak, ancak bunun zor veya verimsiz olduğu ortaya çıkarsa yeni bir operasyon eklemeyi seçmek değerdir.

Özel operasyonunuzu dahil etmek için yapmanız gerekenler:

  1. Yeni işlemi bir C++ dosyasına kaydedin. İşlem kaydı, işlemin uygulanmasından bağımsız olan, işlemin işlevselliği için bir arabirim (özellik) tanımlar. Örneğin, op kaydı, op'un adını ve op'un girdi ve çıktılarını tanımlar. Ayrıca, tensör şekil çıkarımı için kullanılan şekil fonksiyonunu da tanımlar.
  2. İşlemi C++'da uygulayın. Bir işlemin uygulanması, çekirdek olarak bilinir ve 1. Adımda kaydettiğiniz spesifikasyonun somut uygulamasıdır. Farklı giriş/çıkış türleri veya mimarileri (örneğin, CPU'lar, GPU'lar) için birden fazla çekirdek olabilir.
  3. Bir Python sarmalayıcı oluşturun (isteğe bağlı). Bu sarmalayıcı, Python'da işlemi oluşturmak için kullanılan genel API'dir. İşlem kaydından, doğrudan kullanılabilen veya eklenebilen varsayılan bir sarıcı oluşturulur.
  4. İşlem için gradyanları hesaplamak üzere bir işlev yazın (isteğe bağlı).
  5. İşlemi test edin. Bunu genellikle kolaylık sağlamak için Python'da yaparız, ancak işlemi C++'da da test edebilirsiniz. Degradeler tanımlarsanız, bunları Python tf.test.compute_gradient_error ile doğrulayabilirsiniz. Relu benzeri işleçlerin ileri işlevlerini ve gradyanlarını test eden bir örnek olarak relu_op_test.py bakın.

Önkoşullar

İşlem arayüzünü tanımlayın

Bir operasyonun arayüzünü TensorFlow sistemine kaydederek tanımlarsınız. Kayıtta, operasyonunuzun adını, girdilerini (türleri ve adları) ve çıktılarını (türleri ve adları), ayrıca docstringleri ve operasyonun gerektirebileceği tüm nitelikleri belirtirsiniz.

Bunun nasıl çalıştığını görmek için, int32 s tensörünü alan ve tensörün bir kopyasını çıkaran, ilk öğe hariç tümü sıfıra ayarlanmış bir işlem oluşturmak istediğinizi varsayalım. Bunu yapmak için, zero_out.cc adlı bir dosya oluşturun. Ardından, operasyonunuz için arabirimi tanımlayan REGISTER_OP makrosuna bir çağrı ekleyin:

#include "tensorflow/core/framework/op.h"
#include "tensorflow/core/framework/shape_inference.h"

using namespace tensorflow;

REGISTER_OP("ZeroOut")
    .Input("to_zero: int32")
    .Output("zeroed: int32")
    .SetShapeFn([](::tensorflow::shape_inference::InferenceContext* c) {
      c->set_output(0, c->input(0));
      return Status::OK();
    });

Bu ZeroOut işlemi, 32-bit tamsayılardan to_zero bir tensörü girdi olarak alır ve 32-bit tamsayıların zeroed bir tensörünü çıkarır. İşlem, çıkış tensörünün giriş tensörü ile aynı şekilde olmasını sağlamak için bir şekil işlevi de kullanır. Örneğin, girdi [10, 20] şeklindeki bir tensör ise, bu şekil fonksiyonu çıktı şeklinin de [10, 20] olduğunu belirtir.

İşlem için çekirdeği uygulayın

Arayüzü tanımladıktan sonra, işlemin bir veya daha fazla uygulamasını sağlayın. Bu çekirdeklerden birini oluşturmak için OpKernel genişleten ve Compute yöntemini geçersiz kılan bir sınıf oluşturun. Compute yöntemi, giriş ve çıkış tensörleri gibi faydalı şeylere erişebileceğiniz OpKernelContext* türünde bir context argümanı sağlar.

Çekirdeğinizi yukarıda oluşturduğunuz dosyaya ekleyin. Çekirdek şöyle görünebilir:

#include "tensorflow/core/framework/op_kernel.h"

using namespace tensorflow;

class ZeroOutOp : public OpKernel {
 public:
  explicit ZeroOutOp(OpKernelConstruction* context) : OpKernel(context) {}

  void Compute(OpKernelContext* context) override {
    // Grab the input tensor
    const Tensor& input_tensor = context->input(0);
    auto input = input_tensor.flat<int32>();

    // Create an output tensor
    Tensor* output_tensor = NULL;
    OP_REQUIRES_OK(context, context->allocate_output(0, input_tensor.shape(),
                                                     &output_tensor));
    auto output_flat = output_tensor->flat<int32>();

    // Set all but the first element of the output tensor to 0.
    const int N = input.size();
    for (int i = 1; i < N; i++) {
      output_flat(i) = 0;
    }

    // Preserve the first input value if possible.
    if (N > 0) output_flat(0) = input(0);
  }
};

Çekirdeğinizi uyguladıktan sonra, onu TensorFlow sistemine kaydedersiniz. Kayıt sırasında, bu çekirdeğin çalışacağı farklı kısıtlamalar belirlersiniz. Örneğin, CPU'lar için yapılmış bir çekirdeğiniz ve GPU'lar için ayrı bir çekirdeğiniz olabilir.

Bunu ZeroOut işlemi için yapmak üzere, zero_out.cc dosyasına şunu ekleyin:

REGISTER_KERNEL_BUILDER(Name("ZeroOut").Device(DEVICE_CPU), ZeroOutOp);

Çok iş parçacıklı CPU çekirdekleri

Çok iş parçacıklı bir CPU çekirdeği yazmak için work_sharder.h içindeki Shard işlevi kullanılabilir. Bu işlev, işlem içi iş parçacığı için kullanılmak üzere yapılandırılmış iş parçacıkları boyunca bir hesaplama işlevi parçalar (bkz. config.proto içindeki intra_op_parallelism_threads).

GPU çekirdekleri

Bir GPU çekirdeği iki kısımda uygulanır: OpKernel ve CUDA çekirdeği ve başlatma kodu.

Bazen OpKernel uygulaması, girdilerin incelenmesi ve çıktıların tahsis edilmesi gibi bir CPU ve GPU çekirdeği arasında ortaktır. Bu durumda, önerilen bir uygulama şudur:

  1. Cihazda şablonlanmış OpKernel'i ve tensörün ilkel tipini tanımlayın.
  2. Çıktının asıl hesaplamasını yapmak için Compute işlevi şablonlu bir functor yapısını çağırır.
  3. CPUDevice için bu functor'ın uzmanlığı aynı dosyada tanımlanır, ancak GPUDevice için uzmanlık, CUDA derleyicisi ile derleneceği için bir .cu.cc dosyasında tanımlanır.

İşte örnek bir uygulama.

// kernel_example.h
#ifndef KERNEL_EXAMPLE_H_
#define KERNEL_EXAMPLE_H_

#include <unsupported/Eigen/CXX11/Tensor>

template <typename Device, typename T>
struct ExampleFunctor {
  void operator()(const Device& d, int size, const T* in, T* out);
};

#if GOOGLE_CUDA
// Partially specialize functor for GpuDevice.
template <typename T>
struct ExampleFunctor<Eigen::GpuDevice, T> {
  void operator()(const Eigen::GpuDevice& d, int size, const T* in, T* out);
};
#endif

#endif KERNEL_EXAMPLE_H_
// kernel_example.cc
#include "kernel_example.h"

#include "tensorflow/core/framework/op.h"
#include "tensorflow/core/framework/shape_inference.h"
#include "tensorflow/core/framework/op_kernel.h"

using namespace tensorflow;

using CPUDevice = Eigen::ThreadPoolDevice;
using GPUDevice = Eigen::GpuDevice;

REGISTER_OP("Example")
    .Attr("T: numbertype")
    .Input("input: T")
    .Output("input_times_two: T")
    .SetShapeFn([](::tensorflow::shape_inference::InferenceContext* c) {
      c->set_output(0, c->input(0));
      return Status::OK();
    });

// CPU specialization of actual computation.
template <typename T>
struct ExampleFunctor<CPUDevice, T> {
  void operator()(const CPUDevice& d, int size, const T* in, T* out) {
    for (int i = 0; i < size; ++i) {
      out[i] = 2 * in[i];
    }
  }
};

// OpKernel definition.
// template parameter <T> is the datatype of the tensors.
template <typename Device, typename T>
class ExampleOp : public OpKernel {
 public:
  explicit ExampleOp(OpKernelConstruction* context) : OpKernel(context) {}

  void Compute(OpKernelContext* context) override {
    // Grab the input tensor
    const Tensor& input_tensor = context->input(0);

    // Create an output tensor
    Tensor* output_tensor = NULL;
    OP_REQUIRES_OK(context, context->allocate_output(0, input_tensor.shape(),
                                                     &output_tensor));

    // Do the computation.
    OP_REQUIRES(context, input_tensor.NumElements() <= tensorflow::kint32max,
                errors::InvalidArgument("Too many elements in tensor"));
    ExampleFunctor<Device, T>()(
        context->eigen_device<Device>(),
        static_cast<int>(input_tensor.NumElements()),
        input_tensor.flat<T>().data(),
        output_tensor->flat<T>().data());
  }
};

// Register the CPU kernels.
#define REGISTER_CPU(T)                                          \
  REGISTER_KERNEL_BUILDER(                                       \
      Name("Example").Device(DEVICE_CPU).TypeConstraint<T>("T"), \
      ExampleOp<CPUDevice, T>);
REGISTER_CPU(float);
REGISTER_CPU(int32);

// Register the GPU kernels.
#ifdef GOOGLE_CUDA
#define REGISTER_GPU(T)                                          \
  /* Declare explicit instantiations in kernel_example.cu.cc. */ \
  extern template class ExampleFunctor<GPUDevice, T>;            \
  REGISTER_KERNEL_BUILDER(                                       \
      Name("Example").Device(DEVICE_GPU).TypeConstraint<T>("T"), \
      ExampleOp<GPUDevice, T>);
REGISTER_GPU(float);
REGISTER_GPU(int32);
#endif  // GOOGLE_CUDA
// kernel_example.cu.cc
#ifdef GOOGLE_CUDA
#define EIGEN_USE_GPU
#include "kernel_example.h"
#include "tensorflow/core/util/gpu_kernel_helper.h"

using namespace tensorflow;

using GPUDevice = Eigen::GpuDevice;

// Define the CUDA kernel.
template <typename T>
__global__ void ExampleCudaKernel(const int size, const T* in, T* out) {
  for (int i = blockIdx.x * blockDim.x + threadIdx.x; i < size;
       i += blockDim.x * gridDim.x) {
    out[i] = 2 * __ldg(in + i);
  }
}

// Define the GPU implementation that launches the CUDA kernel.
template <typename T>
void ExampleFunctor<GPUDevice, T>::operator()(
    const GPUDevice& d, int size, const T* in, T* out) {
  // Launch the cuda kernel.
  //
  // See core/util/gpu_kernel_helper.h for example of computing
  // block count and thread_per_block count.
  int block_count = 1024;
  int thread_per_block = 20;
  ExampleCudaKernel<T>
      <<<block_count, thread_per_block, 0, d.stream()>>>(size, in, out);
}

// Explicitly instantiate functors for the types of OpKernels registered.
template struct ExampleFunctor<GPUDevice, float>;
template struct ExampleFunctor<GPUDevice, int32>;

#endif  // GOOGLE_CUDA

İşlem kitaplığını oluşturun

Sistem derleyicinizi kullanarak işlemi derleyin (TensorFlow ikili kurulum)

zero_out.cc sisteminizde bulunan g++ veya clang gibi bir C++ derleyicisi ile derleyebilmeniz gerekir. İkili PIP paketi, işlemlerinizi sisteme özel konumlarda derlemek için ihtiyaç duyduğunuz başlık dosyalarını ve kitaplığı kurar. Ancak, TensorFlow python kitaplığı, başlık dizinini almak için get_include işlevini sağlar ve get_lib dizini, bağlantı kurulacak paylaşılan bir nesneye sahiptir. İşte bir Ubuntu makinesinde bu işlevlerin çıktıları.

$ python
>>> import tensorflow as tf
>>> tf.sysconfig.get_include()
'/usr/local/lib/python3.6/site-packages/tensorflow/include'
>>> tf.sysconfig.get_lib()
'/usr/local/lib/python3.6/site-packages/tensorflow'

Bilgisayarınızda g++ kurulu olduğunu varsayarsak, operasyonunuzu dinamik bir kitaplıkta derlemek için kullanabileceğiniz komut dizisi aşağıdadır.

TF_CFLAGS=( $(python -c 'import tensorflow as tf; print(" ".join(tf.sysconfig.get_compile_flags()))') )
TF_LFLAGS=( $(python -c 'import tensorflow as tf; print(" ".join(tf.sysconfig.get_link_flags()))') )
g++ -std=c++14 -shared zero_out.cc -o zero_out.so -fPIC ${TF_CFLAGS[@]} ${TF_LFLAGS[@]} -O2

macOS'ta, .so dosyası oluşturulurken "-undefined dynamic_lookup" ek bayrağı gerekir.

gcc sürümü >=5 ile ilgili not: gcc, sürüm 5 beri yeni C++ ABI'yi kullanır. TensorFlow 2.8 ve öncesi, 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.

Bazel kullanarak işlemi derleyin (TensorFlow kaynak kurulumu)

Yüklü TensorFlow kaynaklarınız varsa, işlemlerinizi derlemek için TensorFlow'un oluşturma sisteminden yararlanabilirsiniz. tensorflow/core/user_ops dizinine aşağıdaki Bazel oluşturma kuralına sahip bir BUILD dosyası yerleştirin.

load("//tensorflow:tensorflow.bzl", "tf_custom_op_library")

tf_custom_op_library(
    name = "zero_out.so",
    srcs = ["zero_out.cc"],
)

zero_out.so oluşturmak için aşağıdaki komutu çalıştırın.

$ bazel build --config opt //tensorflow/core/user_ops:zero_out.so

Example işlemi CUDA Çekirdeği ile derlemek için tf_custom_op_library gpu_srcs parametresini kullanmanız gerekir. Aşağıdaki Bazel oluşturma kuralına sahip bir BUILD dosyasını tensorflow/core/user_ops dizini (örn. "example_gpu") içindeki yeni bir klasöre yerleştirin.

load("//tensorflow:tensorflow.bzl", "tf_custom_op_library")

tf_custom_op_library(
    # kernel_example.cc  kernel_example.cu.cc  kernel_example.h
    name = "kernel_example.so",
    srcs = ["kernel_example.h", "kernel_example.cc"],
    gpu_srcs = ["kernel_example.cu.cc", "kernel_example.h"],
)

kernel_example.so oluşturmak için aşağıdaki komutu çalıştırın.

$ bazel build --config opt //tensorflow/core/user_ops/example_gpu:kernel_example.so

Python'da op'u kullanın

TensorFlow Python API, dinamik kitaplığı yüklemek ve işlemi TensorFlow çerçevesine kaydetmek için tf.load_op_library işlevini sağlar. load_op_library işlem ve çekirdek için Python sarmalayıcılarını içeren bir Python modülü döndürür. Böylece, işlemi bir kez oluşturduğunuzda, onu Python'dan çalıştırmak için aşağıdakileri yapabilirsiniz:

import tensorflow as tf
zero_out_module = tf.load_op_library('./zero_out.so')
print(zero_out_module.zero_out([[1, 2], [3, 4]]).numpy())

# Prints
array([[1, 0], [0, 0]], dtype=int32)

Unutmayın, oluşturulan işleve bir snake_case adı verilecektir ( PEP8 ile uyumlu olması için). Bu nedenle, C++ dosyalarında işleminizin adı ZeroOut ise, python işlevinin adı zero_out olacaktır.

İşlemi bir Python modülünden import normal bir işlev olarak kullanılabilir hale getirmek için, load_op_library çağrısının bir Python kaynak dosyasında şu şekilde olması yararlı olabilir:

import tensorflow as tf

zero_out_module = tf.load_op_library('./zero_out.so')
zero_out = zero_out_module.zero_out

İşlemin çalıştığını doğrulayın

İşleminizi başarıyla uyguladığınızı doğrulamanın iyi bir yolu, bunun için bir test yazmaktır. Şu içerikle birlikte zero_out_op_test.py dosyasını oluşturun:

import tensorflow as tf

class ZeroOutTest(tf.test.TestCase):
  def testZeroOut(self):
    zero_out_module = tf.load_op_library('./zero_out.so')
    with self.test_session():
      result = zero_out_module.zero_out([5, 4, 3, 2, 1])
      self.assertAllEqual(result.eval(), [5, 0, 0, 0, 0])

if __name__ == "__main__":
  tf.test.main()

Ardından testinizi çalıştırın (tensorflow'un kurulu olduğunu varsayarak):

$ python zero_out_op_test.py

Operasyonunuzda gelişmiş özellikler oluşturun

Artık temel (ve biraz kısıtlı) bir operasyon ve uygulamayı nasıl oluşturacağınızı bildiğinize göre, operasyonunuzda genellikle ihtiyaç duyacağınız daha karmaşık şeylerden bazılarına bakacağız. Bu içerir:

Koşullu kontroller ve doğrulama

Yukarıdaki örnek, işlemin herhangi bir şekle sahip bir tensöre uygulandığını varsaymıştır. Ya sadece vektörlere uygulansaydı? Bu, yukarıdaki OpKernel uygulamasına bir kontrol eklemek anlamına gelir.

  void Compute(OpKernelContext* context) override {
    // Grab the input tensor
    const Tensor& input_tensor = context->input(0);

    OP_REQUIRES(context, TensorShapeUtils::IsVector(input_tensor.shape()),
                errors::InvalidArgument("ZeroOut expects a 1-D vector."));
    // ...
  }

Bu, girdinin bir vektör olduğunu iddia eder ve öyle değilse InvalidArgument durumunu ayarlayarak geri döner. OP_REQUIRES makrosu üç bağımsız değişken alır:

Alternatif olarak, bir işlevden döndürülen Status nesnesinin bir hata olup olmadığını test etmek ve öyleyse onu döndürmek istiyorsanız, OP_REQUIRES_OK kullanın. Bu makroların her ikisi de hata durumunda işlevden döner.

operasyon kaydı

Öznitelikler

İşlemler, bir grafiğe işlem eklendiğinde değerleri ayarlanan özniteliklere sahip olabilir. Bunlar, işlemi yapılandırmak için kullanılır ve değerlerine hem çekirdek uygulamasından hem de işlem kaydındaki giriş ve çıkış türlerinden erişilebilir. Girdiler daha esnek olduğundan, mümkün olduğunda bir öznitelik yerine bir girdi kullanmayı tercih edin. Bunun nedeni, özniteliklerin sabit olmasıdır ve grafik oluşturma zamanında tanımlanmalıdır. Bunun aksine girdiler, değerleri dinamik olabilen Tensörlerdir; yani, girişler her adımı değiştirebilir, bir besleme vb. t adımdan adıma değişir.

İşlemi kaydettiğinizde, formun bir özelliğini bekleyen Attr yöntemini kullanarak adını ve türünü belirterek bir öznitelik tanımlarsınız:

<name>: <attr-type-expr>

burada <name> bir harfle başlar ve alfanümerik karakterlerden ve alt çizgilerden oluşabilir ve <attr-type-expr> aşağıda açıklanan formun bir tür ifadesidir.

Örneğin, ZeroOut işleminin yalnızca 0'ıncı öğe yerine kullanıcı tanımlı bir dizini korumasını istiyorsanız, işlemi şu şekilde kaydedebilirsiniz:

REGISTER_OP("ZeroOut")
    .Attr("preserve_index: int")
    .Input("to_zero: int32")
    .Output("zeroed: int32");

( Öznitelik türleri kümesinin, girişler ve çıkışlar için kullanılan tf.DType farklı olduğunu unutmayın.)

Çekirdeğiniz daha sonra bu özniteliğe yapıcısında context parametresi aracılığıyla erişebilir:

class ZeroOutOp : public OpKernel {
 public:
  explicit ZeroOutOp(OpKernelConstruction* context) : OpKernel(context) {
    // Get the index of the value to preserve
    OP_REQUIRES_OK(context,
                   context->GetAttr("preserve_index", &preserve_index_));
    // Check that preserve_index is positive
    OP_REQUIRES(context, preserve_index_ >= 0,
                errors::InvalidArgument("Need preserve_index >= 0, got ",
                                        preserve_index_));
  }
  void Compute(OpKernelContext* context) override {
    // ...
  }
 private:
  int preserve_index_;
};

bu daha sonra Compute yönteminde kullanılabilir:

  void Compute(OpKernelContext* context) override {
    // ...

    // We're using saved attr to validate potentially dynamic input
    // So we check that preserve_index is in range
    OP_REQUIRES(context, preserve_index_ < input.dimension(0),
                errors::InvalidArgument("preserve_index out of range"));

    // Set all the elements of the output tensor to 0
    const int N = input.size();
    for (int i = 0; i < N; i++) {
      output_flat(i) = 0;
    }

    // Preserve the requested input value
    output_flat(preserve_index_) = input(preserve_index_);
  }

Attr türleri

Bir öznitelikte aşağıdaki türler desteklenir:

  • string : Herhangi bir bayt dizisi (UTF8 olması gerekli değildir).
  • int : İşaretli bir tamsayı.
  • float : Bir kayan noktalı sayı.
  • bool : Doğru veya yanlış.
  • type : DataType öğesinin (başvuru olmayan) değerlerinden biri.
  • shape : Bir TensorShapeProto .
  • list(<type>) : <type> listesi, burada <type> yukarıdaki türlerden biridir. list(list(<type>)) geçersiz olduğuna dikkat edin.

Kesin bir liste için ayrıca bkz. op_def_builder.cc:FinalizeAttr .

Varsayılan değerler ve kısıtlamalar

Öznitelikler varsayılan değerlere sahip olabilir ve bazı öznitelik türlerinin kısıtlamaları olabilir. Kısıtlı bir öznitelik tanımlamak için aşağıdaki <attr-type-expr> s'yi kullanabilirsiniz:

{'<string1>', '<string2>'} : Değer, <string1> veya <string2> değerine sahip bir dize olmalıdır. Bu sözdizimini kullandığınızda türün adı olan string ima edilir. Bu bir numaralandırma öykünür:

REGISTER_OP("EnumExample")
    .Attr("e: {'apple', 'orange'}");

{<type1>, <type2>} : Değer type type ve <type1> veya <type2> türünden biri olmalıdır, burada <type1> ve <type2> desteklenir tf.DType . Attr türünün type olduğunu belirtmezsiniz. Bu, {...} içinde bir tür listeniz olduğunda ima edilir. Örneğin, bu durumda attr t int32 , float veya bool olması gereken bir türdür:

REGISTER_OP("RestrictedTypeExample")
    .Attr("t: {int32, float, bool}");

Yaygın tür kısıtlamaları için kısayollar vardır:

  • numbertype : Sayısal (dize olmayan ve bool olmayan) türlerle sınırlı type .
  • realnumbertype : Karmaşık türleri olmayan numbertype gibi.
  • quantizedtype : numbertype gibi, ancak yalnızca nicelenmiş sayı türleri.

Bunlar tarafından izin verilen belirli tür listeleri tensorflow/core/framework/types.h içindeki işlevler ( NumberTypes() gibi) tarafından tanımlanır. Bu örnekte, öznitelik t sayısal türlerden biri olmalıdır:

REGISTER_OP("NumberType")
    .Attr("t: numbertype");

Bu işlem için:

tf.number_type(t=tf.int32)  # Valid
tf.number_type(t=tf.bool)   # Invalid

Listeler, diğer listeler ve tek türlerle birleştirilebilir. Aşağıdaki işlem, attr t sayısal türlerden herhangi biri veya bool türü olmasına izin verir:

REGISTER_OP("NumberOrBooleanType")
    .Attr("t: {numbertype, bool}");

Bu işlem için:

tf.number_or_boolean_type(t=tf.int32)  # Valid
tf.number_or_boolean_type(t=tf.bool)   # Valid
tf.number_or_boolean_type(t=tf.string) # Invalid

int >= <n> : Değer, <n> doğal sayı olmak üzere <n> değerinden büyük veya ona eşit olan bir int olmalıdır. Örneğin, aşağıdaki işlem kaydı, özniteliğin a değerinin en az 2 olması gerektiğini belirtir:

REGISTER_OP("MinIntExample")
    .Attr("a: int >= 2");

list(<type>) >= <n> : Uzunluğu <n> den büyük veya ona eşit olan <type> türünde bir liste. Örneğin, aşağıdaki op kaydı, a özniteliğinin bir tür listesi ( int32 veya float ) olduğunu ve bunlardan en az 3 tane olması gerektiğini belirtir:

REGISTER_OP("TypeListExample")
    .Attr("a: list({int32, float}) >= 3");

Bir öznitelik için varsayılan bir değer ayarlamak için (oluşturulan kodda isteğe bağlı hale getirir), aşağıdaki gibi sonuna = <default> ekleyin:

REGISTER_OP("AttrDefaultExample")
    .Attr("i: int = 0");

Ek olarak, hem bir kısıtlama hem de bir varsayılan değer belirtilebilir:

REGISTER_OP("AttrConstraintAndDefaultExample")
    .Attr("i: int >= 1 = 1");

Varsayılan değerin desteklenen sözdizimi, ortaya çıkan GraphDef tanımının proto gösteriminde kullanılacak olandır.

Tüm türler için bir varsayılanın nasıl belirleneceğine ilişkin örnekler aşağıda verilmiştir:

REGISTER_OP("AttrDefaultExampleForAllTypes")
   .Attr("s: string = 'foo'")
   .Attr("i: int = 0")
   .Attr("f: float = 1.0")
   .Attr("b: bool = true")
   .Attr("ty: type = DT_INT32")
   .Attr("sh: shape = { dim { size: 1 } dim { size: 2 } }")
   .Attr("te: tensor = { dtype: DT_INT32 int_val: 5 }")
   .Attr("l_empty: list(int) = []")
   .Attr("l_int: list(int) = [2, 3, 5, 7]");

Özellikle type type değerlerinin tf.DType kullandığına dikkat edin.

polimorfizm

Tip polimorfizmi

Farklı türleri girdi olarak alabilen veya farklı çıktı türleri üretebilen işlemler için, işlem kaydında bir girdi veya çıktı türünde bir öznitelik belirtebilirsiniz. Genellikle desteklenen her tür için bir OpKernel kaydedersiniz.

Örneğin, ZeroOut op'unun int32 s'ye ek olarak float s üzerinde çalışmasını isterseniz, op kaydınız şöyle görünebilir:

REGISTER_OP("ZeroOut")
    .Attr("T: {float, int32}")
    .Input("to_zero: T")
    .Output("zeroed: T");

İşlem kaydınız artık giriş türünün float veya int32 olması gerektiğini ve her ikisinin de türü T olduğundan çıktısının aynı tür olacağını belirtir.

Adlandırma

Girişler, çıkışlar ve özniteliklere genellikle yılan_durum adları verilmelidir. Tek istisna, bir girdi türü veya bir çıktı türü olarak kullanılan özniteliklerdir. Bu nitelikler, işlem grafiğe eklendiğinde çıkarılabilir ve bu nedenle işlemin işlevinde görünmez. Örneğin, ZeroOut'un bu son tanımı, şuna benzeyen bir Python işlevi üretecektir:

def zero_out(to_zero, name=None):
  """...
  Args:
    to_zero: A `Tensor`. Must be one of the following types:
        `float32`, `int32`.
    name: A name for the operation (optional).

  Returns:
    A `Tensor`. Has the same type as `to_zero`.
  """

to_zero bir int32 tensörü iletilirse, T otomatik olarak int32 ayarlanır (aslında DT_INT32 ). Bu çıkarsanan özniteliklere Büyük Harf veya CamelCase adları verilir.

Bunu, çıktı türünü belirleyen bir tür özniteliği olan bir işlemle karşılaştırın:

REGISTER_OP("StringToNumber")
    .Input("string_tensor: string")
    .Output("output: out_type")
    .Attr("out_type: {float, int32} = DT_FLOAT");
    .Doc(R"doc(
Converts each string in the input Tensor to the specified numeric type.
)doc");

Bu durumda, kullanıcı, oluşturulan Python'da olduğu gibi çıktı türünü belirtmelidir:

def string_to_number(string_tensor, out_type=None, name=None):
  """Converts each string in the input Tensor to the specified numeric type.

  Args:
    string_tensor: A `Tensor` of type `string`.
    out_type: An optional `tf.DType` from: `tf.float32, tf.int32`.
      Defaults to `tf.float32`.
    name: A name for the operation (optional).

  Returns:
    A `Tensor` of type `out_type`.
  """
Tip polimorfizmi örneği
#include "tensorflow/core/framework/op_kernel.h"

class ZeroOutInt32Op : public OpKernel {
  // as before
};

class ZeroOutFloatOp : public OpKernel {
 public:
  explicit ZeroOutFloatOp(OpKernelConstruction* context)
      : OpKernel(context) {}

  void Compute(OpKernelContext* context) override {
    // Grab the input tensor
    const Tensor& input_tensor = context->input(0);
    auto input = input_tensor.flat<float>();

    // Create an output tensor
    Tensor* output = NULL;
    OP_REQUIRES_OK(context,
                   context->allocate_output(0, input_tensor.shape(), &output));
    auto output_flat = output->template flat<float>();

    // Set all the elements of the output tensor to 0
    const int N = input.size();
    for (int i = 0; i < N; i++) {
      output_flat(i) = 0;
    }

    // Preserve the first input value
    if (N > 0) output_flat(0) = input(0);
  }
};

// Note that TypeConstraint<int32>("T") means that attr "T" (defined
// in the op registration above) must be "int32" to use this template
// instantiation.
REGISTER_KERNEL_BUILDER(
    Name("ZeroOut")
    .Device(DEVICE_CPU)
    .TypeConstraint<int32>("T"),
    ZeroOutInt32Op);
REGISTER_KERNEL_BUILDER(
    Name("ZeroOut")
    .Device(DEVICE_CPU)
    .TypeConstraint<float>("T"),
    ZeroOutFloatOp);

Geriye dönük uyumluluğu korumak için, mevcut bir işleme öznitelik eklerken varsayılan bir değer belirtmelisiniz:

REGISTER_OP("ZeroOut")
  .Attr("T: {float, int32} = DT_INT32")
  .Input("to_zero: T")
  .Output("zeroed: T")

Diyelim ki daha fazla tür eklemek istiyorsunuz, diyelim ki double :

REGISTER_OP("ZeroOut")
    .Attr("T: {float, double, int32}")
    .Input("to_zero: T")
    .Output("zeroed: T");

Yukarıdaki gibi gereksiz kod içeren başka bir OpKernel yazmak yerine, bunun yerine genellikle bir C++ şablonu kullanabileceksiniz. Her aşırı yük için hala bir çekirdek kaydınız ( REGISTER_KERNEL_BUILDER çağrısı) olacaktır.

template <typename T>
class ZeroOutOp : public OpKernel {
 public:
  explicit ZeroOutOp(OpKernelConstruction* context) : OpKernel(context) {}

  void Compute(OpKernelContext* context) override {
    // Grab the input tensor
    const Tensor& input_tensor = context->input(0);
    auto input = input_tensor.flat<T>();

    // Create an output tensor
    Tensor* output = NULL;
    OP_REQUIRES_OK(context,
                   context->allocate_output(0, input_tensor.shape(), &output));
    auto output_flat = output->template flat<T>();

    // Set all the elements of the output tensor to 0
    const int N = input.size();
    for (int i = 0; i < N; i++) {
      output_flat(i) = 0;
    }

    // Preserve the first input value
    if (N > 0) output_flat(0) = input(0);
  }
};

// Note that TypeConstraint<int32>("T") means that attr "T" (defined
// in the op registration above) must be "int32" to use this template
// instantiation.
REGISTER_KERNEL_BUILDER(
    Name("ZeroOut")
    .Device(DEVICE_CPU)
    .TypeConstraint<int32>("T"),
    ZeroOutOp<int32>);
REGISTER_KERNEL_BUILDER(
    Name("ZeroOut")
    .Device(DEVICE_CPU)
    .TypeConstraint<float>("T"),
    ZeroOutOp<float>);
REGISTER_KERNEL_BUILDER(
    Name("ZeroOut")
    .Device(DEVICE_CPU)
    .TypeConstraint<double>("T"),
    ZeroOutOp<double>);

Birden fazla aşırı yüklemeniz varsa, kaydı bir makroya koyabilirsiniz.

#include "tensorflow/core/framework/op_kernel.h"

#define REGISTER_KERNEL(type)                                       \
  REGISTER_KERNEL_BUILDER(                                          \
      Name("ZeroOut").Device(DEVICE_CPU).TypeConstraint<type>("T"), \
      ZeroOutOp<type>)

REGISTER_KERNEL(int32);
REGISTER_KERNEL(float);
REGISTER_KERNEL(double);

#undef REGISTER_KERNEL

Çekirdeği kaydettiğiniz türlerin listesine bağlı olarak, tensorflow/core/framework/register_types.h tarafından sağlanan bir makroyu kullanabilirsiniz:

#include "tensorflow/core/framework/op_kernel.h"
#include "tensorflow/core/framework/register_types.h"

REGISTER_OP("ZeroOut")
    .Attr("T: realnumbertype")
    .Input("to_zero: T")
    .Output("zeroed: T");

template <typename T>
class ZeroOutOp : public OpKernel { ... };

#define REGISTER_KERNEL(type)                                       \
  REGISTER_KERNEL_BUILDER(                                          \
      Name("ZeroOut").Device(DEVICE_CPU).TypeConstraint<type>("T"), \
      ZeroOutOp<type>)

TF_CALL_REAL_NUMBER_TYPES(REGISTER_KERNEL);

#undef REGISTER_KERNEL
Girişleri ve çıkışları listeleyin

Farklı türleri kabul edebilmenin veya üretebilmenin yanı sıra, operasyonlar değişken sayıda tensör tüketebilir veya üretebilir.

Bir sonraki örnekte, öznitelik T , türlerin bir listesini tutar ve hem in hem de out türü olarak kullanılır. Girdi ve çıktı, o türdeki tensörlerin listeleridir (ve çıktıdaki tensörlerin sayısı ve türleri, her ikisi de T türüne sahip olduğundan, girdiyle aynıdır).

REGISTER_OP("PolymorphicListExample")
    .Attr("T: list(type)")
    .Input("in: T")
    .Output("out: T");

Listede hangi türlerin belirtilebileceğine de kısıtlamalar getirebilirsiniz. Bu sonraki durumda, girdi bir float ve double tensör listesidir. İşlem, örneğin giriş tiplerini (float, double, float) kabul eder ve bu durumda çıktı tipi de (float, double, float) olur.

REGISTER_OP("ListTypeRestrictionExample")
    .Attr("T: list({float, double})")
    .Input("in: T")
    .Output("out: T");

Bir listedeki tüm tensörlerin aynı türde olmasını istiyorsanız, şöyle bir şey yapabilirsiniz:

REGISTER_OP("IntListInputExample")
    .Attr("N: int")
    .Input("in: N * int32")
    .Output("out: int32");

Bu, int32 tensörlerinin bir listesini kabul eder ve listenin uzunluğunu belirtmek için bir int öznitelik N kullanır.

Bu tip polimorfik de yapılabilir. Bir sonraki örnekte girdi, aynı (ancak belirtilmemiş) türdeki ( "T" ) tensörlerin ( "N" uzunluğunda) bir listesidir ve çıktı, eşleşen türde tek bir tensördür:

REGISTER_OP("SameListInputExample")
    .Attr("N: int")
    .Attr("T: type")
    .Input("in: N * T")
    .Output("out: T");

Varsayılan olarak, tensör listelerinin minimum uzunluğu 1'dir . İlgili öznitelikte bir ">=" kısıtlaması kullanarak bu varsayılanı değiştirebilirsiniz. Sonraki örnekte, girdi en az 2 int32 tensör listesidir:

REGISTER_OP("MinLengthIntListExample")
    .Attr("N: int >= 2")
    .Input("in: N * int32")
    .Output("out: int32");

Aynı sözdizimi "list(type)" öznitelikleri ile çalışır:

REGISTER_OP("MinimumLengthPolymorphicListExample")
    .Attr("T: list(type) >= 3")
    .Input("in: T")
    .Output("out: T");

Girdiler ve çıktılar

Yukarıdakileri özetlemek gerekirse, bir işlem kaydının birden çok girdisi ve çıktısı olabilir:

REGISTER_OP("MultipleInsAndOuts")
    .Input("y: int32")
    .Input("z: float")
    .Output("a: string")
    .Output("b: int32");

Her giriş veya çıkış özelliği şu şekildedir:

<name>: <io-type-expr>

burada <name> bir harfle başlar ve alfanümerik karakterlerden ve alt çizgilerden oluşabilir. <io-type-expr> , aşağıdaki tür ifadelerinden biridir:

  • <type> , burada <type> desteklenen bir giriş türüdür (örn. float , int32 , string ). Bu, verilen türden tek bir tensörü belirtir.

    tf.DType bakın.

    REGISTER_OP("BuiltInTypesExample")
        .Input("integers: int32")
        .Input("complex_numbers: complex64");
    
  • <attr-type> , burada <attr-type> , type veya list(type) (olası bir tür kısıtlamasıyla) içeren bir Attr'nin adıdır. Bu sözdizimi polimorfik işlemlere izin verir.

    REGISTER_OP("PolymorphicSingleInput")
        .Attr("T: type")
        .Input("in: T");
    
    REGISTER_OP("RestrictedPolymorphicSingleInput")
        .Attr("T: {int32, int64}")
        .Input("in: T");
    

    Bir tür list(type) özniteliğine başvurmak, bir tensör dizisini kabul etmenize izin verir.

    REGISTER_OP("ArbitraryTensorSequenceExample")
        .Attr("T: list(type)")
        .Input("in: T")
        .Output("out: T");
    
    REGISTER_OP("RestrictedTensorSequenceExample")
        .Attr("T: list({int32, int64})")
        .Input("in: T")
        .Output("out: T");
    

    Her ikisi de T tipinde olduğundan, output out tensörlerin sayısı ve tiplerinin in in aynı olduğuna dikkat edin.

  • Aynı türdeki tensör dizisi için: <number> * <type> , burada <number> int türündeki bir Attr'nin adıdır. <type> , bir tf.DType veya type type bir özniteliğin adı olabilir. İlkinin bir örneği olarak, bu işlem 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 herhangi bir tensör listesini kabul eder:

    REGISTER_OP("SameTypeSequenceExample")
        .Attr("NumTensors: int")
        .Attr("T: type")
        .Input("in: NumTensors * T")
    
  • Bir tensöre referans için: Ref(<type>) , burada <type> önceki türlerden biridir.

Bir giriş türünde kullanılan herhangi bir öznitelik çıkarsanacaktır. Geleneksel olarak, bu türetilmiş öznitelikler büyük adlar kullanır ( T veya N gibi). Aksi takdirde girdiler, çıktılar ve öznitelikler işlev parametreleri gibi adlara sahiptir (örn. num_outputs ). Daha fazla ayrıntı için, adlandırma ile ilgili önceki bölüme bakın.

Daha fazla ayrıntı için bkz. tensorflow/core/framework/op_def_builder.h .

Geriye dönük uyumluluk

Diyelim ki güzel, özel bir operasyon yazdınız ve başkalarıyla paylaştınız, böylece operasyonunuzu kullanan mutlu müşterileriniz oldu. Ancak, operasyonda bir şekilde değişiklik yapmak istiyorsunuz.

Genel olarak, mevcut, teslim edilmiş özelliklerde yapılan değişiklikler geriye dönük olarak uyumlu olmalıdır: bir işlemin belirtiminin değiştirilmesi, daha eski özelliklerden oluşturulan önceki seri hale getirilmiş GraphDef protokolü arabelleklerini bozmamalıdır. GraphDef uyumluluğunun ayrıntıları burada açıklanmıştır .

Geriye dönük uyumluluğu korumanın birkaç yolu vardır.

  1. Bir işleme eklenen herhangi bir yeni öznitelik tanımlanmış varsayılan değerlere sahip olmalıdır ve bu varsayılan değerle, işlem orijinal davranışa sahip olmalıdır. Polimorfik olmayan bir işlemi polimorfik olarak değiştirmek için, orijinal imzayı varsayılan olarak korumak için yeni attr tipine varsayılan bir değer vermelisiniz . Örneğin, işleminiz şuysa:

    REGISTER_OP("MyGeneralUnaryOp")
        .Input("in: float")
        .Output("out: float");
    

    aşağıdakileri kullanarak geriye dönük uyumlu bir şekilde polimorfik yapabilirsiniz:

    REGISTER_OP("MyGeneralUnaryOp")
        .Input("in: T")
        .Output("out: T")
        .Attr("T: numerictype = DT_FLOAT");
    
  2. Daha az kısıtlayıcı bir öznitelik üzerinde güvenli bir şekilde kısıtlama yapabilirsiniz. Örneğin, {int32, int64} ı {int32, int64, float} olarak değiştirebilir veya type değiştirebilirsiniz. Veya {"apple", "orange"} dan {"apple", "banana", "orange"} veya string değiştirebilirsiniz.

  3. Liste tipi için varsayılan eski imzayla eşleştiği sürece, tek girişleri / çıkışları liste girişleri / çıkışları olarak değiştirebilirsiniz.

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

  5. İşlem adlarının önüne projenize özel bir şey ekleyerek, oluşturduğunuz tüm yeni işlemleri ad alanı yapın. Bu, operasyonunuzun TensorFlow'un gelecekteki sürümlerinde yer alabilecek herhangi bir operasyonla çakışmasını önler.

  6. Önceden planlamak! Operasyon için gelecekteki kullanımları tahmin etmeye çalışın. Bazı imza değişiklikleri uyumlu bir şekilde yapılamaz (örneğin, aynı türdeki bir listeyi farklı türlerdeki bir listeye dönüştürmek).

Güvenli ve güvenli olmayan değişikliklerin tam listesi tensorflow/core/framework/op_compatibility_test.cc adresinde bulunabilir. Bir işlemde yaptığınız değişikliği geriye dönük uyumlu hale getiremezseniz, yeni semantikle yeni bir adla yeni bir işlem oluşturun.

Ayrıca, bu değişikliklerin GraphDef uyumluluğunu koruyabilmesine rağmen, oluşturulan Python kodunun eski arayanlarla uyumlu olmayan bir şekilde değişebileceğini unutmayın. Python API, muhtemelen sona yeni isteğe bağlı bağımsız değişkenler eklemek dışında eski imzayı koruyarak, elle yazılmış bir Python sarmalayıcısında dikkatli değişiklikler yapılarak uyumlu tutulabilir. Genel olarak uyumsuz değişiklikler, yalnızca TensorFlow ana sürümleri değiştirdiğinde yapılabilir ve GraphDef sürüm semantiğine uygun olmalıdır.

GPU desteği

Farklı OpKernel'leri uygulayabilir ve tıpkı farklı türler için çekirdekleri kaydedebildiğiniz gibi CPU ve diğerini GPU için kaydedebilirsiniz. tensorflow/core/kernels/ içinde GPU destekli birkaç çekirdek örneği vardır. Bazı çekirdeklerin bir .cc dosyasında bir CPU sürümüne, _gpu.cu.cc ile biten bir dosyada bir GPU sürümüne ve bir .h dosyasında ortak olarak paylaşılan bazı kodlara sahip olduğuna dikkat edin.

Örneğin, tf.pad , tensorflow/core/kernels/pad_op.cc içindeki GPU çekirdeği dışında her şeye sahiptir. GPU çekirdeği tensorflow/core/kernels/pad_op_gpu.cu.cc içindedir ve paylaşılan kod tensorflow/core/kernels/pad_op.h içinde tanımlanan şablonlu bir sınıftır. Kodu iki nedenden dolayı bu şekilde düzenliyoruz: CPU ve GPU uygulamaları arasında ortak kod paylaşmanıza izin veriyor ve GPU uygulamasını yalnızca GPU derleyicisi tarafından derlenebilmesi için ayrı bir dosyaya koyuyor.

Unutulmaması gereken bir nokta, pad GPU çekirdek sürümü kullanıldığında bile, yine de CPU belleğinde "paddings" girdisine ihtiyaç duyar. Girdilerin veya çıktıların CPU'da tutulduğunu işaretlemek için, çekirdek kaydına bir HostMemory() çağrısı ekleyin, örneğin:

#define REGISTER_GPU_KERNEL(T)                         \
  REGISTER_KERNEL_BUILDER(Name("Pad")                  \
                              .Device(DEVICE_GPU)      \
                              .TypeConstraint<T>("T")  \
                              .HostMemory("paddings"), \
                          PadOp<GPUDevice, T>)

GPU aygıtı için çekirdeğin derlenmesi

Bir işlemi uygulamak için CUDA çekirdeği kullanan bir örnek için cuda_op_kernel.cu.cc'ye bakın. tf_custom_op_library , CUDA çekirdeklerini ( *.cu.cc dosyaları) içeren kaynak dosyaların listesinin belirtilebildiği bir gpu_srcs bağımsız değişkenini kabul eder. TensorFlow'un ikili kurulumuyla kullanım için, CUDA çekirdeklerinin NVIDIA'nın nvcc derleyicisi ile derlenmesi gerekir. cuda_op_kernel.cu.cc ve cuda_op_kernel.cc'yi dinamik olarak yüklenebilir tek bir kitaplıkta derlemek için kullanabileceğiniz komutların sırası şöyledir:

nvcc -std=c++14 -c -o cuda_op_kernel.cu.o cuda_op_kernel.cu.cc \
  ${TF_CFLAGS[@]} -D GOOGLE_CUDA=1 -x cu -Xcompiler -fPIC

g++ -std=c++14 -shared -o cuda_op_kernel.so cuda_op_kernel.cc \
  cuda_op_kernel.cu.o ${TF_CFLAGS[@]} -fPIC -lcudart ${TF_LFLAGS[@]}

Yukarıda üretilen cuda_op_kernel.so , tf.load_op_library işlevi kullanılarak Python'da her zamanki gibi yüklenebilir.

CUDA kitaplıklarınız /usr/local/lib64 içinde yüklü değilse, yukarıdaki ikinci (g++) komutta yolu açıkça belirtmeniz gerekeceğini unutmayın. Örneğin, CUDA'nız /usr/local/cuda-8.0 içinde kuruluysa -L /usr/local/cuda-8.0/lib64/ ekleyin.

Gradyanı Python'da uygulama

Bir operasyon grafiği verildiğinde, TensorFlow, mevcut operasyonlara göre gradyanları temsil eden yeni operasyonlar eklemek için otomatik farklılaşmayı (geriye yayılım) kullanır. Otomatik farklılaştırmanın yeni operasyonlarda çalışmasını sağlamak için, operasyonların çıktılarına göre gradyanlar verildiğinde operasyonların girişlerine göre gradyanları hesaplayan bir gradyan fonksiyonunu kaydetmeniz gerekir.

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

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

ZeroOut durumunda, girişteki yalnızca bir giriş çıkışı etkiler, bu nedenle girişe göre gradyan seyrek bir "tek sıcak" tensördür. Bu şu şekilde ifade edilir:

from tensorflow.python.framework import ops
from tensorflow.python.ops import array_ops
from tensorflow.python.ops import sparse_ops

@ops.RegisterGradient("ZeroOut")
def _zero_out_grad(op, grad):
  """The gradients for `zero_out`.

  Args:
    op: The `zero_out` `Operation` that we are differentiating, which we can use
      to find the inputs and outputs of the original op.
    grad: Gradient with respect to the output of the `zero_out` op.

  Returns:
    Gradients with respect to the input of `zero_out`.
  """
  to_zero = op.inputs[0]
  shape = array_ops.shape(to_zero)
  index = array_ops.zeros_like(shape)
  first_grad = array_ops.reshape(grad, [-1])[0]
  to_zero_grad = sparse_ops.sparse_to_dense([index], shape, first_grad, 0)
  return [to_zero_grad]  # List of one Tensor, since we have one input

Gradyan işlevlerini tf.RegisterGradient ile kaydetmeyle ilgili ayrıntılar:

  • Tek çıkışlı bir işlem için, gradyan işlevi bir tf.Operation , op ve bir tf.Tensor grad alır ve op.inputs[i] , op.outputs[i] ve grad tensörlerinden yeni işlemler oluşturur. Herhangi bir öznitelik hakkında bilgi tf.Operation.get_attr aracılığıyla bulunabilir.

  • İşlemin birden çok çıktısı varsa, gradyan işlevi op ve grads alır; burada grads , her çıktıya göre degradelerin bir listesidir. Gradyan fonksiyonunun sonucu, her girişe göre gradyanları temsil eden Tensor nesnelerinin bir listesi olmalıdır.

  • İndeks olarak kullanılan tamsayı girişleri gibi bazı girdiler için iyi tanımlanmış bir gradyan yoksa, karşılık gelen döndürülen gradyan None olmalıdır. Örneğin, x kayan nokta tensörü ve i tamsayı dizini alan bir işlem için, gradyan işlevi return [x_grad, None] .

  • İşlem için anlamlı bir gradyan yoksa, genellikle herhangi bir gradyan kaydetmeniz gerekmez ve operasyonun gradyanına hiçbir zaman ihtiyaç duyulmadığı sürece sorun olmaz. Bazı durumlarda, bir işlemin iyi tanımlanmış bir gradyanı yoktur ancak gradyanın hesaplanmasına dahil edilebilir. Burada sıfırları otomatik olarak geriye doğru yaymak için ops.NotDifferentiable kullanabilirsiniz.

Gradyan işlevi çağrıldığında, tensör verilerinin kendisinin değil, yalnızca işlemlerin veri akış grafiğinin mevcut olduğunu unutmayın. Bu nedenle, tüm hesaplamalar, grafik yürütme zamanında çalıştırılmak üzere diğer tensorflow işlemleri kullanılarak gerçekleştirilmelidir.

Veri doğrulama yoluyla kodu daha okunabilir, hata ayıklanabilir, bakımı kolay ve daha sağlam hale getirmek için bir işlem türü için özel gradyanı kaydederken tür ipuçları ekleyin. Örneğin, bir işlevde parametre olarak bir op alırken, gradyan işlevinin parametre türü olarak bir tf.Operation alacağını belirtin.

C++'da şekil fonksiyonları

TensorFlow API, grafiği yürütmek zorunda kalmadan tensörlerin şekilleri hakkında bilgi sağlayan "şekil çıkarımı" adlı bir özelliğe sahiptir. Şekil çıkarımı, C++ REGISTER_OP bildiriminde her işlem türü için kaydedilen ve iki rolü yerine getiren "şekil işlevleri" tarafından desteklenir: grafik oluşturma sırasında girdilerin şekillerinin uyumlu olduğunu iddia etmek ve çıktılar için şekilleri belirtmek.

Şekil işlevleri, shape_inference::InferenceContext sınıfındaki işlemler olarak tanımlanır. Örneğin, ZeroOut için şekil işlevinde:

    .SetShapeFn([](::tensorflow::shape_inference::InferenceContext* c) {
      c->set_output(0, c->input(0));
      return Status::OK();
    });

c->set_output(0, c->input(0)); ilk çıktının şeklinin ilk girdinin şekline ayarlanması gerektiğini beyan eder. Çıktı, yukarıdaki örnekte olduğu gibi dizinine göre seçilirse, set_output ikinci parametresi bir ShapeHandle nesnesi olmalıdır. Varsayılan yapıcısı ile boş bir ShapeHandle nesnesi oluşturabilirsiniz. idx dizinli bir girdi için ShapeHandle nesnesi c->input(idx) ile elde edilebilir.

Common_shape_fns.h'de bulunabilen ve aşağıdaki şekilde kullanılabilen shape_inference::UnchangedShape gibi birçok operasyon için geçerli olan bir dizi ortak şekil işlevi vardır:

REGISTER_OP("ZeroOut")
    .Input("to_zero: int32")
    .Output("zeroed: int32")
    .SetShapeFn(::tensorflow::shape_inference::UnchangedShape);

Bir şekil işlevi, bir girdinin şeklini de kısıtlayabilir. ZeroOut vektör şekli kısıtlaması olan sürümü için şekil işlevi aşağıdaki gibi olacaktır:

    .SetShapeFn([](::tensorflow::shape_inference::InferenceContext* c) {
      ::tensorflow::shape_inference::ShapeHandle input;
      TF_RETURN_IF_ERROR(c->WithRank(c->input(0), 1, &input));
      c->set_output(0, input);
      return Status::OK();
    });

WithRank çağrısı c->input(0) giriş şeklinin tam olarak tek boyutlu bir şekle sahip olduğunu doğrular (veya giriş şekli bilinmiyorsa, çıkış şekli tek boyutu bilinmeyen bir vektör olacaktır).

İşleminiz birden çok girişle çok biçimliyse , kontrol edilecek şekillerin sayısını belirlemek için InferenceContext üyelerini ve şekillerin tümünün uyumlu olduğunu doğrulamak için Merge kullanabilirsiniz (alternatif olarak, InferenceContext::GetAttr ile uzunlukları gösteren özniteliklere erişin, operasyonun özelliklerine erişim sağlar).

    .SetShapeFn([](::tensorflow::shape_inference::InferenceContext* c) {
      ::tensorflow::shape_inference::ShapeHandle input;
      ::tensorflow::shape_inference::ShapeHandle output;
      for (size_t i = 0; i < c->num_inputs(); ++i) {
        TF_RETURN_IF_ERROR(c->WithRank(c->input(i), 2, &input));
        TF_RETURN_IF_ERROR(c->Merge(output, input, &output));
      }
      c->set_output(0, output);
      return Status::OK();
    });

Şekil çıkarımı isteğe bağlı bir özellik olduğundan ve tensörlerin şekilleri dinamik olarak değişebileceğinden, şekil fonksiyonları, girdilerin herhangi biri için eksik şekil bilgilerine karşı dayanıklı olmalıdır. InferenceContext Merge yöntemi, arayanın iki şeklin aynı olduğunu iddia etmesine izin verir, bunlardan biri veya her ikisi de tam bilgiye sahip olmasa bile. Şekil işlevleri, tüm temel TensorFlow işlemleri için tanımlanır ve birçok farklı kullanım örneği sağlar.

InferenceContext sınıfı, şekil işlevi manipülasyonlarını tanımlamak için kullanılabilecek bir dizi işleve sahiptir. Örneğin, InferenceContext::Dim ve InferenceContext::WithValue kullanarak belirli bir boyutun çok özel bir değere sahip olduğunu doğrulayabilirsiniz; bir çıktı boyutunun, InferenceContext::Add ve InferenceContext::Multiply kullanarak iki girdi boyutunun toplamı / ürünü olduğunu belirtebilirsiniz. Belirleyebileceğiniz tüm çeşitli şekil işlemleri için InferenceContext sınıfına bakın. Aşağıdaki örnek, ilk çıktının şeklini (n, 3) olarak ayarlar, burada ilk girdinin şekli (n, ...)

.SetShapeFn([](::tensorflow::shape_inference::InferenceContext* c) {
    c->set_output(0, c->Matrix(c->Dim(c->input(0), 0), 3));
    return Status::OK();
});

Karmaşık bir şekil işleviniz varsa, çeşitli girdi şekli kombinasyonlarının beklenen çıktı şekli kombinasyonlarını ürettiğini doğrulamak için bir test eklemeyi düşünmelisiniz. Bazı core ops testlerimizde bu testlerin nasıl yazılacağına dair örnekler görebilirsiniz. ( INFER_OK ve INFER_ERROR sözdizimi biraz gizemlidir, ancak testlerde girdi ve çıktı şekli belirtimlerini temsil ederken kompakt olmaya çalışın. Şimdilik, şekil dizisi belirtimi hakkında bir fikir edinmek için bu testlerdeki çevreleyen yorumlara bakın).

Özel operasyonunuz için bir pip paketi oluşturun

İşleminiz için bir pip paketi oluşturmak üzere tensorflow/özel işlem örneğine bakın. Bu kılavuz, TensorFlow'u kaynaktan oluşturmak yerine TensorFlow pip paketinden özel operasyonların nasıl oluşturulacağını gösterir.