Alternatif untuk ML-Agents: mengintegrasikan jaringan saraf ke dalam proyek Unity menggunakan PyTorch C ++ API





Saya akan menjelaskan secara singkat apa yang akan terjadi di artikel ini:



  • Saya akan menunjukkan cara menggunakan PyTorch C ++ API untuk mengintegrasikan jaringan saraf ke dalam proyek di mesin Unity;
  • Saya tidak akan menjelaskan proyek secara rinci, tidak masalah untuk artikel ini;
  • Saya menggunakan model jaringan saraf siap pakai, mengubah penelusurannya menjadi biner yang akan dimuat saat runtime;
  • Saya akan menunjukkan bahwa pendekatan ini sangat memfasilitasi penyebaran proyek yang kompleks (misalnya, tidak ada masalah dengan sinkronisasi lingkungan Unity dan Python).


Selamat Datang di dunia nyata



Teknik pembelajaran mesin, termasuk jaringan saraf, masih sangat nyaman di lingkungan eksperimental, dan meluncurkan proyek semacam itu di dunia nyata seringkali sulit. Saya akan berbicara sedikit tentang kesulitan-kesulitan ini, menjelaskan batasan tentang cara keluar darinya, dan juga memberikan solusi langkah demi langkah untuk masalah mengintegrasikan jaringan saraf ke dalam proyek Unity. 



Dengan kata lain, saya perlu mengubah proyek penelitian di PyTorch menjadi solusi siap pakai yang dapat bekerja dengan mesin Unity dalam kondisi pertempuran.



Ada beberapa cara untuk mengintegrasikan jaringan saraf ke Unity. Saya sarankan menggunakan C ++ API untuk PyTorch (disebut libtorch) untuk membuat perpustakaan bersama asli yang kemudian dapat dicolokkan ke Unity sebagai plugin. Ada pendekatan lain (misalnya, menggunakan ML-Agents ), yang dalam kasus tertentu bisa lebih sederhana dan efektif. Tetapi keuntungan dari pendekatan saya adalah memberikan lebih banyak fleksibilitas dan lebih banyak kekuatan. 



Katakanlah Anda memiliki beberapa model eksotis dan hanya ingin menggunakan kode PyTorch yang ada (yang ditulis tanpa maksud untuk berkomunikasi dengan Unity); atau tim Anda sedang mengembangkan model mereka sendiri dan tidak ingin terganggu oleh pemikiran Unity. Dalam kedua kasus, kode model dapat serumit yang Anda inginkan dan menggunakan semua fitur PyTorch. Dan jika tiba-tiba berintegrasi, C ++ API akan ikut bermain dan membungkus semuanya di perpustakaan tanpa perubahan sedikit pun pada kode model PyTorch asli.



