iT邦幫忙

第 11 屆 iThome 鐵人賽

DAY 12
0
AI & Data

深度學習裡的冰與火之歌 : Tensorflow vs PyTorch系列 第 12

Day 12: PyTorch C++ front-end API

  • 分享至 

  • xImage
  •  

在很多時候,因為需要一個高效能,低延遲,高度平行化的執行環境,使用 python API 建立的模型不一定能夠達成需求,因此能夠直接使用 C++ 來撰寫模型,和訓練模型就變成了一個必要的需求。 PyTorch C++ front-end API 就是為了滿足這一條件而提供給使用者。

PyTorch C++ front-end API 以 C++11 的標準化函式庫寫成,擁有和 python 相似的介面,接著,我們會用 PyTorch C++ front-end 去建立一個 DCGAN 模型。在這個例子中的檔案結構如下:

dcgan/
  CMakeLists.txt
  dcgan.cpp
  build

我們會建立一個 dcgan 的專案資料夾,在這個專案資料夾放入一個 top-level 的 CMakeLists.txt,還有等一下會介紹的 DCGAN 原始檔:dcgan.cpp。在編譯的時候,為了能把產生的檔案都聚在一起,所以通常會另外件一個資料夾,在我們專案的例子是 build 資料夾,再轉移到該資料夾內執行 cmake 命令。
另外 CMakeLists.txt 的內容,誠如官方所提供的樣本

cmake_minimum_required(VERSION 3.0 FATAL_ERROR)
project(dcgan)

find_package(Torch REQUIRED)

add_executable(dcgan dcgan.cpp)
target_link_libraries(dcgan "${TORCH_LIBRARIES}")
set_property(TARGET dcgan PROPERTY CXX_STANDARD 11)

若要編譯,則在 build 資料夾下先呼叫命令 cmake -DCMAKE_PREFIX_PATH="<virtual environment>/lib/python3.7/site-packages/torch/share/cmake" ../
一個成功的輸出如下:

-- Looking for pthread.h
-- Looking for pthread.h - found
-- Performing Test CMAKE_HAVE_LIBC_PTHREAD
-- Performing Test CMAKE_HAVE_LIBC_PTHREAD - Success
-- Found Threads: TRUE  
-- Found torch: <virutal environment root>/lib/python3.7/site-packages/torch/lib/libtorch.dylib  
-- Configuring done
-- Generating done
-- Build files have been written to: <project root>/dcgan/build

如果見到 Build files have been written ...,就可以在 build 裏呼叫 'make' 即可開始編譯,一個成功的輸出如下:

Scanning dependencies of target dcgan
[ 50%] Building CXX object CMakeFiles/dcgan.dir/dcgan.cpp.o
[100%] Linking CXX executable dcgan
[100%] Built target dcgan

至於主程式的部分,分為三個大項來解說,分別是:

  1. 如何建立資料管線:這個部分會利用 MNIST 資料集,我們會用 torch::data::datasets::MNIST 程式碼來讀進 MNIST 手寫數字,黑白影像資料集,同時也會建立資料處理管線,對原資料做處理。
  2. 建立模型:我們將會建立兩個模型 generator 和 discriminator
  3. 訓練迴圈:我們將會在迴圈中處理正向和反向傳播,以及呼叫 optimizer 做參數更新。

引入的標頭檔如下,除了 torch.h 由 PyTroch 提供外,其餘皆是 C++ 標準標頭檔

#include <torch/torch.h>

#include <cstddef>
#include <cstdio>
#include <iostream>
#include <string>
#include <vector>

Data pipeline

這個部分的原始碼,主要是建立 DataLoader 物件,做正規化以及包覆成批次檔餵送給模型做訓練。

讀入資料建立資料集

