ML-Agents の代替: PyTorch C ++ API を使用してニューラル ネットワークを Unity プロジェクトに統合する





この記事で何が起こるかを簡単に説明します。



  • PyTorch C ++ API を使用して、ニューラル ネットワークを Unity エンジンのプロジェクトに統合する方法を示します。
  • プロジェクトについては詳しく説明しません。この記事では重要ではありません。
  • 私は既成のニューラル ネットワーク モデルを使用して、そのトレースを実行時にロードされるバイナリに変換します。
  • このアプローチにより、複雑なプロジェクトの展開が大幅に容易になることを示します (たとえば、Unity 環境と Python 環境の同期に問題はありません)。


現実の世界へようこそ



ニューラル ネットワークを含む機械学習技術は、実験環境では依然として非常に快適であり、そのようなプロジェクトを現実世界で開始することはしばしば困難です。これらの困難について少し説明し、それらを克服する方法の制限について説明し、ニューラル ネットワークを Unity プロジェクトに統合する問題に対する段階的な解決策も示します。 



言い換えれば、PyTorch の研究プロジェクトを、戦闘状態で Unity エンジンと連携できる既製のソリューションに変える必要があります。



ニューラル ネットワークを Unity に統合するには、いくつかの方法があります。PyTorch ( libtorchと呼ばれる)用の C ++ API を使用して、プラグインとして Unity にプラグインできるネイティブ共有ライブラリを作成することをお勧めし ます。他のアプローチ (たとえば、ML-Agents の使用 ) があり、場合によっては、よりシンプルでより効果的です。しかし、私のアプローチの利点は、柔軟性とパワーが向上することです。 



エキゾチックなモデルがあり、既存の PyTorch コード (Unity と通信することを意図せずに作成されたもの) を使用したいとします。または、チームが独自のモデルを開発していて、Unity の考えに気を取られたくない場合。どちらの場合も、モデル コードは必要に応じて複雑にすることができ、PyTorch のすべての機能を使用できます。そして、それが突然統合された場合、C ++ API が機能し、モデルの元の PyTorch コードに少しも変更を加えることなく、すべてをライブラリにラップします。



