動的形状#

入力形状の変更で説明したように、Core::compile_model でモデルをコンパイルする前に入力形状の変更をサポートするモデルがあります。モデルの再形状により、最終アプリケーションの正確なサイズに合わせてモデルの入力形状をカスタマイズする機能が提供されます。ここでは、モデルの再形状機能をより動的なシナリオでさらに活用する方法について説明します。

動的形状の適用#

従来の “静的” モデルの再形状は、同じ形状の多数のモデル推論呼び出しごとに 1 回実行できる場合に機能します。ただし、入力テンソル形状が推論呼び出しごとに変化する場合、このアプローチは効率よく実行されません。サイズが変わるたびに reshape() メソッドと compile_model() メソッドを呼び出すのは時間がかかります。一般的な例として、任意のサイズのユーザー入力シーケンスによる自然言語処理モデル (BERT など) の推論があります。この場合、シーケンス長は予測できず、推論が呼び出されるたびに変化する可能性があります。頻繁に変更される次元は動的次元と呼ばれます。実際の入力形状が、compile_model() メソッドの呼び出し時に不明である場合、動的な形状を考慮する必要があります。

以下に、自然な流れで動的になる次元の例をいくつか示します:

  • BERT などのさまざまなシーケンス処理モデルのシーケンス長次元

  • セグメント化およびスタイル転送モデルの空間次元

  • バッチ次元

  • 物体検出モデル出力における任意の数の検出

事前に再形成された複数のモデルと入力データのパディングを組み合わせることにより、入力の動的次元に対処する方法があります。この方法はモデル内部の影響を受けやすいため、常に最適なパフォーマンスが得られるとは限らず、また複雑です。メソッドの概要については、動的形状 API が適用できない場合を参照してください。これらのメソッドは、次のセクションで説明するネイティブの動的形状 API が機能しない場合、または期待どおりに動作しない場合にのみ適用してください。

動的形状を使用するかどうかは、実際のデータを使用したアプリケーションの適切なベンチマークに基づいて決定する必要があります。静的形状モデルとは異なり、動的形状モデルは、入力データの形状または入力テンソルの内容に応じて、異なる推論時間をもたらします。さらに、動的形状を使用すると、使用するハードウェア・プラグインとモデルよっては、メモリーのオーバーヘッドが増加し、推論呼び出しの実行時間が増加する可能性があります。

動的形状の処理#

ここでは、OpenVINO ランタイム API バージョン 2022.1 以降を使用して動的形状モデルを処理する方法について説明します。動的形状を使用する場合、静的形状と比べてワークフローに 3 つの違いがあります:

  • モデルの構成

  • 動的データの準備と推論

  • 出力の動的形状

モデルの構成#

モデルの入力次元は、model.reshape メソッドを使用して動的に指定できます。動的次元を設定するには、その次元の値として -1ov::Dimension() (C++)、または ov.Dimension() (Python) を使用します。

一部のモデルには、すぐに使用できる動的形状がすでに備わっていることがあり、その場合、追加の構成は必要ありません。これは、ソース・フレームワークから動的形状を使用して生成されたか、動的形状を使用するようにモデル・トランスフォーメーション API で変換されたことが原因であると考えられます。詳細については、動的次元の “アウトオブボックス” を参照してください。

次の例は、静的な [1, 3, 224, 224] 入力形状を持つモデルで動的次元を設定する方法を示しています。最初の例は、最初の次元 (バッチサイズ) を動的に変更する方法を示しています。2 番目の例では、3 番目と 4 番目の次元 (高さと幅) が動的に設定されます。

core = ov.Core() 

# 最初の次元を動的に設定し他の次元は静的のまま残す 
model.reshape([-1, 3, 224, 224]) 

# または、3 番目と 4 番目の次元を動的として設定 
model.reshape([1, 3, -1, -1])

Python では、すべての次元を文字列として渡し、動的次元には ? を使用することもできます (例: model.reshape(“1, 3, ?, ?”))。

ov::Core core; 
auto model = core.read_model("model.xml"); 

// 最初の次元を動的 (ov::Dimension()) として設定し残りの次元を静的として設定 
model->reshape({{ov::Dimension(), 3, 224, 224}}); // {?,3,224,224} 

// または、3 番目と 4 番目の次元を動的として設定 
model->reshape({{1, 3, ov::Dimension(), ov::Dimension()}}); // {1,3,?,?}
ov_core_t* core = NULL; 
ov_core_create(&core); 

ov_model_t* model = NULL; 
ov_core_read_model(core, "model.xml", NULL, &model); 

