Sekitar setahun yang lalu, pengembang PyTorch memperkenalkan komunitas TorchScript , sebuah alat yang memungkinkan Anda untuk membuat solusi yang dapat diasingkan dari pipa dengan python dengan beberapa klik mouse yang dapat disematkan dalam sistem C ++. Di bawah ini saya membagikan pengalaman penggunaannya dan mencoba menggambarkan perangkap yang ditemui di sepanjang jalan ini. Saya akan memberikan perhatian khusus pada implementasi proyek di Windows, karena meskipun penelitian dalam ML biasanya dilakukan di Ubuntu, solusi terakhirnya sering (tiba-tiba!) Diperlukan di bawah "windows".
Kode contoh untuk mengekspor model dan proyek C ++ menggunakan model dapat ditemukan di repositori di GitHub .

Pengembang PyTorch tidak tertipu. Alat baru ini benar-benar memungkinkan Anda untuk mengubah proyek penelitian di PyTorch menjadi kode yang tertanam dalam sistem C ++ dalam beberapa hari kerja, dan dengan beberapa keterampilan lebih cepat.
TorchScript muncul di PyTorch versi 1.0 dan terus berkembang dan berubah. Jika versi pertama setahun yang lalu penuh dengan bug dan lebih bersifat eksperimental, maka versi saat ini setidaknya pada poin kedua sangat berbeda: Anda tidak dapat menyebutnya eksperimental lagi, ini cukup cocok untuk penggunaan praktis. Saya akan fokus padanya.
Inti dari TorchScript adalah kompiler mandiri (bebas-Python) sendiri dari bahasa mirip-python, serta alat untuk mengonversi program yang ditulis dengan Python dan PyTorch ke dalamnya, metode untuk menyimpan dan memuat modul yang dihasilkan, dan perpustakaan untuk menggunakannya dalam C ++. Agar berhasil, Anda harus menambahkan beberapa DLL ke proyek dengan berat total sekitar 70MB (untuk Windows) untuk bekerja pada CPU dan 300MB untuk versi GPU. TorchScript mendukung sebagian besar fitur PyTorch dan fitur utama bahasa python. Tetapi perpustakaan pihak ketiga, seperti OpenCV atau NumPy, harus dilupakan. Untungnya, banyak fungsi dari NumPy memiliki analog di PyTorch.
Ubah pipeline menjadi model PyTorch di TorchScript
TorchScript menawarkan dua cara untuk mengonversi kode Python ke format internalnya: tracing dan scripting (tracing dan scripting). Mengapa dua? Tidak, jelas, tentu saja, bahwa dua lebih baik dari satu ...

