この章は試験中です。 学習済みのモデルを、実行時に読み込むのではなくコンパイルの時点でコードへ焼き込みます(読み込む処理も、同時に配布するファイルも不要です)。
| 属性 | 計算する場所 | 結果 |
|---|
[Onnx("…")] | Udon | 数として読み出せます。小さいモデル向けです |
[OnnxGpu("…")] | GPU | はるかに大きいモデルを扱えますが、結果はバッファに出るので、数として使うには読み戻しが 1 段いります |
ジェスチャの判定のように値を読んで分岐するなら前者、絵を作り替えるようにそのまま画面へ出すなら後者を選びます。
| モデルのファイル名 | 先頭に付く名前 |
|---|
gesture.onnx | Gesture |
hand_pose.onnx | HandPose |
下線と拡張子は落ちます。
- 書き出した
.onnx を、属性を書くスクリプトと同じフォルダへ置きます
- クラスに
partial を付け、属性だけを書いて保存します
- Unity へ戻るとインポートが実行され、スクリプトの隣に生成されたファイルが現れます
- 生成された名前を使う処理を書きます
| |
|---|
| 同時に書いたとき | 手順 2 と 4 を 1 度に書くと、生成される名前がまだ無いためコンパイルのエラーになります |
| エラーになったとき | 生成されたファイルは作られず、受け取れなかった理由が Console に出ます |
| モデルを差し替えたとき | 手順 3 が自動でやり直され、要素数の定数も新しい値になります |
入力と出力を配列で渡し、結果を数として読み出します。
| |
|---|
| モデルのファイル | Judge.cs と同じフォルダの gesture.onnx |
| 入力 | pose — 要素が 12 個(形は [1,12]) |
| 出力 | scores — 要素が 4 個(形は [1,4]) |
| 生成されるファイル | Judge.Gesture.Model.cs |
// 属性の引数は、このスクリプトから見たモデルのファイルの経路です。
// 属性を 2 つ以上書くと、モデルごとに名前が生えます。
public partial class Judge : TsukimiBehaviour
// 生成されたファイルがこのクラスの残りになるので、partial が必要です。
// 入力に詰める値の取り出し元。Inspector で 12 個挿します。
public Transform[] joints;
// 入力と出力をまとめて持つ値。型は生成されたファイルが宣言します。
// 選ばれた番号。他の Behaviour から読めます。
// 配列の確保は 1 度だけです。以後は同じ配列の中身を書き換えます。
// 要素数は生成された定数を使います(モデルを差し替えると値も変わります)。
io.In0 = new float[SizeIn0];
io.Out0 = new float[SizeOut0];
// 入力は全部詰めます。詰め残した要素は前回の値のまま計算されます。
for (int i = 0; i < SizeIn0; i++) io.In0[i] = joints[i].localPosition.y;
// 結果は io.Out0 に入ります。ここでは最も大きい成分の番号を選びます。
for (int i = 1; i < SizeOut0; i++)
if (io.Out0[i] > io.Out0[chosen]) chosen = i;
重みと層の中身を省いた抜き書きです。
// This file was generated from an ONNX model. Manual edits are lost on the next generation.
// 入力と出力をまとめた型。もとのテンソルの名前と形がコメントに残ります。
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];
// 層と層の間に挟む配列。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++)
// out: Copy g2 -> scores
for (int i = 0; i < 4; i++)
| 生成される名前 | 説明 |
|---|
GestureIO | 入力と出力を持つ型。中身は配列で、In0・Out0 のように並びます |
SizeIn0 / SizeOut0 | 配列ごとの要素数。配列は Start で 1 度だけ確保します |
RunGesture(io) | その値の入力で計算し、出力を同じ値へ書き戻します |
In0 / In1 … | 番号はモデルの入力の並び順です。出力も同じ規則です |
| |
|---|
| 呼び出せる場所 | 計算するメソッドは外部から呼び出せません。入力を代入しないまま計算することを防ぐためです |
| 費用 | 計算は Udon の上で 1 つずつ進みます。毎フレーム計算する前にプロファイラで計測してください |
| 重みの格納先 | 重みはコードの中の定数です。モデルのファイルは配布物に含める必要がありません |
画像をバッファへ入れ、結果を別のバッファで受け取ります。
| |
|---|
| モデルのファイル | 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 |
// 属性の引数は、このスクリプトから見たモデルのファイルの経路です。
public partial class Sharpen : TsukimiBehaviour
// 通す元の画像と、結果を映す面。Inspector で挿します。
// バッファを作ります。大きさはモデルの側から決まるので、指定しません。
// 元の画像を入力のバッファへ入れます。大きさが違うときは伸び縮みして入ります。
Gpu.Load(FilterInput, source);
// 1 回ぶん計算します。層の数だけカーネルが順に走ります。
// 結果は FilterOutput に残るだけなので、そのまま面へ映します。
Gpu.Show(FilterOutput, display);
層の中身を省いた抜き書きです。
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);
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>.
Gpu.Run(nameof(FilterLayer0), Filterb1, FilterInput, Filterw0);
| 生成される名前 | 説明 |
|---|
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 組のままで、重みだけを並べた数だけ持ちます |
ここに無い演算を含むモデルは、コンパイルの時点でエラーになります。
| 演算 | 条件 |
|---|
Gemm | A と B は 2 次元です。transA・transB・alpha・beta を扱います |
MatMul | 1 次元の相手は受け取れません。前にある軸はブロードキャストします |
Add | 2 本の入力をブロードキャストします |
Sub | 2 本の入力をブロードキャストします |
Mul | 2 本の入力をブロードキャストします |
Relu | 条件はありません |
Identity | 条件はありません |
Reshape | 形は定数で渡します(実行時に渡す形は受け取れません) |
Flatten | axis は入力の次元の範囲内です |
| 演算 | 条件 |
|---|
Conv | kernel_shape は縦と横が同じです。strides と dilations を扱います。group は 1 か、入力と出力の channel 数に等しい形(depthwise)だけです。auto_pad は受け取れません |
DepthToSpace | 並べ方は DCR だけです。倍率は 2 の冪で、channel が倍率の 2 乗で割り切れる必要があります |
Concat | channel の軸で継ぐ形だけです。継げるのは 4 本までで、各入力の channel は 4 の倍数です |
Add | 2 本とも前の演算の出力で、形が同じです。定数を足す形は受け取れません |
QuantizeLinear | 計算ではなく、量子化の尺度として読みます。尺度とゼロ点は定数です |
DequantizeLinear | 計算ではなく、量子化の尺度として読みます。量子化の代わりに Cast(float16)と Cast(float32)の組を置くと、その間は半精度の実数で運びます |
Pad | reflect で、縁を埋めない Conv の直前に置いた形だけです(Conv に畳みます) |
InstanceNormalization | scale と B は定数です |
Resize | 補間は linear だけです。coordinate_transformation_mode は half_pixel・pytorch_half_pixel・align_corners・asymmetric を扱います。変えられるのは縦と横だけです |
PRelu | 傾きは定数で、1 個か channel の数だけ持ちます |
MaxPool | kernel_shape は縦と横が同じで、dilations は受け取れません。最大値の位置(Indices)は出せません |
Elu | 8 ビットの入力だけです |
Relu | 半精度の実数の入力も受け取ります |
LeakyRelu | 半精度の実数の入力も受け取ります |
Tanh | 8 ビットの入力だけです |
Sigmoid | 8 ビットの入力だけです |
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 が半精度で格納するときの丸め方に従うので、書き出し元の実行環境で計算した値とはその分だけ違います。