Erstellen Sie eine op

Wenn Sie eine Operation erstellen möchten, die nicht in der vorhandenen TensorFlow-Bibliothek enthalten ist, empfehlen wir, zunächst die Operation in Python als Komposition vorhandener Python-Operationen oder -Funktionen zu schreiben. Wenn dies nicht möglich ist, können Sie eine benutzerdefinierte C ++ - Operation erstellen. Es gibt mehrere Gründe, warum Sie möglicherweise eine benutzerdefinierte C ++ - Operation erstellen möchten:

  • Es ist nicht einfach oder möglich, Ihre Operation als Zusammensetzung bestehender Operationen auszudrücken.
  • Es ist nicht effizient, Ihre Operation als Zusammensetzung vorhandener Grundelemente auszudrücken.
  • Sie möchten eine Komposition von Grundelementen von Hand verschmelzen, die für einen zukünftigen Compiler schwierig zu verschmelzen wäre.

Stellen Sie sich zum Beispiel vor, Sie möchten so etwas wie "Median Pooling" implementieren, ähnlich dem Operator "MaxPool", aber Mediane über Schiebefenster anstelle von Maximalwerten berechnen. Dies unter Verwendung einer Zusammenstellung von Vorgängen zu tun, ist möglicherweise möglich (z. B. unter Verwendung von ExtractImagePatches und TopK), ist jedoch möglicherweise nicht so leistungs- oder speichereffizient wie ein nativer Vorgang, bei dem Sie in einem einzelnen, zusammengeführten Vorgang etwas Klügeres ausführen können. Wie immer lohnt es sich in der Regel zunächst, mithilfe der Operatorkomposition auszudrücken, was Sie möchten, und nur dann eine neue Operation hinzuzufügen, wenn sich dies als schwierig oder ineffizient herausstellt.

Um Ihre benutzerdefinierte Operation zu integrieren, müssen Sie:

  1. Registrieren Sie die neue Operation in einer C ++ - Datei. Die Op-Registrierung definiert eine Schnittstelle (Spezifikation) für die Funktionalität des Op, die unabhängig von der Implementierung des Op ist. Beispielsweise definiert die Op-Registrierung den Namen des Op und die Ein- und Ausgänge des Op. Außerdem wird die Formfunktion definiert, die für die Tensorforminferenz verwendet wird.
  2. Implementieren Sie die Operation in C ++. Die Implementierung einer Operation wird als Kernel bezeichnet und ist die konkrete Implementierung der Spezifikation, die Sie in Schritt 1 registriert haben. Es können mehrere Kernel für verschiedene Eingabe- / Ausgabetypen oder Architekturen (z. B. CPUs, GPUs) vorhanden sein.
  3. Erstellen Sie einen Python-Wrapper (optional). Dieser Wrapper ist die öffentliche API, mit der die Operation in Python erstellt wird. Aus der Op-Registrierung wird ein Standard-Wrapper generiert, der direkt verwendet oder hinzugefügt werden kann.
  4. Schreiben Sie eine Funktion zum Berechnen von Verläufen für die Operation (optional).
  5. Testen Sie die op. Wir tun dies normalerweise aus Bequemlichkeitsgründen in Python, aber Sie können die Operation auch in C ++ testen. Wenn Sie Farbverläufe definieren, können Sie diese mit Python tf.test.compute_gradient_error . Siehe relu_op_test.py als Beispiel, das die Vorwärtsfunktionen von Relu-ähnlichen Operatoren und ihre Verläufe testet.

Voraussetzungen

Definieren Sie die Op-Schnittstelle

Sie definieren die Schnittstelle eines Op, indem Sie ihn beim TensorFlow-System registrieren. In der Registrierung geben Sie den Namen Ihrer Operation, ihre Eingaben (Typen und Namen) und Ausgaben (Typen und Namen) sowie Dokumentzeichenfolgen und alle für die Operation erforderlichen Attribute an .

Angenommen, Sie int32 eine int32 erstellen, die einen Tensor von int32 s verwendet und eine Kopie des Tensors ausgibt, wobei alle außer dem ersten Element auf Null gesetzt sind. Erstellen Sie dazu eine Datei mit dem Namen zero_out.cc . REGISTER_OP Makro REGISTER_OP einen Aufruf hinzu, der die Schnittstelle für Ihre REGISTER_OP definiert:

#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();
    });

Diese ZeroOut einen Tensor bis to_zero von 32-Bit-Ganzzahlen als Eingabe und gibt einen Tensor aus, der aus 32-Bit-Ganzzahlen auf zeroed . Die Operation verwendet auch eine Formfunktion, um sicherzustellen, dass der Ausgangstensor dieselbe Form wie der Eingangstensor hat. Wenn die Eingabe beispielsweise ein Tensor der Form ist [10, 20], gibt diese Formfunktion an, dass die Ausgabeform auch [10, 20] ist.

Implementieren Sie den Kernel für die Operation