Tetapi dalam kasus metode-metode ini, ternyata, seperti dalam aforisme terkenal, tentang penyimpangan kiri dan kanan: keduanya lebih buruk. Yah, dunia ini tidak sempurna. Hanya dalam situasi tertentu, Anda harus memilih yang lebih cocok.
Metode penelusuran sangat sederhana. Sampel data diambil (biasanya diinisialisasi dengan angka acak), dikirim ke fungsi atau metode kelas yang menarik minat kami, dan PyTorch membuat dan menyimpan grafik perhitungan dengan cara yang sama seperti yang biasanya dilakukan saat melatih jaringan saraf. Voila - skrip siap:
import torch import torchvision model = torchvision.models.resnet34(pretrained = True) model.eval() sample = torch.rand(1, 3, 224, 224) scripted_model = torch.jit.trace(model, sample)
Contoh di atas menghasilkan objek dari kelas ScriptModule. Itu bisa diselamatkan
scripted_model.save('my_script.pth')
dan kemudian memuatnya ke dalam program C ++ (lebih lanjut tentang itu di bawah ) atau ke dalam kode Python alih-alih objek asli:
Contoh kode Python menggunakan model yang disimpan import cv2 from torchvision.transforms import Compose, ToTensor, Normalize transforms = Compose([ToTensor(), Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])]) img = cv2.resize(cv2.imread('pics/cat.jpg'), (224,224)) img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) x = transforms(img).unsqueeze(0)
tensor(282) tensor(12.8130, grad_fn=<SelectBackward>)
ScriptModule
dihasilkan dapat muncul di mana saja nn.Module
umumnya digunakan.
Dengan cara yang dijelaskan, Anda dapat melacak instance dari kelas dan fungsi nn.Module
(dalam kasus terakhir, turunan dari torch._C.Function
Kelas torch._C.Function
).
Metode ini (pelacakan) memiliki keuntungan penting: cara ini Anda dapat mengkonversi hampir semua kode Python yang tidak menggunakan pustaka eksternal. Tetapi ada kelemahan yang sama pentingnya: untuk cabang mana pun, hanya cabang yang dieksekusi pada data uji yang akan diingat:
def my_abs(x): if x.max() >= 0: return x else: return -x my_abs_traced = torch.jit.trace(my_abs, torch.tensor(0)) print(my_abs_traced(torch.tensor(1)), my_abs_traced(torch.tensor(-1)))
c:\miniconda3\lib\site-packages\ipykernel_launcher.py:2: TracerWarning: Converting a tensor to a Python boolean might cause the trace to be incorrect. We can't record the data flow of Python values, so this value will be treated as a constant in the future. This means that the trace might not generalize to other inputs! tensor(1) tensor(-1)
Ups! Ini sepertinya bukan yang kita inginkan, bukan? Sangat bagus bahwa setidaknya pesan peringatan (TracerWarning) dikeluarkan. Perlu memperhatikan pesan-pesan seperti itu.
Di sini metode kedua datang ke bantuan kami - scripting:
my_abs_script = torch.jit.script(my_abs) print(my_abs_script(torch.tensor(1)), my_abs_script(torch.tensor(-1)))
tensor(1) tensor(1)
Hore, hasil yang diharapkan diterima! Scripting secara rekursif menganalisis kode Python dan mengubahnya menjadi kode dalam bahasanya sendiri. Pada output, kita juga mendapatkan kelas ScriptModule
(untuk modul) atau torch._C.Function
(untuk fungsi). Tampaknya, ini dia, kebahagiaan! Tetapi masalah lain muncul: bahasa internal TorchScript sangat diketik, tidak seperti Python. Jenis setiap variabel ditentukan oleh penugasan pertama, jenis argumen fungsi secara default adalah Tensor
. Karena itu, misalnya, pola yang sudah dikenal
def my_func(x): y = None if x.max() > 0: y = x return y my_func = torch.jit.script(my_func)
Pelacakan akan gagal.
Kesalahan penelusuran terlihat seperti ini RuntimeError Traceback (most recent call last) <ipython-input-9-25414183a687> in <module>() ----> 1 my_func = torch.jit.script(my_func) d:\programming\3rd_party\pytorch\pytorch_ovod_1.3.0a0_de394b6\torch\jit\__init__.py in script(obj, optimize, _frames_up, _rcb) 1224 if _rcb is None: 1225 _rcb = _gen_rcb(obj, _frames_up) -> 1226 fn = torch._C._jit_script_compile(qualified_name, ast, _rcb, get_default_args(obj)) 1227 # Forward docstrings 1228 fn.__doc__ = obj.__doc__ RuntimeError: Variable 'y' previously has type None but is now being assigned to a value of type Tensor : at <ipython-input-8-75677614fca6>:4:8 def my_func(x): y = None if x.max() > 0: y = x ~ <--- HERE return y
Perlu dicatat bahwa, meskipun kesalahan terjadi ketika torch.jit.script
dipanggil, tempat yang menyebabkannya dalam kode skrip juga ditunjukkan.
Bahkan poin setelah konstanta mulai berperan:
def my_func(x): if x.max() > 0: y = 1.25 else: y = 0 return y my_func = torch.jit.script(my_func)
akan memberikan kesalahan RuntimeError Traceback (most recent call last) <ipython-input-10-0a5f18586763> in <module>() 5 y = 0 6 return y ----> 7 my_func = torch.jit.script(my_func) d:\programming\3rd_party\pytorch\pytorch_ovod_1.3.0a0_de394b6\torch\jit\__init__.py in script(obj, optimize, _frames_up, _rcb) 1224 if _rcb is None: 1225 _rcb = _gen_rcb(obj, _frames_up) -> 1226 fn = torch._C._jit_script_compile(qualified_name, ast, _rcb, get_default_args(obj)) 1227 # Forward docstrings 1228 fn.__doc__ = obj.__doc__ d:\programming\3rd_party\pytorch\pytorch_ovod_1.3.0a0_de394b6\torch\jit\__init__.py in _rcb(name) 1240 # closure rcb fails 1241 result = closure_rcb(name) -> 1242 if result: 1243 return result 1244 return stack_rcb(name) RuntimeError: bool value of Tensor with more than one value is ambiguous
Karena itu perlu untuk menulis bukan 0
, tetapi 0.
sehingga jenis di kedua cabang adalah sama! Manja, Anda tahu, dengan python Anda!
Ini hanyalah awal dari daftar perubahan yang perlu Anda buat untuk kode python sehingga dapat berhasil diubah menjadi modul TorchScript. Saya akan membuat daftar kasus yang paling umum secara lebih rinci nanti . Pada prinsipnya, tidak ada ilmu roket di sini dan kode Anda dapat diperbaiki. Tetapi paling sering saya tidak ingin memperbaiki modul pihak ketiga, termasuk yang standar dari torchvision
, dan seperti biasa mereka biasanya tidak cocok untuk skrip.
Untungnya, kedua teknologi dapat dikombinasikan: apa yang sedang ditulis sedang ditulis dan apa yang tidak sedang dituliskan sedang melacak:
class MyModule(torch.nn.Module): def __init__(self): super(MyModule, self).__init__() self.resnet = torchvision.models.resnet34(pretrained = True)
Pada contoh di atas, penelusuran digunakan untuk menyertakan modul yang tidak dapat skrip dalam modul di mana tidak ada cukup jejak dan skrip diperlukan. Ada situasi sebaliknya. Misalnya, jika kita perlu mengunggah model ke ONNX, tracing digunakan. Tetapi model yang dilacak mungkin menyertakan fungsi TorchScript, sehingga logika yang membutuhkan cabang dan loop dapat diimplementasikan di sana! Contoh diberikan dalam dokumentasi resmi untuk torch.onnx .
Fitur-fitur yang disediakan oleh PyTorch untuk membuat modul-modul TorchScript dijelaskan secara lebih terperinci dalam dokumentasi resmi dan torch.jit
. Secara khusus, saya tidak menyebutkan cara mudah untuk menggunakan torch.jit.trace
dan torch.jit.script
dalam bentuk dekorator, tentang kekhasan debugging kode skrip. Ini dan banyak lagi ada di dokumentasi.
Kami menyertakan model dalam proyek C ++
Sayangnya, dokumentasi resmi terbatas pada contoh bentuk "tambahkan 2 tensor yang dihasilkan menggunakan torch.ones
". Saya menyiapkan contoh proyek yang lebih dekat dengan kenyataan yang mengirimkan gambar dari OpenCV ke jaringan saraf dan menerima hasilnya dalam bentuk tensor respons, tupel variabel, gambar dengan hasil segmentasi.
Agar contoh berfungsi, Anda memerlukan skrip klasifikasi yang disimpan menggunakan ResNet34 dan segmentasi menggunakan DeepLabV3. Untuk menyiapkan skrip ini, Anda perlu menjalankan notepad jupyter ini .
Kami membutuhkan perpustakaan torchlib
. Anda bisa mendapatkannya dengan beberapa cara:
- Jika Anda sudah menginstal PyTorch menggunakan
pip install
, Anda dapat menemukannya di direktori Python: <Miniconda3>\Lib\site-packages\torch
; - Jika Anda memiliki PyTorch yang dikompilasi dari sumber, maka ada di sana:
<My Pytorch repo>\build\lib.win-amd64-3.6\torch
; - Terakhir, Anda dapat mengunduh perpustakaan secara terpisah dari pytorch.org dengan memilih Language = C ++, dan unzip arsip.
Kode C ++ cukup sederhana. Itu perlu:
- Sertakan file header
#include <torch/script.h>
- Unduh Model
torch::jit::script::Module module = torch::jit::load("../resnet34_infer.pth");
- Siapkan data
torch::Tensor tensor = torch::from_blob(img.data, { img.rows, img.cols, 3 }, torch::kByte);
- Call
forward
berfungsi dan dapatkan hasilnya
auto output = module.forward( { tensor } )
- Dapatkan data dari hasilnya. Cara melakukan ini tergantung pada apa yang dikembalikan jaringan saraf. Ngomong-ngomong, dalam kasus umum, ia juga dapat menerima tidak hanya satu gambar, oleh karena itu lebih baik untuk melihat kode sumber dari seluruh contoh , ada beberapa opsi. Misalnya, untuk mendapatkan data dari tensor satu dimensi dari tipe float:
float* data = static_cast<float*>(output.toTensor().data_ptr());
- Ada satu lagi kehalusan. Jangan lupa untuk memasukkan analog
with torch.no_grad()
dalam kode agar tidak membuang sumber daya untuk menghitung dan menyimpan gradien yang tidak kita butuhkan. Sayangnya, perintah ini tidak dapat dimasukkan dalam skrip, jadi Anda harus menambahkannya ke kode C ++:
torch::NoGradGuard no_grad;
Cara membangun proyek menggunakan CMake dijelaskan dalam panduan resmi . Tetapi topik proyek di Visual Studio tidak diungkapkan di sana, jadi saya akan menjelaskannya secara lebih rinci. Anda harus mengubah pengaturan proyek secara manual:
- Saya menguji pada Visual Studio 2017. Saya tidak bisa mengatakan tentang versi lain.
- Toolset v14.11 v141 harus diinstal (tanda centang
"VC++ 2017 version 15.4 v14.11 toolset"
di penginstal VS). - Platform harus
x64
. - Secara
General → Platform Toolset
pilih v141(Visual Studio 2017)
- Dalam
C/C++ → General → Additional Include Directories
tambahkan <libtorch dir>\include
- Di
Linker → General → Additional Library Directories
tambahkan <libtorch dir>\lib
- Di
Linker → Input → Additional Dependencies
tambahkan torch.lib; c10.lib
torch.lib; c10.lib
. Di Internet, mereka menulis bahwa caffe2.lib
mungkin masih diperlukan, dan untuk GPU dan yang lainnya dari <libtorch dir>\lib
, tetapi dalam versi saat ini, menambahkan dua perpustakaan ini sudah cukup bagi saya. Mungkin ini informasi yang sudah ketinggalan zaman. - Mereka juga menulis bahwa Anda perlu mengatur
C/C++ → Language → Conformance Mode
= No
, tapi saya tidak melihat perbedaannya.
Juga, variabel __cplusplus
TIDAK boleh dinyatakan dalam proyek. Mencoba menambahkan opsi /Zc:__cplusplus
akan menghasilkan kesalahan kompilasi dalam file ivalue.h
.
Dalam proyek terlampir, pengaturan jalur (tidak hanya ke TorchLib, tetapi juga ke OpenCV dan CUDA) dikeluarkan dalam file props , sebelum perakitan, Anda harus mendaftarkannya di sana sesuai dengan konfigurasi lokal Anda. Faktanya, itu saja.
Apa lagi yang perlu diingat
Jika proses yang dijelaskan itu tampak terlalu sederhana bagi Anda, intuisi Anda tidak menipu Anda. Ada sejumlah nuansa yang perlu dipertimbangkan untuk mengkonversi model PyTorch yang ditulis dengan Python ke TorchScript. Saya akan daftar di bawah ini yang harus saya hadapi. Saya sudah menyebutkan beberapa, tapi saya ulangi untuk mengumpulkan semuanya di satu tempat.