// 最初の次元を動的 ({-1, -1}) に設定し、残りの次元を静的に設定 
{ 
ov_partial_shape_t partial_shape; 
ov_dimension_t dims[4] = {{-1, -1}, {3, 3}, {224, 224}, {224, 224}}; 
ov_partial_shape_create(4, dims, &partial_shape); 
ov_model_reshape_single_input(model, partial_shape); // {?,3,224,224} 
ov_partial_shape_free(&partial_shape); 
} 

// または、3 次元目と 4 次元目を動的に設定 
{ 
ov_partial_shape_t partial_shape; 
ov_dimension_t dims[4] = {{1, 1}, {3, 3}, {-1, -1}, {-1, -1}}; 
ov_partial_shape_create(4, dims, &partial_shape); 
ov_model_reshape_single_input(model, partial_shape); // {1,3,?,?} 
ov_partial_shape_free(&partial_shape); }

上記の例では、モデルに単一の入力レイヤーがあることを前提としています。複数の入力レイヤーを持つモデル (NLP モデルなど) を変更するには、すべての入力レイヤーを反復処理し、レイヤーごとの形状を更新し、model.reshape メソッドを適用します。例えば、次のコードは、すべての入力レイヤーで 2 番目の次元を動的に設定します:

# すべての入力レイヤーの 2 番目の次元に動的形状を割り当て 
shapes = {} 
for input_layer in model.inputs: 
    shapes[input_layer] = input_layer.partial_shape 
    shapes[input_layer][1] = -1 
model.reshape(shapes)
// すべての入力レイヤーの 2 番目の次元に動的形状を割り当てる 
std::map<ov::Output<ov::Node>, ov::PartialShape> port_to_shape; 
for (const ov::Output<ov::Node>& input : model->inputs()) { 
    ov::PartialShape shape = input.get_partial_shape(); 
    shape[1] = -1; 
    port_to_shape[input] = shape; 
} 
model->reshape(port_to_shape);

複数の入力レイヤーを変更する方法の例については、入力形状の変更を参照してください。

未定義の次元 “アウトオブボックス”#

多くの DL フレームワークは、動的な (または未定義の) 次元を持つモデルの生成をサポートしています。このようなモデルが convert_model() で変換されるか、Core::read_model で直接読み取られる場合、その動的次元は保持されます。これらのモデルを動的形状で使用するのに追加の構成は必要ありません。

モデルに動的次元があるか確認するには、read_model() メソッドを使用してモデルをロードし、各レイヤーの partial_shape プロパティーを確認します。モデルに動的次元ある場合、それらは ? として報告されます。例えば、次のコードは各入力レイヤーの名前と次元を出力します:

# モデル入力レイヤー情報を出力 
for input_layer in model.inputs: 
    print(input_layer.names, input_layer.partial_shape)
ov::Core core; 
auto model = core.read_model("model.xml"); 

// 最初の入力レイヤーの情報を出力 
std::cout << model->input(0).get_partial_shape() << "\n"; 

// 2番目の入力レイヤーの情報を出力 
std::cout << model->input(1).get_partial_shape() << "\n"; 

// その他

入力モデルにすでに動的次元がある場合、それは推論中に変更されることはありません。入力が動的ではない場合、アプリケーションのメモリーを節約し、推論速度を向上させる可能性があるため、reshape メソッドを使用して入力を静的な値に設定することを推奨します。OpenVINO API は、静的次元と動的次元の任意の組み合わせをサポートします。

静的および動的次元は、convert_model() でモデルを変換する際に設定することもできます。これは reshape メソッドと同じ機能を備えているため、アプリケーション・コード内ではなく、事前に動的形状にモデルを変換することで時間を節約できます。convert_model() を使用して入力形状を設定する方法については、入力形状の設定を参照してください。

次元境界#

動的次元の下限および/または上限も指定できます。これには、次元に許可される値の範囲を定義します。次元境界は、以下に示すオプションを使用して reshape メソッドに下限と上限を渡すことで設定できます。

これらの各オプションは同等です:

  • 下限と上限を reshape メソッドに直接渡します (例: model.reshape([1, 10), (8,512)]))。

  • ov.Dimension を使用して下限と上限を渡します (例: model.reshape([ov.Dimension(1, 10), (8, 512)]))。

  • 次元範囲を文字列として渡します (例: model.reshape(“1..10, 8..512”))。

以下の例は、デフォルトの静的形状 [1,3,224,224] を使用して mobilenet-v2 モデルの動的な次元境界を設定する方法を示しています。