Nachdem Sie die Schnittstelle definiert haben, stellen Sie eine oder mehrere Implementierungen der Operation bereit. Um einen dieser Kernel zu erstellen, erstellen Sie eine Klasse, die OpKernel und die Compute Methode überschreibt. Die Compute Methode bietet ein context vom Typ OpKernelContext* , über das Sie auf nützliche Dinge wie die Eingabe- und Ausgabe-Tensoren zugreifen können.

Fügen Sie Ihren Kernel zu der oben erstellten Datei hinzu. Der Kernel könnte ungefähr so ​​aussehen:

#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);
  }
};

Nachdem Sie Ihren Kernel implementiert haben, registrieren Sie ihn beim TensorFlow-System. In der Registrierung geben Sie verschiedene Einschränkungen an, unter denen dieser Kernel ausgeführt wird. Beispielsweise könnte ein Kernel für CPUs und ein separater für GPUs erstellt werden.

ZeroOut Sie dazu für die ZeroOut Folgendes zu zero_out.cc :

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

Multithread-CPU-Kernel

Zum Schreiben eines Multithread-CPU-Kernels kann die Shard-Funktion in work_sharder.h verwendet werden. Diese Funktion speichert eine Berechnungsfunktion für alle Threads, die für das Intra-Op-Threading konfiguriert sind (siehe intra_op_parallelism_threads in config.proto ).

GPU-Kernel

Ein GPU-Kernel besteht aus zwei Teilen: dem OpKernel und dem CUDA-Kernel sowie seinem Startcode.

Manchmal ist die OpKernel-Implementierung zwischen einem CPU- und einem GPU-Kernel üblich, z. B. beim Überprüfen von Eingaben und beim Zuweisen von Ausgaben. In diesem Fall wird eine Implementierung vorgeschlagen:

  1. Definieren Sie den OpKernel-Template auf dem Gerät und den primitiven Typ des Tensors.
  2. Um die eigentliche Berechnung der Ausgabe durchzuführen, ruft die Compute-Funktion eine Funktionsfunktion mit Vorlagen auf.
  3. Die Spezialisierung dieses Funktors für das CPUDevice ist in derselben Datei definiert, die Spezialisierung für das GPUDevice ist jedoch in einer .cu.cc-Datei definiert, da sie mit dem CUDA-Compiler kompiliert wird.

Hier ist eine Beispielimplementierung.

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

Erstellen Sie die Op-Bibliothek

Kompilieren Sie die Operation mit Ihrem System-Compiler (TensorFlow-Binärinstallation).

Sie sollten in der Lage sein, zero_out.cc mit einem C++ Compiler wie g++ oder clang zu kompilieren, der auf Ihrem System verfügbar ist. Das binäre PIP-Paket installiert die Header-Dateien und die Bibliothek, die Sie zum Kompilieren Ihrer Operation an systemspezifischen Speicherorten benötigen. Die TensorFlow-Python-Bibliothek bietet jedoch die Funktion get_include zum get_include des Header-Verzeichnisses, und das Verzeichnis get_lib verfügt über ein freigegebenes Objekt, mit dem eine Verknüpfung hergestellt werden kann. Hier sind die Ausgaben dieser Funktionen auf einem Ubuntu-Computer.

$ 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'

Angenommen, Sie haben g++ installiert, finden Sie hier die Befehlsfolge, mit der Sie Ihre Operation in eine dynamische Bibliothek kompilieren können.

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++11 -shared zero_out.cc -o zero_out.so -fPIC ${TF_CFLAGS[@]} ${TF_LFLAGS[@]} -O2

Unter macOS ist beim .so der .so Datei das zusätzliche Flag "-undefined dynamic_lookup" erforderlich.

Hinweis zur gcc Version >=5 : gcc verwendet seit Version 5 das neue C ++ ABI . Die auf der TensorFlow-Website verfügbaren binären Pip-Pakete werden mit gcc4 , das den älteren ABI verwendet. Wenn Sie Ihre Op-Bibliothek mit gcc>=5 kompilieren, fügen Sie -D_GLIBCXX_USE_CXX11_ABI=0 zur Befehlszeile hinzu, um die Bibliothek mit dem älteren abi kompatibel zu machen.

Kompilieren Sie die Operation mit Bazel (TensorFlow-Quellinstallation)

Wenn Sie TensorFlow-Quellen installiert haben, können Sie das Build-System von TensorFlow verwenden, um Ihre Operation zu kompilieren. Platzieren Sie eine BUILD-Datei mit der folgenden Bazel-Build-Regel im tensorflow/core/user_ops .

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

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

Führen Sie den folgenden Befehl aus, um zero_out.so zu zero_out.so .

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

Für die Zusammenstellung Example des Betriebs mit dem CUDA Kernel, müssen Sie die verwenden gpu_srcs Parameter von tf_custom_op_library . Platzieren Sie eine BUILD-Datei mit der folgenden Bazel-Build-Regel in einem neuen Ordner im tensorflow/core/user_ops (z. B. "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"],
)

Führen Sie den folgenden Befehl aus, um kernel_example.so zu kernel_example.so .

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

Verwenden Sie die Operation in Python

