Yazı dizisinin bu yazısında LibTorch‘u kullanarak modelleri nasıl çalıştıracağımızı, çıkarım (inference) için nasıl kullanacağımızı göreceğiz. Yazı dizisinin ilk yazısında kullanım senaryolarını açıklarken Python‘da eğitilen modeli C++‘da çıkarım için kullanıp bazı darboğazları atlatabileceğinizi aktarmıştım. Şimdi Python‘da bir modeli eğitip (bununla zaman kaybetmemek için öneğitimli bir modeli kullanacağım), kaydedip C++‘da yükleyip çıkarım yapacağız.
İlk önce Python‘da bir model eğittimizi ve modelin resnet152
nesnesinde tutulduğunu varsayalım. Dediğim gibi ben doğrudan öneğitimli bir model kullanacağım:
import torchvision.models as models
import torch
resnet152 = models.resnet152(pretrained=True)
script = torch.jit.script(resnet152)
traced = torch.jit.trace(resnet152, torch.rand(1, 3, 224, 224)) #Traced model için örnek girdi sağlanmalıdır.
script.save("./model_zoo/resnet152_sc.pt")
traced.save("./model_zoo/resnet152_tr.pt")
Burada modeli torch
‘un jit
modülünü kullanarak hem script
modunda hem de trace
modunda kaydedelim. Python API‘da jit
modülünün kullanımı hakkında bilgi sahibi değilseniz kısa bir ara verip dokümantasyonu okumanızı tavsiye ederim. Her ikisini de kaydetme nedenim ileride karşılaştırma için kullanacak olmamdır. Şimdi kaydedilen modelleri kullanarak C++‘da hızlıca çıkarım yapalım:
#include <torch/script.h>
#include <iostream>
int main(int argc, const char* argv[]) {
if (argc != 2) {
std::cerr << "Usage: infer <path-to-exported-script-module>\n";
return -1;
}
at::globalContext().setBenchmarkCuDNN(1);
torch::jit::script::Module module;
try {
// Deserialize the ScriptModule from a file.
module = torch::jit::load(argv[1]);
std::cout << "Module loaded successfully.\n";
module.eval();
}
catch (const c10::Error& e) {
std::cerr << "Error loading the model.\n";
return -1;
}
//RESNET input shape (BATCH_SIZE, 3, 224, 224)
const int BATCH_SIZE = 8, CHANNELS = 3, HEIGHT = 224, WIDTH = 224;
torch::NoGradGuard no_grad;
// Create a vector of inputs.
std::vector<torch::jit::IValue> inputs;
inputs.emplace_back(torch::ones({BATCH_SIZE, CHANNELS, HEIGHT, WIDTH}));
// Execute the model and turn its output into a tensor.
auto output = module.forward(inputs).toTensor();
for (int i = 0; i < BATCH_SIZE; ++i) {
std::cout << i << "th element class: " << torch::argmax(output.data()[i]).item<long>() << "\n";
}
return 0;
}
Bu kodu derleyip çalıştıralım ve arkasından kodu inceleyelim:
$ ./infer ./model_zoo/resnet152_sc.pt
Module loaded successfully.
Class of 0th element: 600
Class of 1th element: 600
Class of 2th element: 600
Class of 3th element: 600
Class of 4th element: 600
Kodu en baştan inceleyecek olursak ilk olarak torch/script.h
başlık dosyasını kodumuza dahil etmemiz yeterli olacaktır. Ardından torch::jit::script::Module
sınıfının bir örneğini oluşturuyoruz. Bu aslında Python da kullandığımız torch.nn.Module
sınıfıdır. Sonuç olarak model nesnesini taşır ve Python API’ın sağladığı özellikleri kullanmamızı sağlar:
torch::jit::script::Module module;
Ardından daha önce Python‘da kaydettiğimiz modelleri dosyadan okuyup bu nesneye aktarıyoruz. Kaydedilen modeli komut satırından argüman olarak alıp modeli okuyoruz ve hata yakalama mekanizmasını kullanıyoruz:
try {
// Deserialize the ScriptModule from a file.
module = torch::jit::load(argv[1]);
std::cout << "Module loaded successfully.\n";
}
catch (const c10::Error& e) {
std::cerr << "Error loading the model.\n";
return -1;
}
Burada model CPU üzerinde çalışacak şekilde kullanıyoruz. GPU kullanmak isterseniz aşağıdaki kodu modeli yüklediğiniz satırdan sonraki satıra eklemeniz yeterli olacaktır:
module.to(at::kCUDA);
Girdilerimizi modele beslemek için ihtiyacımız olan vektörü hazırlayacağız. torch::jit::script::Module
sınıfının forward
fonksiyonu bizden std::vector<torch::jit::IValue>
türünden bir vektör bekliyor. Fonksiyon argümanı olan vektörü std::move()
ile taşıma semantiğine uygun olarak kullandığından bellek kullanımı ve hız kaybını olası en düşük hale getirmeyi amaçladığını hatırlatmak isterim:
std::vector<torch::jit::IValue> inputs;
inputs.push_back(torch::ones({BATCH_SIZE, CHANNELS, HEIGHT, WIDTH}));
//for GPU tensors:
//inputs.push_back(torch::ones({BATCH_SIZE, CHANNELS, HEIGHT, WIDTH}).to(at::kCUDA));
torch::jit::IValue
bir sınıf olarak tanımlanmıştır. Interpreter Value‘nun kısaltması olarak IValue
sınıfı TorchScript interpreter tarafından desteklenen tüm temel türleri sarmalamaktadır. IValue
sınıfı modellere girdi ve çıktılar için kullanılır. Bu sınıfın arayüzü oldukça geniş olmasına rağmen temel iki fonksiyonu için aşağıya bakabilirsiniz:
/// // Make the IValue
torch::IValue my_ivalue(26);
std::cout << my_ivalue << "\n";
///
/// // Unwrap the IValue
int64_t my_int = my_ivalue.toInt() //toX() instead of X use appropriate data type for wrapped data.
std::cout << my_int << "\n";
Bu noktada neden böyle bir sınıfa ihtiyaç duyulduğuna gelecek olursak C++ tarafından sağlanan temel türlerden farklı olarak LibTorch‘un sağladığı türler farklıdır. Python API ile C++ API arasında uyumu sağlamaya yardımcı olduğundan öğrenme/kullanma/alışmayı kolaylaştırmaktadır.
Artık son olarak modele girdileri besleyip çıktıda yığındaki (batch) her bir eleman en olası sınıfı standart çıktıya yazdırıyoruz:
auto output = module.forward(inputs).toTensor();
for (int i = 0; i < BATCH_SIZE; ++i) {
std::cout << "Class of " << i << "th element: " << torch::argmax(output.data()[i]).item<long>() << "\n";
}
Bu noktada bir karşılaştırma yapmakta fayda olacaktır. Bunun için Python’da jit
modülünü kullanmadan, script
ve trace
modunda farklı yığın boyutları için CPU ve GPU’da kaydedilen modeli okuma ve çalışma süreleriyle bu işlemlerin C++’da yaptığımızda elde edeceğimiz süreleri (tüm testler 10 defa çalıştırılıp süre ortalama olarak hesaplanmıştır) karşılaştıracağız. İlk olarak kaydedilen model dosyalarının boyutlarına bakalım. Dosya boyutları arasında anlamlı bir fark bulunmadığını söyleyebiliriz:
Modül Tipi | Dosya Boyutu |
---|---|
torch.nn.Module | 241.6 MB |
torch.jit.script | 242 MB |
torch.jit.trace | 242.2 MB |
Şimdi model dosyalarının diskten okunup CPU üzerinde modelin çalıştırılabilir hale getirilmesi için geçen süreye bakalım. Görüldüğü üzere öncelikle okumayı C++’da çok daha hızlı yapabiliyoruz:
CPU Okuma zamanı (ms) | Python | C++ |
---|---|---|
Script | 0.985 | 0.449 |
Trace | 0.912 | 0.356 |
Şimdi de model dosyasının okunması ve GPU üzerinde çalıştırılabilir hale getirilmesi için geçen süreye bakalım. Hız farkı azalsa da C++ hala daha hızlı okuyor ve modeli çalıştırılabilir hale getiriyor:
GPU Okuma zamanı (ms) | Python | C++ |
---|---|---|
Script | 2.286 | 2.137 |
Trace | 2.214 | 2.025 |
Artık yığın işleme sürelerini inceleyelim. Soldaki çizgede CPU, sağdaki çizgedeyse GPU üzerinde farklı yığın boyutlarında bir örnek için modelin çıkarım süreleri görülmektedir. Test durumlarının %95’inde değişen oranlarda C++ API’ı daha hızlı çalışmaktadır. CPU üzerinde genelde script
modunda kaydedilen modeller, GPU tarafındaysa trace
modunda kaydedilen modeller daha hızlı çalışmışlardır. Bu hesaplamalarda tek işlem/iş parçacığı kullanılmıştır:
Sonuç olarak aslında C++ API kullanarak modelleri çalıştırmanın hiç de zahmetli olmadığını gördüğünüzü düşünüyorum. Aslında sıfırdan bir modeli C++ üzerinde geliştirmekte çok zor değil. Sonraki yazılarda bunu da açıklamaya çalışacağım. Ama öncelikle verilerle nasıl başa çıkacağımızı incelemenin daha iyi olacağını düşünüyorum. Sonraki yazıda verileri (resim, video, csv, metin vb.) okumayı ve LibTorch‘a çıkarım ve eğitim sırasında bu verileri nasıl sağlayacağımızı açıklayacağım.
Dizinin diğer yazıları: