在很多時候,因為需要一個高效能,低延遲,高度平行化的執行環境,使用 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
至於主程式的部分,分為三個大項來解說,分別是:
引入的標頭檔如下,除了 torch.h 由 PyTroch 提供外,其餘皆是 C++ 標準標頭檔
#include <torch/torch.h>
#include <cstddef>
#include <cstdio>
#include <iostream>
#include <string>
#include <vector>
這個部分的原始碼,主要是建立 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) 其父類別為 TensorTransform。Normalize類別建構子接受兩個雙精度浮點數(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));
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 噪點的大小
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 吧!
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)
)。
首先建立兩個 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。