Google I/O bir tamamlamadır! TensorFlow oturumlarını takip edin Oturumları görüntüleyin

Operasyon oluştur

Mevcut TensorFlow kitaplığı tarafından kapsanmayan bir operasyon oluşturmak istiyorsanız, önce operasyonu Python'da mevcut Python operasyonlarının veya işlevlerinin bir bileşimi olarak yazmayı denemenizi öneririz. Bu mümkün değilse, özel bir C++ işlemi oluşturabilirsiniz. Özel bir C++ işlemi oluşturmak istemenizin birkaç nedeni olabilir:

  • Operasyonunuzu mevcut operasyonların bir bileşimi olarak ifade etmek kolay veya mümkün değil.
  • İşleminizi mevcut ilkellerin bir bileşimi olarak ifade etmek verimli değil.
  • Gelecekteki bir derleyicinin birleştirmeyi zor bulacağı ilkellerin bir bileşimini elle birleştirmek istiyorsunuz.

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

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

  1. Yeni operasyonu bir C++ dosyasına kaydedin. Operasyon kaydı, operasyonun uygulamasından bağımsız olan, operasyonun işlevselliği için bir arayüz (şartname) tanımlar. Örneğin, işlem kaydı işlemin adını ve işlemin giriş ve çıkışlarını tanımlar. Ayrıca tensör şekil çıkarımı için kullanılan şekil fonksiyonunu da tanımlar.
  2. İşlemi C++ ile uygulayın. Bir işlemin uygulanması çekirdek olarak bilinir ve 1. Adımda kaydettiğiniz belirtimin somut uygulamasıdır. Farklı giriş/çıkış türleri veya mimariler (örneğin CPU'lar, GPU'lar) için birden çok çekirdek olabilir.
  3. Bir Python sarmalayıcı oluşturun (isteğe bağlı). Bu sarmalayıcı, Python'da op'u oluşturmak için kullanılan genel API'dir. Doğrudan kullanılabilen veya eklenebilen op kaydından varsayılan bir sarmalayıcı oluşturulur.
  4. İşlem için gradyanları hesaplamak için bir fonksiyon yazın (isteğe bağlı).
  5. İşlemi test edin. Bunu genellikle kolaylık olması için Python'da yaparız, ancak işlemi C++'da da test edebilirsiniz. Degradeleri tanımlarsanız, bunları Python tf.test.compute_gradient_error ile doğrulayabilirsiniz. Relu benzeri operatörlerin ileri fonksiyonlarını ve onların gradyanlarını test eden bir örnek olarak relu_op_test.py bakın.

Önkoşullar

op arayüzünü tanımlayın

Bir operasyonun arayüzünü, onu TensorFlow sistemine kaydederek tanımlarsınız. Kayıtta, operasyonunuzun adını, girdilerini (türler ve adlar) ve çıktılarını (türler ve adlar) ve ayrıca belge dizilerini ve operasyonun gerektirebileceği tüm özellikleri belirtirsiniz.

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

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

using namespace tensorflow;

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

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

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

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

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

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

using namespace tensorflow;

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

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

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

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

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

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

ZeroOut operasyonu için bunu yapmak için, ZeroOut zero_out.cc ekleyin:

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

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

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

GPU çekirdekleri

Bir GPU çekirdeği iki parça halinde uygulanır: OpKernel ve CUDA çekirdeği ve başlatma kodu.

Bazen OpKernel uygulaması, girdileri denetleme ve çıktıları ayırma gibi, bir CPU ve GPU çekirdeği arasında ortaktır. Bu durumda, önerilen bir uygulama şudur:

  1. Cihazda şablonlanmış OpKernel'i ve tensörün ilkel türünü tanımlayın.
  2. Çıktının gerçek hesaplamasını yapmak için, Hesaplama işlevi şablonlu bir işlev yapısını çağırır.
  3. Bu işlevin CPUDevice için uzmanlaşması aynı dosyada tanımlanır, ancak GPUDevice için uzmanlaşma CUDA derleyicisi ile derleneceği için bir .cu.cc dosyasında tanımlanır.

İşte örnek bir uygulama.

// kernel_example.h
#ifndef KERNEL_EXAMPLE_H_
#define KERNEL_EXAMPLE_H_

#include <unsupported/Eigen/CXX11/Tensor>

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

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

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

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

using namespace tensorflow;

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

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

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

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

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

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

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

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

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

using namespace tensorflow;

using GPUDevice = Eigen::GpuDevice;

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

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

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

#endif  // GOOGLE_CUDA

op kitaplığını oluşturun

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

