TensorFlow : Extend : 他の言語での TensorFlow (翻訳/解説)
翻訳 : (株)クラスキャット セールスインフォメーション
作成日時 : 04/04/2017
* 本ページは、TensorFlow の本家サイトの Extend – TensorFlow in other languages を翻訳した上で
適宜、補足説明したものです:
https://www.tensorflow.org/extend/language_bindings
* ご自由にリンクを張って頂いてかまいませんが、sales-info@classcat.com までご一報いただけると嬉しいです。
背景
このドキュメントは他のプログラミング言語で TensorFlow 機能を作成または開発することに興味のある人たちへのガイドとなることを意図しています。TensorFlow の特徴と同じ事を他のプログラミング言語で利用可能にするための推奨されるステップについて記述します。
Python は TensorFlow でサポートされた最初のクライアント言語で現在では多くの特徴をサポートしています。それらの機能は段々と (C++ で実装された) TensorFlow コアに移されてきて C API 経由で公開されてきています。クライアント言語は TensorFlow 機能を提供するために C API を活用するためには言語の foreign function interface (FFI) を使用するべきです。
概要
プログラミング言語で TensorFlow 機能を提供することは幅広いカテゴリに分類されます :
- 事前定義されたグラフを実行する : GraphDef (or MetaGraphDef) protocol message が与えられた時、session を作成して、query を実行して、そしてテンソル結果を得ることができます。これは事前トレーニングされたモデル上でインターフェイスを実行することを望むモバイル app またはサーバのためには十分です。
- グラフ構築 : グラフに演算を追加する、定義された TensorFlow op 毎に少なくとも一つの関数。理想的にはこれらの関数は自動的に生成されるでしょう、そうすれば op 定義が変更されたときに同期的に留まれます。
- 勾配 (AKA 自動微分) : グラフと入力と出力演算のリストが与えられた時、出力に関する入力の偏導関数 (勾配) を計算する演算をグラフに追加します、グラフの特定の演算に対する勾配関数のカスタマイゼーションを許します。
- 関数 : 主要な GraphDef の複数の場所で呼び出されるかもしれないサブグラフを定義します。GraphDef に含まれる FunctionDefLibrary の FunctionDef を定義します。
- 制御フロー : ユーザ指定のサブグラフで “If” と “While” を構築します。理想的にはこれらは勾配と一緒に動作します (上を参照)。
- ニューラルネットワーク・ライブラリ : ニューラルネットワーク・モデルの作成を一緒にサポートしてそれらをトレーニングする多くのコンポーネント (おそらくは分散セッティングで)。他の言語でこれを利用可能にすることは便利である一方で、これを Python 以外の言語でサポートする予定は今のところありません。これらのライブラリは典型的には上述した特徴に渡るラッパーです。
少なくとも、言語バインディングは事前定義されたグラフの実行をサポートするべきですが、多くはグラフ構築もサポートするべきです。TenosrFlow Python API はこれら全ての特徴を提供します。
現在のステータス
新しい言語サポートは C API の上に構築されるべきです。けれども、下のテーブルで見て取れるように、まだ全ての機能が C で利用可能ではありません。C API で更なる機能を提供することは on-going なプロジェクトです。
特徴 | Python | C |
---|---|---|
事前定義されたグラフの実行 | tf.import_graph_def, tf.Session | TF_GraphImportGraphDef, TF_NewSession |
生成された op 関数でグラフ構築 | Yes | Yes (C API はこれを行なうクライアント言語をサポート) |
勾配 | tf.gradients | |
関数 | tf.python.framework.function.Defun | |
制御フロー | tf.cond, tf.while_loop | |
ニューラルネットワーク・ライブラリ | tf.train, tf.nn, tf.contrib.layers, tf.contrib.slim | |
推奨されるアプローチ
事前定義されたグラフを実行する
言語バインディングは以下のクラスを定義することが期待されます :
- グラフ : グラフは TensorFlow 計算を表します。(クライアント言語では Operations で表される、) 演算 (= operations) から構成され C API では TF_Graph に相当します。主として新しい Operation オブジェクトを作成する時と Session を開始する時に引数として使用されます。またグラフにおける演算を通して反復を行ない (TF_GraphNextOperation)、名前で演算を検索したり (TF_GraphOperationByName)、GraphDef protocol message へ/から変換をサポートします (C API では TF_GraphToGraphDef と TF_GraphImportGraphDef)。
- 演算 (Operation) : グラフにおける計算ノードを表します。C API では TF_Operation に相当します。
- 出力 : グラフの演算の出力の一つを表します。DataType (そして結局は shape) を持ちます。グラフに演算を追加するための関数、あるいはその出力をテンソルとして取得する Session の Run() メソッドへの入力引数として渡されるかもしれません。C API では TF_Output に相当します。
- Session: TensorFlow ランタイムの特定のインスタンスへのクライアントを表します。その主要なジョブは Graph と幾つかのオプションともに構築されてそれからグラフを Run() するために呼び出します。C API では TF_Session に相当します。
- テンソル : 要素が全て同じ DataType である N-次元 (矩形) 配列を表します。データは Session の Run() 呼び出しへ/から取得します。C API では TF_Tensor に相当します。
- DataType: TensorFlow でサポートされる全ての可能なテンソル型の列挙です。C API では TF_DataType に相当して Python API ではしばしば dtype として参照されます。
グラフ構築
TensorFlow は多くの ops を持ち、そのリストは静的ではないので、グラフに ops を追加するためにはそれらを個々に手動で書く代わりに関数を生成することを推奨します(ジェネレータが何を生成すべきかを見つけ出すためには 2, 3 を手書きしてみることは良い方法ですが)。関数を生成するために必要な情報は OpDef protocol message に含まれます。
登録された ops について OpDefs のリストを取得するには 2, 3の方法があります :
- C API の TF_GetAllOpList は全ての登録された OpDef protocol message を取得します。これはクライアント言語でジェネレータを書くために利用できます。これは、OpDef messages を解釈するためにクライアント言語が protocol buffer サポートを持つことを要求します。
- C++ 関数 OpRegistry::Global()->GetRegisteredOps() は全ての登録された OpDefs の同じリストを返します ([tensorflow/core/framework/op.h] で定義されています)。これは C++ でジェネレータを書くために利用できます(特に protocol buffer サポートを持たない言語のために有用です)。
- そのリストの ASCII-serialized 版は自動化されたプロセスで定期的に [tensorflow/core/ops/ops.pbtxt] に check in されます。
OpDef は以下を指定します :
- op の名前はキャメルケースです。生成された関数のためには言語の慣例に追随します。例えば、言語がスネークケースを使用するならば、op 関数の名前のためにキャメルケースの代わりにそれを使用します。
- 入力と出力のリスト。新しい op を追加する の入出力のセクションで記述されているように、これらの型は属性を参照することで多型 (polymorphic) かもしれません、
- 属性のリスト、それらの (もしあれば) デフォルト値に沿って。これらの幾つかは (それらが入力で決定されるのであれば) 推測され、幾つかは (デフォルト値を持てば) オプションで、そして幾つかは (デフォルト値がないならば) 必要です。
- 一般論として op、そして入力、出力、そして推測できない属性についてドキュメント。
- ランタイムでは使用されずコード・ジェネレータは無視できる幾つかのフィールド。
OpDef は、(言語の FFI でラップされる) TF_OperationDescription C API を使用してグラフに op を追加する関数のテキストに変換されます。
- TF_OperationDescription* を作成するために TF_NewOperation() で開始します。
- 入力毎に TF_AddInput() または TF_AddInputList() を一度呼び出します(入力がリスト型を持つか否かに依存します)。
- 推測できない属性を設定するために TF_SetAttr*() 関数を呼び出します。デフォルト値をオーバーライドすることを望まないならば属性はデフォルトでスキップできるかもしれません。
- 必要ならばオプション・フィールドを設定します :
- TF_SetDevice() : 演算を特定のデバイス上に強制します。
- TF_AddControlInput(): この演算が動作する前に他の演算は終了しているという要求を追加します。
- TF_SetAttrString(“_kernel”) : カーネル・ラベルを設定するために (滅多に使われません)。
- TF_ColocateWith() 一つの演算を他のものと同じ場所に配置するために。
- 終了したときに TF_FinishOperation() を呼び出します。これは、終了後に変更できないように、グラフに演算を追加します。
既存のサンプルはコード・ジェネレータをビルド・プロセスの一部として実行します (Bazel genrule を使用して)。或いは、コード・ジェネレータは自動化された cron プロセスで実行することもできます、結果を check in することもできます。
これは生成されたコードとレポジトリに check in された OpDefs が分岐するリスクも生みますが、Go の “go get” と Rust の cargo ops のようにより前にコードが生成されることが期待される言語には有用です。その対極に、ある言語に対してはコードは [tensorflow/core/ops/ops.pbtxt] から動的に生成され得るかもしれまん。
定数を処理する
もしユーザが入力引数に定数 (constants) を提供できるのであればコードの呼び出しは遥に簡潔になります。生成されたコードはこれらの定数を演算に変換すべきです、それはグラフに追加されてインスタンス化される op への入力として使用されます。
オプション・パラメータ
言語がオプションのパラメータを関数に許すのであれば (Python のデフォルト値を持つキーワード引数のように)、それらをオプション属性、演算名、デバイス、制御入力 etc. に使用します。ある言語では、(Python の “with” ブロックのように) これらのオプション・パラメータは動的スコープを使用してセットされます。これらの特徴がない場合には、TensorFlow API の C++ 版のように、ライブラリは “builer pattern” に頼るかもしれません。
名前スコープ
階層的なスコーピングの類を使用して名前付けられたグラフ演算のサポートを持つことは良いアイデアです、特に TensorBoard が妥当な方法で巨大なグラフを表示するためにそれに依存しているという事実を考慮に入れて。既存の Python と C++ API は異なるアプローチを取ります : Python では、名前 (everything up to the last “/”) の “directory” パートはブロックに由来します。実際に、名前を階層的に定義するスコープを持つスレッド局所スタック (thread-local stack) があります。名前の最後のコンポーネントは (オプション名前キーワード引数を使用して) ユーザから明示的に提供されるか追加される op の型の名前へのデフォルトであるかです。C++ では名前の “directory” パートは明示的にスコープ・オブジェクトで保持されます。NewSubScope() メソッドは名前のパートに追加して新しいスコープを返します。名前の最後のコンポーネントは WithOpName() メソッドを使用して設定され、そして Python のように op の型の名前へデフォルトが追加されます。スコープ・オブジェクトはコンテキストの名前を指定するために明示的に渡されます。
ラッパー
生成された関数を幾つかの ops に対しては private として保持することは意味があるかもしれません、追加のワークを行なうラッパー関数を代わりに使用できるようにして。これはまた、生成されたコードのスコープの外で特徴をサポートするための緊急避難口を与えます。
ラッパーの一つの使い方は SparseTensor 入力と出力をサポートするためです。SparseTensor は 3 dense (密な) テンソルのタプルです : indices, values, そして shape。value はベクトルサイズ [n]、shape はベクトルサイズ [rank]、そして indices は行列サイズ [n, rank] です。単一のスパース (疎な)・テンソルを表すために triple を使用する幾つかのスパース ops があります。
ラッパーを使用する他の理由は状態を保持する ops のためです。その状態上で動作するための幾つかの companion ops を持つ 2, 3 の ops (e.g. 変数) があります。Python API は、コンストラクタが op を作成するこれらの ops のためのクラスを持ち、そのクラスの上のメソッドは状態の上で動作するグラフに演算を追加します。
他の考察
- 言語のキーワードとぶつかる op 関数と引数を名前変更するために使われるキーワードのリストを持つは良いことです (あるいは、ライブラリ関数の名前や生成されるコードで参照される変数のような、障害を引き起こす他のシンボル)。
- Const 演算をグラフに追加するための関数は典型的にはラッパーです、何故ならば生成された関数は典型的には冗長な DataType 入力を持つからです。
勾配、関数と制御フロー
現時点で、勾配、関数そして制御フロー演算 (“if” と “while”) へのサポートは Python 以外の言語では利用可能ではありません。C API が必要なサポートを提供する時にはこれは更新されます。
以上