# 例 1 - 最初の次元を動的 (境界なし) に設定し、3 番目と 4 番目の次元を 112..448 の範囲に設定 
model.reshape([-1, 3, (112, 448), (112, 448)]) 

# 例 2 - 最初の次元を 1..8 の範囲に設定し、3 番目と 4 番目の次元を 112..448 の範囲に設定 
model.reshape([(1, 8), 3, (112, 448), (112, 448)])

以下に示すように、次元境界は ov::Dimension の引数として記述できます:

// どちらの次元も動的で、最初のサイズは 1..10 以内、2 番目のサイズは 8..512 以内 
model->reshape({{ov::Dimension(1, 10), ov::Dimension(8, 512)}}); // {1..10,8..512} 

// どちらの次元も動的であり、最初の次元には境界がなく、2 番目の次元は 8..512 の範囲 
model->reshape({{-1, ov::Dimension(8, 512)}}); // {?,8..512}

以下に示すように、次元境界は ov_dimension の引数として記述できます:

// どちらの次元も動的で、最初のサイズは 1..10 以内、2 番目のサイズは 8..512 以内 
{ 
ov_partial_shape_t partial_shape; 
ov_dimension_t dims[2] = {{1, 10}, {8, 512}}; 
ov_partial_shape_create(2, dims, &partial_shape); 
ov_model_reshape_single_input(model, partial_shape); // {1..10,8..512} 
ov_partial_shape_free(&partial_shape); 
} 

// どちらの次元も動的で、最初の次元には境界がなく、2 番目の次元は 8..512 の範囲 
{ 
ov_partial_shape_t partial_shape; 
ov_dimension_t dims[2] = {{-1, -1}, {8, 512}}; 
ov_partial_shape_create(2, dims, &partial_shape); 
ov_model_reshape_single_input(model, partial_shape); // {?,8..512} 
ov_partial_shape_free(&partial_shape); 
}

境界に関する情報は、推論プラグインに最適化の可能性を与えます。動的形状では、プラグインがモデルのコンパイル中により柔軟な最適化アプローチを適用することを前提としています。モデルのコンパイルと推論により時間/メモリーが必要になる場合があります。したがって、境界などの追加情報を提供することが有益である場合があります。同様の理由で、実際に必要としない限り、次元を未定義にすることはお勧めできません。

境界を指定する場合、下限は上限ほど重要ではありません。上限を指定することで、推論デバイスが中間テンソルにメモリーをより正確に割り当てることができます。また、異なるサイズに対してより少数の調整されたカーネルを使用することも可能になります。下限または上限を指定する利点はデバイスによって異なります。プラグインによっては、上限の指定が必要な場合があります。各デバイスでの動的形状のサポート状況については、機能サポート表を参照してください。

プラグインが境界なしでモデルを実行できる場合でも、次元の下限と上限が判明している場合は、それらを指定することを推奨します。

動的データの準備と推論#

reshape メソッドを使用してモデルを構成した後に、適切なデータ形状を持つテンソルを作成し、それらを推論要求としてモデルに渡します。これは、OpenVINO™ とアプリケーションの統合で説明されている通常の手順と似ています。ただし、テンソルをさまざまな形状のモデルに渡すことができることが異なります。

以下のサンプルは、モデルがさまざまな入力形状をどのように受け入れるかを示しています。最初の例では、モデルは 1x128 入力形状に対して推論を実行し、結果を返します。2 番目の例では、1x200 の入力形状が使用されますが、動的形状であるため、モデルはこれを処理できます。

# 最初の推論呼び出しでは、1x128 形状の入力テンソルを準備し、推論要求を実行 
input_data1 = np.ones(shape=[1,128]) 
infer_request.infer({input_tensor_name: input_data1}) 

# 結果の出力を取得 
output_tensor1 = infer_request.get_output_tensor() 
output_data1 = output_tensor1.data[:]
# 2 番目の推論呼び出しでは、1x200 入力テンソルを準備し、推論要求を実行 
input_data2 = np.ones(shape=[1,200]) 
infer_request.infer({input_tensor_name: input_data2}) 

# 結果の出力を取得 
output_tensor2 = infer_request.get_output_tensor() 
output_data2 = output_tensor2.data[:]
// 最初の推論呼び出し 

// モデル入力と互換性のあるテンソルを作成します 
// Shape {1, 128} は、前の例で行われたすべての reshape ステートメントと互換性があります 
auto input_tensor_1 = ov::Tensor(model->input().get_element_type(), {1, 128}); 
// ... Input_tensor_1 に値を書き込む 

// テンソルを推論リクエストの入力として設定 
infer_request.set_input_tensor(input_tensor_1); 