zero_out.cc sisteminizde mevcut olan g++ veya clang gibi bir C++ derleyicisi ile derleyebilmelisiniz. İkili PIP paketi, op'unuzu derlemek için ihtiyaç duyduğunuz başlık dosyalarını ve kitaplığı sisteme özel konumlarda kurar. Bununla birlikte, TensorFlow python kitaplığı, başlık dizinini almak için get_include işlevini sağlar ve get_lib dizininin bağlantı kurulacak paylaşılan bir nesnesi vardır. İşte bir Ubuntu makinesinde bu işlevlerin çıktıları.

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

g++ ın kurulu olduğunu varsayarsak, operasyonunuzu dinamik bir kitaplıkta derlemek için kullanabileceğiniz komutların sırası aşağıdadır.

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

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

gcc sürümü >=5 ile ilgili not: gcc, sürüm 5 bu yana yeni C++ ABI'yi kullanır. TensorFlow web sitesinde bulunan ikili pip paketleri, eski gcc4 kullanan gcc4 ile oluşturulmuştur. Op kitaplığınızı gcc>=5 ile derlerseniz, kitaplığı eski abi ile uyumlu hale getirmek için komut satırına -D_GLIBCXX_USE_CXX11_ABI=0 ekleyin.

Operasyonu bazel kullanarak derleyin (TensorFlow kaynak kurulumu)

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

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

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

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

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

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

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

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

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

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

Python'da op'u kullanın

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

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

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

Oluşturulan işleve bir snake_case adı verileceğini unutmayın ( PEP8 ile uyumlu olması için). Bu nedenle, işleminiz C++ dosyalarında ZeroOut olarak adlandırılmışsa, python işlevi zero_out olarak adlandırılır.

Op'u bir Python modülünden import normal bir işlev olarak kullanılabilir kılmak için, bir Python kaynak dosyasında load_op_library çağrısının aşağıdaki gibi yapılması yararlı olabilir:

import tensorflow as tf

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

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

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

import tensorflow as tf

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

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

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

$ python zero_out_op_test.py

Operasyonunuza gelişmiş özellikler ekleyin

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

Koşullu kontroller ve doğrulama

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

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

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

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

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

operasyon kaydı

Özellikler

Ops, op bir grafiğe eklendiğinde değerleri belirlenen özniteliklere sahip olabilir. Bunlar op'u yapılandırmak için kullanılır ve değerlerine hem çekirdek uygulamasında hem de op kaydındaki giriş ve çıkış türlerinde erişilebilir. Girişler daha esnek olduğundan, mümkün olduğunda attr yerine giriş kullanmayı tercih edin. Bunun nedeni, özniteliklerin sabit olmaları ve grafik oluşturma zamanında tanımlanmaları gerektiğidir. Buna karşılık girdiler, değerleri dinamik olabilen Tensörlerdir; yani, girdiler her adımı değiştirebilir, bir besleme vb. kullanılarak ayarlanabilir. Öznitelikler, girdilerle yapılamayacak şeyler için kullanılır: imzayı etkileyen (giriş veya çıktıların sayısı veya türü) veya " t adımdan adıma değişir.

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

<name>: <attr-type-expr>

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

Örneğin, ZeroOut operasyonunun yalnızca 0. öğe yerine kullanıcı tarafından belirlenen bir dizini korumasını istiyorsanız, operasyonu şu şekilde kaydedebilirsiniz:

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

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

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

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

daha sonra Compute yönteminde kullanılabilir:

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

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

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

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

özellik türleri

Bir attr'de aşağıdaki türler desteklenir:

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

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

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

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

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

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

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

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

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

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

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

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

Bu işlem için:

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

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

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

Bu işlem için:

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

int >= <n> : Değer, değeri < <n> > değerinden büyük veya buna eşit olan bir int olmalıdır, burada <n> bir doğal sayıdır. Örneğin, aşağıdaki op kaydı, attr a öğesinin en az 2 değerinde olması gerektiğini belirtir:

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

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

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

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

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

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

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

Varsayılan değerin desteklenen sözdizimi, elde edilen GraphDef tanımının proto temsilinde kullanılacak olan şeydir.

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

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

Özellikle, tür type değerlerinin tf.DType kullandığını unutmayın.

polimorfizm

tip polimorfizmi

Farklı türleri girdi olarak alabilen veya farklı çıktı türleri üretebilen işlemler için, bir girdide bir attr veya op kaydında çıktı türü belirtebilirsiniz. Tipik olarak, desteklenen her tür için bir OpKernel kaydedersiniz.

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

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

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

