見出し画像

Barracuda 1.0.0 - Unity用の軽量でクロスプラットフォームなニューラルネットワーク推論ライブラリ

以下の記事を参考に書いてます。

Introduction to Barracuda | Barracuda | 1.0.0

1. Barracuda

Barracuda」は、Unity用の軽量でクロスプラットフォームなニューラルネットワーク推論ライブラリです。GPUとCPUの両方で推論できます。

次のようなシンプルなコードで、ニューラルネットワークモデルで推論することができます。

var model = ModelLoader.Load(filename);
var engine = WorkerFactory.CreateWorker(model, WorkerFactory.Device.GPU);

var input = new Tensor(1, 1, 1, 10);
var output = engine.Execute(input).PeekOutput();

ニューラルネットワークモデルの形式は「ONNX」のため、「Pytorch」「TensorFlow」「Keras」などの様々なフレームワークから、ニューラルネットワークモデルを読み込むことができます。

Unity 2018.4以降」で利用可能です。

2. サポートしているプラットフォーム

サポートしているプラットフォームは、次のとおりです。

・CPU推論:全てのUnityプラットフォームでサポート。
・GPU推論:以下を除くすべてのUnityプラットフォームでサポート。
 ・Android / iOS上のOpenGL ES:Vulkan / Metalを使用。
 ・Mac上のOpenGL Core:Metalを使用。
 ・WebGL:CPU推論を使用。

3. サポートしているニューラルネットワークアーキテクチャ

サポートしているニューラルネットワークアーキテクチャは、次のとおりです。

ML-Agents (強化学習)
MobileNet v1/v2 (画像分類)
Tiny YOLO v2 (物体検出)
・U-Net (セグメンテーション)
・Fully convolutional (セグメンテーション)
・Fully dense (セグメンテーション)
Spade

4. ONNXモデルへのエクスポート

訓練済みのニューラルネットワークを使用するには、「ONNXモデル」にエクスポートする必要があります。次の深層学習フレームワークからエクスポートできます。

・Pytorch
・Tensorflow
・Keras

◎ Pytorch
「Pytorchモデル」の「ONNXモデル」へのエクスポートは、APIが用意されてるため簡単です。

# ニューラルネットワーク
net = ...

# モデルの入力
x = torch.randn(1, 3, 256, 256)

# モデルのエクスポート
torch.onnx.export(net,         # モデル
    x,                         # モデルの入力
    "example.onnx",            # ONNXファイル名
    export_params=True,        # 重みをONNXファイルに保存
    opset_version=9,           # ONNXのバージョン
    do_constant_folding=True,  # 最適化のために定数の折りたたみの実行
    input_names = ['X'],       # モデルの入力名
    output_names = ['Y']       # モデルの出力名
    )

◎ TensorFlow
「TensorFlowモデル」を「ONNX」にエクスポートするには、PyTorchより少し時間がかかりますが、簡単です。

(1) 「tf2onnx」をインストール。
使用方法は、以下の記事が参考になります。

Jupyterノートブックチュートリアル
TensorFlowフリーズグラフからの保存、読み込み、推論に関する記事

(2) TensorFlowを.pd形式で保存。

# ニューラルネットワーク
net = ...

# モデルのエクスポート
tf.saved_model.save(net, "saved_model")
# またh
tf.train.write_graph(self.sess.graph_def, directory,
    'saved_model.pb', as_text=False)

(3) 「tp2onnx」を使用して.pbファイルを.onnxに変換。

# 保存したニューラルネットワークの読み込み
graph_def = tf.compat.v1.GraphDef()
with open(modelPathIn, 'rb') as f:
    graph_def.ParseFromString(f.read())

with tf.Graph().as_default() as graph:
    tf.import_graph_def(graph_def, name='')

# 最適化してONNXに保存
# 【注意】tfはレイヤー名に:0を追加
inputs[:] = [i+":0" for i in inputs]
outputs[:] = [o+":0" for o in outputs]