したがって、私のアプローチは 4 つの重要なステップに要約されます。



  1. 環境を整えています。
  2. ネイティブライブラリ (C++) を準備しています。
  3. ライブラリ/プラグイン接続(Unity/C#)からの関数のインポート。 
  4. モデルの保存/デプロイ。





重要: このプロジェクトは Linux の下で行っているため、一部のコマンドと設定はこの OS に基づいています。しかし、私はここで何かが彼女に過度に依存すべきだとは思わない. したがって、Windows 用のライブラリの準備で問題が発生することはほとんどありません。



環境の設定



libtorchインストールする前に、



  • CMake


GPU を使用する場合は、次のものが必要です。



  • CUDA ツールキット(この記事の執筆時点では、バージョン 10.1 が関連していました)。
  • CUDNN ライブラリ


ドライバー、ライブラリ、その他のパーシモンは互いにフレンドでなければならないため、CUDA では問題が発生する可能性があります。そして、これらのライブラリを Unity プロジェクトと一緒に出荷して、すべてがすぐに機能するようにする必要があります。だから、これは私にとって最も不快な部分です。 GPU と CUDA を使用する予定がない場合は、次のことを知っておく必要があります。計算が 50 ~ 100 倍遅くなります。また、ユーザーの GPU がかなり弱い場合でも、GPU がないよりはある方がよいでしょう。ニューラル ネットワークが非常にまれにオンになっている場合でも、これらのまれなオンになっていると、ユーザーを悩ませる遅延が発生します。あなたの場合は違うかもしれませんが、このリスクは必要ですか?



上記のソフトウェアをインストールしたら、libtorch をダウンロードして (ローカルに) インストールします。すべてのユーザーにインストールする必要はありません。プロジェクト ディレクトリに配置して、CMake の起動時に参照するだけです。



ネイティブ ライブラリの準備



次のステップは CMake の構成です。私はPyTorchドキュメントの例をベースとして取り、 ビルド後に実行可能ファイルではなくライブラリを取得するように変更しました。このファイルをネイティブ ライブラリ プロジェクトのルート ディレクトリに配置します。



CMakeLists.txt


cmake_minimum_required(VERSION 3.0 FATAL_ERROR)

project(networks)

find_package(Torch REQUIRED)

set(CMAKE_CXX_FLAGS «${CMAKE_CXX_FLAGS} ${TORCH_CXX_FLAGS}»)

add_library(networks SHARED networks.cpp)

target_link_libraries(networks «${TORCH_LIBRARIES}»)

set_property(TARGET networks PROPERTY CXX_STANDARD 14)

if (MSVC)

	file(GLOB TORCH_DLLS «${TORCH_INSTALL_PREFIX}/lib/*.dll»)

	add_custom_command(TARGET networks

		POST_BUILD

		COMMAND ${CMAKE_COMMAND} -E copy_if_different

		${TORCH_DLLS}

		$<TARGET_FILE_DIR:example-app>)

endif (MSVC)
      
      





ライブラリのソース コードは、 networks.cpp にあります。 



このアプローチには、もう 1 つの優れた機能があります。Unity で使用するニューラル ネットワークについて、まだ考える必要がありません。その理由 (私は少し先んじています) は、いつでも Python でネットワークを実行し、そのトレースを取得して、libtorch に「このトレースをこれらの入力に適用する」ように指示できるからです。したがって、私たちのネイティブ ライブラリは、I/O で動作する一種のブラック ボックスを提供しているだけだと言えます。



ただし、タスクを複雑にし、たとえば Unity 環境の実行中にネットワーク トレーニングを直接実装する場合は、ネットワーク アーキテクチャとトレーニング アルゴリズムを C ++ で作成する必要があります。ただし、それはこの記事の範囲外であるため、詳細については、 PyTorch のドキュメント コード例の リポジトリの関連セクションを参照してください



とにかく、network.cpp では、ネットワークを初期化する外部関数 (ディスクからのブート) と、入力データでネットワークを開始して結果を返す外部関数を定義する必要があります。



network.cpp


#include <torch/script.h>

#include <vector>

#include <memory> 

extern «C»

{

// This is going to store the loaded network

torch::jit::script::Module network;
      
      





Unity から直接ライブラリ関数を呼び出すには、エントリ ポイントに関する情報を渡す必要があります。 Linux では、これに __attribute __ ((visibility ("default"))) を使用します。 Windows では、 this の__declspec (dllexport)指定子があります が、正直なところ、そこで機能するかどうかはテストしていません それでは、ディスクからニューラルネットワークトレースをロードする機能から始めましょう。ファイルは相対パスにあります - Assets /ではなく、Unity プロジェクトのルートにあり ます。ので注意してください。 Unity からファイル名を渡すこともできます。  






extern __attribute__((visibility(«default»))) void InitNetwork()

{
	network = torch::jit::load(«network_trace.pt»);

	network.to(at::kCUDA); // If we're doing this on GPU
}

      
      





次に、ネットワークに入力データを供給する関数に移りましょう。ポインター (Unity によって管理される) を使用してデータを前後にループする C ++ コードを作成しましょう。この例では、ネットワークの入力と出力が固定されていると想定しており、Unity がこれを変更できないようにしています。ここでは、たとえば、Tensor {1,3,64,64} と Tensor {1,5,64,64} を取り上げます (たとえば、RGB 画像のピクセルを 5 つのグループにセグメント化するには、このようなネットワークが必要です) .



一般に、バッファ オーバーフローを避けるために、データの次元と量に関する情報を渡す必要があります。



データを libtorch が動作する形式に変換するには、torch :: from_blob 関数を使用し ます。... 浮動小数点数の配列とテンソルの説明 (次元を含む) を取り、生成されたテンソルを返します。



ニューラル ネットワークは複数の入力引数を取ることができます (たとえば、call forward () は x、y、z を入力として受け取ります)。これを処理するために、すべての入力テンソルはtorch :: jit :: IValue標準テンプレート ライブラリのベクトルにラップされます (引数が 1 つしかない場合でも)。



テンソルからデータを取得するには、要素ごとに処理するのが最も簡単な方法ですが、これで処理速度が遅くなる場合は、Tensor :: アクセサーを使用してデータ読み取りプロセスを最適化できます 個人的には必要ありませんでしたが。



その結果、私のニューラル ネットワーク用に次の単純なコードが取得されます。



extern __attribute__((visibility(«default»))) void ApplyNetwork(float *data, float *output)

{

Tensor x = torch::from_blob(data, {1,3,64,64}).cuda();

std::vector<torch::jit::IValue> inputs;

inputs.push_back(x);

Tensor z = network.forward(inputs).toTensor();

for (int i=0;i<1*5*64*64;i++)

output[i] = z[0][i].item<float>();

}

}

      
      





コードをコンパイルするには、ドキュメントの指示に従って、 ビルド/サブディレクトリ作成し 、次のコマンドを実行します。



cmake -DCMAKE_PREFIX_PATH=/absolute/path/to/libtorch <strong>..</strong>

cmake --build <strong>.</strong> --config Release

      
      





すべてがうまくいけば、libnetworks.soまたは networks.dllファイルが生成されます あなたが置くことができるという の資産/プラグイン/あなたのユニティプロジェクト。



プラグインを Unity に接続する



ライブラリから関数をインポートするには、DllImport を使用し ます最初に必要な関数は InitNetwork () です。プラグインを接続すると、Unity はプラグインを呼び出します。



using System.Runtime.InteropServices;

public class Startup : MonoBehaviour

{

...

[DllImport(«networks»)]

private static extern void InitNetwork();

void Start()

{

...

InitNetwork();

...

}

}

      
      





Unity エンジン (C#) がライブラリ (C++) と通信できるように、メモリ管理作業はすべて Unity エンジンに任せます。



  • Unity 側で必要なサイズの配列にメモリを割り当てます。
  • 配列の最初の要素のアドレスをApplyNetwork関数に渡します(その前にインポートする必要もあります)。
  • データの送受信時に、C ++ アドレス算術演算がそのメモリにアクセスできるようにするだけです。


ライブラリ コード (C ++) では、メモリの割り当てまたは割り当て解除を避ける必要があります。一方、配列の最初の要素のアドレスを Unity から ApplyNetwork 関数に渡す場合、ニューラル ネットワークがデータの処理を完了するまで、このポインター (および対応するメモリ チャンク) を保存する必要があります。



幸いなことに、私のネイティブ ライブラリはデータを抽出するという単純な仕事をしてくれるので、追跡するのは簡単でした。ただし、プロセスを並列化して、ニューラル ネットワークがユーザーのデータを同時に学習して処理するようにするには、何らかの解決策を探す必要があります。



[DllImport(«networks»)]

private static extern void ApplyNetwork(ref float data, ref float output);

void SomeFunction() {

float[] input = new float[1*3*64*64];

float[] output = new float[1*5*64*64];

// Load input with whatever data you want

...

ApplyNetwork(ref input[0], ref output[0]);

// Do whatever you want with the output

...

}

      
      





モデルの保存



記事は終わりに近づきましたが、プロジェクトにどのニューラル ネットワークを選択したかについては引き続き議論しました。これは、画像のセグメント化に使用できる単純な畳み込みニューラル ネットワークです。モデルにはデータの収集とトレーニングを含めませんでした。私の仕事は Unity との統合について話すことであり、複雑なニューラル ネットワークのトレースに関する問題について話すことではありません。私を責めないでください。



興味が ある場合は、ここにいくつかの特殊なケースと潜在的な問題の概要を示す、良い複雑な例があります。主な問題の 1 つは、トレースがすべてのデータ型で正しく機能しないことです。ドキュメントでは、注釈と明示的なコンパイルを使用して問題を解決する方法について説明しています。



シンプルなモデルの Python コードは次のようになります。



import torch

import torch.nn as nn

import torch.nn.functional as F

class Net(nn.Module):

def __init__(self):

super().__init__()

self.c1 = nn.Conv2d(3,64,5,padding=2)

self.c2 = nn.Conv2d(64,5,5,padding=2)

def forward(self, x): z = F.leaky_relu(self.c1(x)) z = F.log_softmax(self.c2(z), dim=1)

return

   , , , ,  .

 ()       :

network = Net().cuda()

example = torch.rand(1, 3, 32, 32).cuda()

traced_network = torch.jit.trace(network, example)

traced_network.save(«network_trace.pt»)

      
      





モデルの拡張



静的ライブラリを作成しましたが、これでは展開には不十分です。追加のライブラリをプロジェクトに含める必要があります。残念ながら、どのライブラリを含める必要があるか 100% 確信が持てません。私が選んだ libtorch、libc10、libc10_cuda、libnvToolsExtとlibcudartを合計で、元のプロジェクト サイズに 2 GB が追加されます。 



LibTorch と ML エージェント



多くのプロジェクト、特に研究とプロトタイピングの場合、Unity 専用に構築されたプラグインである ML-Agents は選択する価値があると思います。しかし、プロジェクトがより複雑になったときは、何か問題が発生した場合に備えて、安全に取り組む必要があります。そして、これはかなり頻繁に起こります...



2 週間前、私は ML-Agents を使用して、Unity のデモ ゲームと Python で作成されたいくつかのニューラル ネットワークとの間で通信しました。ゲーム ロジックに応じて、Unity はこれらのネットワークの 1 つを異なるデータセットで呼び出します。



ML-Agents の Python API を深く掘り下げる必要がありました。 1 次元フォールドや転置など、私がニューラル ネットワークで使用した操作の一部は、バラクーダではサポートされていませんでした (これは、ML-Agents で現在使用されているトレース ライブラリです)。



私が遭遇した問題は、ML-Agents が特定の時間間隔でエージェントから「リクエスト」を収集し、評価のためにそれらをたとえば Jupyter ノートブックに送信することでした。ただし、一部のニューラル ネットワークは、他のネットワークの出力に依存していました。そして、私のニューラル ネットワークのチェーン全体の見積もりを取得するには、リクエストを行うたびに、しばらく待って結果を取得し、別のリクエストを作成し、待って、結果を取得する必要があります。さらに、これらのネットワークが稼働する順序は、ユーザーの入力に大きく依存していました。つまり、ニューラル ネットワークを連続して実行することはできませんでした。 



また、場合によっては、送信する必要のあるデータの量を変える必要がありました。また、ML-Agents は、各エージェントの固定ディメンション向けに設計されています (オンザフライで変更できるようですが、これには懐疑的です)。



ニューラル ネットワークをオンデマンドで呼び出すシーケンスを計算し、適切な入力を Python API に送信するようなことができます。しかし、これが原因で、私のコードは、Unity 側と Python 側の両方で複雑すぎるか、冗長になることさえあります。そこで、libtorchを使ってアプローチを検討することにしましたが、正解でした。



以前、誰かから GPT-2 または MAML 予測モデルを Unity プロジェクトに組み込むように言われたら、それを使わずに実行するようアドバイスします。ML-Agents を使用してこのようなタスクを実装するのは非常に複雑です。しかし、今では PyTorch を使用して任意のモデルを見つけたり開発したりして、通常のプラグインのように Unity に接続するネイティブ ライブラリにラップすることができます。






Macleod のクラウド サーバー 高速で安全です。



上記のリンクを使用するか、バナーをクリックして登録すると、任意の構成のサーバーをレンタルした最初の 1 か月間で 10% の割引が受けられます。






All Articles