// 推論を実行 
infer_request.infer(); 

// 出力データを表すテンソルを取得 
ov::Tensor output_tensor = infer_request.get_output_tensor(); 

// 動的モデルの場合、出力形状は通常入力形状に依存します。 
// つまり、出力テンソルの形状は最初の推論後にのみ初期化され、 
// 推論要求ごとに照会する必要があります。 
auto output_shape_1 = output_tensor.get_shape(); 

// テンソルデータへの適切な型のポインタを取り、形状に応じて要素を読み取り 
// モデル出力が f32 データタイプであると仮定 
auto data_1 = output_tensor.data<float>(); 
// ... 値を読みとり 

// 2 回目の推論呼び出しでは、手順を繰り返します: 
// 別のテンソルを作成 (前のテンソルが利用できない場合) 
// input_tensor_1 とは形状が異なることに注意 
auto input_tensor_2 = ov::Tensor(model->input().get_element_type(), {1, 200}); 
// ... 値を input_tensor_2 へ書き込み 

infer_request.set_input_tensor(input_tensor_2); 

infer_request.infer(); 

// infer_request.get_output_tensor() を再度呼び出す必要なし 
// 上記の最初の推論呼び出しの後に照会された Output_tensor は、ここでは有効です。
// ただし、形状が変化したため、下のメモリーには当てはまらない可能性があるため、ポインターを再取得: 
auto data_2 = output_tensor.data<float>(); 

// 新しい形状 
auto output_shape_2 = output_tensor.get_shape(); 

// ... 形状 output_shape_2 に従って data_2 の値を読み取り
ov_output_port_t* input_port = NULL; 
ov_element_type_e* type = NULL; 
ov_shape_t input_shape_1; ov_tensor_t* input_tensor_1 = NULL; 
ov_tensor_t* output_tensor = NULL; 
ov_shape_t output_shape_1; 
void* data_1 = NULL; 
ov_shape_t input_shape_2; 
ov_tensor_t* input_tensor_2 = NULL; 
ov_shape_t output_shape_2; 
void* data_2 = NULL; 
// 最初の推論呼び出し 

// モデル入力と互換性のあるテンソルを作成します 
// Shape {1, 128} は、前の例で行われたすべての reshape ステートメントと互換性があります 
{ 
ov_model_input(model, &input_port); 
ov_port_get_element_type(input_port, type); 
int64_t dims[2] = {1, 128}; 
ov_shape_create(2, dims, &input_shape_1); 
ov_tensor_create(type, input_shape_1, &input_tensor_1); 
// ... Input_tensor に値を書き込む 
} 

// テンソルを推論リクエストの入力として設定 
ov_infer_request_set_input_tensor(infer_request, input_tensor_1); 

// 推論を実行 
ov_infer_request_infer(infer_request); 

// 出力データを表すテンソルを取得 
ov_infer_request_get_output_tensor(infer_request, &output_tensor); 

// 動的モデルの場合、出力形状は通常入力形状に依存します。 
// つまり、出力テンソルの形状は最初の推論後にのみ初期化され、 
// 推論要求ごとに照会する必要があります。 
ov_tensor_get_shape(output_tensor, &output_shape_1); 

// テンソルデータへの適切な型のポインタを取り、形状に応じて要素を読み取り 
// モデル出力が f32 データタイプであると仮定 
ov_tensor_data(output_tensor, &data_1); 
// ... 値を読みとり 

// 2 回目の推論呼び出しでは、手順を繰り返します: 
// 別のテンソルを作成 (前のテンソルが利用できない場合) 
// input_tensor_1 とは形状が異なることに注意してください。 
{ 
int64_t dims[2] = {1, 200}; 
ov_shape_create(2, dims, &input_shape_2); 
ov_tensor_create(type, input_shape_2, &input_tensor_2); 
// ... input_tensor_2 に値を書き込み 
} 

ov_infer_request_set_input_tensor(infer_request, input_tensor_2); 
ov_infer_request_infer(infer_request); 

// infer_request.get_output_tensor() を再度呼び出す必要はありません 
// 上記の最初の推論呼び出しの後に照会された Output_tensor は、ここでは有効です。
// ただし、形状が変化したため、下のメモリーには当てはまらない可能性があるため、ポインターを再取得: 
ov_tensor_data(output_tensor, &data_2); 

// 新しい形状 
ov_tensor_get_shape(output_tensor, &output_shape_2); 
// ... 形状 output_shape_2 に従って data_2 の値を読み取り 