- Jenis variabel yang diteruskan ke fungsi adalah Tensor secara default. Jika dalam beberapa kasus (sangat sering) hal ini tidak dapat diterima, Anda harus mendeklarasikan tipe secara manual menggunakan anotasi tipe MyPy-style, seperti ini:
def calc_letter_statistics(self, cls_preds: List[Tensor], cls_thresh: float)->Tuple[int, Tuple[Tensor, Tensor, Tensor]]
atau lebih:
def calc_letter_statistics(self, cls_preds, cls_thresh):
- Variabel diketik dengan kuat dan jenisnya, jika tidak ditentukan secara eksplisit, ditentukan oleh penugasan pertama. Konstruksi yang dikenali dari form
x=[]; for ...: x.append(y)
x=[]; for ...: x.append(y)
harus diedit, karena pada saat menugaskan []
kompiler tidak dapat mengetahui tipe apa yang akan ada dalam daftar. Oleh karena itu, Anda harus menentukan jenis secara eksplisit, misalnya:
from typing import List x: List[float] = []
atau ("misalnya") lainnya
from torch import Tensor from typing import Dict, Tuple, List x: Dict[int: Tuple[float, List[Tensor], List[List[int]]]] = {}
- Dalam contoh di atas, itu adalah nama-nama yang perlu diimpor, karena nama-nama ini dijahit ke dalam kode TorchScript. Alternatif, pendekatan yang tampaknya legal
import torch import typing x: typing.List[torch.Tensor] = []
akan menghasilkan kesalahan ketik typing.List konstruktor tipe tidak dikenal saat scripting
- Desain akrab lainnya yang harus Anda pisahkan:
x = None if smth: x = torch.tensor([1,2,3])
Ada dua opsi. Atau tetapkan Tensor dua kali (fakta bahwa dimensi berbeda tidak menakutkan):
x = torch.tensor(0) if smth: x = torch.tensor([1,2,3])
dan jangan lupa mencari apa yang akan rusak setelah penggantian seperti itu. Atau cobalah untuk menulis dengan jujur:
x: Optional[Tensor] = None if smth: x = torch.tensor([1,2,3])
tetapi kemudian dengan penggunaan lebih lanjut dari x
mana tensor diharapkan, kita kemungkinan besar akan mendapatkan kesalahan: Diharapkan nilai tipe 'Tensor' untuk argumen 'x' tetapi sebaliknya ditemukan tipe 'Opsional [Tensor]'.
Jangan lupa untuk menulis, misalnya, x=0.
selama tugas pertama x=0.
bukannya x=0
, dll, jika variabel x
harus bertipe float
.
Jika di suatu tempat kami menggunakan inisialisasi gaya lama dari tensor melalui x = torch.Tensor(...)
, Anda harus berpisah dengannya dan menggantinya dengan versi yang lebih muda dengan huruf kecil x = torch.tensor(...)
. Kalau tidak, selama scripting itu akan terbang: Unin builtin op: aten :: Tensor. Berikut adalah beberapa saran: aten :: tensor . Tampaknya mereka bahkan menjelaskan apa masalahnya, dan jelas apa yang perlu dilakukan. Namun, jelas jika Anda sudah tahu jawaban yang benar.
Kode ditulis dalam konteks modul di mana torch.jit.script
dipanggil. Oleh karena itu, jika di suatu tempat, di dalam usus dari kelas atau fungsi yang math.pow
, misalnya, math.pow
, Anda harus menambahkan import math
ke modul kompilasi. Dan lebih baik untuk skrip kelas di mana ia dideklarasikan: baik menggunakan dekorator @torch.jit.script
, atau dengan mendeklarasikan fungsi tambahan di sebelahnya yang membuat ScriptModule keluar dari itu. Jika tidak, kami mendapatkan pesan kesalahan matematika nilai yang tidak ditentukan ketika kami mencoba untuk mengkompilasi kelas dari modul di mana, tampaknya, impor math
dibuat.
Jika di suatu tempat Anda memiliki konstruksi formulir my_tensor[my_tensor < 10] = 0
atau serupa, maka Anda akan mendapatkan kesalahan samar ketika membuat skrip:
*aten::index_put_(Tensor(a!) self, Tensor?[] indices, Tensor values, bool accumulate=False) -> (Tensor(a!)):* *Expected a value of type 'Tensor' for argument 'values' but instead found type 'int'.* *aten::index_put_(Tensor(a!) self, Tensor[] indices, Tensor values, bool accumulate=False) -> (Tensor(a!)):* *Expected a value of type 'List[Tensor]' for argument 'indices' but instead found type 'List[Optional[Tensor]]'.*
Yang Anda butuhkan adalah mengganti nomor dengan tensor: my_tensor[my_tensor < 10] = torch.tensor(0.).to(my_tensor.device)
. Dan jangan lupa a) tentang korespondensi jenis my_tensor
dan tensor yang dibuat (dalam hal ini, float) dan b) tentang .to(my_tensor.device)
. Jika Anda lupa yang kedua, semuanya akan dituliskan, tetapi sudah dalam proses bekerja dengan GPU, Anda akan kesal, yang akan terlihat seperti kata-kata error CUDA samar : akses memori ilegal ditemukan , tanpa menunjukkan di mana kesalahan terjadi!
Jangan lupa bahwa secara default nn.Module
dan, karenanya, model dari torchvision dibuat dalam "mode kereta" (Anda tidak akan mempercayainya, tetapi ternyata ada mode seperti itu ). Dalam hal ini, Dropout dan trik lain dari mode kereta api digunakan, yang memecah jejak atau menyebabkan hasil yang tidak memadai ketika dieksekusi. Ingat untuk memanggil model.eval()
sebelum membuat skrip atau melacak.
Untuk fungsi dan kelas biasa, Anda perlu mengetikkan skripnya, untuk nn.Module - sebuah instance
Mencoba dalam metode skrip untuk mengakses variabel global
cls_thresh = 0.3 class MyModule(torch.nn.Module): ... x = r < cls_thresh ...
akan menghasilkan kesalahan penulisan nilai bentuk python tipe 'float' tidak dapat digunakan sebagai nilai . Kita perlu membuat variabel sebagai atribut dalam konstruktor:
cls_thresh = 0.3 class MyModule(torch.nn.Module): def __init__(self): ... self.cls_thresh = cls_thresh ... x = r < self.cls_thresh ...
- Kehalusan lain muncul jika atribut class digunakan sebagai parameter slice:
class FPN(nn.Module): def __init__(self, block, num_blocks, num_layers =5): ... self.num_layers = num_layers def forward(self, x): ... return (p3, p4, p5, p6, p7)[:self.num_layers]
menyebabkan scripting error tuple slice index harus konstanta integer . Penting untuk menunjukkan bahwa atribut num_layers adalah konstan dan tidak akan berubah:
class FPN(nn.Module): num_layers: torch.jit.Final[int] def __init__(self, block, num_blocks, num_layers =5): ...
- Dalam beberapa kasus, di mana tensor yang digunakan sesuai dengan normal, Anda perlu memberikan nomor secara eksplisit:
xx1 = x1.clamp(min=x1[i])
melempar kesalahan saat membuat skrip Expected a value of type 'Optional[number]' for argument 'min' but instead found type 'Tensor'.
. Nah, di sini dari pesan kesalahan jelas apa yang harus dilakukan:
xx1 = x1.clamp(min=x1[i].item())
Masalah di atas terjadi saat melacak. Itu karena mereka bahwa biasanya tidak mungkin untuk hanya mengkompilasi solusi yang sudah jadi di TorchScript, dan Anda harus memijat kode sumber untuk waktu yang lama (jika kode sumber sesuai untuk diedit), atau gunakan penelusuran. Namun jejaknya memiliki nuansa tersendiri:
- Konstruksi bentuk tidak berfungsi dalam penelusuran
tensor_a.to(tensor_b.device)
Perangkat tempat tensor dimuat diperbaiki pada saat penelusuran dan tidak berubah selama eksekusi. Masalah ini dapat diatasi sebagian dengan mendeklarasikan tensor sebagai anggota nn.Module
type Parameter
. Kemudian, ketika memuat model, itu akan boot ke perangkat yang ditentukan dalam fungsi torch.jit.load
.
Epilog
Semua hal di atas tentu saja menciptakan masalah. Tetapi TorchScript memungkinkan Anda untuk menggabungkan dan mengirim ke solusi sebagai satu keseluruhan model itu sendiri dan kode Python yang menyediakan pra dan pasca pemrosesan. Ya, dan waktu untuk menyiapkan solusi untuk kompilasi, meskipun menghadapi kesulitan di atas, jauh lebih murah daripada biaya untuk membuat solusi, tetapi di sini PyTorch menawarkan keuntungan besar, sehingga permainan ini layak untuk dijadikan lilin.