# オプション。読みやすさとBarracudaへのインポートを容易にするのに役立つ
newGraphModel_Optimized = tf2onnx.tfonnx.tf_optimize(inputs, outputs, graph_def)

# モデルの保存
tf.compat.v1.reset_default_graph()
tf.import_graph_def(newGraphModel_Optimized, name='')

with tf.compat.v1.Session() as sess:
    # NCHWのONNXとNHWC形式のTensorflowでは、input_as_nchwを追加することをお勧めする
    g = tf2onnx.tfonnx.process_tf_graph(sess.graph,input_names=inputs, output_names=outputs, inputs_as_nchw=inputs)

    model_proto = g.make_model(modelPathOut)
    checker = onnx.checker.check_model(model_proto)

    tf2onnx.utils.save_onnx_model("./", "saved_model", feed_dict={}, model_proto=model_proto)


# onnxruntimeの検証
if(args.validate_onnx_runtime):
    print("validating onnx runtime")
    import onnxruntime as rt
    sess = rt.InferenceSession("saved_model.onnx")

次のように、コマンドラインを使用することもできます。

python -m tf2onnx.convert --graphdef model.pb --inputs=input:0 --outputs=output:0 --output model.onnx

◎ Keras
「Kerasモデル」を「ONNXモデル」にエクスポートするには、「keras2onnx」を使います。

使用方法は、以下の記事が参考になります。

KerasモデルをONNXに変換することに関する記事
Keras ONNX Githubサイト

次のコードは、上記Kerasチュートリアルからの抜粋です。

# ニューラルネットワーク
net = ...

# モデルをONNXに変換
onnx_model = keras2onnx.convert_keras(net,  # モデル
    name="example",                         # 変換されたONNXモデルの内部名
    target_opset=9,                         # ONNXのバージョン
    channel_first_inputs=None               # NHWCからNCHWに転置する入力
    )

onnx.save_model(onnx_model, "example.onnx")
【注意】基本的に、「ONNX opset=9」を使用してください。これは、Barracudaでの適用範囲が広いためです。また、TensorFlowまたはKerasからエクスポートする場合は、「TF-2」ではなく「TF-1」を使用してください。

5. ONNXモデルのインポート

訓練済みモデルをインポートする前に、モデルをONNX形式にエクスポートしておく必要があります。

◎ リソースの読み込み
有効なONNXモデルがある場合は、プロジェクトにインポートします。これを行うには、.onnxファイルをプロジェクトのAssetsフォルダに追加します。
UnityはモデルをNNModelアセットとしてインポートします。

画像1

画像2

その後、次のようにスクリプトでこのアセットを直接参照できます。

public NNModel modelAsset;

モデルはアセットラッパーであり、バイナリ形式で保存されます。次のように、それを(Model型の)ランタイムモデルにコンパイルする必要があります。

public NNModel modelAsset;
private Model m_RuntimeModel;

void Start()
{
    m_RuntimeModel = ModelLoader.Load(modelAsset);
}

◎ ランタイムの読み込み
ModelLoaderを使用して、指定したパスからアセットを直接読み込むこともできます。

Model model = ModelLoader.Load(modelPath);

詳細については、「実行時のリソースの読み込み」を参照してください。

6. IWorkerインターフェース:エンジンの中核

「Barracuda」のコアエンジンインターフェイスは「IWorker」と呼ばれます。「IWorker」はモデルを実行可能なタスクに分解し、それらをGPUまたはCPUでスケジュールします。

【注意】プラットフォームによっては、一部のバックエンドをサポートしていない場合があります。詳しくは、「サポートされているプラットフォーム」を参照してください。

◎ 推論エンジンの作成(Worker)
WorkerFactoryからWorkerを作成できます。 バックエンドとロードされたモデルを指定する必要があります。

Model model = ...

// GPU
var worker = WorkerFactory.CreateWorker(WorkerFactory.Type.ComputePrecompiled, model)
var worker = WorkerFactory.CreateWorker(WorkerFactory.Type.Compute, model)
// slow - GPU path
var worker = WorkerFactory.CreateWorker(WorkerFactory.Type.ComputeRef, model)

