PyTorch C++ Ön Uç Arayüzü

PyTorch, doğrudan C++ üzerinde derin öğrenme modelleri geliştirmeye olanak tanıyan bir C++ arayüzü sağlar. Tensörler, otomatik geçiş, sinir ağı modülleri, optimize ediciler ve veri yükleyiciler dahil olmak üzere PyTorch’un tüm temel işlevleri mevcuttur. Programlama arayüzü Python’a çok benzer ve bir modeli Python’dan C++’a dönüştürmek çok kolaydır. PyTorch C++ ön ucu LibTorch olarak anılır. Bu adı bu belgenin geri kalanında kullanacağız.

Kurulum ve Derleme

Önemli: TRUBA’da bu öğretici yalnızca PyTorch pip kullanılarak yüklendiyse çalışır. PyTorch conda kullanılarak kurulduğunda çalışmaz.

Bir LibTorch programını derlemek için atılan adımları göstereceğiz. Daha karmaşık programları derlemek için bu adımlar izlenebilir.

cuDNN yüklemek

  1. Resmi Nvidia web sitesinden cuDNN Linux (x64) sürümünü indirin https://developer.nvidia.com/cudnn

  2. İndirilen dosyayı TRUBA’ya taşıdıktan sonra çıkartın:

    tar -xvf cudnn-10.2-linux-x64-v8.2.2.26.tgz
    
  3. Bu, cuda klasörünü oluşturacak, cuda klasörünün yolunu <cudnn_root> olarak belirteceğiz.

Proje dizini oluşturma

  1. Projeniz için bir klasör oluşturun

    mkdir libt_project
    cd libt_project
    
  2. Derlemek için basit bir C++ programı oluşturalım:

    vim libt_project.cpp
    
  3. libt_project.cpp dosyasında basit bir LibTorch programı oluşturalım. Bu program, 2x2 boyutlarında bir rank-2 tensörü oluşturacak ve onu rastgele değerlerle dolduracaktır:

    #include <torch/torch.h>
    #include <iostream>
    
    int main() {
      torch::Tensor tensor = torch::rand({2, 3});
      std::cout << tensor << std::endl;
    }
    

Derleme

  1. Derleme için CMake kullanacağız. Bunu yapmak için bir CMakeLists.txt dosyası oluşturuyoruz. LibTorch programları oluşturmak için CMake kullanmak gerekli değildir, ancak geliştiriciler tarafından önerilen derleme sistemi budur.

    cmake_minimum_required(VERSION 3.0 FATAL_ERROR)
    project(libt_project)
    
    find_package(Torch REQUIRED)
    set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${TORCH_CXX_FLAGS}")
    
    add_executable(libt_project libt_project.cpp)
    
  2. Bir derleme dizini oluşturacağız, ona gideceğiz ve yapım prosedürüne başlayacağız. Ortamımızda halihazırda kurulu olan PyTorch sürümünü kullanacağız. Not: TRUBA’da bu yalnızca pip kullanılarak kurulan bir PyTorch ile çalışır:

    mkdir build
    cd build
    cmake -DCMAKE_PREFIX_PATH=`python -c 'import torch;print(torch.utils.cmake_prefix_path)'` ..
    
  3. Oluşturma işlemi başarıyla tamamlanmıyor. CMakeCache.txt dosyasını açıp CUDNN_ROOT değişkenini düzenleyerek cuDNN kurulum adımında CUDNN‘yi ayıkladığımızda aldığımız cuda klasörünün konumuna ayarlıyoruz:

    vim CMakeCache.txt
    ...
    //NVIDIA cuDNN içeren dosyanın konumu
    CUDNN_ROOT:PATH=<cudnn_root>
    
  4. Son olarak make dosyalarını oluşturuyoruz ve programı derliyoruz.

    cmake -DCMAKE_PREFIX_PATH=`python -c 'import torch;print(torch.utils.cmake_prefix_path)'` ..
    cmake --build . --config Release
    
  5. Yürütülebilir dosya mevcut dizinimizde libt_project adıyla olacaktır. Sonuçları görmek için çalıştırıyoruz:

       ./libt_project
    
    Output:
    
    0.9432  0.2726  0.5261
    0.4113  0.0114  0.8928
    [ CPUFloatType{2,3} ]
    