一開始我們使用 torch::data::datasets::MNIST 這個類別來生成資料集物件。MNIST 物件繼承 Dataset,擁有一個建構自可接受兩個引數:
第一個是資料的路徑,原始碼由常數字串 kDataFolder 儲存。
第二個則是模式,需傳入 MNIST::Mode enum 型態,該型態包括兩個值(kTrain, kTest),預設為 kTrain。
關於 torch::data::datasets::MNIST 類別定義內容可以在 PyTorch repo 的這個位置找到:torch/csrc/api/include/torch/data/datasets/mnist.h

建立資料管線以進行資料轉換

如果我們更仔細觀察 MNIST 的父類別,我們可以發現有兩個資料集類別分別在 base.h 和 map.h 的定義中:
一個是允許隨機提取元素的 BatchDataset,該類別運喜使用者使用 indexing 的方始提取元素。BatchDataset 也是 Dataset 的父類別,如果大家沒忘的話,就是 datasets::MNIST 的祖父類別,所以 Dataset 擁有 get_batch 的方法,同時有傳回單一 example 的 get 方法。(torch/data/datasets/base.h)

另一個是 MapDataset,該資料集繼承 BatchDataset 主要的目的是提供資料轉換用,該類別包括父類別所沒有的方法包括 dataset() 來提取來源資料集,和 transform() 提取進行 transform 的 TransformType 物件。(torch/data/datasets/map.h)

為了進行轉換, Dataset 物件呼叫定義在父類別的 map(TransformType& or TransformType&&)方法,該方法是非類別方法 datasets::map(DatasetType, TransformType) 的 wrapper,該方法定義在 (torch/data/datasets/map.h)

另外一個 TransformType 則是 Stack,做的轉換則是把所有的張量資料堆疊成一個張量。

TransformType 可以接受的型別物件都定義在 torch::data::transforms 下,如例子原始碼所用的 Normalize,定義在 (torch/data/transforms/tensor.h) 其父類別為 TensorTransformNormalize類別建構子接受兩個雙精度浮點數(aka double)引數,分別是平均值和表準差。父類別提供 apply 方法,而由子類別作運算元加載(overload),關於實作細節就不在這裡贅述。

原始碼的最後一行,則是呼叫 torch::data::make_data_loader 生成 dataloader。make_data_loader定義在 torch/data/dataloader.h,在例子中所使用的是接受兩個引數的版本,這兩個引數分別是:Dataset dataset, 和 DataLoaderOptions options。

DataLoaderOptions 定義在 torch/data/dataloader_options.h,包含的是一些可調整 dataloader 的參數。實例化的時候可以傳入 batch size,但在原始碼中所用的方法則是使用一個 TORCH_ARG 的 macro 來初始化 DataLoaderOptions 的屬性物件,如 batch_size 設為 kBatchSize 而 workers 則設為 2。TORCH_ARG 接受兩個引數,型別和值,目的就是作為該類別的 getter 和 setter 而已。(見 torch/csrc/api/include/torch/arg.h)

// 以資料集所在位置來初始化字串常數,kDataFolder,這裡是在原始碼所在目錄的 data 子資料夾
const char* kDataFolder = "./data";
// 初始 kBatchSize 64 位元整數常數為所訓練需要用的 batch size
const int64_t kBatchSize = 64;

// 使用 `kDataFolder` 常數來表示資料集在磁碟的位置;
auto dataset = torch::data::datasets::MNIST(kDataFolder)
                     .map(torch::data::transforms::Normalize<>(0.5, 0.5))
                     .map(torch::data::transforms::Stack<>());
// 計算每一個 epoch 需要 iterate 多少 batch
const int64_t batches_per_epoch =
      std::ceil(dataset.size().value() / static_cast<double>(kBatchSize));
// 建立 data_loader
auto data_loader = torch::data::make_data_loader(
      std::move(dataset),
      torch::data::DataLoaderOptions().batch_size(kBatchSize).workers(2));

DCGAN vs Module API Basics