adlandırma

Girişler, çıkışlar ve attr'lere genellikle snake_case isimleri verilmelidir. Bunun tek istisnası, bir girdinin türü olarak veya bir çıktının türünde kullanılan attr'lerdir. Bu özellikler, işlem grafiğe eklendiğinde çıkarılabilir ve bu nedenle işlemin işlevinde görünmez. Örneğin, ZeroOut'un bu son tanımı şuna benzeyen bir Python işlevi üretecektir:

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

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

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

Bunu, çıktı türünü belirleyen attr türüne sahip bir işlemle karşılaştırın:

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

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

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

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

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

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

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

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

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

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

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

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

Geriye dönük uyumluluğu korumak için mevcut bir operasyona attr eklerken varsayılan bir değer belirtmelisiniz:

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

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

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

Yukarıdaki gibi fazlalık kodla başka bir OpKernel yazmak yerine, bunun yerine genellikle bir C++ şablonu kullanabileceksiniz. Aşırı yük başına yine de bir çekirdek kaydınız ( REGISTER_KERNEL_BUILDER çağrısı) olacaktır.

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

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

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

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

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

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

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

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

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

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

#undef REGISTER_KERNEL

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

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

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

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

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

TF_CALL_REAL_NUMBER_TYPES(REGISTER_KERNEL);

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

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

Sonraki örnekte, attr T bir tür listesi tutar ve hem in hem de out türü olarak kullanılır. Giriş ve çıkış, bu tür tensörlerin listeleridir (ve çıkıştaki tensörlerin sayısı ve türleri, her ikisi de T türüne sahip olduğundan, girişle aynıdır).

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

Ayrıca listede hangi türlerin belirtilebileceği konusunda kısıtlamalar koyabilirsiniz. Bu sonraki durumda, girdi bir float ve double tensör listesidir. Op, örneğin giriş türlerini (float, double, float) kabul eder ve bu durumda çıktı türü de (float, double, float) olur.

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

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

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

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

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

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

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

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

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

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

Girdiler ve çıktılar

Yukarıdakileri özetlemek gerekirse, bir op kaydının birden çok girişi ve çıkışı olabilir:

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

Her girdi veya çıktı özelliği şu şekildedir:

<name>: <io-type-expr>

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

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

    Bkz. tf.DType .

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

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

    list(type) türünde bir özniteliğe başvurmak, bir dizi tensörü kabul etmenize olanak tanır.

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

    Her ikisi de T türünde olduğundan, çıkış out tensörlerin sayısı ve türlerinin girişteki in aynı olduğuna dikkat edin.

  • Aynı türe sahip bir tensör dizisi için: <number> * <type> , burada <number> int türündeki bir Attr'nin adıdır. <type> bir tf.DType olabilir veya type type ile bir attr 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 türden tensör listesini kabul eder:

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

Bir girdi türünde kullanılan herhangi bir öznitelik çıkarılacaktır. Kural olarak, bu çıkarsanan nitelikler büyük adlar kullanır ( T veya N gibi). Aksi takdirde girişler, çıkışlar ve attr'ler, işlev parametreleri gibi adlara sahiptir (örn. num_outputs ). Daha fazla ayrıntı için, adlandırmayla ilgili önceki bölüme bakın .

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

geriye dönük uyumluluk

Güzel, özel bir operasyon yazdığınızı ve başkalarıyla paylaştığınızı varsayalım, böylece operasyonunuzu kullanan mutlu müşterileriniz olur. Ancak, operasyonda bir şekilde değişiklik yapmak istersiniz.

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

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

  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 op, orijinal davranışa sahip olmalıdır. Bir işlemi polimorfik olmayandan polimorfik olarak değiştirmek için, orijinal imzayı varsayılan olarak korumak için yeni tür attr'ye varsayılan bir değer vermelisiniz . Örneğin, işleminiz şöyle olsaydı:

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

    şunu kullanarak geriye dönük uyumlu bir şekilde polimorfik yapabilirsiniz:

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

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

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

  5. Operasyon adlarını projenize özgü bir şeyle önekleyerek, oluşturduğunuz tüm yeni operasyonları adlandırın. Bu, operasyonunuzun gelecekteki TensorFlow sürümlerinde yer alabilecek operasyonlarla çakışmasını önler.

  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 içinde bulunabilir. Geriye dönük uyumlu bir işlemde yaptığınız değişikliği yapamıyorsanız, yeni semantik ile yeni bir adla yeni bir işlem oluşturun.

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

GPU desteği

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

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