Gelecek derlemeler

Bu adımları izledikten sonra bir dahaki sefere programımızı derlemek istediğimizde sadece tek bir komut kullanmamız yeterli:

cmake --build . --config Release

Derin sinir ağı modeli oluşturma

MNIST görüntü veri setinde çok sınıflı sınıflandırmayı önceden oluşturacak derin bir sinir ağı oluşturacağız. Bu ağ iki lineer katmandan oluşacak. torch::nn::Module sınıfından miras alan bir C++ yapısı oluşturarak LibTorch‘ta modüller oluşturuyoruz. LibTorch‘ta modül bildirmenin iki yöntemi vardır. İlki değer semantiğini kullanır ve LibTorch tarafından tam olarak desteklenmezken, referans semantiğini (paylaşılan işaretçiler) kullanan ikincisi LibTorch tarafından tam olarak desteklenir. İkisi arasındaki kesin fark hakkında daha fazla bilgi için lütfen aşağıdaki bağlantıyı kontrol edin link. Bu derste referans yöntemini kullanacağız.

Modül Deklerasyonu

Bir sinir ağı modülü bildirirken, torch::nn::Module‘den miras alan bir yapı oluşturuyoruz ve buna MODULE_NAMEImpl biçiminde bir ad vermeliyiz. Ayrıca sınıf bildirimini TORCH_MODULE(MODULE_NAME) makrosu ile takip ediyoruz. Bu, MODULE_NAMEImpl sınıfımızın bir shared_ptr’sinin (paylaşılan işaretçi) etrafındaki sarmalayıcı olan MODULE_NAME adlı yeni bir sınıf oluşturacaktır. Bu, bir MODULE_NAME örneği oluşturduğumuzda, onu işaretçiler kullanır gibi kullanacağımız ve . yerine -> kullanacağımız anlamına gelir. Bu örnek için kullanacağımız derin sinir ağının bildirimi aşağıdadır:

struct DNNImpl : torch::nn::Module{
};
TORCH_MODULE(DNN);

...

int main(){
  DNN model();
  model->forward(); // modeli bir shared_ptr olarak kullanıyoruz
}

Modül içeriği

Her modülün içinde şunlar olmalıdır:

  1. Veri üyeleri: modülün parametrelerinin yanı sıra doğrusal katmanlar ve evrişim katmanları gibi değişkenler.

  2. Yapıcı: modülün, parametrelerinin ve katmanlarının başlatıldığı fonksiyon.

  3. İleri fonksiyonu: bu fonksiyon, modülün ileriye doğru yayılmasını gerçekleştirecektir.

Veri üyeleri

Örneğimiz için modülümüze tam bağlantılı iki lineer katman ekliyoruz.

struct DNNImpl : torch::nn::Module{
  // linear1 ve linear2, sinir ağımızda olacak iki katmandır
  torch::nn::Linear linear1, linear2;
  ...
};

Yapıcı

Yapıcımızda, lineer katmanlarımızı uygun giriş ve çıkış boyutlarıyla başlatacağız ve onları kaydedeceğiz. Bu, optimize edicilerin optimizasyon adımlarını gerçekleştirirken bu parametreleri görmelerini sağlar.

struct DNNImpl : torch::nn::Module{
...
  // Yapıcımız girdi özelliklerinin, çıktı özelliklerinin ve gizli katman özelliklerinin sayısını alır.
  // Başlatıcı listesinde hem lineer katmanlarımızı başlatıyoruz hem de kaydediyoruz
  DNNImpl(int in_channels, int hidden_channels, int out_channels):
    linear1(register_module("linear1", torch::nn::Linear(in_channels, hidden_channels))),
    linear2(register_module("linear2", torch::nn::Linear(hidden_channels, out_channels))) {}
};

İleri fonksiyonu

Son olarak modülümüzde forward fonksiyonunu tanımlıyoruz. Bu işlev, bir grup veriyi alır ve her girdi için puanlar üretmek üzere ileriye doğru yayar.

struct DNNImpl : torch::nn::Module{