// CPU
var worker = WorkerFactory.CreateWorker(WorkerFactory.Type.CSharpBurst, model)
var worker = WorkerFactory.CreateWorker(WorkerFactory.Type.CSharp, model)
// very slow - CPU path
var worker = WorkerFactory.CreateWorker(WorkerFactory.Type.CSharpRef, model)
労働者のタイプ

◎ Worker種別
ネットワークを実行するために選択できる、いくつかの異なるバックエンドがあります。

◎ CPU
・CSharpBurst : Burstを介してコンパイルされた、非常に効率的でジョブ化および並列化されたCPUコード。
・CSharp : 少し効率の悪いCPUコード。
・CSharpRef : 効率は落ちますが、より安定したリファレンス実装。

◎ GPU
・ComputePrecompiled : すべてのオーバーヘッドコードが取り除かれ、ワーカーにプリコンパイルされた非常に効率的なGPUコード。
・Compute : 非常に効率的なGPUですが、ロジックオーバーヘッドが多少あります。
・ComputeRef : 効率は落ちますが、より安定したリファレンス実装。

【注意】リファレンス実装を他の実装と比較するためのstable baselineとして使用できます。

バグや間違った推論に気付いた場合は、より単純なワーカーを選択することで問題が解決するかどうかを確認してください。 Barracuda GitHubリポジトリに報告してください。

7. モデルの実行

Barracudaでモデルを実行するには、モデルをロードしてWorkerを作成する必要があります。

◎ 基本的な実行
単一のTensorオブジェクト(モデルに単一の入力がある場合)または名前とTensorのペアの辞書として入力を提供できます。

    Tensor input = new Tensor(batch, height, width, channels); 
    worker.Execute(input);
    var inputs = new Dictionary<string, Tensor>();
    inputs[name1] = new Tensor(batch, height1, width1, channels1);
    inputs[name2] = new Tensor(batch, height2, width2, channels2);
    worker.Execute(inputs);

GPUバックエンドの実行は非同期です。実行は、CPUバーストバックエンドでは非同期で、残りのCPUバックエンドでは同期です。

◎ スケジュールされた実行
実行は数フレームにわたってスケジュールできます。次のコードを使用してWorkerをスケジュールできます。

Tensor ExecuteInParts(IWorker worker, Tensor I, int syncEveryNthLayer = 5)
{
    var executor = worker.ExecuteAsync(I);
    var it = 0;
    bool hasMoreWork;

    do
    {
        hasMoreWork = executor.MoveNext();
        if (++it % syncEveryNthLayer == 0)
            worker.WaitForCompletion();

    } while (hasMoreWork);

    return worker.CopyOutput();
}

8. モデルの出力の取得

モデルを実行した後、出力または中間情報を取得できます。

◎ 出力の取得
モデルに単一出力がある場合は、worker.PeekOutput() を使用できます。それ以外の場合は、出力名を指定します。

var output = worker.PeekOutput(outputName);
// または
var output = worker.PeekOutput(); // warning: returns outputs[0]
【注意】worker.PeekOutput()はTensorの所有権をあなたに譲渡しません。Workerによって所有されています。worker.PeekOutput()を呼び出すことをお勧めし、メモリ割り当てを減らします。ただし、Tensorを長期間使用することが予想される場合は、worker.Fetch()を呼び出します。それ以外の場合、worker.Execute()の次の呼び出し後、またはworker.Dispose()の呼び出し後にTensor値が失われます。

◎ 中間ノードの取得
Workerの作成時に照会するノード名のリストを渡すことにより、中間ノード値を分析することもできます。

// クエリするノードのリスト
var additionalOutputs = new string[] {"layer0", "layer1"}
m_Worker = WorkerFactory.CreateWorker(<WorkerFactory.Type>, m_RuntimeModel, additionalOutputs);
...
var outputLayer0 = worker.PeekOutput("layer0");
var outputLayer1 = worker.PeekOutput("layer1");
// ネットワークの元の出力をクエリすることも可能
var output = worker.PeekOutput(outputName);

