LoginSignup
15
18

More than 5 years have passed since last update.

C++でTensorFlow: Opを実装する( 1 )

Last updated at Posted at 2016-03-08

Opを実装してみる

今までMatMul、Sub、Div、…などいろいろなOpがあったと思いますが、ここではOp新たに定義して使ってみたいと思います。

tensorflow/core/user_ops/fact.cc
tenserflow/user_ops/ackermann_op.cc

というexampleがありましたが、これについてはAdding New Opを見た方が良いと思うので、これを元に進めたいと思います。

1回ですべてまとめるとかなり長くなりそうだったので、多分3回くらいに分割してやっていきたいと思います。
1回目: Op実装の仕方
2回目: Attrの追加
3回目: Opのポリモーフィズム

もしかしたら2回かも

説明がわかりにくかったらごめんなさい!

1. 追加するOpのインターフェースの定義

まず初めにやることは、OpDefというものを定義して、OpRegistryというものに登録することになります。これは、以下に示したREGISTER_OPというマクロを呼んで定義することになります。

tensorflow/core/framework/op.h
REGISTER_OP("my_op_name")                                                                                                                 
    .Attr("<name>:<type>")                                                                                                                
    .Attr("<name>:<type>=<default>")                                                                                                      
    .Input("<name>:<type-expr>")                                                                                                          
    .Input("<name>:Ref(<type-expr>)")                                                                                                     
    .Output("<name>:<type-expr>")                                                                                                         
    .Doc(R"(                                                                                                                              
 <1-line summary>                                                                                                                          
 <rest of the description (potentially many lines)>                                                                                        
 <name-of-attr-input-or-output>: <description of name>                                                                                     
 <name-of-attr-input-or-output>: <description of name;                                                                                     
   if long, indent the description on subsequent lines>                                                                                    )"); 

今回実装したOpの例を示します。
Opの名前のルールは、
1. CamelCase
2. アンダースコアから始めない
です。
今回の例は、

