コンテンツにスキップ

ONNX モデル(試験中)

この章は試験中です。 学習済みのモデルを、実行時に読み込むのではなくコンパイルの時点でコードへ焼き込みます(読み込む処理も、同時に配布するファイルも不要です)。

属性計算する場所結果
[Onnx("…")]Udon数として読み出せます。小さいモデル向けです
[OnnxGpu("…")]GPUはるかに大きいモデルを扱えますが、結果はバッファに出るので、数として使うには読み戻しが 1 段いります

ジェスチャの判定のように値を読んで分岐するなら前者、絵を作り替えるようにそのまま画面へ出すなら後者を選びます。

モデルのファイル名先頭に付く名前
gesture.onnxGesture
hand_pose.onnxHandPose

下線と拡張子は落ちます。

  1. 書き出した .onnx を、属性を書くスクリプトと同じフォルダへ置きます
  2. クラスに partial を付け、属性だけを書いて保存します
  3. Unity へ戻るとインポートが実行され、スクリプトの隣に生成されたファイルが現れます
  4. 生成された名前を使う処理を書きます
同時に書いたとき手順 2 と 4 を 1 度に書くと、生成される名前がまだ無いためコンパイルのエラーになります
エラーになったとき生成されたファイルは作られず、受け取れなかった理由が Console に出ます
モデルを差し替えたとき手順 3 が自動でやり直され、要素数の定数も新しい値になります

入力と出力を配列で渡し、結果を数として読み出します。

例にするモデル(ジェスチャの判定)

Section titled “例にするモデル(ジェスチャの判定)”
モデルのファイルJudge.cs と同じフォルダの gesture.onnx
入力pose — 要素が 12 個(形は [1,12])
出力scores — 要素が 4 個(形は [1,4])
生成されるファイルJudge.Gesture.Model.cs

属性を書いたスクリプト(ジェスチャの判定)

Section titled “属性を書いたスクリプト(ジェスチャの判定)”
using Tsukimi;
using UnityEngine;
// 属性の引数は、このスクリプトから見たモデルのファイルの経路です。
// 属性を 2 つ以上書くと、モデルごとに名前が生えます。
[Onnx("gesture.onnx")]
public partial class Judge : TsukimiBehaviour
{
// 生成されたファイルがこのクラスの残りになるので、partial が必要です。
// 入力に詰める値の取り出し元。Inspector で 12 個挿します。
public Transform[] joints;
// 入力と出力をまとめて持つ値。型は生成されたファイルが宣言します。
private GestureIO io;
// 選ばれた番号。他の Behaviour から読めます。
public int chosen;
void Start()
{
io = new GestureIO();
// 配列の確保は 1 度だけです。以後は同じ配列の中身を書き換えます。
// 要素数は生成された定数を使います(モデルを差し替えると値も変わります)。
io.In0 = new float[SizeIn0];
io.Out0 = new float[SizeOut0];
}
public void Decide()
{
// 入力は全部詰めます。詰め残した要素は前回の値のまま計算されます。
for (int i = 0; i < SizeIn0; i++) io.In0[i] = joints[i].localPosition.y;
// 計算します。返るまでに全部の層が進みます。
RunGesture(io);
// 結果は io.Out0 に入ります。ここでは最も大きい成分の番号を選びます。
chosen = 0;
for (int i = 1; i < SizeOut0; i++)
if (io.Out0[i] > io.Out0[chosen]) chosen = i;
}
}

生成されたファイル(ジェスチャの判定)

Section titled “生成されたファイル(ジェスチャの判定)”

重みと層の中身を省いた抜き書きです。