Unutulmaması gereken bir şey, pad GPU çekirdek sürümü kullanıldığında bile, CPU belleğinde "paddings" girişine ihtiyaç duyar. Girişlerin veya çıkışların CPU'da tutulduğunu işaretlemek için çekirdek kaydına bir HostMemory() çağrısı ekleyin, örneğin:

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

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

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

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

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

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

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

Gradyanı Python'da uygulayın

Bir işlem grafiği verildiğinde, TensorFlow, mevcut işlemlere göre gradyanları temsil eden yeni işlemler eklemek için otomatik farklılaşmayı (geri yayılım) kullanır. Yeni operasyonlar için otomatik farklılaşmanın çalışmasını sağlamak için, operasyonların çıktılarına göre degradeler verilen operasyon girişlerine göre degradeleri hesaplayan bir degrade işlevi kaydetmeniz gerekir.

Matematiksel olarak, eğer bir op \(y = f(x)\) hesaplarsa, kayıtlı gradyan op, \(\partial L/ \partial y\) göre kayıp \(L\) gradyanlarını zincir kuralı yoluyla \(x\) göre \(\partial L/ \partial x\)\(y\) dönüştürür:

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

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

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

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

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

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

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

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

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

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

  • Operasyon için anlamlı bir gradyan yoksa, genellikle herhangi bir gradyan kaydetmeniz gerekmeyecektir ve op'un gradyanına hiçbir zaman ihtiyaç duyulmadığı sürece, iyi olacaksınız. Bazı durumlarda, bir işlemin iyi tanımlanmış bir gradyanı yoktur, ancak gradyanın hesaplanmasında yer alabilir. Burada sıfırları otomatik olarak geriye doğru yaymak için ops.NotDifferentiable kullanabilirsiniz.

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

C++'da şekil fonksiyonları

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

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

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

c->set_output(0, c->input(0)); ilk çıktının şeklinin ilk girdinin şekline ayarlanması gerektiğini bildirir. Çıktı, yukarıdaki örnekte olduğu gibi dizini tarafından seçilirse, set_output ikinci parametresi bir ShapeHandle nesnesi olmalıdır. Varsayılan yapıcısıyla boş bir ShapeHandle nesnesi oluşturabilirsiniz. idx indeksli bir giriş için ShapeHandle nesnesi, c->input(idx) ile elde edilebilir.

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

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

Bir şekil işlevi, bir girdinin şeklini de sınırlayabilir. Vektör şekli kısıtlamalı ZeroOut sürümü için şekil işlevi aşağıdaki gibi olacaktır:

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

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

İşleminiz birden çok girişe sahip polimorfikse, kontrol edilecek şekillerin sayısını belirlemek için InferenceContext üyelerini ve şekillerin hepsinin uyumlu olduğunu doğrulamak için Merge üyelerini kullanabilirsiniz (alternatif olarak, InferenceContext::GetAttr ile uzunlukları gösteren erişim nitelikleri, bu, op'un özelliklerine erişim sağlar).

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

Şekil çıkarımı isteğe bağlı bir özellik olduğundan ve tensörlerin şekilleri dinamik olarak değişebildiğinden, şekil fonksiyonları herhangi bir girdi için eksik şekil bilgisine karşı sağlam olmalıdır. InferenceContext Merge yöntemi, arayan kişinin, ikisinden biri veya her ikisi de tam bilgiye sahip olmasa bile, iki şeklin aynı olduğunu iddia etmesine olanak tanır. Şekil işlevleri, tüm temel TensorFlow işlemleri için tanımlanmıştır ve birçok farklı kullanım örneği sunar.

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

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

Karmaşık bir şekil fonksiyonunuz varsa, çeşitli girdi şekli kombinasyonlarının beklenen çıktı şekli kombinasyonlarını ürettiğini doğrulamak için bir test eklemeyi düşünmelisiniz. Bu testlerin nasıl yazılacağına dair örnekleri bazı temel operasyon testlerimizde görebilirsiniz. ( INFER_OK ve INFER_ERROR sözdizimi biraz şifrelidir, ancak testlerde giriş ve çıkış şekli özelliklerini temsil etmede kompakt olmaya çalışın. Şimdilik, şekil dizisi belirtimi hakkında bir fikir edinmek için bu testlerde çevreleyen yorumlara bakın).

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

Operasyonunuz için bir pip paketi oluşturmak için tensorflow/özel operasyon örneğine bakın. Bu kılavuz, kaynaktan TensorFlow oluşturmak yerine TensorFlow pip paketinden özel operasyonların nasıl oluşturulacağını gösterir.