Jadi pendekatan saya bermuara pada empat langkah utama:



  1. Menyiapkan lingkungan.
  2. Mempersiapkan perpustakaan asli (C ++).
  3. Impor fungsi dari koneksi perpustakaan / plugin (Unity / C #). 
  4. Menyimpan / menyebarkan model.





PENTING: karena saya mengerjakan proyek sambil duduk di Linux, beberapa perintah dan pengaturan didasarkan pada OS ini; tetapi saya tidak berpikir bahwa apa pun di sini harus terlalu bergantung padanya. Oleh karena itu, persiapan perpustakaan untuk Windows tidak mungkin menimbulkan kesulitan.



Menyiapkan lingkungan



Sebelum menginstal libtorch pastikan Anda memiliki



  • CMake


Dan jika Anda ingin menggunakan GPU, Anda perlu:





Kesulitan bisa muncul dengan CUDA, karena driver, perpustakaan, dan kesemek lainnya harus berteman satu sama lain. Dan Anda harus mengirimkan perpustakaan ini dengan proyek Unity Anda untuk membuat semuanya bekerja di luar kotak. Jadi ini adalah bagian yang paling tidak nyaman bagi saya. Jika Anda tidak berencana menggunakan GPU dan CUDA, maka Anda harus tahu: perhitungan akan melambat 50-100 kali. Dan bahkan jika pengguna memiliki GPU yang agak lemah, lebih baik dengan itu daripada tanpanya. Bahkan jika jaringan saraf Anda jarang dihidupkan, penyalaan yang jarang ini akan menyebabkan penundaan yang akan mengganggu pengguna. Ini mungkin berbeda dalam kasus Anda, tetapi ... apakah Anda memerlukan risiko ini?



Setelah Anda menginstal perangkat lunak di atas, saatnya untuk mengunduh dan (secara lokal) menginstal libtorch. Tidak perlu menginstalnya untuk semua pengguna: Anda cukup meletakkannya di direktori proyek Anda dan merujuknya saat memulai CMake.



Mempersiapkan perpustakaan asli



Langkah selanjutnya adalah mengkonfigurasi CMake. Saya mengambil contoh dari dokumentasi PyTorch sebagai dasar dan mengubahnya sehingga setelah membangun kami mendapatkan perpustakaan, bukan file yang dapat dieksekusi. Tempatkan file ini di direktori root proyek perpustakaan asli Anda.



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)
      
      





Kode sumber perpustakaan akan ditempatkan di networks.cpp



Pendekatan ini memiliki fitur bagus lainnya: kita belum perlu memikirkan jaringan saraf mana yang ingin kita gunakan dengan Unity. Alasannya (menjadi sedikit lebih maju) adalah bahwa kapan saja kita dapat menjalankan jaringan dengan Python, mendapatkan jejaknya, dan hanya memberi tahu libtorch untuk "menerapkan jejak ini ke input ini." Oleh karena itu, kita dapat mengatakan bahwa perpustakaan asli kita hanya menyajikan semacam kotak hitam, bekerja dengan I / O.



Tetapi jika Anda ingin memperumit tugas dan, misalnya, menerapkan pelatihan jaringan secara langsung saat lingkungan Unity sedang berjalan, maka Anda harus menulis arsitektur jaringan dan algoritma pelatihan dalam C ++. Namun, itu di luar cakupan artikel ini, jadi untuk informasi lebih lanjut, saya merujuk Anda ke bagian yang relevan dari dokumentasi PyTorch dan repositori contoh kode .



Bagaimanapun, di network.cpp kita perlu mendefinisikan fungsi eksternal untuk menginisialisasi jaringan (boot dari disk) dan fungsi eksternal yang memulai jaringan dengan data input dan mengembalikan hasil.



jaringan.cpp


#include <torch/script.h>

#include <vector>

#include <memory> 

extern «C»

{

// This is going to store the loaded network

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





Untuk memanggil fungsi perpustakaan kita langsung dari Unity, kita perlu memberikan informasi tentang titik masuknya. Di Linux, saya menggunakan __attribute __ ((visibility ("default"))) untuk ini. Di Windows ada specifier __declspec (dllexport) untuk this , tapi sejujurnya, saya belum menguji apakah itu berfungsi di sana . Jadi, mari kita mulai dengan fungsi memuat jejak jaringan saraf dari disk. File berada di jalur relatif - itu ada di root proyek Unity, bukan di Assets / . Jadi berhati-hatilah. Anda juga dapat meneruskan nama file dari 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
}

      
      





Sekarang mari kita beralih ke fungsi yang memberi makan jaringan dengan data input. Mari kita menulis kode C++ yang menggunakan pointer (dikelola oleh Unity) untuk mengulang data bolak-balik. Dalam contoh ini, saya mengasumsikan jaringan saya memiliki input dan output tetap, dan saya mencegah Unity mengubahnya. Di sini, misalnya, saya akan mengambil Tensor {1,3,64,64} dan Tensor {1,5,64,64} (misalnya, jaringan seperti itu diperlukan untuk mengelompokkan piksel gambar RGB menjadi 5 grup) .