// リソースを解放 
ov_output_port_free(input_port); 
ov_shape_free(&input_shape_1); 
ov_tensor_free(input_tensor_1); 
ov_shape_free(&output_shape_1); 
ov_shape_free(&input_shape_2); 
ov_tensor_free(input_tensor_2); 
ov_shape_free(&output_shape_2); 
ov_tensor_free(output_tensor);

入力データをモデルに適用して推論を実行する方法の詳細については、OpenVINO™ 推論要求を参照してください。

出力の動的形状#

モデルの入力で動的次元を使用する場合、動的入力がモデルにどのように伝播されるかに応じて、出力次元も動的になる場合があります。例えば、入力形状のバッチ次元は通常、モデル全体に伝播され、出力形状に表示されます。また、NLP モデルのシーケンス長やセグメント化モデルの空間次元など、ネットワーク全体に伝播される他の次元にも適用されます。

出力に動的次元があるか確認するには、モデルの読み取りまたは再形状後に、モデルの出力レイヤーの partial_shape プロパティーを照会します。同じプロパティーをモデル入力に対しても照会できます。例:

# 出力部分形状をプリント 
print(model.output().partial_shape) 

# 入力部分形状をプリント 
print(model.input().partial_shape)
// 出力部分形状をプリント 
std::cout << model->output().get_partial_shape() << "\n"; 

// 入力部分形状をプリント 
std::cout << model->input().get_partial_shape() << "\n";
ov_output_port_t* output_port = NULL; 
ov_output_port_t* input_port = NULL; 
ov_partial_shape_t partial_shape; 
char * str_partial_shape = NULL; 

// 出力部分形状のプリント 
{ 
ov_model_output(model, &output_port); 
ov_port_get_partial_shape(output_port, &partial_shape); 
str_partial_shape = ov_partial_shape_to_string(partial_shape); 
printf("The output partial shape: %s", str_partial_shape); 
} 

// 入力部分形状のプリント { 
ov_model_input(model, &input_port); 
ov_port_get_partial_shape(input_port, &partial_shape); 
str_partial_shape = ov_partial_shape_to_string(partial_shape); 
printf("The input partial shape: %s", str_partial_shape); } 

// 割り当てられたリソースを解放 
ov_free(str_partial_shape); 
ov_partial_shape_free(&partial_shape); 
ov_output_port_free(output_port); 
ov_output_port_free(input_port);

出力に動的次元ある場合、それらは ? または範囲で (例:1..10) として報告されます。

出力レイヤーは、partial_shape.is_dynamic() プロパティーを使用して動的次元をチェックすることもできます。これは、次のように、出力レイヤー全体で使用することも、個々の次元で使用することもできます:

 if model.input(0).partial_shape.is_dynamic: # 入力は動的 
    pass 

if model.output(0).partial_shape.is_dynamic: # 出力は動的 
    pass 

if model.output(0).partial_shape[1].is_dynamic: # 出力の 1 次元目は動的 
    pass
auto model = core.read_model("model.xml"); 

if (model->input(0).get_partial_shape().is_dynamic()) { 
    // 入力は動的 
} 

if (model->output(0).get_partial_shape().is_dynamic()) { 
    // 出力は動的 
} 

if (model->output(0).get_partial_shape()[1].is_dynamic()) { 
    // 出力の 1 次元目は動的 
}
ov_model_t* model = NULL; 
ov_output_port_t* input_port = NULL; 
ov_output_port_t* output_port = NULL; 
ov_partial_shape_t partial_shape; 

ov_core_read_model(core, "model.xml", NULL, &model); 

// 入力 
{ 
ov_model_input_by_index(model, 0, &input_port); 
ov_port_get_partial_shape(input_port, &partial_shape); 
if (ov_partial_shape_is_dynamic(partial_shape)) { 
    // 入力は動的 
} 
} 

// 出力 
{ 
ov_model_output_by_index(model, 0, &output_port); 
ov_port_get_partial_shape(output_port, &partial_shape); 
if (ov_partial_shape_is_dynamic(partial_shape)) { 
    // 出力は動的 
} 
} 

// 割り当てられたリソースを解放 
ov_partial_shape_free(&partial_shape); 
ov_output_port_free(input_port); 
ov_output_port_free(output_port);

少なくとも 1 つの動的次元がモデルの出力レイヤーに存在する場合、出力テンソルの実際の形状は推論中に決定されます。最初の推論の前に出力テンソルのメモリーは割り当てられず、形状は [0] です。

メモリー内に出力テンソル用のスペースを事前に割り当てるには、出力で予想される形状を指定して set_output_tensor メソッドを使用します。これにより、内部で set_shape メソッドが呼び出され、初期形状が計算された形状に置き換えられます。