Die TensorFlow Python-API bietet die Funktion tf.load_op_library zum Laden der dynamischen Bibliothek und zum Registrieren der tf.load_op_library beim TensorFlow-Framework. load_op_library gibt ein Python-Modul zurück, das die Python-Wrapper für op und den Kernel enthält. Sobald Sie die Operation erstellt haben, können Sie Folgendes tun, um sie in Python auszuführen:

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)

Beachten Sie, dass die generierte Funktion einen snake_case-Namen erhält (um PEP8 zu entsprechen). Wenn Ihre ZeroOut in den C ++ - Dateien zero_out heißt, heißt die Python-Funktion zero_out .

Um die Operation als reguläre Funktion verfügbar zu machen, die aus einem Python-Modul import werden kann, kann es hilfreich sein, den Aufruf load_op_library in einer Python-Quelldatei wie folgt zu haben:

import tensorflow as tf

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

Stellen Sie sicher, dass die Operation funktioniert

Eine gute Möglichkeit, um zu überprüfen, ob Sie Ihre Operation erfolgreich implementiert haben, besteht darin, einen Test dafür zu schreiben. Erstellen Sie die Datei zero_out_op_test.py mit dem Inhalt:

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()

Führen Sie dann Ihren Test aus (vorausgesetzt, Sie haben Tensorflow installiert):

$ python zero_out_op_test.py

Bauen Sie erweiterte Funktionen in Ihre Operation ein

Nachdem Sie nun wissen, wie Sie eine grundlegende (und etwas eingeschränkte) Operation und Implementierung erstellen, werden wir uns einige der komplizierteren Dinge ansehen, die Sie normalerweise in Ihre Operation einbauen müssen. Das beinhaltet:

Bedingte Prüfungen und Validierung

Im obigen Beispiel wurde angenommen, dass die Operation auf einen Tensor beliebiger Form angewendet wird. Was ist, wenn es nur auf Vektoren angewendet wird? Dies bedeutet, dass der obigen OpKernel-Implementierung eine Prüfung hinzugefügt wird.

  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."));
    // ...
  }

Dies bestätigt, dass die Eingabe ein Vektor ist, und gibt zurück, nachdem der InvalidArgument Status festgelegt wurde, falls dies nicht der InvalidArgument ist. Das Makro OP_REQUIRES drei Argumente:

Alternativ, wenn Sie testen wollen , ob ein Status - Objekt von einer Funktion zurückgegeben wird , ist ein Fehler, und wenn ja zurückgeben, verwenden OP_REQUIRES_OK . Diese beiden Makros kehren bei einem Fehler von der Funktion zurück.

Op Registrierung

Attrs

Ops können attrs haben, deren Werte festgelegt werden, wenn die Operation zu einem Diagramm hinzugefügt wird. Diese werden zum Konfigurieren der Operation verwendet, und auf ihre Werte kann sowohl innerhalb der Kernel-Implementierung als auch in den Arten von Ein- und Ausgängen in der Operationsregistrierung zugegriffen werden. Verwenden Sie nach Möglichkeit lieber einen Eingang als einen attr, da die Eingänge flexibler sind. Dies liegt daran, dass attrs Konstanten sind und zum Zeitpunkt der Diagrammkonstruktion definiert werden müssen. Im Gegensatz dazu sind Eingaben Tensoren, deren Werte dynamisch sein können. Das heißt, Eingaben können jeden Schritt ändern, mithilfe eines Feeds festgelegt werden usw. Attribute werden für Dinge verwendet, die mit Eingaben nicht ausgeführt werden können: jede Konfiguration, die sich auf die Signatur auswirkt (Anzahl oder Art der Eingaben oder Ausgaben) oder die kann ' t von Schritt zu Schritt wechseln.

Sie definieren ein attr, wenn Sie das op registrieren, indem Sie seinen Namen und Typ mit der Attr Methode Attr , die eine Spezifikation des Formulars erwartet:

<name>: <attr-type-expr>

Dabei beginnt <name> mit einem Buchstaben und kann aus alphanumerischen Zeichen und Unterstrichen bestehen. <attr-type-expr> ist ein <attr-type-expr> der unten beschriebenen Form.

Wenn Sie beispielsweise ZeroOut , dass die ZeroOut einen benutzerdefinierten Index anstelle nur des 0. Elements ZeroOut , können Sie die ZeroOut registrieren:

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

(Beachten Sie, dass sich der Satz von Attributtypen von dem für Ein- und Ausgänge verwendetentf.DType unterscheidet.)

Ihr Kernel kann dann über den context auf dieses Attribut in seinem Konstruktor zugreifen:

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_;
};

die dann in der Compute Methode verwendet werden kann:

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

Die folgenden Typen werden in einem attr unterstützt:

  • string : Beliebige Folge von Bytes (muss nicht UTF8 sein).
  • int : Eine vorzeichenbehaftete Ganzzahl.
  • float : Eine Gleitkommazahl.
  • bool : Richtig oder falsch.
  • type : Einer der (nicht ref) Werte von DataType .
  • shape : Ein TensorShapeProto .
  • list(<type>) : Eine Liste von <type> , wobei <type> einer der oben genannten Typen ist. Beachten Sie, dass die list(list(<type>)) ungültig ist.