PyTorch 的 C++ front-end API 力求和 Python Module API 一樣簡單,事實上兩者非常相像。和類神經網路相關的函式定義可以見 torch/csrc/api/include/torch/nn 目錄下的標頭檔,接下來我們就來看原始碼。為了能和 Python Module API 比較,相對應的 python 語法在程式碼的註釋中,python code 中 ngf 是 generator 的 depth(ngf = 64) 而 nz 則是 latent vector 的長度(nz = 100)。python 的原始碼,則是取自於官方 dcgan 的 tutorial

generator

// 用於 generator 噪點的大小
const int64_t kNoiseSize = 100;

nn::Sequential generator( // self.main = nn.Sequential(
      // Layer 1
      // nn.ConvTranspose2d(nz, ngf * 8, 4, 1, 0, bias=False),
      nn::Conv2d(nn::Conv2dOptions(kNoiseSize, 256, 4) 
                     .with_bias(false)
                     .transposed(true)),
      //nn.BatchNorm2d(ngf * 8),
      nn::BatchNorm(256),
      //nn.ReLU(True),
      nn::Functional(torch::relu),
      // Layer 2
      //nn.ConvTranspose2d(ngf * 8, ngf * 4, 4, 2, 1, bias=False),
      nn::Conv2d(nn::Conv2dOptions(256, 128, 3)
                     .stride(2)
                     .padding(1)
                     .with_bias(false)
                     .transposed(true)),
      //nn.BatchNorm2d(ngf * 4),
      nn::BatchNorm(128),
      //nn.ReLU(True),
      nn::Functional(torch::relu),
      // Layer 3
      //nn.ConvTranspose2d( ngf * 4, ngf * 2, 4, 2, 1, bias=False),
      nn::Conv2d(nn::Conv2dOptions(128, 64, 4)
                     .stride(2)
                     .padding(1)
                     .with_bias(false)
                     .transposed(true)),
      //nn.BatchNorm2d(ngf * 2),ww
      nn::BatchNorm(64),
      //nn.ReLU(True),
      nn::Functional(torch::relu),
      // Layer 4
      //nn.ConvTranspose2d( ngf, nc, 4, 2, 1, bias=False),
      nn::Conv2d(nn::Conv2dOptions(64, 1, 4)
                     .stride(2)
                     .padding(1)
                     .with_bias(false)
                     .transposed(true)),
      //nn.Tanh()
      nn::Functional(torch::tanh));

可以由註釋裡的 python code 和 C++ 比較,這兩者還真的像啊!所以應該也不需要我的講解,任何看得懂 python code 應該瞬間也懂得 C++ code 吧!

discriminator