// This file was generated from an ONNX model. Manual edits are lost on the next generation.
// Source model: gesture
using UnityEngine;
// 入力と出力をまとめた型。もとのテンソルの名前と形がコメントに残ります。
public struct GestureIO
{
public float[] In0; // pose [1,12]
public float[] Out0; // scores [1,4]
}
public partial class Judge
{
// 配列を確保するときに使う要素数。コンパイルの時点で決まっています。
public const int SizeIn0 = 12; // [1,12]
public const int SizeOut0 = 4; // [1,4]
// private なので、入力を詰めずに外から呼ぶことはできません。
private void RunGesture(GestureIO io)
{
float[] In0 = io.In0; // pose
float[] Out0 = io.Out0; // scores
// 学習した重みは、配列への代入として並びます。
float[] c0 = new float[12];
c0[0] = 0.0002085293f;
c0[1] = 0.71990794f;
// …(重みの数だけ続きます)…
// 層と層の間に挟む配列。4 つずつまとめられる層は Vector4 になります。
float[] t5 = new float[12]; // centered [1,12]
Vector4[] t7 = new Vector4[6]; // g0 [1,24]
// 層は、もとのモデルでの名前と演算つきで 1 つずつ並びます。
// center: Elementwise pose mean -> centered
for (int i = 0; i < 12; i++)
{
t5[i] = In0[i] - c0[i];
}
// …(層の数だけ続きます)…
// out: Copy g2 -> scores
for (int i = 0; i < 4; i++)
{
Out0[i] = t11[i];
}
}
}

生成される名前の一覧(ジェスチャの判定)

Section titled “生成される名前の一覧(ジェスチャの判定)”
生成される名前説明
GestureIO入力と出力を持つ型。中身は配列で、In0・Out0 のように並びます
SizeIn0 / SizeOut0配列ごとの要素数。配列は Start で 1 度だけ確保します
RunGesture(io)その値の入力で計算し、出力を同じ値へ書き戻します
In0 / In1 …番号はモデルの入力の並び順です。出力も同じ規則です
呼び出せる場所計算するメソッドは外部から呼び出せません。入力を代入しないまま計算することを防ぐためです
費用計算は Udon の上で 1 つずつ進みます。毎フレーム計算する前にプロファイラで計測してください
重みの格納先重みはコードの中の定数です。モデルのファイルは配布物に含める必要がありません

画像をバッファへ入れ、結果を別のバッファで受け取ります。

例にするモデル(画像のフィルタ)

Section titled “例にするモデル(画像のフィルタ)”
モデルのファイルSharpen.cs と同じフォルダの filter.onnx
入力image — 64×64 の画像 3 成分(形は [1,3,64,64])
出力out — 同じ大きさの画像 3 成分(形は [1,3,64,64])
層畳み込みが 4 つ。間の channel は 8 → 16 → 16 と増えます
生成されるファイルSharpen.Filter.Model.cs

属性を書いたスクリプト(画像のフィルタ)

Section titled “属性を書いたスクリプト(画像のフィルタ)”
using Tsukimi;
using UnityEngine;
// 属性の引数は、このスクリプトから見たモデルのファイルの経路です。
[OnnxGpu("filter.onnx")]
public partial class Sharpen : TsukimiBehaviour
{
// 通す元の画像と、結果を映す面。Inspector で挿します。
public Texture source;
public Renderer display;
void Start()
{
// バッファを作ります。大きさはモデルの側から決まるので、指定しません。
LoadFilter();
// 元の画像を入力のバッファへ入れます。大きさが違うときは伸び縮みして入ります。
Gpu.Load(FilterInput, source);
}
void Update()
{
// 1 回ぶん計算します。層の数だけカーネルが順に走ります。
RunFilter();
// 結果は FilterOutput に残るだけなので、そのまま面へ映します。
Gpu.Show(FilterOutput, display);
}
}

生成されたファイル(画像のフィルタ)

Section titled “生成されたファイル(画像のフィルタ)”

層の中身を省いた抜き書きです。