Siehe auch: op_def_builder.cc:FinalizeAttr für eine endgültige Liste.

Standardwerte und Einschränkungen

Attrs können Standardwerte haben, und einige Arten von Attrs können Einschränkungen haben. Um ein attr mit Einschränkungen zu definieren, können Sie die folgenden <attr-type-expr> s verwenden:

{'<string1>', '<string2>'} : Der Wert muss ein String sein, der entweder den Wert <string1> oder <string2> . Der Name des Typs, string , wird impliziert, wenn Sie diese Syntax verwenden. Dies emuliert eine Aufzählung:

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

{<type1>, <type2>} : Der Wert ist vom Typ type und muss einer von <type1> oder <type2> , wobei <type1> und <type2> von tf.DType unterstützt tf.DType . Sie geben nicht an, dass der Typ des attr type . Dies ist impliziert, wenn Sie eine Liste von Typen in {...} . In diesem Fall ist attr t beispielsweise ein Typ, der ein int32 , ein float oder ein bool :

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

Es gibt Verknüpfungen für allgemeine Typeinschränkungen:

  • numbertype : Typ type in den numerischen (Nicht-String und nicht-Bool) Typen beschränkt.
  • realnumbertype : Wie numbertype ohne komplexe Typen.
  • quantizedtype numbertype : Wie numbertype jedoch nur die quantisierten numbertype .

Die spezifischen Listen der von diesen zugelassenen Typen werden durch die Funktionen (wie NumberTypes() ) in tensorflow/core/framework/types.h . In diesem Beispiel ist die attr t muss eine der numerischen Typen sein:

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

Für diese Operation:

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

Listen können mit anderen Listen und einzelnen Typen kombiniert werden. Mit der folgenden Option kann attr t einer der numerischen Typen oder der Bool-Typ sein:

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

Für diese Operation:

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> : Der Wert muss ein int sein, dessen Wert größer oder gleich <n> , wobei <n> eine natürliche Zahl ist. Die folgende op-Registrierung gibt beispielsweise an, dass das attr a einen Wert von mindestens 2 :

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

list(<type>) >= <n> : Eine Liste vom Typ <type> deren Länge größer oder gleich <n> . Die folgende op-Registrierung gibt beispielsweise an, dass attr a eine Liste von Typen ist (entweder int32 oder float ) und dass mindestens drei davon vorhanden sein müssen:

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

Um einen Standardwert für ein attr festzulegen (der im generierten Code optional ist), fügen Sie am Ende = <default> , wie in:

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

Darüber hinaus können sowohl eine Einschränkung als auch ein Standardwert angegeben werden:

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

Die unterstützte Syntax des Standardwerts wird in der Protodarstellung der resultierenden GraphDef-Definition verwendet.

Hier sind Beispiele für die Angabe eines Standardwerts für alle Typen:

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]");

Beachten Sie insbesondere, dass die Werte vom Typ typetf.DType .

Polymorphismus

Typ Polymorphismus

Für Operationen, die unterschiedliche Typen als Eingabe verwenden oder unterschiedliche Ausgabetypen erzeugen können, können Sie in der Eingabe- Registrierung einen attr in einem Eingabe- oder Ausgabetyp angeben. Normalerweise registrieren Sie dann einen OpKernel für jeden unterstützten Typ.

Zum Beispiel, wenn Sie das mögen ZeroOut op Arbeit auf float s zusätzlich zu int32 s, könnte Ihre op Registrierung wie folgt aussehen:

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

Ihre Op-Registrierung gibt jetzt an, dass der Typ der Eingabe float oder int32 muss und dass die Ausgabe vom gleichen Typ ist, da beide den Typ T .

Benennung