REGISTER_OP("My")          //Opの名前は"My"
    .Input("msg: string")  //1つ目の入力はstringのTensorで名前は"msg"
    .Input("a: int32)      //2つ目の入力はint32のTensorで名前は"a"
    .Output("b: int32")    //出力のTensorはint32で名前は"b"
    .Doc(R"doc(
Test Op.
)doc");

Input/Outputはそれぞれ複数指定可能です。

Attrについては次回詳しく。

2. Kernel部分の実装

Opの実際の処理の実装部分をカーネルといい、入力のタイプや、CPU/GPUなどを区別して複数実装できるらしいです(今回は1つ)。
やることは、単純で
1. OpKernelクラスを継承したクラスを定義する
2. void Compute(OpKernelContext* context)をオーバーライドする
、となります。
このComputeに記述する処理が、このOpの処理になります。
また、OpKernelContextとは必要なものをまとめたようなオブジェクトで、入力されたTensorや出力するTensorなどを含んでいます。

今回実装したカーネルは、後述します。

Input Tensorを取り出す

入力TensorはOpKernelContextから以下のように取り出すことができます。

// i番目ののinput tensorを取り出す
const Tensor& input_tensor = context->input(i);

この順番はREGISTER_OPで定義した順番に対応します。

Output Tensorを作る

Tensor* output_tensor = NULL;
// contextにoutput_tensorを確保する
OP_REQUIRES_OK(context, 
    context->allocate_output(0, input_tensor.shape(), &output_tensor));

これでcontextにoutput_tensorが確保されたので、以降output_tensorを操作することで、出力を生成することができます。

3. Kernelの登録

これもマクロを呼ぶだけです。
これは、

tnesorflow/core/framework/op_kernel.h

に定義されていて、
1. Name: カーネルの名前
2. Device: 動作するデバイス
3. TypeConstraint: タイプの制限
4. HostMemory: ホストメモリ上に存在するデータの指定
5. インスタンス化するクラス(MyOp<float>, MyOp<int32>みたいな)
などいくつか設定できるみたいです。
例を見た方が早いと思うので、以下にいくつか載せておきます。

基本は以下の形です。

REGISTER_KERNEL_BUILDER(Name("My").Device(DEVICE_CPU), MyOp);

次は、Tというタイプがfloatの場合のみ動作するカーネルの場合は、

REGISTER_KERNEL_BUILDER(Name("My")
  .Device(DEVICE_CPU)
  .TypeConstraint<float>("T"), 
  MyOp<float>);

次は、動作するデバイスはGPU、入力TensorのうちshapeというTensorがホストメモリにあるという制限をつけたカーネルを登録する場合の例です。

REGISTER_KERNEL_BUILDER(Name("My")
  .Device(DEVICE_GPU)
  .HostMemory("shape"), 
  MyOp);

4. 追加するOpの共有ライブラリを作成する

追加するOpを実装したソースは以下の場所に置いておきます。

tensorflow/core/user_ops/my_op.cc

TensorFlowをバイナリからインストールした人

ここではTensorFlowのPythonライブラリから以下の関数を使用します。
1. get_include
2. get_lib
名前からわかる通り、それぞれ、ヘッダーとライブラリへのパスを返してくれます。

$ python
>>> import tensorflow as tf
>>> tf.sysconfig.get_include()
'(略)/site-packages/tensorflow/include'

といった具合。

これを使用して、共有ライブラリを作成する手順は以下の通りです。

$ TF_INC=$(python -c 'import tensorflow as tf; print(tf.sysconfig.get_include())')
$ TF_LIB=$(python -c 'import tensorflow as tf; print(tf.sysconfig.get_lib())')

$ g++ -std=c++11 -shared my_op.cc -o my_op.so \
-I $TF_INC -l tensorflow_framework -L $TF_LIB \
-fPIC -Wl,-rpath $TF_LIB

TensorFlowをソースからインストールした人

BUILDファイルを置いておくことで、bazelがコンパイルしてくれます。

cc_binary(
    name = "my_op.so",
    srcs = ["my_op.cc"],
    linkopts = [
        "-Wl,-Bsymbolic",
        "-lm",
    ],
    linkshared = 1,
    linkstatic = 1,
    deps = [
        "///tensorflow/core:framework",
    ],
)
$ bazel build tensorflow/core/user_ops/...

使ってみる

pythonの例はあるのですが、C++での例はないようです。
pythonの場合、共有ライブラリをロードするAPIを使用しているようです。
C++ではどのようにしたらよいだろう…
適切な方法でやれば多分bazelがヘッダファイルをuser_ops.hとして作成してくれるのですが、bazelがわからない…

作成した共有ライブラリをロードする

いろいろ探していて、以下のようなAPIを見つけました。

tensorflow/core/client/tensor_c_cpi.cc
TF_Library* TF_LoadLibrary(const char* library_filename, TF_Status* status) {
  TF_Library* lib_handle = new TF_Library;
  status->status = tensorflow::LoadLibrary(
      library_filename, &lib_handle->lib_handle, &lib_handle->op_list.data,
      &lib_handle->op_list.length);
  if (!status->status.ok()) {
    delete lib_handle;
    return nullptr;
  }
  return lib_handle;
}

(ここには他にもいろいろなAPIが実装されていて、今後使用されるようになるのかも?)
また、内部で呼ばれているtensorflow::LoadLibarayは、

tensorflow/core/framework/load_library.cc
Status LoadLibrary(const char* library_filename, void** result,
                   const void** buf, size_t* len) {
  ...
  TF_RETURN_IF_ERROR(env->LoadLibrary(library_filename, &lib));
  ...
}

さらにここで呼ばれているLoadLibraryは、

tensorflow/core/platform/load_library.cc
Status LoadLibrary(const char* library_filename, void** handle) {
  *handle = dlopen(library_filename, RTLD_NOW | RTLD_LOCAL);
  if (!*handle) {
    return errors::NotFound(dlerror());
  }
  return Status::OK();
}

本質的にはdlopenで作成したライブラリをロードして使うが、結局よくわからず保留。


既存のOpと一緒にしてしまう

bazelのことをよく知っている方なら、うまい方法があるのかもしれないが、自分は全然わからなかったので、とりあえず、既存のREGISTER_OP群の末尾に自分のを置くことにしました。

tensorflow/core/ops/math_ops.cc
/* 他のREGISTER_OP */

// "My"というOpを登録する
REGISTER_OP("My")
    .Input("msg: string")
    .Input("a: int32")
    .Output("b: int32")
    .Doc(R"doc(                                                                                                                              
MyOp                                                                                                                                         
)doc");

これによって、bazelがmath_ops.hに自分のOpも含めてくれました。
本当は、user_ops.hに記述されるようにBUILDを記述するのが適切だと思います。

今回実装したカーネルを以下に載せておきます。

tensorflow/core/user_ops/my_ops.cc
class MyOp : public OpKernel {
public:
    explicit MyOp(OpKernelConstruction* context) : OpKernel(context) {}

    void Compute(OpKernelContext* context) override {
        // 1番目の入力を取得する
        const Tensor& msg_tensor = context->input(0);
        auto msg = msg_tensor.flat<string>();
        std::cout << msg(0) << std::endl;

        // 2番目の入力を取得する
        const Tensor& input_tensor = context->input(1);
        auto input = input_tensor.flat<int32>();

        // 出力のTensorを確保する
        Tensor* output_tensor = NULL;
        OP_REQUIRES_OK(context,
                       context->allocate_output(0, input_tensor.shape(), &output_tensor));
        auto output = output_tensor->template flat<int32>();

        // 入力の要素を2倍する
        for (int i = 0; i < input.size(); ++i)
            output(i) = input(i)*2;
    }
};

// "My"というカーネルを作る
REGISTER_KERNEL_BUILDER(Name("My").Device(DEVICE_CPU), MyOp);

次は、これを呼び出す側の処理について説明します。
まず、Computation Graphは以下の通りです。

tensorflow/my_work/main.cc
Node* msg = Const((string)"my op!", b.opts()); // stringのTensor
Node* a = Const({3, 2}, b.opts());             // int32のTensor

// 定義通り、1つ目の入力にstringの、2つ目の入力にint32のTensorを渡す
My(msg, a, b.opts().WithName("my_ops_out"));   // ノード名はmy_ops_out

Sessionの実行と、出力の確認

tensorflow/my_work/main.cc
std::vector<Tensor> outputs;

// 2番目の引数に、出力の欲しいノードの名前を渡す
session->Run({},{"my_ops_out"},{}, &outputs);

// outputsから1番目のTensorを取り出す
auto b = outputs[0].flat<int32>();
for (int i = 0; i < b.size(); ++i)
    std::cout << b(i) << ",";
std::cout << std::endl;

実行

$ bazel build tensorflow/my_work/...
$ bazel-bin/tensorflow/my_work/my_work
my op!
6,4,

入力を2倍しただけですが、うまくいくとうれしいですね

その他

TensorFlowのRoadmapをふと眺めていたら、

Improve support for C++ only users
Graph construction
Gradients
Shape Inference

という項目が…
というか先ほどのtensor_c_apiとかのことだろうか?

15
18
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
15
18