  ...
  // "batch_size" örnekleri içeren bir mini toplu numune alır
  torch::Tensor forward(torch::Tensor x){
    // Her örnek 28x28 piksellik bir görüntü olduğundan giriş tensörü [batch_size, 1, 28, 28] şeklinde olacaktır.
    // Önce onu [batch_size, 28*28] boyutunda bir tensöre yeniden şekillendiriyoruz
    // .size(i), i boyutu boyunca tensörün boyutunu döndürür, dolayısıyla x.size(0), batch_size değerini döndürür
    x = x.view({x.size(0), -1});
    // Girdiyi ilk lineer katmandan geçiriyoruz ve relu aktivasyon fonksiyonunu uyguluyoruz
    x = torch::relu(linear1(x));
    // İkinci lineer katmanı uyguluyoruz
    x = linear2(x);
    // Kayıp fonksiyonu soft-max lineer-olmayanlık uygulayacağı için ikinci katmandan sonra doğrusallık uygulamıyoruz
    return x;
  }

};
TORCH_MODULE(DNN);

Veri kümesi

Veri kümesini indir

LibTorch, MNIST veri kümesini kullanmak için işlevsellik içerir. Ancak, veri kümesini otomatik olarak indirmez. Bu nedenle veri setini manuel olarak indirmeliyiz. Bu komut dosyası, verileri geçerli dizine indirecektir. Örneği çalıştırmadan önce veri setini indirdiğinizden emin olun. Örneğimizde, veri setlerini mnist/ dizinine indiriyoruz.

Veri kümesi nesneleri

Eğitim ve test setleri için veri seti nesneleri oluşturarak ana programımıza başlıyoruz.

int main(){

   auto train_data = torch::data::datasets::MNIST("./mnist", torch::data::datasets::MNIST::Mode::kTrain)
    .map(torch::data::transforms::Normalize<>(0.5, 0.5))
    .map(torch::data::transforms::Stack<>());
   auto test_data = torch::data::datasets::MNIST("./mnist", torch::data::datasets::MNIST::Mode::kTest)
    .map(torch::data::transforms::Normalize<>(0.5, 0.5))
    .map(torch::data::transforms::Stack<>());
...
}

Veri setlerini yüklediğimizde üzerlerine uygulanacak iki dönüşümü belirtiyoruz. Birincisi, Normalize, değer aralığını [0,1] biçiminden [-1,1]’e kaydırır. İkincisi, Stack, tüm bir mini grubun tensörlerini tek bir tensörde toplayacaktır.

Veri kümesi yükleyici

Ardından, her bir veri kümesi için eğitim sırasında mini yığınları getirmek için kullanacağımız bir yükleyici nesnesi oluştururuz. Python’dan farklı olarak, işlev çağrılarına adlandırılmış parametreleri iletemeyiz. Bu nedenle, veri kümesi yükleyicilerine toplu iş boyutu ve paralel çalışan işlem sayısı gibi seçenekler vermek için bir DataLoaderOptions nesnesi oluşturmalı ve bunu DataSetLoader oluşturucu fonksiyonlarına iletmeliyiz.

int main(){
...
    // Veri yükleyicilere parti boyutunu ve kullanması gereken işçi sayısını söyleyecek options nesnesini oluşturuyoruz
    auto data_loader_options = torch::data::DataLoaderOptions().batch_size(batch_size).workers(2);
    // Her iki veri yükleyiciyi de ilgili veri kümelerini kullanarak oluşturuyoruz ve onlara DataLoader Options nesnesini iletiyoruz
    auto train_data_loader = torch::data::make_data_loader(std::move(train_data), data_loader_options);
    auto test_data_loader = torch::data::make_data_loader(std::move(test_data), data_loader_options);
...
}

Cihazı belirtme

Python arayüzüne benzer şekilde, tensörlerin ve modelin nerede olmasını istediğimizi belirtmek için kullanacağımız bir Device nesnesi oluşturacağız. Bu obje CPU veya GPU için olabilir.

int main(){
...
    torch::Device device = torch::kCPU;
    if (torch::cuda::is_available()) {
        std::cout << "CUDA is available! Training on GPU." << std::endl;
          device = torch::kCUDA;
    }
...
}

Modül oluşturma