Eingaben, Ausgaben und Attribute sollten im Allgemeinen mit snake_case-Namen versehen werden. Die einzige Ausnahme sind Attribute, die als Typ einer Eingabe oder als Typ einer Ausgabe verwendet werden. Diese Attribute können abgeleitet werden, wenn die Operation zum Diagramm hinzugefügt wird, und erscheinen daher nicht in der Funktion der Operation. Diese letzte Definition von ZeroOut generiert beispielsweise eine Python-Funktion, die wie folgt aussieht:

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`.
  """

Wenn to_zero ein int32 Tensor übergeben wird, wird T automatisch auf int32 (also tatsächlich DT_INT32 ). Diese abgeleiteten Attribute erhalten Großbuchstaben oder CamelCase-Namen.

Vergleichen Sie dies mit einer Operation mit einem Typ attr, der den Ausgabetyp bestimmt:

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");

In diesem Fall muss der Benutzer den Ausgabetyp wie im generierten Python angeben:

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`.
  """
Beispiel für Typpolymorphismus
#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);

Um die Abwärtskompatibilität zu gewährleisten , sollten Sie beim Hinzufügen eines attr zu einer vorhandenen Operation einen Standardwert angeben:

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

Angenommen, Sie möchten weitere Typen hinzufügen, z. B. double :

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

Anstatt wie oben beschrieben einen anderen OpKernel mit redundantem Code zu schreiben, können Sie häufig stattdessen eine C ++ - Vorlage verwenden. Sie haben weiterhin eine Kernelregistrierung ( REGISTER_KERNEL_BUILDER Aufruf) pro Überladung.

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>);

Wenn Sie mehr als ein paar Überladungen haben, können Sie die Registrierung in ein Makro einfügen.

#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

Abhängig von der Liste der Typen, für die Sie den Kernel registrieren, können Sie möglicherweise ein Makro verwenden, das von tensorflow/core/framework/register_types.h bereitgestellt wird:

#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
Listen Sie die Ein- und Ausgänge auf

Ops können nicht nur verschiedene Typen akzeptieren oder produzieren, sondern auch eine variable Anzahl von Tensoren verbrauchen oder produzieren.

Im nächsten Beispiel wird die attr T hält eine Liste von Typen, und wird als die Art des sowohl den Eingangs verwendet in und dem Ausgang out . Die Eingabe und Ausgabe sind Listen von Tensoren dieses Typs (und die Anzahl und Art der Tensoren in der Ausgabe sind die gleichen wie die Eingabe, da beide den Typ T ).

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

Sie können auch Einschränkungen festlegen, welche Typen in der Liste angegeben werden können. In diesem nächsten Fall ist die Eingabe eine Liste von float und double . Die Operation akzeptiert zum Beispiel Eingabetypen (float, double, float) und in diesem Fall wäre der Ausgabetyp auch (float, double, float) .

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

Wenn Sie möchten, dass alle Tensoren in einer Liste vom gleichen Typ sind, können Sie Folgendes tun:

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

Dies akzeptiert eine Liste von int32 Tensoren und verwendet ein int attr N , um die Länge der Liste anzugeben.

Dies kann auch typpolymorph gemacht werden. Im nächsten Beispiel ist die Eingabe eine Liste von Tensoren (mit der Länge "N" ) desselben (aber nicht spezifizierten) Typs ( "T" ), und die Ausgabe ist ein einzelner Tensor vom passenden Typ:

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

Standardmäßig haben Tensorlisten eine Mindestlänge von 1. Sie können diese Standardeinstellung mit einer ">=" Einschränkung für das entsprechende Attribut ändern . In diesem nächsten Beispiel ist die Eingabe eine Liste von mindestens 2 int32 Tensoren:

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

Die gleiche Syntax funktioniert mit "list(type)" attrs:

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

Eingänge und Ausgänge

Um das Obige zusammenzufassen, kann eine Op-Registrierung mehrere Ein- und Ausgänge haben:

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

Jede Eingabe- oder Ausgabespezifikation hat die Form:

<name>: <io-type-expr>

Dabei beginnt <name> mit einem Buchstaben und kann aus alphanumerischen Zeichen und Unterstrichen bestehen. <io-type-expr> ist einer der folgenden <io-type-expr> :

  • <type> , wobei <type> ein unterstützter Eingabetyp ist (z. B. float , int32 , string ). Dies gibt einen einzelnen Tensor des angegebenen Typs an.

    Siehetf.DType .

    REGISTER_OP("BuiltInTypesExample")
        .Input("integers: int32")
        .Input("complex_numbers: complex64");
    
  • <attr-type> , wobei <attr-type> ist der Name eines Attr mit Typ - type oder list(type) (mit einer möglichen Art Einschränkung). Diese Syntax ermöglicht polymorphe Operationen .

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

    Wenn Sie auf ein Attribut der Typliste list(type) verweisen, können Sie eine Folge von Tensoren akzeptieren.

    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");
    

    Man beachte , dass die Anzahl und die Typen von Tensoren in der Ausgabe out ist die gleiche wie in der Eingabe in , da beide vom Typ sind T .

  • Für eine Folge von Tensoren mit demselben Typ: <number> * <type> , wobei <number> der Name eines Attr mit dem Typ int . Die <type> kann entweder eine seintf.DType , oder der Name eines attr mit Typ - type . Als Beispiel für die erste akzeptiert diese int32 eine Liste von int32 Tensoren:

    REGISTER_OP("Int32SequenceExample")
        .Attr("NumTensors: int")
        .Input("in: NumTensors * int32")
    

    Während diese Operation eine Liste von Tensoren jeglicher Art akzeptiert, solange sie alle gleich sind:

    REGISTER_OP("SameTypeSequenceExample")
        .Attr("NumTensors: int")
        .Attr("T: type")
        .Input("in: NumTensors * T")
    
  • Für einen Verweis auf einen Tensor: Ref(<type>) , wobei <type> einer der vorherigen Typen ist.

Jeder attr, der für den Typ einer Eingabe verwendet wird, wird abgeleitet. Konventionell verwenden diese abgeleiteten Attribute Großbuchstaben (wie T oder N ). Ansonsten haben Eingänge, Ausgänge und Attribute Namen wie Funktionsparameter (z. B. num_outputs ). Weitere Einzelheiten finden Sie im vorherigen Abschnitt zur Benennung .

Weitere Informationen finden Sie unter tensorflow/core/framework/op_def_builder.h .

Abwärtskompatibilität

Nehmen wir an, Sie haben eine nette, benutzerdefinierte Operation geschrieben und mit anderen geteilt, damit Sie zufriedene Kunden haben, die Ihren Betrieb nutzen. Sie möchten jedoch auf irgendeine Weise Änderungen an der Operation vornehmen.

Im Allgemeinen müssen Änderungen an vorhandenen, eingecheckten Spezifikationen abwärtskompatibel sein: Durch Ändern der Spezifikation einer GraphDef dürfen zuvor serialisierte GraphDef Protokollpuffer, die aus älteren Spezifikationen erstellt wurden, nicht GraphDef . Die Details der GraphDef Kompatibilität werden hier beschrieben .

Es gibt verschiedene Möglichkeiten, die Abwärtskompatibilität zu gewährleisten.

  1. Für alle neuen Attribute, die einer Operation hinzugefügt werden, müssen Standardwerte definiert sein, und mit diesem Standardwert muss die Operation das ursprüngliche Verhalten haben. Um eine Operation von nicht polymorph in polymorph zu ändern, müssen Sie dem neuen Typ attr einen Standardwert zuweisen, um die ursprüngliche Signatur standardmäßig beizubehalten. Zum Beispiel, wenn Ihre Operation war:

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

    Sie können es auf abwärtskompatible Weise polymorph machen, indem Sie:

    REGISTER_OP("MyGeneralUnaryOp")
        .Input("in: T")
        .Output("out: T")
        .Attr("T: numerictype = DT_FLOAT");
    
  2. Sie können eine Einschränkung für ein Attribut sicher weniger restriktiv machen. Sie können beispielsweise von {int32, int64} zu {int32, int64, float} oder type {int32, int64, float} . Oder Sie können von {"apple", "orange"} zu {"apple", "banana", "orange"} oder string wechseln.

  3. Sie können einzelne Ein- / Ausgänge in Listenein- / -ausgänge ändern, sofern die Standardeinstellung für den Listentyp mit der alten Signatur übereinstimmt.

  4. Sie können eine neue Listeneingabe / -ausgabe hinzufügen, wenn diese standardmäßig leer ist.

  5. Benennen Sie alle neuen Operationen, die Sie erstellen, mit einem Namespace, indem Sie den Op-Namen etwas Einzigartiges für Ihr Projekt voranstellen. Dadurch wird vermieden, dass Ihre Operation mit Operationen kollidiert, die möglicherweise in zukünftigen Versionen von TensorFlow enthalten sind.

  6. Vorausplanen! Versuchen Sie, zukünftige Verwendungen für die Operation zu antizipieren. Einige Signaturänderungen können nicht auf kompatible Weise vorgenommen werden (z. B. eine Liste desselben Typs in eine Liste unterschiedlicher Typen umwandeln).

Die vollständige Liste der sicheren und unsicheren Änderungen finden Sie in tensorflow/core/framework/op_compatibility_test.cc . Wenn Sie Ihre Änderung an einer Operation nicht abwärtskompatibel machen können, erstellen Sie eine neue Operation mit einem neuen Namen mit der neuen Semantik.

Beachten Sie auch, dass diese Änderungen zwar die GraphDef Kompatibilität beibehalten GraphDef , der generierte Python-Code sich jedoch möglicherweise auf eine Weise ändert, die nicht mit alten Anrufern kompatibel ist. Die Python-API kann durch sorgfältige Änderungen in einem handgeschriebenen Python-Wrapper kompatibel gehalten werden, indem die alte Signatur beibehalten wird, außer möglicherweise neue optionale Argumente am Ende hinzuzufügen. Im Allgemeinen können inkompatible Änderungen nur vorgenommen werden, wenn TensorFlow Hauptversionen ändert und der Semantik der GraphDef Version entsprechen muss.

GPU-Unterstützung

Sie können verschiedene OpKernels implementieren und einen für die CPU und einen für die GPU registrieren , genauso wie Sie Kernel für verschiedene Typen registrieren können . Es gibt mehrere Beispiele für Kernel mit GPU-Unterstützung in tensorflow/core/kernels/ . Beachten Sie, dass einige Kernel eine CPU-Version in einer .cc Datei, eine GPU-Version in einer Datei mit der Endung _gpu.cu.cc und einen Code haben, der in einer .h Datei gemeinsam .h wird.

Zum Beispiel hat das tf.pad alles außer dem GPU-Kernel in tensorflow/core/kernels/pad_op.cc . Der GPU-Kernel befindet sich in tensorflow/core/kernels/pad_op_gpu.cu.cc , und der gemeinsam genutzte Code ist eine Vorlagenklasse, die in tensorflow/core/kernels/pad_op.h . Wir organisieren den Code auf diese Weise aus zwei Gründen: Sie können gemeinsamen Code für die CPU- und GPU-Implementierungen freigeben und die GPU-Implementierung in einer separaten Datei ablegen, sodass sie nur vom GPU-Compiler kompiliert werden kann.

Eine Sache zu beachten, selbst wenn die GPU-Kernel-Version des pad verwendet wird, benötigt es immer noch seine "paddings" im CPU-Speicher. HostMemory() der Kernelregistrierung einen HostMemory() hinzu, um zu kennzeichnen, dass Ein- oder Ausgänge in der CPU HostMemory() Beispiel:

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

Kompilieren des Kernels für das GPU-Gerät

In cuda_op_kernel.cu.cc finden Sie ein Beispiel, das einen CUDA-Kernel zum Implementieren einer Operation verwendet. Die tf_custom_op_library akzeptiert ein gpu_srcs Argument, in dem die Liste der Quelldateien angegeben werden kann, die die CUDA-Kernel ( *.cu.cc Dateien) enthalten. Für die Verwendung mit einer nvcc von TensorFlow müssen die CUDA-Kernel mit dem nvcc Compiler von NVIDIA kompiliert werden. Hier ist die Befehlsfolge , mit der Sie cuda_op_kernel.cu.cc und cuda_op_kernel.cc in einer einzigen dynamisch ladbaren Bibliothek kompilieren können:

nvcc -std=c++11 -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++11 -shared -o cuda_op_kernel.so cuda_op_kernel.cc \
  cuda_op_kernel.cu.o ${TF_CFLAGS[@]} -fPIC -lcudart ${TF_LFLAGS[@]}

cuda_op_kernel.so das oben erstellt wurde, kann wie gewohnt in Python mit der Funktion tf.load_op_library werden.

Beachten Sie, dass Sie den Pfad im zweiten Befehl (g ++) oben explizit angeben müssen, wenn Ihre CUDA-Bibliotheken nicht in /usr/local/lib64 installiert sind. -L /usr/local/cuda-8.0/lib64/ Sie beispielsweise -L /usr/local/cuda-8.0/lib64/ wenn Ihre CUDA in /usr/local/cuda-8.0 installiert /usr/local/cuda-8.0 .

Implementieren Sie den Farbverlauf in Python

Bei einem Diagramm der Operationen verwendet TensorFlow die automatische Differenzierung (Backpropagation), um neue Operationen hinzuzufügen, die Gradienten in Bezug auf die vorhandenen Operationen darstellen. Damit die automatische Differenzierung für neue Operationen funktioniert, müssen Sie eine Gradientenfunktion registrieren, die Gradienten in Bezug auf die Eingaben der Operationen berechnet, wobei Gradienten in Bezug auf die Ausgaben der Operationen angegeben werden.

Wenn ein Op \(y = f(x)\) berechnet, wandelt der registrierte Gradient op den Gradienten \(\partial L/ \partial y\) des Verlusts \(L\) in Bezug auf \(y\) in Gradienten \(\partial L/ \partial x\) in Bezug auf \(x\) über die Kettenregel um:

$$\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}.$$

Im Fall von ZeroOut sich nur ein Eintrag in der Eingabe auf die Ausgabe aus, sodass der Gradient in Bezug auf die Eingabe ein spärlicher "One Hot" -Tensor ist. Dies wird wie folgt ausgedrückt:

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

Details zum Registrieren von Verlaufsfunktionen mit tf.RegisterGradient :

  • Für eine tf.Operation mit einem Ausgang nimmt die Gradientenfunktion eine tf.Operation , op und eine tf.Tensor grad und erstellt neue Operationen aus den Tensoren op.inputs[i] , op.outputs[i] und grad . Informationen zu attrs finden Sie unter tf.Operation.get_attr .

  • Wenn der Op mehrere Ausgänge hat, nimmt die Verlaufsfunktion op und grads , wobei grads eine Liste von Verläufen in Bezug auf jeden Ausgang ist. Das Ergebnis der Verlaufsfunktion muss eine Liste von Tensor , die die Farbverläufe in Bezug auf jede Eingabe darstellen.

  • Wenn für einige Eingaben kein genau definierter Gradient vorhanden ist, z. B. für ganzzahlige Eingaben, die als Indizes verwendet werden, sollte der entsprechende zurückgegebene Gradient None . Zum Beispiel würde für eine return [x_grad, None] die einen Gleitkomma-Tensor x und einen ganzzahligen Index i , die Gradientenfunktion return [x_grad, None] .

  • Wenn es für die Operation überhaupt keinen aussagekräftigen Farbverlauf gibt, müssen Sie häufig keinen Farbverlauf registrieren. Solange der Farbverlauf der Operation nie benötigt wird, geht es Ihnen gut. In einigen Fällen hat eine Operation keinen genau definierten Gradienten, kann jedoch an der Berechnung des Gradienten beteiligt sein. Hier können Sie ops.NotDifferentiable , um Nullen automatisch rückwärts zu verbreiten.

Beachten Sie, dass zum Zeitpunkt des Aufrufs der Gradientenfunktion nur das Datenflussdiagramm von ops verfügbar ist, nicht die Tensordaten selbst. Daher müssen alle Berechnungen unter Verwendung anderer Tensorflow-Operationen durchgeführt werden, um zur Ausführungszeit des Graphen ausgeführt zu werden.

Formfunktionen in C ++

Die TensorFlow-API verfügt über eine Funktion namens "Forminferenz", die Informationen zu den Formen von Tensoren bereitstellt, ohne dass das Diagramm ausgeführt werden muss. Die Forminferenz wird von "Formfunktionen" unterstützt, die für jeden Op-Typ in der C ++ REGISTER_OP Deklaration registriert sind und zwei Rollen ausführen: Feststellen, dass die Formen der Eingaben während der Diagrammkonstruktion kompatibel sind, und Festlegen der Formen für die Ausgaben.

Formfunktionen werden als Operationen für die Klasse shape_inference::InferenceContext . Zum Beispiel in der Formfunktion für ZeroOut:

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

c->set_output(0, c->input(0)); deklariert, dass die Form der ersten Ausgabe auf die Form der ersten Eingabe gesetzt werden soll. Wenn die Ausgabe wie im obigen Beispiel anhand ihres Index ausgewählt wird, sollte der zweite Parameter von set_output ein ShapeHandle Objekt sein. Sie können ein leeres ShapeHandle Objekt mit seinem Standardkonstruktor erstellen. Das ShapeHandle Objekt für eine Eingabe mit dem Index idx kann durch c->input(idx) abgerufen werden.

Es gibt eine Reihe allgemeiner Formfunktionen, die für viele shape_inference::UnchangedShape gelten, z. B. shape_inference::UnchangedShape , die in common_shape_fns.h zu finden sind und wie folgt verwendet werden:

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

Eine Formfunktion kann auch die Form einer Eingabe einschränken. Für die Version von ZeroOut mit einer Vektorformbeschränkung lautet die Formfunktion wie folgt:

    .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();
    });

Der WithRank Aufruf WithRank , ob die Eingabeform c->input(0) eine Form mit genau einer Dimension hat (oder wenn die Eingabeform unbekannt ist, ist die Ausgabeform ein Vektor mit einer unbekannten Dimension).

Wenn Ihre Operation polymorph mit mehreren Eingaben ist , können Sie Mitglieder von InferenceContext , um die Anzahl der zu überprüfenden Formen zu bestimmen, und Merge , um zu überprüfen, ob alle Formen kompatibel sind. Alternativ können Sie mit InferenceContext::GetAttr auf Attribute zugreifen, die die Längen InferenceContext::GetAttr . die Zugriff auf die Attribute der op bietet).

    .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();
    });

Da die Forminferenz ein optionales Merkmal ist und die Formen von Tensoren dynamisch variieren können, müssen Formfunktionen für unvollständige Forminformationen für jede der Eingaben robust sein. Mit der Merge Methode in InferenceContext kann der Aufrufer bestätigen, dass zwei Formen identisch sind, auch wenn eine oder beide keine vollständigen Informationen enthalten. Formfunktionen sind für alle TensorFlow-Kernoperationen definiert und bieten viele verschiedene Anwendungsbeispiele.

Die InferenceContext Klasse verfügt über eine Reihe von Funktionen, mit denen Formfunktionsmanipulationen definiert werden können. Mit InferenceContext::Dim und InferenceContext::WithValue können Sie beispielsweise InferenceContext::WithValue , ob eine bestimmte Dimension einen ganz bestimmten Wert InferenceContext::WithValue . Mit InferenceContext::Add und InferenceContext::Multiply können Sie angeben, dass eine Ausgabedimension die Summe / das Produkt zweier Eingabedimensionen ist. In der InferenceContext Klasse finden Sie alle verschiedenen Formmanipulationen, die Sie angeben können. Im folgenden Beispiel wird die Form der ersten Ausgabe auf (n, 3) gesetzt, wobei die erste Eingabe die Form (n, ...) hat.

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

Wenn Sie eine komplizierte Formfunktion haben, sollten Sie einen Test hinzufügen, um zu überprüfen, ob verschiedene Eingabeformkombinationen die erwarteten Ausgabeformkombinationen ergeben. Beispiele zum Schreiben dieser Tests finden Sie in einigen unserer Core-Ops-Tests . (Die Syntax von INFER_OK und INFER_ERROR ist etwas kryptisch, aber versuchen Sie, die Eingabe- und Ausgabeformspezifikationen in Tests kompakt darzustellen. Sehen Sie sich zunächst die umgebenden Kommentare in diesen Tests an, um einen Eindruck von der INFER_ERROR zu erhalten.)

Erstellen Sie ein Pip-Paket für Ihre benutzerdefinierte Operation

Informationen zum Erstellen eines pip Pakets für Ihre Operation finden Sie im Beispiel für Tensorflow / Custom-Op . Diese Anleitung zeigt, wie Sie benutzerdefinierte Operationen aus dem TensorFlow-Pip-Paket erstellen, anstatt TensorFlow aus dem Quellcode zu erstellen.