従来モデル・オプティマイザーの拡張性

危険

ここで説明されているコードは非推奨になりました。従来のソリューションの適用を避けるため使用しないでください。下位互換性を確保するためにしばらく保持されますが、最新のアプリケーションでは使用してはなりません

このガイドでは、非推奨である TensorFlow 変換方法について説明します。新しい推奨方法に関するガイドは、フロントエンド拡張に記載されています。

この記事では、モデル・オプティマイザーの内部について説明します。これらを変更すると、アプリケーションが不安定になり、将来 API が変更されると下位互換性が失われる可能性があります。

ONNX、TensorFlow Lite、PaddlePaddle、または TensorFlow 操作のサポートを追加したい場合、または OpenVINO の他の拡張機能に慣れていない場合は、代わりにこのガイドをお読みください。

モデル・オプティマイザーの拡張メカニズムにより、ここで説明するように、新しい操作とカスタム変換をサポートして、最適化された中間表現 (IR) を生成できます。このメカニズムはモデル・オプティマイザーの中核であり、モデルをサポートするカスタムロジックを追加する方法を示す例のセットとして機能します。

カスタマイズが必要になるケースはいくつかあります。

  • モデルには、モデル・オプティマイザーにとって不明な操作が含まれていますが、これらの操作はサポートされる操作の組み合わせとして表現できます。この場合、カスタム変換を実装して、サポートされていない操作をサポートされる操作に置き換える必要があります。

  • モデルには、パフォーマンスを向上させるため、少ない数の操作に置き換えることが可能な操作のサブグラフが含まれています。例えば、融合変換 (例: 計算 \(x/(1.0+e^{-(beta*x)})\) を実行するサブグラフを Swish 型の単一演算で置き換えることに対応しています。

  • モデルには、フレームワーク拡張メカニズムを使用して開発されたカスタム・フレームワーク操作 (フレームワークの公式操作セットにはない操作) が含まれています。この場合、モデル・オプティマイザーは、操作を処理し、それに対応する IR 内に対応するセクションを生成する方法を理解している必要があります。

モデル・オプティマイザーの拡張メカニズムの詳細に入る前に、モデル・オプティマイザーがメモリー内でモデルをどのように表現し、IR に変換するかを理解する必要があります。

この記事のすべてのパスは、特に明記されていない限り、モデル・オプティマイザーのインストール・ディレクトリーを基準にして指定されています。

メモリー内のモデル表現

モデルは有向グラフとして表現できます。ノードは操作であり、エッジはプロデューサーの操作 (ノード) からコンシューマーの操作 (ノード) に渡されるデータに対応します。

モデル・オプティマイザーは、Python クラスの mo.graph.graph.Graph インスタンスを使用して、モデル変換中にメモリー内の計算グラフを表します。このクラスは、標準の networkx Python ライブラリーの networkx.MultiDiGraph クラスから継承されます。グラフを走査したり変更したりする便利な多数のメソッドが提供されています。例については、mo/graph/graph.py ファイルを参照してください。

モデル・オプティマイザーは、操作に必要な情報すべてをノード属性に保持します。モデル・オプティマイザーは、mo/graph/graph.py ファイルで定義された mo.graph.graph.Node クラスを使用します。これは、networkx ノード属性辞書の最上位のラッパーであり、ノードを操作する便利なメソッドを多数提供します。例えば、my_node というノードの my_attr 属性は、次のコード my_node.my_attr を使用してノードから取得できます。これは、graph.node[my_node] 辞書内の my_attr 属性を取得するのと同じです。クラス実装の詳細は、mo/graph/graph.py ファイルを参照してください。

操作には複数の入力と出力がある場合があります。例えば、Split 操作には、分割するデータと分割する軸の 2 つの入力があり、属性 num_splits の値に応じて出力数が変化します。操作への各入力データは、特定の操作の入力ポートに渡されます。操作により、出力ポートから出力データが生成されます。入力ポートと出力ポートにはそれぞれ 0 から番号が付けられます。モデル・オプティマイザーは、クラス mo.graph.port.Port および mo.graph.connection.Connection を使用します。これらは、ノードの接続/再接続やグラフの移動などグラフを変更するのに役立つ抽象化です。これらのクラスはモデル・オプティマイザーのコードで広く使用されているため、簡単に使用例を見つけることができます。

エッジに対応する専用のクラスがないため、エッジ属性にアクセスするには、必要に応じて低レベルのグラフ操作が必要です。一方、ノード接続に関するほとんどの操作は、mo.graph.connection.Connection クラスと mo.graph.port.Port クラスを使用して行います。したがって、低レベルのグラフ操作はエラーが発生しやすいため、あまり推奨できません。

メモリー内のモデル表現に関連する詳細と例は、適切な説明を目的として、以下のセクションで説明されています。ポートと接続の使用方法の詳細については、ポートと接続を使用したグラフの移動と変更を参照してください。

モデル変換パイプライン

モデル変換パイプラインは次の図のようになります。

../../../_images/MO_conversion_pipeline.svg

各変換ステップについては、以下で詳しく説明します。

モデルのロード

モデル・オプティマイザーは、トレーニングされたモデルファイルを入力として取得します。モデル・オプティマイザーのモデル・ローダー・コンポーネントは、フレームワークで提供される Python バインディングを使用してモデルファイルを読み取り、計算グラフのメモリー内表現を構築します。サポートされているフレームワークごとに個別のローダーがあります。これらのローダーは、モデル・オプティマイザーの extensions/load/<FRAMEWORK>/loader.py ファイルに実装されています。

モデル・オプティマイザーは、caffe.proto ファイル上に構築された Caffe モデル用の特別なパーサーを使用します。モデルの読み込みに失敗した場合、モデル・オプティマイザーはエラーをスローし、モデルの読み取りが可能であるパーサーの準備を要求します。カスタム Caffe パーサーを準備する方法の詳細については、モデル・オプティマイザー の FAQ質問 1 を参照してください。

モデルの読み込みステップの結果は Graph オブジェクトであり、次の例のように表すことができます。

../../../_images/MO_graph_after_loader.svg

モデル・オプティマイザーのローダーは、入力モデルの各操作に対して、操作インスタンスのフレームワーク記述 (通常は Protobuf メッセージ) を、通常 pb という名前でノード属性に保存します。フレームワーク固有の操作の説明であることが重要です。これは、操作 (畳み込みなど) は、Caffe フレームワークと TensorFlow フレームワークなどでは異なる方法で表現される可能性がありますが、数学的な観点からは同じ計算を行います。

上の画像では、操作 2 には 1 つの入力と 2 つの出力があります。出力ポート 0 から生成されたテンソルは、操作 5 (入力ポート 0) と操作 3 (入力ポート 1) で消費されます。出力ポート 1 から生成されたテンソルは、操作 4 (入力ポート 0) で消費されます。

各エッジには、inout の 2 つの属性があります。これには、コンシューマー・ノードの入力ポート番号とプロデューサー・ノードの出力ポート番号が含まれます。これらの属性は、ノードがいくつかの入力テンソルを消費し、いくつかの出力テンソルを生成する操作であることを説明します。モデル・オプティマイザーの観点から見ると、ノードには実行する操作に必要な情報が含まれていないため、ノード自体はブラックボックスです。

操作属性の抽出

次のステップでは、ノード属性に保存されたフレームワーク依存の操作表現を解析し、操作固有の属性でノード属性を更新します。これを行うには 3 つのオプションがあります。

  1. エクストラクター拡張アプローチ (操作の属性を抽出する推奨方法)。操作のエクストラクターで詳しく説明されています。

  2. ビルトインのエクストラクターを使用した従来のアプローチ。mo/front/<FRAMEWORK>/extractor.py ファイル (例えば、Caffe 用のファイル) は、特定の操作タイプのエクストラクターを含む辞書を定義します。辞書内のキーは抽出関数をトリガーする操作タイプであり、値は関数です。この関数にはパラメーターが 1 つあり、それは属性を抽出するノードです。これは従来の拡張不可能なアプローチであるため、回避する必要があります。このメカニズムは、モデル・オプティマイザーの将来のバージョンで削除される予定です。

エクストラクターの実行順序は次のとおりです。

  • CustomLayersMapping.xml (Caffe モデルのみ)。

  • モデル・オプティマイザー拡張。

  • ビルトインのモデル・オプティマイザー・エクストラクター。

操作属性抽出ステップの結果は、次のように表すことができます。

../../../_images/MO_graph_after_extractors.svg

前のステップとのグラフの唯一の違いは、モデル・オプティマイザーに必要な、抽出された属性と操作固有の属性を含む辞書がノードに含まれていることです。ただし、このステップ以降、モデル・オプティマイザーは操作/モデルの元の表現を必要とせず、モデル・オプティマイザーの表現のみを使用します (モデル・オプティマイザーが引き続き pb 属性を使用する特殊なケースもありますが、この記事では部分的に説明します)。一般的なノード属性とその値の詳細なリストは、モデル・オプティマイザーの操作で提供されています。

フロントフェーズ

従来の理由から、完全に定義されていないすべてのモデルの入力に対して形状を指定する必要があります。対照的に、TensorFlow などの他のマシンラーニング・フレームワークでは、未定義または部分的に定義された入力形状を使用してモデルを作成できます。例えば、未定義の次元は、TensorFlow モデルでは整数値 -1 でマークされるか、ONNX モデルでは何らかの文字列名が付加されます。

フロントフェーズでは、モデル・オプティマイザーはモデルの入力と定数の形状のみを認識し、中間テンソルの形状 (さらにランクも) を認識しません。ただし、形状に関する情報は、特定の変換を実装するのに必要ない場合があります。例えば、変換 extensions/front/TopKNormalize.py は、TopK ノードから属性 k を削除し、値 k を持つ入力定数を追加します。変換は、TopK 操作を変換するために必要です。これはフレームワークに由来しており、多くの出力要素が OpenVINO TopK 操作のセマンティクスに対する操作属性として定義されており、この値を別の入力にする必要があります。

入力または形状には実際の値が必要であるため、フロントフェーズでは変換を実行できないと思われる場合があることが重要です。形状や値の操作は、グラフに追加される操作で実装できます。extensions/front/onnx/flattenONNX_to_reshape.py の変換を考えてみましょう。これは、ONNX Flatten 操作を、以下を実行する操作のサブグラフに置き換えます (が 0 および 1 でない場合)。

  1. ShapeOf 操作を使用して、Flatten 入力テンソルの形状を計算します。

  2. Shape 操作の出力から最初の要素を取得し、ReduceProd 操作を使用してそれらの積を計算します。

  3. ReduceProd の出力と定数を値 -1 で連結します (この値の説明については、Reshape 仕様のページを参照してください)。

  4. 連結された値を Reshape 操作への 2 番目の入力として使用します。

モデルの再形状の問題を回避するため、形状に依存しない変換を行うことを強く推奨します。モデルの再形成に関する詳細については、形状推論の使用ガイドを参照してください。

フロントフェーズ変換の開発方法と専用 API の説明の詳細については、フロントフェーズ変換を参照してください。

部分推論

モデル・オプティマイザーは、モデル変換中にモデルの部分的な推論を実行します。この手順には、モデル内のすべての操作の出力形状の計算と定数の折りたたみ (定数のサブグラフの値の計算) が含まれます。場合によっては、出力形状を計算するのに定数サブグラフの評価が必要となり、形状の推論には定数の折りたたみが必要です。例えば、Reshape 操作の出力形状は、ShapeOf 操作の出力を使用して数式として定義できます。

モデル・オプティマイザーは、デフォルトでは、ShapeOf 操作から始まるサブグラフを折り畳みません。これは、モデルの非再形状化につながるためです (コマンドライン・パラメーター --static_shape でこの動作をオーバーライドできます)。モデルの再形成に関する詳細については、形状推論の使用ガイドを参照してください。

モデル・オプティマイザーは、モデル内のすべての操作の出力形状を計算して、中間表現ファイルに書き込みます。

これは従来の要件です。IR バージョン 10 以降、OpenVINO ランタイムは Const 操作と Parameter 操作の形状のみを認識する必要があります。OpenVINO ランタイムは、それぞれの操作属性で定義された Parameter および Const 操作の形状を使用して、モデル内のすべての操作の出力形状を計算します。

モデル・オプティマイザーは、部分推論フェーズを開始する前に、計算グラフにデータノードを挿入します。データノードは、操作で生成された特定のテンソルに対応します。各データノードには 2 つの属性が含まれます。1 つはテンソルの形状を含む shape、もう 1 つはテンソルの実際の値を含む value です。このテンソル値を計算できない場合、value 属性の値は None に等しくなります。これは 2 つのケースで発生します。テンソル値がモデルの Parameter 操作に渡される値に依存している場合、またはモデル・オプティマイザーに操作の値伝播実装がない場合です。

部分推論を実行する前に、グラフを次の例のように描画ができます。

../../../_images/MO_graph_before_partial_inference.svg

フロントフェーズのグラフとの構造の違いは、データノードだけでなく、エッジの属性にもあります。out 属性は操作ノードからのエッジのみに指定され、in 属性はデータノードからのエッジのみに指定されることに注意してください。これは、テンソル (データノード) が操作の特定の出力ポートから生成され、操作の特定の入力ポートで消費されます。また、操作の出力ポートごとに一意のデータノードが作成されます。このノードは、いくつかの操作ノードの入力ノードとして使用できます。データノード data2_0 と同様に、これは操作 3 の入力ポート 1操作 5 の入力ポート 0 で消費されます。

ここで、モデル・オプティマイザーが形状と値の伝播をどのように実行するかを考えてみます。モデル・オプティマイザーは、グラフノードのトポロジカル・ソートを実行します。グラフにサイクルが含まれる場合、エラーメッセージがスローされます。次に、トポロジー順序に従って、グラフ内の各ノードに対して形状推論関数が呼び出されます。グラフの各ノードには、形状推論関数を備えた infer 属性が必要です。これは、1 つのパラメーター (Node クラスのインスタンス) を持つ関数です。infer 属性は通常、操作エクストラクターで設定されるか、mo.pos.Op クラスから継承されたモデル・オプティマイザー操作クラスを使用して何らかの変換でノードが追加されるときに設定されます。形状推論関数を指定する方法の詳細については、モデル・オプティマイザー操作および操作エクストラクターを参照してください。

形状推論関数は、入力形状と操作 (ノード) 属性に基づいて操作 (ノード) 出力形状を計算し、対応するデータノードの shape とオプションで value 属性を更新する必要があります。
Reshape 操作の形状推論関数の簡略化された例 (完全バージョンは mo/ops/reshape.py ファイルで入手可能):

 @staticmethod
 def infer(node: Node):
     name = node.soft_get('name', node.id)

     input_shape = node.in_port(0).data.get_shape()  # get the input tensor shape
     new_shape = node.in_port(1).data.get_value()  # get the value defining the output tensor shape. This tensor may
                                                   # have special values like 0 and -1

     output_shape = ... # calculate output shape without special values like 0 and -1

     if node.in_port(0).data.get_value() is not None:  # if the input value is defined then calculate output value;
                                                       # shape will be updated automatically with the value shape
         node.out_port(0).data.set_value(node.in_port(0).data.get_value().reshape(output_shape))
     else:  # in the opposite case calculate the output shape only
         node.out_port(0).data.set_shape(output_shape)

Node クラスのメソッド in_port() および output_port() は、データノード属性を取得および設定するのに使用されます。ポートと接続の使用方法の詳細については、ポートと接続を使用したグラフの走査と変更を参照してください。

形状推論関数は、元のモデルレイアウトで出力形状計算を実行する必要があります。例えば、OpenVINO™ は NCHW レイアウトでのみ畳み込み操作をサポートしますが、TensorFlow は NHWC レイアウトもサポートします。モデル・オプティマイザーの形状推論機能は、NHWC レイアウト内の NHWC 畳み込みの出力形状を計算し、レイアウト変更フェーズ中にのみ形状が NCHW に変換されます。

input_shape = op_node.in_node(0).shape のようにデータノード属性を読み取り、op_node.out_node(0).shape = some_value のようにデータノード属性を変更する従来のアプローチがあります。このアプローチはモデル・オプティマイザーのコードでまだ使用されていますが、推奨されません。代わりに、ポートで説明されているアプローチを使用してください。

ミドルフェーズ

ミドルフェーズは部分推論の後に始まります。このフェーズでは、グラフにはデータノードが含まれており、グラフ内のすべての操作の出力形状が計算されています。このステージで実装された変換は、新たに追加されたすべての操作の shape 属性を更新する必要があります。ポートと接続を使用したグラフの走査と変更で説明されている API を使用することを推奨します。この API を使用してグラフを変更すると、影響を受けるノードの自動再推論と必要なデータノードの作成が行われるためです。

ミドルフェーズ変換の開発方法と専用 API の説明の詳細については、ミドルフェーズ変換を参照してください。

NHWC か​​ら NCHW へレイアウト変更

モデルレイアウトを NHWC か​​ら NCHW に変更する中間変換がいくつかあります。TensorFlow は NHWC レイアウトでの畳み込み走査をサポートしているため、これらの変換は TensorFlow モデルに対してデフォルトでトリガーされます。

OpenVINO™ が NCHW レイアウトで実行する必要がある操作 (NHWC レイアウトの畳み込みなど) がモデルにない場合、このレイアウト変更は自動的に無効になります。

動作の詳細については、以下のプロセスの概要で説明されている変換のソースコードを参照してください。

  1. モデル・オプティマイザーは、4D および 5D (4 次元および 5 次元) テンソルを生成するほとんどの走査の出力形状を、NHWC レイアウトから NCHW レイアウトに変更します: 4D の場合は nchw_shape = np.array(nhwc_shape)[0, 3, 1, 2]、5D の場合は nchw_shape = np.array(nhwc_shape)[0, 4, 1, 2, 3]。この順列は、モデル変換中に特定された特定の条件を持つ一部の操作では発生しません。

  2. モデル・オプティマイザーは、正しいレイアウトで形状計算を実行するため、形状計算に関連するサブグラフに Gather 操作を挿入します。

  3. モデル・オプティマイザーは、モデル変換中に特定された特定の条件を持つ一部の演算に対して転置操作を挿入し、正しい推論結果を生成します。

レイアウト変更の原因となる主な変換は次のとおりです。

  • extensions/middle/ApplyPermutations.py

  • extensions/middle/InsertLayoutPropagationTransposes.py

  • extensions/middle/MarkSubgraphsWithCorrectLayout.py

  • extensions/middle/ApplyNHWCtoNCHWpermutation.py

  • extensions/middle/LayoutChangeForConstantShapePaths.py

バックフェーズ

バックフェーズは NCHW へのレイアウト変更後に開始します。このフェーズには主に次の変換が含まれます。

  1. NCHW レイアウトのグラフで機能するため、ミドルフェーズでは実装できない変換。

  2. 内部のモデル・オプティマイザー操作に対応するノードを opset 操作に対応するノードに置き換える変換。

  3. 仕様に従って操作入力を正規化する変換。

  4. 最終的な最適化変換。

バックフェーズのグラフ構造はミドルフェーズと同じです。ミドルとバック変換の書き方に違いはありません。

バックフェーズ変換の開発方法と専用 API の説明の詳細については、バックフェーズ変換を参照してください。

中間表現の生成

モデル変換の最後のフェーズは、中間表現の生成です。モデル・オプティマイザーは次の手順を実行します。

  1. グラフ内のすべての操作ノードを反復処理し、すべてのノードに type 属性が設定されていることを確認します。この属性は操作タイプを定義し、ノードの version 属性で指定された opset から適切な操作のインスタンス化に OpenVINO で使用されます。ノードに属性タイプがない場合、またはその値が None に等しい場合、モデル・オプティマイザーはエラーで終了します。

  2. 形状推論と同様に、グラフ操作のタイプ推論を実行します。推論されたデータタイプは、IR のポート属性に保存されます。

  3. グラフのトポロジカル・ソートを実行し、すべての演算ノードの id 属性を 0 から始まる連続した整数値に変更します。

  4. すべての定数値を .bin ファイルに保存します。同じ値を持つ定数は、異なる操作間で共有されます。

  5. グラフ構造を定義する .xml ファイルを生成します。操作の入出力に関する情報は、操作のタイプに関係なく、すべての操作に対して一律に用意されます。.xml ファイルに保存される属性のリストは、グラフノードのインスタンス化に使用される Op クラスの backend_attrs() または supported_attrs() で定義されます。操作属性を XML に保存する方法の詳細については、mo/pipeline/common.py ファイルの関数 prepare_emit_ir() およびモデル・オプティマイザーの操作に関する記事を参照してください。