Modülü oluşturup oluşturduğumuz cihaza taşıyacağız.

int main(){
...
    // Giriş kanallarının sayısı, her görüntüdeki piksellerin sayısıdır ve çıkış kanallarının sayısı, tahmin etmek istediğimiz sınıfların sayısıdır.
    DNN model(28*28, 15, 10);
    model->to(device);
...
}

Optimize Edici

Bir Adam optimizer yaratıyoruz ve ona modelimizin parametrelerini iletiyoruz. Optimize ediciyi oluşturduğumuzda, optimize edicimizin sahip olmasını istediğimiz seçenekleri iletmek için bir AdamOptions nesnesi kullanırız. Bu durumda, optimize ediciye 2e-1’lik bir öğrenme oranı kullanmasını söylüyoruxz.

int main(){
...
    torch::optim::Adam optim(
          model->parameters(), torch::optim::AdamOptions(2e-4));
...
}

Eğitim döngüsü

Eğitim döngümüzü 2 dönem için yineleyeceğiz. Her dönemde, train_data_loader nesnesinin içindeki tüm yığınları gözden geçireceğiz. Veri yükleyicinin torch::data:Example<> türünde nesneler oluşturacağını unutmayın. Bu türdeki her nesnenin data ve target olmak üzere iki üyesi vardır. data, bu grubun girdi tensörüdür ve target, bu grubun etiket tensörüdür.

int main(){
...
  int epochs = 2;
  for (int i =0; i< epochs; ++i){
    int batch_count = 0;
    // Her toplu iş, giriş değerlerini içeren tensör batch.data ve toplu iş etiketlerini içeren tbatch.target'ı içerir
    for (torch::data::Example<> & batch : *train_data_loader){
      // eski gradyanları modelimizden kaldır
      model->zero_grad();
      // giriş verilerini cihaza gönder
      torch::Tensor inputs = batch.data.to(device);
      // etiketleri cihaza gönder
      torch::Tensor labels = batch.target.to(device);
      // girdi grubunu model üzerinden ileriye doğru yay
      torch::Tensor output = model->forward(inputs);
      // model kaybını hesapla
      torch::Tensor loss = torch::cross_entropy_loss(output, labels);
      // gradyanları hesapla
      loss.backward();
      // Parametreleri güncellemek için optimize ediciyi kullanın
      optim.step();
      if (batch_count %100 == 0){
        std::cout <<"Epoch " << i <<" batch: " << batch_count << " - loss: " << loss.item<float>() << std::endl;
      }
      batch_count++;
    }
  }
...
}

Değerlendirme

Son olarak daha önce oluşturduğumuz test setini kullanarak modelin doğruluğunu değerlendiriyoruz. Her zamanki gibi, değerlendirme için yapılan hesaplamanın kaydedilmesini ve gradyan hesaplaması için kullanılmasını istemiyoruz. Bu nedenle değerlendirme hesaplamasını bir kod bloğunun içine yerleştiriyoruz ve bir torch::NoGradGuard nesnesi oluşturuyoruz. Bu, aynı kod bloğunda yer alacak tüm hesaplamaların kaydedilmeyeceğini ve gradyan hesaplaması için kullanılmayacağını garanti eder.

int main(){
...
  int correct_predictions = 0;
  int total_predictions = 0;
  {
    // Bu nesne var olduğu sürece, gradyan hesaplaması için hesaplamalar kaydedilecektir.
    torch::NoGradGuard no_grad;
    for (torch::data::Example<> & batch : *test_data_loader){
      torch::Tensor output = model->forward(batch.data.to(device));
      // max fonksiyonu bir dizi tensör döndürür, ilk tensör her satırdaki en yüksek puanları içerir ve ikinci tensör en yüksek puanın dizinini içerir
      auto score_predicted = at::max(output, 1);
      auto predicted = std::get<1>(score_predicted);
      // Doğru tahminlerin sayısını sayar ve kullanırız
      correct_predictions+= (predicted == batch.target.to(device)).sum().item<int>();
      total_predictions+= output.size(0);

    }
  }
  std::cout << "Final score: " << correct_predictions << "/" << total_predictions << " - accuracy: " << float(correct_predictions)/total_predictions*100 << std::endl;
}