Secara umum, Anda harus memberikan informasi tentang dimensi dan jumlah data untuk menghindari buffer overflows.



Untuk mengonversi data ke format yang berfungsi dengan libtorch, kami menggunakan fungsi obor :: from_blob... Dibutuhkan array angka floating point dan deskripsi tensor (dengan dimensi) dan mengembalikan tensor yang dihasilkan.



Jaringan saraf dapat mengambil beberapa argumen input (misalnya, call forward () mengambil x, y, z sebagai input). Untuk menangani ini, semua tensor input dibungkus ke dalam vektor torch :: jit :: library templating standar IValue (meskipun hanya ada satu argumen).



Untuk mendapatkan data dari tensor, cara termudah adalah dengan memprosesnya elemen demi elemen, tetapi jika ini memperlambat kecepatan pemrosesan, Anda dapat menggunakan Tensor :: accessor untuk mengoptimalkan proses pembacaan data . Meskipun secara pribadi saya tidak membutuhkannya.



Akibatnya, kode sederhana berikut diperoleh untuk jaringan saraf saya:



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>();

}

}

      
      





Untuk mengkompilasi kode, ikuti petunjuk dalam dokumentasi , buat build / subdirektori dan jalankan perintah berikut:



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

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

      
      





Jika semuanya berjalan dengan baik, file libnetworks.so atau networks.dll akan dihasilkan yang dapat Anda tempatkan di Aset / Plugin / proyek Unity Anda.



Menghubungkan plugin ke Unity



Untuk mengimpor fungsi dari perpustakaan, gunakan DllImport . Fungsi pertama yang kita butuhkan adalah InitNetwork(). Saat menghubungkan plugin, Unity akan menyebutnya:



using System.Runtime.InteropServices;

public class Startup : MonoBehaviour

{

...

[DllImport(«networks»)]

private static extern void InitNetwork();

void Start()

{

...

InitNetwork();

...

}

}

      
      





Agar mesin Unity (C #) dapat berkomunikasi dengan perpustakaan (C ++), saya akan mempercayakannya dengan semua pekerjaan manajemen memori:



  • Saya akan mengalokasikan memori untuk array dengan ukuran yang diperlukan di sisi Unity;
  • meneruskan alamat elemen pertama array ke fungsi ApplyNetwork (juga perlu diimpor sebelum itu);
  • biarkan saja C++ address aritmatika mengakses memori itu ketika data diterima atau dikirim.


Dalam kode perpustakaan (C ++), saya harus menghindari alokasi atau dealokasi memori. Di sisi lain, jika saya meneruskan alamat elemen pertama array dari Unity ke fungsi ApplyNetwork, saya harus menyimpan pointer ini (dan potongan memori yang sesuai) sampai jaringan saraf selesai memproses data.



Untungnya, perpustakaan asli saya melakukan pekerjaan sederhana untuk menyaring data, jadi cukup mudah untuk dilacak. Tetapi jika Anda ingin memparalelkan proses sehingga jaringan saraf secara bersamaan mempelajari dan memproses data untuk pengguna, Anda harus mencari semacam solusi.



[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

...

}

      
      





Menyimpan model



Artikel ini hampir berakhir, dan kami masih mendiskusikan jaringan saraf mana yang saya pilih untuk proyek saya. Ini adalah jaringan saraf convolutional sederhana yang dapat digunakan untuk segmen gambar. Saya tidak menyertakan pengumpulan data dan pelatihan dalam model: tugas saya adalah berbicara tentang integrasi dengan Unity, dan bukan tentang masalah dengan melacak jaringan saraf yang kompleks. Jangan salahkan aku.



Jika Anda penasaran, ada contoh bagus dan kompleks di sini yang menguraikan beberapa kasus khusus dan masalah potensial. Salah satu masalah utama adalah bahwa pelacakan tidak bekerja dengan benar untuk semua tipe data. Dokumentasi menjelaskan cara menyelesaikan masalah menggunakan anotasi dan kompilasi eksplisit.



Seperti inilah kode Python untuk model sederhana kita:



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»)

      
      