◎ Barracudaモデルの出力の取得
Barracudaモデルは単純な記憶表現を持っています。モデルが読み込まれると、入力と出力をクエリできます。

string[] inputNames = model.inputs;   // モデルの入力のクエリ
string[] outputNames = model.outputs; // モデルの出力のクエリ

または、レイヤーを直接反復処理して、モデルが何を行うかを調査できます。

foreach (var layer in model.layers)
    Debug.Log(layer.name + " does " + layer.type);

定数を照会することもできます

Tensor[] constants = worker.PeekConstants(layerName);

◎ Verboseモード
「Barracuda」のさまざまな部分で詳細モードを有効にできます。

bool verbose = true;
var model = ModelLoader.LoadModel(onnxAsset, verbose); // verbose loader
var worker = WorkerFactory.CreateWorker(WorkerFactory.Type.ComputePrecompiled, model, verbose); // verbose execution

9. Tensorのデータ処理

◎ Tensor
Barracudaでは、バッチ、高さ、幅、チャネルを介してTensor値にアクセスできます。

画像3

【注意】ネイティブのONNXデータレイアウトはNCHW、つまりチャンネルファーストです。Barracudaは、ONNXモデルをNHWCレイアウトに自動的に変換します。

◎ データアクセス
多次元配列演算子を介してTensorデータを操作できます。

var tensor = new Tensor(batchCount, height, width, channelCount);

// as N batches of 3 dimensional data: N x {X, Y, C}
tensor[n, y, x, c] = 1.0f;
// as N batches of 1 dimensional data: N x {C}
tensor[n,       c] = 2.0f;
// as flat array
tensor[         i] = 3.0f;

◎ コンストラクタ
複数のTensorコンストラクタが様々なシナリオをカバーしています。デフォルトでは、初期化配列を提供しない限り、Tensorは構築時に0で初期化されます。

// batch of 3 dimensional data, 0 initialized: batchCount x {height, width, channelCount}
tensor = new Tensor(batchCount, height, width, channelCount);    
// batch of 1 dimensional data, 0 initialized: batchCount x {elementCount}
tensor = new Tensor(batchCount, elementCount);  
var stridedArray = new float[batchCount * elementCount] { ... };
// batch of 1 dimensional data, initialized from strided array
tensor = new Tensor(batchCount, elementCount, stridedArray); 
var jaggedArray = new float[batchCount][elementCount] { ... };
// batch of 1 dimensional data, initialized from jagged array
tensor = new Tensor(batchCount, elementCount, jaggedArray); 
Texture2D texture = ...;
// tensor initialized with texture data: 1 x { texture.width, texture.height, 3}
tensor = new Tensor(texture);

Tensorオブジェクトの形状をクエリできますが、Tensorの形状を変更することはできません。Tensorの別の形状が必要な場合は、Tensorオブジェクトの新しいインスタンスを作成する必要があります。

var shape = tensor.shape;
Debug.Log(shape + " or " + shape.batch + shape.height + shape.width + shape.channels);

◎ Textureコンストラクタ
入力TextureからTensorを作成するか、TensorをTextureに直接保存できます。

【注意】これを行うと、ピクセル値は[0,1]の範囲になります。たとえば、ネットワークが[0,255]の間の値を期待している場合は、それに応じてデータを変換します。

(1) 入力Texture
CPUの個々のピクセルにアクセスせずに、直接Texture2D、Texture2DArray、Texture3D、またはRenderTextureをBarracudaに渡すことができます。

// you can treat input pixels as 1 (grayscale), 3 (color) or 4 (color with alpha) channels
var channelCount = 3; 
var tensor = new Tensor(texture, channelCount);

複数のテクスチャを単一のTensorオブジェクトにバッチ処理できます。

// these textures form a batch
var textures = new [] { texture0, texture1, texture2, texture3 }; 
var tensor = new Tensor(textures, channelCount);
【注意】バッチ内のすべてのテクスチャは、幅と高さが同じだけなりなりません。