using Tsukimi;
using UnityEngine;
public partial class Sharpen
{
/// Input. Write values from 0 to 1 (mapped to 0 to 1).
public GpuBuffer2D FilterInput;
/// Output. Read values from 0 to 1 (corresponding to -2.39134 to 1.3269).
public GpuBuffer2D FilterOutput;
// 層と層の間のバッファ。名前はモデルの名前で始まります。
private GpuBuffer2D Filterb1;
// …(層の数だけ続きます)…
// 層ごとの重みの表。
public Vector4[] Filterw0;
// …(層の数だけ続きます)…
/// Loads the weights and creates the buffers. Call once from <c>Start</c>.
private void LoadFilter()
{
// 入口は 64×64。channel が 4 以下なので、横に伸びません。
FilterInput = Gpu.Buffer(64, 64);
// 次の層は channel が 8=4 成分の帯が 2 本なので、横が 2 倍になります。
Filterb1 = Gpu.Buffer(128, 64);
// …(層の数だけ続きます)…
FilterOutput = Gpu.Buffer(64, 64);
}
// 層ごとに 1 つ、カーネルが生まれます。
[Kernel]
static Color4 FilterLayer0(KernelId id, GpuBuffer2D prev, [Capacity(128)] Vector4[] t)
{
// …(畳み込みの積和が並びます)…
}
/// Runs the model once. Write <c>FilterInput</c> before calling, then read <c>FilterOutput</c>.
private void RunFilter()
{
Gpu.Run(nameof(FilterLayer0), Filterb1, FilterInput, Filterw0);
// …(層の数だけ続きます)…
}
}

生成される名前の一覧(画像のフィルタ)

Section titled “生成される名前の一覧(画像のフィルタ)”
生成される名前説明
FilterInput入力のバッファ(GPU カーネルの実行と同じ型)。セルに代入する値の範囲は、生成されたファイルの行き先の欄に書いてあります
FilterOutput出力のバッファ。セルを 0 から 1 で読み出します
LoadFilter()バッファを作成し、重みを読み込みます。Start から 1 度だけ呼びます
RunFilter()1 回ぶん計算します
FilterLayer0層ごとのカーネル。Gpu.Run から起動されるので、直接は呼びません
FilterStyles経路を 2 本以上並べたときだけ生成されます。並べた数です
UseFilter(int style)経路を 2 本以上並べたときだけ生成されます。使う重みを、並べた順の番号(0 から)で選びます。LoadFilter() の直後は 0 です
値の範囲多くのモデルでは、セルに 0 から 1 の値を代入すれば、そのまま正しく渡ります。8 ビットの刻みがモデルの受け取る範囲とちょうど重なるためです。入力を 16 ビットの小数で受け取るモデルでは、0 から 1 の外の値もそのまま渡ります。どちらになるかはモデルが決め、生成されたファイルの入力の欄に書いてあります
出力の意味出た値が実際にどの範囲を指すかは、生成されたファイルに書いてあります
読み出し結果はバッファに出ます。数として使うときは Gpu.Texture で取り出し、実行環境の非同期の読み戻しへ渡します
重みの差し替え同じグラフで重みだけが違うモデルは、[OnnxGpu("a.onnx", "b.onnx")] のように経路を並べると 1 つにまとまります。バッファとカーネルは 1 組のままで、重みだけを並べた数だけ持ちます

ここに無い演算を含むモデルは、コンパイルの時点でエラーになります。