Memperluas model



Kami membuat pustaka statis, tetapi ini tidak cukup untuk penerapan: pustaka tambahan perlu disertakan dalam proyek. Sayangnya, saya tidak 100% yakin perpustakaan mana yang harus disertakan. Saya memilih libtorch, libc10, libc10_cuda, libnvToolsExt dan libcudart . Secara total, mereka menambahkan 2 GB ke ukuran proyek asli. 



LibTorch vs ML-Agen



Saya percaya bahwa untuk banyak proyek, terutama dalam penelitian dan pembuatan prototipe, ML-Agents, sebuah plugin yang dibuat khusus untuk Unity, sangat layak untuk dipilih. Tetapi ketika proyek menjadi lebih kompleks, Anda harus bermain aman - jika terjadi kesalahan. Dan ini cukup sering terjadi ...



Beberapa minggu yang lalu, saya baru saja menggunakan ML-Agents untuk berkomunikasi antara game demo di Unity dan beberapa jaringan saraf yang ditulis dengan Python. Bergantung pada logika permainan, Unity akan memanggil salah satu jaringan ini dengan kumpulan data yang berbeda.



Saya harus menggali lebih dalam ke dalam Python API untuk ML-Agents. Beberapa operasi yang saya gunakan di jaringan saraf saya, seperti 1d fold dan transpose, tidak didukung di Barracuda (ini adalah tracing library yang saat ini digunakan oleh ML-Agents).



Masalah yang saya hadapi adalah bahwa ML-Agents mengumpulkan "permintaan" dari agen selama interval waktu tertentu, dan kemudian mengirimkannya untuk evaluasi, misalnya, ke notebook Jupyter. Namun, beberapa jaringan saraf saya bergantung pada keluaran jaringan saya yang lain. Dan untuk mendapatkan perkiraan seluruh rantai jaringan saraf saya, saya harus menunggu beberapa saat, mendapatkan hasilnya, membuat permintaan lain, menunggu, mendapatkan hasilnya, dan seterusnya setiap kali saya mengajukan permintaan. Selain itu, urutan pengoperasian jaringan ini tidak terlalu bergantung pada input pengguna. Ini berarti saya tidak bisa menjalankan jaringan saraf begitu saja secara berurutan. 



Juga, dalam beberapa kasus, jumlah data yang saya perlukan untuk mengirim harus bervariasi. Dan ML-Agents lebih dirancang untuk dimensi tetap untuk setiap agen (tampaknya dapat diubah dengan cepat, tetapi saya skeptis tentang ini).



Saya bisa melakukan sesuatu seperti menghitung urutan pemanggilan jaringan saraf sesuai permintaan, mengirimkan input yang sesuai ke Python API. Tetapi karena ini, kode saya, baik di sisi Unity maupun di sisi Python, akan menjadi terlalu rumit, atau bahkan berlebihan. Oleh karena itu, saya memutuskan untuk mempelajari pendekatan menggunakan libtorch, dan itu benar.



Jika sebelumnya seseorang meminta saya untuk membangun model prediktif GPT-2 atau MAML ke dalam proyek Unity, saya akan menyarankan dia untuk mencoba melakukannya tanpa itu. Menerapkan tugas seperti itu menggunakan ML-Agents terlalu rumit. Tetapi sekarang saya dapat menemukan atau mengembangkan model apa pun dengan PyTorch, dan kemudian membungkusnya di perpustakaan asli yang terhubung ke Unity seperti plugin biasa.






Server cloud dari Macleod cepat dan aman.



Daftar menggunakan tautan di atas atau dengan mengklik spanduk dan dapatkan diskon 10% untuk bulan pertama menyewa server dengan konfigurasi apa pun!






All Articles