(2) 出力Texture
Barracudaの実行結果をグラフィックスパイプラインでさらに使用したい場合は、CPUまたはGPUを停止させることなく、TensorからRenderTextureにデータをコピーできます。

var tensor = worker.PeekOutput();
var texture = BarracudaTextureUtils.TensorToRenderTexture(tensor);

同じRenderTextureを複数回再利用できます。

var texture = new RenderTexture(width, height, 0);
// ...
tensor = worker.PeekOutput();
BarracudaTextureUtils.TensorToRenderTexture(tensor, texture);

◎ クリーンアップ
Barracudaユーザーは、workers.Fetch() を介して作成、受信、または tensor.TakeOwnership() を呼び出して所有権を取得した入力、出力、およびデータに対して Dispose() を呼び出す必要があります。

【注意】これは、GPUリソースを適切に解放するために必要です。
tensor.Dispose();
【注意】worker.PeekOutput() 呼び出しで受け取ったTensorで Dispose() を呼び出す必要はありません。

10. メモリ管理

Barracudaのユーザーは、すべてのWorkerでDispose()を呼び出す必要があります。worker.CopyOutput()経由で取得する場合、またはtensor.TakeOwnership() を呼び出してそれらの所有権を取得する場合は、出力でDispose()を呼び出す必要があります。

【注意】GPUリソースを適切に解放するには、Dispose()を呼び出す必要があります。
public void OnDestroy()
{
    worker?.Dispose();

    // 辞書として渡された複数の入力を持つ想定モデル
    foreach (var key in inputs.Keys)
    {
        inputs[key].Dispose();
    }

    inputs.Clear();
}
【注意】worker.PeekOutput()を呼び出しで受け取ったTensorに対してDispose()を呼び出す必要はありません。

【おまけ】 サポートされるONNXオペレータ

「Barracuda」がサポートするONNXオペレータとパラメータは、次のとおりです。

◎ オペレータ
・Constant
・Reshape
・Shape
・Unsqueeze
・Squeeze
・Flatten
・Concat
・Slice
・Gather
・OneHot
・LSTM
・Add
・Sum
・Sub
・Mul
・Div
・Pow
・Min
・Max
・Mean
・Greater
・Equal
・Or
・And
・Not
・Xor
・Pad
・AveragePool
・MaxPool
・GlobalAveragePool
・GlobalMaxPool
・Upsample
・Resize
・Transpose
・Gemm
・MatMul
・Conv
・ConvTranspose
・BatchNormalization
・ImageScaler
・InstanceNormalization
・RandomNormal
・RandomNormalLike
・RandomUniform
・RandomUniformLike
・Multinomial
・ReduceMax
・ReduceMean
・ReduceMin
・ReduceProd
・ReduceSum
・Identity
・Cast
・Dropout

◎ アクティベーション

・Relu
・Softmax
・LogSoftmax
・Tanh
・Sqrt
・Sigmoid
・Elu
・LeakyRelu
・Selu
・PRelu
・Exp
・Log
・Reciprocal
・Abs
・Neg
・Ceil
・Floor
・Clip

◎ Constant

sparse_value : not supported

◎ Unsqueeze

axis <= 1 : not supported

◎ Squeeze

axis <= 1 : not supported

◎ OneHot

axis : not supported

◎ AveragePool

ceil_mode : not supported
count_include_pad : not supported

◎ MaxPool

ceil_mode : not supported
dilations : not supported
storage_order : not supported

◎ Resize

opset-11 : not supported
=>
coordinate_transformation_mode : not supported
cubic_coeff_a : not supported, default to -0.75
exclude_outside : not supported, default to 0
extrapolation_value : not supported, default to 0
nearest_mode : not supported

◎ Gemm

alpha : not supported, default to 1
beta : not supported, default to 1
transA : not supported, default to 0

◎ ConvTranspose

dilations : not supported, default to {1,1}
group : not supported, default to 1
output_shape : not supported, default to [0]

◎ Softmax

axis : not supported

◎ LogSoftmax

axis : not supported


この記事が気に入ったらサポートをしてみませんか?