discrimiator 是同樣的情況(都是好幾層的 convolution layers)。但是對應的 python 語法在程式碼的註釋中,python code 中 nc 是輸入影像的 channel(nc = 3) 而 ndf 則是 feaiture vector 的長度(ndf = 64)。由於與 python 版本的 discriminator 太過相像,所以這裡不再列出。
關於 DCGAN 要注意的是使用 deconvolution layer ,呼叫 nn.ConvTranspose2d 來完成(C++ code 則是 nn::Conv2d(nn::Conv2dOptions(....).transposed(true))。而 discriminator 則是尋常的 convolution layer,呼叫 nn.Conv2d 來完成(C++ code 則是 nn::Conv2d(nn::Conv2dOptions(....).transposed(false))。

training loop

首先建立兩個 optimizers,一個是 optimize generator 的參數,另外一個則是 optimize discrimiator。
torch::optim::Adam 定義在 torch/csrc/api/include/torch/optim。

 torch::optim::Adam generator_optimizer(
      generator->parameters(), torch::optim::AdamOptions(2e-4).beta1(0.5));
  torch::optim::Adam discriminator_optimizer(
      discriminator->parameters(), torch::optim::AdamOptions(2e-4).beta1(0.5));

接著用兩個迴圈來進行訓練:第一個迴圈是 epoch,而在大迴圈內圈的是 data_loader 批次餵送訓練資料。下面是一個不能運行的 pseudo code,大概簡單示意在 training loop 中需要處理什麼問題。大致上原始碼和 python code 很像,大家可以閱讀DCGAN TUTORIAL閱讀 python code 的詳盡註釋。

int64_t checkpoint_counter = 1; // 用於 torch::save checkpoint 
for (int64_t epoch = 1; epoch <= kNumberOfEpochs; ++epoch) {
    int64_t batch_index = 0;
    for (torch::data::Example<>& batch : *data_loader) {
         // Train discriminator with real images.
         discriminator->zero_grad();
         //.... 省略原始碼:從 data_loader 提取 real_images 並建立相關 real_labels
         // real_labels
         torch::Tensor real_output = discriminator->forward(real_images);
         torch::Tensor d_loss_real =
                   torch::binary_cross_entropy(real_output, real_labels);
         d_loss_real.backward();
         // Train discriminator with fake images.
         torch::Tensor noise = 
                 torch::randn({batch.data.size(0), kNoiseSize, 1, 1}, device);
         torch::Tensor fake_output =
                 discriminator>forward(fake_images.detach());
         torch::Tensor d_loss_fake =
                  torch::binary_cross_entropy(fake_output, fake_labels);
         d_loss_fake.backward();
         
         // 你可以累積 loss 在一並才交予 optimizer 更新參數
         torch::Tensor d_loss = d_loss_real + d_loss_fake;
         discriminator_optimizer.step();
         
         // Train generator.
         generator->zero_grad();
         fake_labels.fill_(1);
         fake_output = discriminator->forward(fake_images);
         torch::Tensor g_loss =
          torch::binary_cross_entropy(fake_output, fake_labels);
         g_loss.backward();
         generator_optimizer.step();
         batch_index++;
         //... 省略列印結果
     }
}

迴圈的部分可以看到大致上與 python 原始碼特別的相像。最後則稍微提一下 model checkpointn 的原始碼

// 使用布林常數來表示是否儲存並從 checkpoint 檔回覆
const bool kRestoreFromCheckpoint = false;
kNumberOfSamplesPerCheckpoint
// 設定取樣的大小
const int64_t kNumberOfSamplesPerCheckpoint = 10;

// 在 main training loop 外
if (kRestoreFromCheckpoint) {
    torch::load(generator, "generator-checkpoint.pt");
    torch::load(generator_optimizer, "generator-optimizer-checkpoint.pt");
    torch::load(discriminator, "discriminator-checkpoint.pt");
    torch::load(
        discriminator_optimizer, "discriminator-optimizer-checkpoint.pt");
  }
//... training loop {
   if (batch_index % kCheckpointEvery == 0) {
        // Checkpoint the model and optimizer state.
        torch::save(generator, "generator-checkpoint.pt");
        torch::save(generator_optimizer, "generator-optimizer-checkpoint.pt");
        torch::save(discriminator, "discriminator-checkpoint.pt");
        torch::save(
            discriminator_optimizer, "discriminator-optimizer-checkpoint.pt");
        // Sample the generator and save the images.
        torch::Tensor samples = generator->forward(torch::randn(
            {kNumberOfSamplesPerCheckpoint, kNoiseSize, 1, 1}, device));
        torch::save(
            (samples + 1.0) / 2.0,
            torch::str("dcgan-sample-", checkpoint_counter, ".pt"));
        std::cout << "\n-> checkpoint " << ++checkpoint_counter << '\n';
      }
//}

這裡其實沒有 magic,一樣是利用 torch::save 儲存模型,optimizer 和 samples。至於回覆的部分則由 torch::load 讀入檔案來進行回覆。
關於如何使用 torch::load 和 torch::save 則可見 torch/csrc/api/include/torch/serialize.h。


上一篇
Day 11: 中場休息 CMake 教學
下一篇
Day 13: 使用 python C extension 來擴充 PyTroch
系列文
深度學習裡的冰與火之歌 : Tensorflow vs PyTorch31
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言