演算条件
GemmA と B は 2 次元です。transA・transB・alpha・beta を扱います
MatMul1 次元の相手は受け取れません。前にある軸はブロードキャストします
Add2 本の入力をブロードキャストします
Sub2 本の入力をブロードキャストします
Mul2 本の入力をブロードキャストします
Relu条件はありません
Identity条件はありません
Reshape形は定数で渡します(実行時に渡す形は受け取れません)
Flattenaxis は入力の次元の範囲内です
演算条件
Convkernel_shape は縦と横が同じです。strides と dilations を扱います。group は 1 か、入力と出力の channel 数に等しい形(depthwise)だけです。auto_pad は受け取れません
DepthToSpace並べ方は DCR だけです。倍率は 2 の冪で、channel が倍率の 2 乗で割り切れる必要があります
Concatchannel の軸で継ぐ形だけです。継げるのは 4 本までで、各入力の channel は 4 の倍数です
Add2 本とも前の演算の出力で、形が同じです。定数を足す形は受け取れません
QuantizeLinear計算ではなく、量子化の尺度として読みます。尺度とゼロ点は定数です
DequantizeLinear計算ではなく、量子化の尺度として読みます。量子化の代わりに Cast(float16)と Cast(float32)の組を置くと、その間は半精度の実数で運びます
Padreflect で、縁を埋めない Conv の直前に置いた形だけです(Conv に畳みます)
InstanceNormalizationscale と B は定数です
Resize補間は linear だけです。coordinate_transformation_mode は half_pixel・pytorch_half_pixel・align_corners・asymmetric を扱います。変えられるのは縦と横だけです
PRelu傾きは定数で、1 個か channel の数だけ持ちます
MaxPoolkernel_shape は縦と横が同じで、dilations は受け取れません。最大値の位置(Indices)は出せません
Elu8 ビットの入力だけです
Relu半精度の実数の入力も受け取ります
LeakyRelu半精度の実数の入力も受け取ります
Tanh8 ビットの入力だけです
Sigmoid8 ビットの入力だけです
Clip上限と下限は定数です。半精度の実数の入力も受け取ります
エラーになる形対処理由
量子化されていない重みを 8 ビットへ量子化し、出力も 8 ビットへ量子化して書き出す重みは 8 ビットの整数で焼き込みます。入力と層の間の値は、8 ビットか半精度の実数(Cast の組)で運べますが、出力は 8 ビットで受け取ります
尺度が実行時に決まる書き出す時点で決まる形にする焼き込むには、値の刻みがコンパイルの時点で決まっている必要があります
8 ビット整数でない8 ビットの整数へ量子化する同じ 8 ビットでも、小数を表す形式は受け取れません
形状が動的入力の形状を固定してから書き出す軸の長さが実行時に決まるものは、テクスチャの大きさを決められません
並べたモデルが 1 本目と重み以外で違う同じグラフのまま、重みだけを変えて書き出すどの層が違うかを名指しします。計算に焼き込む尺度が違うモデルも、ここでエラーになります
未対応の演算が含まれるその演算を使わない形へ変更する何が含まれていたかを名指しします。省いたまま通すことはありません

エラーはモデルの側の言葉で出るので、書き出した側を修正してください。

1 セルは 4 成分channel が 4 を超えるモデルは、テクスチャが横に伸びます
重みの格納先小さい層は定数として、大きい層はテクスチャとして保持します。どちらにするかは層ごとに決まります
刻みConv と MaxPool の strides は 1 以外も扱えます
幅channel が 4 を超える層では、2 の冪であることが必要です。Concat と DepthToSpace は channel の数によらず必要です。高さに制限はありません
クラスの宣言partial が必要です。生成されたファイルが、そのクラスの残りになります
名前スクリプトとモデルの名前をつないだものです。Judge.cs に gesture.onnx の属性を書くと Judge.Gesture.Model.cs になります
配置先属性を書いたスクリプトと同じフォルダです
モデルの探し先属性に書いた経路を、スクリプトの隣・プロジェクトのフォルダの順に探します
再生成スクリプトかモデルのどちらかが変わると生成し直され、属性を削除すると消えます。手で編集しないでください。
大きさ重みがそのままコードになるので、重みの数より多い行数になり、コンパイルのたびに全部読まれます
記述の順序属性と、生成される名前を使う処理を同時に書くとコンパイルのエラーになります。先に属性だけを保存してください。

元のモデルと同じ結果になるとは限りません。8 ビットへ量子化する時点で値が丸まり、半精度で運ぶ値は GPU が半精度で格納するときの丸め方に従うので、書き出し元の実行環境で計算した値とはその分だけ違います。