NumPy配列をモデルに渡す
| 項目 | 値 |
|---|---|
| カテゴリ | モデルと推論 |
| 難易度 | 中級 |
| 推定所要時間 | 10-15 minutes |
| ラベル | numpy, pytorch, tensor, io |
Neatを既存の推論スタックに統合する場合、これが連携の境界となります。ホストデータがNeatのTensorに、そしてNeatのTensorが再びホストデータになる方法です。これを最初から正しく行うことで、レイアウトの誤り、暗黙のデータ型変換、2つの環境間の予期しないエイリアシングなど、一般的な統合時のバグを防ぐことができます。
ここが、2つの言語が最も異なる点でもあります。PythonユーザーはNumPy/PyTorchを、C++ユーザーはOpenCVを使用します。変換の概念は同じですが、API名と型が異なるため、以下に示す言語ごとの説明が重要になります。最終的には、ホストデータをNeatのテンソルに変換し、コピーせずにペイロードを検査し、ソースバッファが削除されても安全な、所有権のあるコピーを作成で きるようになります。
ウォークスルー
ホストデータをテンソルとしてラップ
最初のステップでは、すでに保持しているデータをNeatのTensorに変換します。画像のレイアウトを明示的にタグ付けします(RGB)。これにより、ランタイムがバイトを正しく解釈し、推測しなくなります。copy=True(またはC++のCPUメモリの選択)は、テンソルが自身のバイトを所有するか、ソースとエイリアスするかを決定します。ソースバッファが変更または解放される可能性がある場合は、明示的な所有権が安全なデフォルトです。
simaai::neat::from_cv_mat(mat, ImageSpec::PixelFormat::RGB, TensorMemory::CPU)は、cv::MatをCPUベースのテンソルにラップします。
// from_cv_mat converts a cv::Mat into a CPU-backed Neat Tensor.
simaai::neat::Tensor tensor = simaai::neat::from_cv_mat(
rgb, simaai::neat::ImageSpec::PixelFormat::RGB, simaai::neat::TensorMemory::CPU);
ペイロードを検査
データがテンソルになったら、それを読み戻すことができます。これは、連携の半分の処理です。下流にデータを渡す前に、形状とバイトが変換によって保持されていることを確認します。
tensor.map_read()は、生のdataポインタとsize_bytesを公開するMappingを返します。これはテンソルのストレージへのビューであり、コピーは行われません。そのため、例では先頭のバイトを直接チェックサムできます。
// map_read yields a Mapping with a raw pointer and size in bytes.
simaai::neat::Mapping mapped = tensor.map_read();
コピーを所有する
最後に、元のソースバッファから完全に切り離されたデータを作成します。これにより、入力が削除された後も安全にデー タを保持できます。これは、長期間使用するコンシューマに渡すコピーです。
tensor.clone()は、それが由来するcv::Matとは独立した、新しいCPU所有のストレージにコピーします。
// clone() copies into CPU-owned storage, detached from the cv::Mat buffer.
simaai::neat::Tensor owned = tensor.clone();
if (owned.dense_bytes_tight() == 0U)
throw std::runtime_error("cloned tensor is empty");
実行
Neat インストールルート(share/ と lib/ を含むディレクトリ)から、Python および C++(事前にビルドされたもの) コマンドを実行します。ソースからビルド コマンドは、リポジトリのルートから実行します。この章では、モデルアーカイブは必要ありません。
C++ (prebuilt):
./lib/sima-neat/tutorials/tutorial_009_pass_numpy_to_model \
--width 128 --height 96
C++ (build from source):
./build.sh --target tutorial_009_pass_numpy_to_model
./build/tutorials-standalone/tutorial_009_pass_numpy_to_model \
--width 128 --height 96
予想される出力(C++):
tensor_rank=3
tensor_bytes=36864
head_checksum=4342
clone_bytes=36864
[OK] 009_pass_numpy_to_model
予想される出力(Python、torch がインストールされている場合):
numpy_roundtrip_shape=(96, 128, 3)
torch_roundtrip_shape=(96, 128, 3)
(torch がない場合、Python ビルドは torch 行の代わりに torch_roundtrip_skipped=True を出力します。)この章の C++ ソースを、カスタムの CMakeLists.txt を使用して独自のプロジェクトに統合する方法(追加のフォルダーは不要)については、ランディングページにある チュートリアルの実行方法 を参照してください。
実践
インターオペラビリティの概要を、簡単な参照のために、往復デモを過ぎた後に示します。
変換 API
- NumPy:
pyneat.Tensor.from_numpy(array, copy=..., image_format=...)(入力)、tensor.to_numpy(copy=...)(出力)。 - PyTorch:
pyneat.Tensor.from_torch(tensor, copy=..., image_format=...)(入力)、tensor.to_torch(copy=...)(出力)。 - OpenCV (C++):
simaai::neat::from_cv_mat(mat, pixel_format, memory)(入力)、ゼロコピービューの場合はtensor.map_read()、所有権のあるコピーの場合はtensor.clone()。
コピーとビュー
copy=True(Python)/clone()(C++)は、ソースから切り離されたデータを提供します。ソースが解放または変更された後でも、安全に使用できます。copy=False/map_read()は、ソースを参照するビューを提供します。コストは低いですが、ソースが存続し、変更されていない場合にのみ有効です。
レイアウトと dtype
- 画像データの場合は、常に明示的な
image_format/PixelFormatを渡して、レイアウトを解釈させ、推測させないようにします。 - Neat は、dtype を自動的に変換しません。テンソルの dtype をモデルの入力コントラクトに一致させてから、モデルに渡します。
完全なソース
完全なソースプログラムを表示
// Convert a cv::Mat into a Neat Tensor, map it read-only, and clone it.
//
// Usage:
// tutorial_009_pass_numpy_to_model [--width 128] [--height 96]
#include "neat.h"
#include <opencv2/core.hpp>
#include <algorithm>
#include <cstdint>
#include <iostream>
#include <stdexcept>
#include <string>
namespace {
bool get_arg(int argc, char** argv, const std::string& key, std::string& out) {
for (int i = 1; i + 1 < argc; ++i) {
if (key == argv[i]) {
out = argv[i + 1];
return true;
}
}
return false;
}
int parse_int_arg(int argc, char** argv, const std::string& key, int def) {
std::string value;
if (!get_arg(argc, argv, key, value))
return def;
return std::stoi(value);
}
} // namespace
int main(int argc, char** argv) {
try {
const int width = parse_int_arg(argc, argv, "--width", 128);
const int height = parse_int_arg(argc, argv, "--height", 96);
cv::Mat rgb(height, width, CV_8UC3, cv::Scalar(7, 17, 27));
if (!rgb.isContinuous())
rgb = rgb.clone();
// CORE LOGIC
// from_cv_mat converts a cv::Mat into a CPU-backed Neat Tensor.
simaai::neat::Tensor tensor = simaai::neat::from_cv_mat(
rgb, simaai::neat::ImageSpec::PixelFormat::RGB, simaai::neat::TensorMemory::CPU);
// map_read yields a Mapping with a raw pointer and size in bytes.
simaai::neat::Mapping mapped = tensor.map_read();
std::uint64_t checksum = 0;
const auto* bytes = static_cast<const std::uint8_t*>(mapped.data);
const std::size_t n = std::min<std::size_t>(mapped.size_bytes, 256);
for (std::size_t i = 0; i < n; ++i)
checksum += bytes[i];
if (tensor.shape.size() != 3U || mapped.size_bytes == 0U)
throw std::runtime_error("mapped tensor is empty or rank is wrong");
// CORE LOGIC
// clone() copies into CPU-owned storage, detached from the cv::Mat buffer.
simaai::neat::Tensor owned = tensor.clone();
if (owned.dense_bytes_tight() == 0U)
throw std::runtime_error("cloned tensor is empty");
std::cout << "tensor_rank=" << tensor.shape.size() << "\n";
std::cout << "tensor_bytes=" << mapped.size_bytes << "\n";
std::cout << "head_checksum=" << checksum << "\n";
std::cout << "clone_bytes=" << owned.dense_bytes_tight() << "\n";
std::cout << "[OK] 009_pass_numpy_to_model\n";
return 0;
} catch (const std::exception& e) {
std::cerr << "[FAIL] " << e.what() << "\n";
return 1;
}
}