使用 PyTorch C++ 前端#
創建於:2019 年 1 月 15 日 | 最後更新:2025 年 9 月 22 日 | 最後驗證:2024 年 11 月 5 日
如何構建一個利用 PyTorch C++ 前端的 C++ 應用程式
如何使用 PyTorch 抽象在 C++ 中定義和訓練神經網路
PyTorch 1.5 或更高版本
對 C++ 程式設計有基本瞭解
基本的 Ubuntu Linux 環境,帶有 CMake >= 3.5;在 MacOS / Windows 環境中,類似的命令也能工作
(可選)用於 GPU 訓練部分的基於 CUDA 的 GPU
PyTorch C++ 前端是 PyTorch 機器學習框架的一個純 C++ 介面。雖然 PyTorch 的主要介面自然是 Python,但這個 Python API 建立在大量的 C++ 程式碼庫之上,提供了基礎的資料結構和功能,例如張量和自動微分。C++ 前端公開了一個純 C++17 API,它透過機器學習訓練和推理所需的工具擴充套件了這個底層的 C++ 程式碼庫。這包括一個用於神經網路建模的常用元件的內建集合;一個用於擴充套件此集合的自定義模組的 API;一個用於流行最佳化演算法(如隨機梯度下降)的庫;一個具有定義和載入資料集 API 的並行資料載入器;序列化例程等等。
本教程將引導您完成使用 C++ 前端訓練模型的端到端示例。具體來說,我們將訓練一個 DCGAN(一種生成模型),以生成 MNIST 數字的影像。雖然這是一個概念上簡單的示例,但它足以讓您對 PyTorch C++ 前端有一個快速的概述,並激發您訓練更復雜模型的興趣。我們將從一些激勵性的言論開始,解釋為什麼要使用 C++ 前端,然後直接深入定義和訓練我們的模型。
提示
觀看 2018 年 CppCon 的這個閃電演講,快速(且幽默)地瞭解 C++ 前端。
提示
這篇筆記 提供了對 C++ 前端的元件和設計理念的廣泛概述。
提示
PyTorch C++ 生態系統的文件可在 https://pytorch.com.tw/cppdocs 找到。您可以在那裡找到高層描述以及 API 級別的文件。
動機#
在我們開始 GAN 和 MNIST 數字的激動人心的旅程之前,讓我們退一步,討論一下為什麼一開始您要使用 C++ 前端而不是 Python 前端。我們(PyTorch 團隊)建立 C++ 前端是為了在無法使用 Python 或 Python 根本不適合的任務的環境中進行研究。例如,此類環境包括:
低延遲系統:您可能希望在具有高幀率和低延遲要求的純 C++ 遊戲引擎中進行強化學習研究。對於此類環境,使用純 C++ 庫比 Python 庫更合適。由於 Python 直譯器的慢速,Python 可能完全不可行。
高度多執行緒環境:由於全域性直譯器鎖 (GIL),Python 一次只能執行一個系統執行緒。多程序是另一種選擇,但可伸縮性不如 C++,並且存在嚴重的缺點。C++ 沒有此類限制,並且執行緒易於使用和建立。需要大量並行化的模型,例如 Deep Neuroevolution 中使用的模型,可以從中受益。
現有的 C++ 程式碼庫:您可能是一個現有的 C++ 應用程式的所有者,該應用程式執行從後端伺服器中提供網頁到在照片編輯軟體中渲染 3D 圖形的所有操作,並且希望將機器學習方法整合到您的系統中。C++ 前端允許您停留在 C++ 中,從而避免了在 Python 和 C++ 之間反覆繫結(binding)的麻煩,同時保留了傳統 PyTorch (Python) 體驗的大部分靈活性和直觀性。
C++ 前端無意與 Python 前端競爭。它旨在對其進行補充。我們知道研究人員和工程師都喜歡 PyTorch 的簡潔性、靈活性和直觀的 API。我們的目標是確保您能夠在所有可能的環境中利用這些核心設計原則,包括上面描述的環境。如果其中一種場景很好地描述了您的用例,或者您只是感興趣或好奇,請繼續閱讀,我們將在接下來的幾段中詳細探討 C++ 前端。
提示
C++ 前端試圖提供一個儘可能接近 Python 前端 API 的 API。如果您熟悉 Python 前端,並且曾經問過自己“如何在 C++ 前端中使用 X?”,那麼用您在 Python 中編寫程式碼的方式來編寫,大多數情況下,相同的函式和方法在 C++ 中都會可用,就像在 Python 中一樣(只需記住將點替換為雙冒號)。
編寫一個基本應用程式#
讓我們從編寫一個最小的 C++ 應用程式開始,以驗證我們在設定和構建環境方面是否一致。首先,您需要獲取 LibTorch 發行版 — 這是一個預編譯的 zip 存檔,其中包含使用 C++ 前端所需的所有相關標頭檔案、庫和 CMake 構建檔案。LibTorch 發行版可在 Linux、MacOS 和 Windows 的 PyTorch 網站上下載。本教程的其餘部分將假定一個基本的 Ubuntu Linux 環境,但您也可以在 MacOS 或 Windows 上進行操作。
提示
關於 安裝 PyTorch 的 C++ 發行版 的說明更詳細地描述了以下步驟。
提示
在 Windows 上,除錯和釋出構建不相容 ABI。如果您計劃以除錯模式構建專案,請嘗試使用 LibTorch 的除錯版本。另外,請確保在下面的 cmake --build . 行中指定正確的配置。
第一步是透過從 PyTorch 網站檢索到的連結在本地下載 LibTorch 發行版。對於標準的 Ubuntu Linux 環境,這意味著執行
# If you need e.g. CUDA 9.0 support, please replace "cpu" with "cu90" in the URL below.
wget https://download.pytorch.org/libtorch/nightly/cpu/libtorch-shared-with-deps-latest.zip
unzip libtorch-shared-with-deps-latest.zip
接下來,讓我們編寫一個名為 dcgan.cpp 的小型 C++ 檔案,該檔案包含 torch/torch.h,並暫時只打印出一個三乘三的單位矩陣
#include <torch/torch.h>
#include <iostream>
int main() {
torch::Tensor tensor = torch::eye(3);
std::cout << tensor << std::endl;
}
為了構建這個小型應用程式以及我們稍後功能齊全的訓練指令碼,我們將使用這個 CMakeLists.txt 檔案
cmake_minimum_required(VERSION 3.5 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 17)
注意
雖然 CMake 是 LibTorch 推薦的構建系統,但它不是硬性要求。您還可以使用 Visual Studio 專案檔案、QMake、純 Makefiles 或您認為舒適的任何其他構建環境。但是,我們不提供開箱即用的支援。
請注意上面 CMake 檔案中的第 4 行:find_package(Torch REQUIRED)。這指示 CMake 查詢 LibTorch 庫的構建配置。為了讓 CMake 知道在哪裡可以找到這些檔案,我們在呼叫 cmake 時必須設定 CMAKE_PREFIX_PATH。在此之前,讓我們就 dcgan 應用程式的以下目錄結構達成一致
dcgan/
CMakeLists.txt
dcgan.cpp
此外,我將把解壓後的 LibTorch 發行版的路徑稱為 /path/to/libtorch。請注意,這必須是絕對路徑。特別是,將 CMAKE_PREFIX_PATH 設定為 ../../libtorch 之類的路徑將導致意外的錯誤。相反,請寫 $PWD/../../libtorch 來獲取相應的絕對路徑。現在,我們準備構建我們的應用程式
root@fa350df05ecf:/home# mkdir build
root@fa350df05ecf:/home# cd build
root@fa350df05ecf:/home/build# cmake -DCMAKE_PREFIX_PATH=/path/to/libtorch ..
-- The C compiler identification is GNU 5.4.0
-- The CXX compiler identification is GNU 5.4.0
-- Check for working C compiler: /usr/bin/cc
-- Check for working C compiler: /usr/bin/cc -- works
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Detecting C compile features
-- Detecting C compile features - done
-- Check for working CXX compiler: /usr/bin/c++
-- Check for working CXX compiler: /usr/bin/c++ -- works
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Detecting CXX compile features
-- Detecting CXX compile features - done
-- Looking for pthread.h
-- Looking for pthread.h - found
-- Looking for pthread_create
-- Looking for pthread_create - not found
-- Looking for pthread_create in pthreads
-- Looking for pthread_create in pthreads - not found
-- Looking for pthread_create in pthread
-- Looking for pthread_create in pthread - found
-- Found Threads: TRUE
-- Found torch: /path/to/libtorch/lib/libtorch.so
-- Configuring done
-- Generating done
-- Build files have been written to: /home/build
root@fa350df05ecf:/home/build# cmake --build . --config Release
Scanning dependencies of target dcgan
[ 50%] Building CXX object CMakeFiles/dcgan.dir/dcgan.cpp.o
[100%] Linking CXX executable dcgan
[100%] Built target dcgan
上面,我們首先在 dcgan 目錄中建立了一個 build 資料夾,進入該資料夾,執行 cmake 命令生成必要的構建(Make)檔案,最後透過執行 cmake --build . --config Release 成功編譯了專案。現在我們已經準備好執行我們的最小二進位制檔案,並完成關於基本專案配置的這一部分
root@fa350df05ecf:/home/build# ./dcgan
1 0 0
0 1 0
0 0 1
[ Variable[CPUFloatType]{3,3} ]
看起來像一個單位矩陣!
定義神經網路模型#
現在我們已經配置好了基本環境,我們可以深入到本教程更令人感興趣的部分。首先,我們將討論如何在 C++ 前端中定義和與模組互動。我們將從基本的小型示例模組開始,然後使用 C++ 前端提供的豐富的內建模組庫來實現一個功能齊全的 GAN。
模組 API 基礎#
與 Python 介面一致,基於 C++ 前端的神經網路由稱為模組的可重用構建塊組成。有一個基模組類,所有其他模組都從中派生。在 Python 中,這個類是 torch.nn.Module,而在 C++ 中,它是 torch::nn::Module。除了實現模組封裝的演算法的 forward() 方法外,模組通常包含以下三種子物件之一:引數、緩衝區和子模組。
引數和緩衝區以張量形式儲存狀態。引數記錄梯度,而緩衝區則不記錄。引數通常是神經網路的可訓練權重。緩衝區的示例包括批次歸一化(batch normalization)的均值和方差。為了重用特定的邏輯塊和狀態,PyTorch API 允許巢狀模組。巢狀模組稱為子模組。
引數、緩衝區和子模組必須顯式註冊。一旦註冊,就可以使用 parameters() 或 buffers() 等方法來檢索整個(巢狀)模組層次結構中所有引數的容器。類似地,to(...) 等方法,例如 to(torch::kCUDA) 將所有引數和緩衝區從 CPU 移動到 CUDA 記憶體,可以在整個模組層次結構上工作。
定義模組和註冊引數#
為了將這些話付諸實踐,讓我們考慮這個在 Python 介面中編寫的簡單模組
import torch
class Net(torch.nn.Module):
def __init__(self, N, M):
super(Net, self).__init__()
self.W = torch.nn.Parameter(torch.randn(N, M))
self.b = torch.nn.Parameter(torch.randn(M))
def forward(self, input):
return torch.addmm(self.b, input, self.W)
在 C++ 中,它看起來像這樣
#include <torch/torch.h>
struct Net : torch::nn::Module {
Net(int64_t N, int64_t M) {
W = register_parameter("W", torch::randn({N, M}));
b = register_parameter("b", torch::randn(M));
}
torch::Tensor forward(torch::Tensor input) {
return torch::addmm(b, input, W);
}
torch::Tensor W, b;
};
與 Python 類似,我們定義了一個名為 Net 的類(為簡單起見,這裡是 struct 而非 class),並從基模組類派生它。在建構函式內部,我們使用 torch::randn 建立張量,就像我們在 Python 中使用 torch.randn 一樣。一個有趣的區別是我們如何註冊引數。在 Python 中,我們將張量包裝在 torch.nn.Parameter 類中,而在 C++ 中,我們必須透過 register_parameter 方法而不是直接傳遞張量。原因是 Python API 可以檢測到屬性是 torch.nn.Parameter 型別並自動註冊此類張量。在 C++ 中,反射非常有限,因此提供了一種更傳統(且不那麼神奇)的方法。
註冊子模組和遍歷模組層次結構#
以與註冊引數相同的方式,我們也可以註冊子模組。在 Python 中,子模組在作為模組的屬性分配時會自動檢測和註冊
class Net(torch.nn.Module):
def __init__(self, N, M):
super(Net, self).__init__()
# Registered as a submodule behind the scenes
self.linear = torch.nn.Linear(N, M)
self.another_bias = torch.nn.Parameter(torch.rand(M))
def forward(self, input):
return self.linear(input) + self.another_bias
這允許,例如,使用 parameters() 方法遞迴訪問我們模組層次結構中的所有引數
>>> net = Net(4, 5)
>>> print(list(net.parameters()))
[Parameter containing:
tensor([0.0808, 0.8613, 0.2017, 0.5206, 0.5353], requires_grad=True), Parameter containing:
tensor([[-0.3740, -0.0976, -0.4786, -0.4928],
[-0.1434, 0.4713, 0.1735, -0.3293],
[-0.3467, -0.3858, 0.1980, 0.1986],
[-0.1975, 0.4278, -0.1831, -0.2709],
[ 0.3730, 0.4307, 0.3236, -0.0629]], requires_grad=True), Parameter containing:
tensor([ 0.2038, 0.4638, -0.2023, 0.1230, -0.0516], requires_grad=True)]
要在 C++ 中註冊子模組,請使用名稱恰當的 register_module() 方法來註冊 torch::nn::Linear 之類的模組
struct Net : torch::nn::Module {
Net(int64_t N, int64_t M)
: linear(register_module("linear", torch::nn::Linear(N, M))) {
another_bias = register_parameter("b", torch::randn(M));
}
torch::Tensor forward(torch::Tensor input) {
return linear(input) + another_bias;
}
torch::nn::Linear linear;
torch::Tensor another_bias;
};
提示
您可以在 torch::nn 名稱空間的文件 此處 找到 torch::nn::Linear、torch::nn::Dropout 或 torch::nn::Conv2d 等可用內建模組的完整列表。
上面程式碼的一個微妙之處在於,為什麼子模組是在建構函式的初始化列表中建立的,而引數是在建構函式體內部建立的。對此有一個很好的理由,我們將在下面關於 C++ 前端的所有權模型的部分中討論。然而,最終結果是,我們可以像在 Python 中一樣遞迴訪問我們模組樹的引數。呼叫 parameters() 返回一個 std::vector<torch::Tensor>,我們可以對其進行迭代
int main() {
Net net(4, 5);
for (const auto& p : net.parameters()) {
std::cout << p << std::endl;
}
}
這將列印
root@fa350df05ecf:/home/build# ./dcgan
0.0345
1.4456
-0.6313
-0.3585
-0.4008
[ Variable[CPUFloatType]{5} ]
-0.1647 0.2891 0.0527 -0.0354
0.3084 0.2025 0.0343 0.1824
-0.4630 -0.2862 0.2500 -0.0420
0.3679 -0.1482 -0.0460 0.1967
0.2132 -0.1992 0.4257 0.0739
[ Variable[CPUFloatType]{5,4} ]
0.01 *
3.6861
-10.1166
-45.0333
7.9983
-20.0705
[ Variable[CPUFloatType]{5} ]
就像在 Python 中一樣,有三個引數。要檢視這些引數的名稱,C++ API 提供了一個 named_parameters() 方法,它返回一個 OrderedDict,就像在 Python 中一樣
Net net(4, 5);
for (const auto& pair : net.named_parameters()) {
std::cout << pair.key() << ": " << pair.value() << std::endl;
}
我們可以再次執行此操作來檢視輸出
root@fa350df05ecf:/home/build# make && ./dcgan 11:13:48
Scanning dependencies of target dcgan
[ 50%] Building CXX object CMakeFiles/dcgan.dir/dcgan.cpp.o
[100%] Linking CXX executable dcgan
[100%] Built target dcgan
b: -0.1863
-0.8611
-0.1228
1.3269
0.9858
[ Variable[CPUFloatType]{5} ]
linear.weight: 0.0339 0.2484 0.2035 -0.2103
-0.0715 -0.2975 -0.4350 -0.1878
-0.3616 0.1050 -0.4982 0.0335
-0.1605 0.4963 0.4099 -0.2883
0.1818 -0.3447 -0.1501 -0.0215
[ Variable[CPUFloatType]{5,4} ]
linear.bias: -0.0250
0.0408
0.3756
-0.2149
-0.3636
[ Variable[CPUFloatType]{5} ]
注意
文件 torch::nn::Module 包含操作模組層次結構的完整方法列表。
以前向模式執行網路#
要在 C++ 中執行網路,我們只需呼叫我們自己定義的 forward() 方法
int main() {
Net net(4, 5);
std::cout << net.forward(torch::ones({2, 4})) << std::endl;
}
這將列印類似以下內容
root@fa350df05ecf:/home/build# ./dcgan
0.8559 1.1572 2.1069 -0.1247 0.8060
0.8559 1.1572 2.1069 -0.1247 0.8060
[ Variable[CPUFloatType]{2,5} ]
模組所有權#
此時,我們知道如何在 C++ 中定義模組、註冊引數、註冊子模組,並透過 parameters() 等方法遍歷模組層次結構,最後執行模組的 forward() 方法。雖然 C++ API 中還有許多方法、類和主題需要深入研究,但我將引導您參考 文件 以獲取完整選單。當我們實現 DCGAN 模型和端到端訓練管道時,我們還將涉及一些其他概念。在此之前,讓我簡要介紹一下 C++ 前端為 torch::nn::Module 的子類提供的所有權模型。
對於這次討論,所有權模型是指模組的儲存和傳遞方式 — 決定誰或什麼擁有某個模組例項。在 Python 中,物件始終是動態分配的(在堆上)並且具有引用語義。這非常容易使用並且易於理解。事實上,在 Python 中,您可以大致忘記物件在哪裡生活以及它們如何被引用,而專注於完成任務。
C++ 作為一種低階語言,在此領域提供了更多選項。這增加了複雜性,並極大地影響了 C++ 前端的設計和人體工程學。特別地,對於 C++ 前端中的模組,我們可以選擇使用值語義或引用語義。第一種情況最簡單,並且在前面的示例中已顯示:模組物件在堆疊上分配,當傳遞到函式時,可以複製、移動(使用 std::move)或透過引用或指標獲取
struct Net : torch::nn::Module { };
void a(Net net) { }
void b(Net& net) { }
void c(Net* net) { }
int main() {
Net net;
a(net);
a(std::move(net));
b(net);
c(&net);
}
對於第二種情況 — 引用語義 — 我們可以使用 std::shared_ptr。引用語義的優點是,就像在 Python 中一樣,它減少了思考模組必須如何傳遞給函式以及引數必須如何宣告的認知開銷(假設您在所有地方都使用 shared_ptr)。
struct Net : torch::nn::Module {};
void a(std::shared_ptr<Net> net) { }
int main() {
auto net = std::make_shared<Net>();
a(net);
}
根據我們的經驗,來自動態語言的研究人員非常喜歡引用語義而不是值語義,即使後者對 C++ 來說更“原生”。還需要注意的是,torch::nn::Module 的設計,為了保持接近 Python API 的人體工程學,依賴於共享所有權。例如,以我們之前(此處縮短)的 Net 定義為例
struct Net : torch::nn::Module {
Net(int64_t N, int64_t M)
: linear(register_module("linear", torch::nn::Linear(N, M)))
{ }
torch::nn::Linear linear;
};
為了使用 linear 子模組,我們希望將其直接儲存在我們的類中。但是,我們也希望基模組類瞭解並能夠訪問此子模組。為此,它必須儲存對該子模組的引用。此時,我們已經需要共享所有權。 torch::nn::Module 類和具體的 Net 類都需要對子模組的引用。因此,基類將模組儲存為 shared_ptr,因此具體類也必須這樣做。
但是等等!我在上面的程式碼中沒有看到任何關於 shared_ptr 的提及!為什麼呢?好吧,因為 std::shared_ptr<MyModule> 的輸入量很大。為了保持研究人員的生產力,我們想出了一個複雜的方案來隱藏 shared_ptr 的提及 — 這通常是值語義的好處 — 同時保留引用語義。要了解其工作原理,我們可以看一下核心庫中 torch::nn::Linear 模組的簡化定義(完整定義 在此)。
struct LinearImpl : torch::nn::Module {
LinearImpl(int64_t in, int64_t out);
Tensor forward(const Tensor& input);
Tensor weight, bias;
};
TORCH_MODULE(Linear);
簡而言之:該模組不稱為 Linear,而是稱為 LinearImpl。一個宏 TORCH_MODULE 然後定義了實際的 Linear 類。這個“生成的”類實際上是 std::shared_ptr<LinearImpl> 的一個包裝器。它是一個包裝器而不是一個簡單的 typedef,所以,除其他外,建構函式仍然按預期工作,即您可以編寫 torch::nn::Linear(3, 4) 而不是 std::make_shared<LinearImpl>(3, 4)。我們將宏建立的類稱為模組持有者。與(共享)指標一樣,您使用箭頭運算子(如 model->forward(...))訪問底層物件。最終結果是所有權模型,它非常接近 Python API 的所有權模型。引用語義成為預設值,但沒有 std::shared_ptr 或 std::make_shared 的額外型別。對於我們的 Net,使用模組持有者 API 看起來像這樣
struct NetImpl : torch::nn::Module {};
TORCH_MODULE(Net);
void a(Net net) { }
int main() {
Net net;
a(net);
}
這裡有一個值得提及的細微問題。預設構造的 std::shared_ptr 是“空的”,即包含一個空指標。預設構造的 Linear 或 Net 是什麼?這是一個棘手的選擇。我們可以說它應該是一個空的(空)std::shared_ptr<LinearImpl>。但是,請記住 Linear(3, 4) 與 std::make_shared<LinearImpl>(3, 4) 相同。這意味著如果我們決定 Linear linear; 應該是一個空指標,那麼就沒有辦法構造一個不接受任何建構函式引數或預設所有建構函式引數的模組。出於這個原因,在當前 API 中,預設構造的模組持有者(如 Linear())會呼叫底層模組(LinearImpl())的預設建構函式。如果底層模組沒有預設建構函式,您將收到編譯器錯誤。要改為構造空的持有者,您可以將 nullptr 傳遞給持有者的建構函式。
實際上,這意味著您可以使用子模組,就像前面所示一樣,模組在初始化列表中註冊和構造
struct Net : torch::nn::Module {
Net(int64_t N, int64_t M)
: linear(register_module("linear", torch::nn::Linear(N, M)))
{ }
torch::nn::Linear linear;
};
或者您可以先用空指標構造持有者,然後在建構函式中為其賦值(對 Python 使用者來說更熟悉)
struct Net : torch::nn::Module {
Net(int64_t N, int64_t M) {
linear = register_module("linear", torch::nn::Linear(N, M));
}
torch::nn::Linear linear{nullptr}; // construct an empty holder
};
總結:您應該使用哪種所有權模型 — 哪種語義?C++ 前端的 API 最支援模組持有者提供的所有權模型。此機制唯一的缺點是在模組宣告下方的樣板程式碼多了一行。也就是說,最簡單的模型仍然是 C++ 模組介紹中所示的值語義模型。對於小型、簡單的指令碼,您也可以這樣用。但您遲早會發現,出於技術原因,它並非總是得到支援。例如,序列化 API(torch::save 和 torch::load)僅支援模組持有者(或純 shared_ptr)。因此,模組持有者 API 是使用 C++ 前端定義模組的推薦方式,在本教程中我們將以後也使用此 API。
定義 DCGAN 模組#
現在我們有了必要的背景和介紹,可以定義我們要在此帖中解決的機器學習任務的模組。回顧一下:我們的任務是生成 MNIST 資料集的數字影像。我們要使用一個 生成對抗網路 (GAN) 來解決這個任務。具體來說,我們將使用 DCGAN 架構 — 這是最早和最簡單的架構之一,但對於這個任務來說完全足夠。
提示
您可以在 此儲存庫 中找到本教程中提供的完整原始碼。
GAN 是什麼?#
GAN 由兩個獨立的神經網路模型組成:一個生成器和一個判別器。生成器接收來自噪聲分佈的樣本,其目標是將每個噪聲樣本轉換為看起來像目標分佈(在本例中為 MNIST 資料集)的影像。判別器則接收來自 MNIST 資料集的真實影像或生成器產生的虛假影像。它被要求發出一個機率,判斷給定影像的真實度(接近 1)或虛假度(接近 0)。來自判別器關於生成器產生的影像有多真實的反饋用於訓練生成器。關於判別器有多擅長識別真實性的反饋用於最佳化判別器。理論上,生成器和判別器之間的微妙平衡使它們協同改進,從而使生成器產生與目標分佈無法區分的影像,欺騙判別器的(當時)出色的眼睛,使其對真實和虛假影像都發出 0.5 的機率。對我們來說,最終結果是一個接收噪聲作為輸入並生成逼真數字影像的機器。
生成器模組#
我們首先定義生成器模組,它由一系列轉置二維卷積、批次歸一化和 ReLU 啟用單元組成。我們在自己定義的模組的 forward() 方法中顯式地(以函式式的方式)傳遞模組之間的輸入
struct DCGANGeneratorImpl : nn::Module {
DCGANGeneratorImpl(int kNoiseSize)
: conv1(nn::ConvTranspose2dOptions(kNoiseSize, 256, 4)
.bias(false)),
batch_norm1(256),
conv2(nn::ConvTranspose2dOptions(256, 128, 3)
.stride(2)
.padding(1)
.bias(false)),
batch_norm2(128),
conv3(nn::ConvTranspose2dOptions(128, 64, 4)
.stride(2)
.padding(1)
.bias(false)),
batch_norm3(64),
conv4(nn::ConvTranspose2dOptions(64, 1, 4)
.stride(2)
.padding(1)
.bias(false))
{
// register_module() is needed if we want to use the parameters() method later on
register_module("conv1", conv1);
register_module("conv2", conv2);
register_module("conv3", conv3);
register_module("conv4", conv4);
register_module("batch_norm1", batch_norm1);
register_module("batch_norm2", batch_norm2);
register_module("batch_norm3", batch_norm3);
}
torch::Tensor forward(torch::Tensor x) {
x = torch::relu(batch_norm1(conv1(x)));
x = torch::relu(batch_norm2(conv2(x)));
x = torch::relu(batch_norm3(conv3(x)));
x = torch::tanh(conv4(x));
return x;
}
nn::ConvTranspose2d conv1, conv2, conv3, conv4;
nn::BatchNorm2d batch_norm1, batch_norm2, batch_norm3;
};
TORCH_MODULE(DCGANGenerator);
DCGANGenerator generator(kNoiseSize);
我們現在可以呼叫 DCGANGenerator 上的 forward() 來將噪聲樣本對映到影像。
選擇的特定模組,如 nn::ConvTranspose2d 和 nn::BatchNorm2d,遵循前面概述的結構。kNoiseSize 常量確定輸入噪聲向量的大小,並設定為 100。當然,超引數是透過梯度下降(grad student descent)找到的。
注意
沒有梯度學生在發現超引數時受到傷害。他們定期食用 Soylent。
注意
關於如何將選項傳遞給 C++ 前端的內建模組(如 Conv2d)的簡要說明:每個模組都有一些必需的選項,例如 BatchNorm2d 的特徵數。如果您只需要配置必需的選項,您可以直接將其傳遞給模組的建構函式,例如 BatchNorm2d(128) 或 Dropout(0.5) 或 Conv2d(8, 4, 2)(用於輸入通道數、輸出通道數和卷積核大小)。但是,如果您需要修改其他通常是預設的選項,例如 Conv2d 的 bias,您需要構造並傳遞一個選項物件。C++ 前端的每個模組都有一個相關的選項結構,稱為 ModuleOptions,其中 Module 是模組的名稱,例如 Linear 的 LinearOptions。這就是我們在上面的 Conv2d 模組中所做的。
判別器模組#
判別器同樣是一系列卷積、批次歸一化和啟用。但是,現在卷積是常規卷積而不是轉置卷積,並且我們使用 alpha 值為 0.2 的 leaky ReLU 而不是 vanilla ReLU。此外,最終啟用成為 Sigmoid,它將值壓縮到 0 到 1 之間的範圍。然後,我們可以將這些壓縮後的值解釋為判別器分配給影像為真實的機率。
為了構建判別器,我們將嘗試一些不同的東西:一個Sequential 模組。與 Python 類似,PyTorch 在這裡提供了兩種模型定義 API:一種是函式式 API,其中輸入透過連續函式(例如,生成器模組示例),另一種是更面向物件的 API,我們在其中構建一個包含整個模型作為子模組的Sequential 模組。使用Sequential,判別器將如下所示
nn::Sequential discriminator(
// Layer 1
nn::Conv2d(
nn::Conv2dOptions(1, 64, 4).stride(2).padding(1).bias(false)),
nn::LeakyReLU(nn::LeakyReLUOptions().negative_slope(0.2)),
// Layer 2
nn::Conv2d(
nn::Conv2dOptions(64, 128, 4).stride(2).padding(1).bias(false)),
nn::BatchNorm2d(128),
nn::LeakyReLU(nn::LeakyReLUOptions().negative_slope(0.2)),
// Layer 3
nn::Conv2d(
nn::Conv2dOptions(128, 256, 4).stride(2).padding(1).bias(false)),
nn::BatchNorm2d(256),
nn::LeakyReLU(nn::LeakyReLUOptions().negative_slope(0.2)),
// Layer 4
nn::Conv2d(
nn::Conv2dOptions(256, 1, 3).stride(1).padding(0).bias(false)),
nn::Sigmoid());
提示
Sequential 模組簡單地執行函式組合。第一個子模組的輸出成為第二個子模組的輸入,第三個子模組的輸出成為第四個子模組的輸入,依此類推。
載入資料#
現在我們已經定義了生成器和判別器模型,我們需要一些資料來訓練這些模型。C++ 前端,就像 Python 前端一樣,帶有一個強大的並行資料載入器。這個資料載入器可以從(您可以自己定義的)資料集中讀取資料批次,並提供許多配置選項。
注意
雖然 Python 資料載入器使用多程序,但 C++ 資料載入器是真正多執行緒的,並且不會啟動任何新程序。
資料載入器是 C++ 前端 data API 的一部分,包含在 torch::data:: 名稱空間中。該 API 由幾個不同的元件組成
資料載入器類,
定義資料集的 API,
定義轉換(transforms)的 API,這些轉換可以應用於資料集,
定義取樣器(samplers)的 API,這些取樣器生成用於索引資料集的索引,
現有資料集、轉換和取樣器的庫。
對於本教程,我們可以使用 C++ 前端自帶的 MNIST 資料集。讓我們為此例項化一個 torch::data::datasets::MNIST,並應用兩個轉換:首先,我們對影像進行歸一化,使其範圍在 -1 到 +1 之間(原始範圍為 0 到 1)。其次,我們應用 Stack組合(collation),它獲取一批張量並將它們堆疊成一個張量(沿著第一個維度)
auto dataset = torch::data::datasets::MNIST("./mnist")
.map(torch::data::transforms::Normalize<>(0.5, 0.5))
.map(torch::data::transforms::Stack<>());
請注意,MNIST 資料集應位於訓練二進位制檔案執行位置的相對 ./mnist 目錄中。您可以使用 此指令碼 下載 MNIST 資料集。
接下來,我們建立一個數據載入器並將其傳遞給此資料集。要建立一個新的資料載入器,我們使用 torch::data::make_data_loader,它返回一個正確型別的 std::unique_ptr(該型別取決於資料集的型別、取樣器的型別以及其他一些實現細節)
auto data_loader = torch::data::make_data_loader(std::move(dataset));
資料載入器確實附帶了很多選項。您可以在 此處 檢視完整的選項集。例如,為了加快資料載入速度,我們可以增加工作執行緒的數量。預設數量為零,這意味著將使用主執行緒。如果我們設定 workers 為 2,將啟動兩個執行緒並行載入資料。我們還應該將批次大小從預設的 1 增加到一個更合理的值,例如 64(kBatchSize 的值)。因此,讓我們建立一個 DataLoaderOptions 物件並設定適當的屬性
auto data_loader = torch::data::make_data_loader(
std::move(dataset),
torch::data::DataLoaderOptions().batch_size(kBatchSize).workers(2));
我們現在可以編寫一個迴圈來載入資料批次,目前我們只將其列印到控制檯
for (torch::data::Example<>& batch : *data_loader) {
std::cout << "Batch size: " << batch.data.size(0) << " | Labels: ";
for (int64_t i = 0; i < batch.data.size(0); ++i) {
std::cout << batch.target[i].item<int64_t>() << " ";
}
std::cout << std::endl;
}
在這種情況下,資料載入器返回的型別是 torch::data::Example。此型別是一個簡單的結構體,包含用於資料的 data 欄位和用於標籤的 target 欄位。因為我們之前應用了 Stack 組合,所以資料載入器只返回一個這樣的示例。如果我們沒有應用組合,資料載入器將返回 std::vector<torch::data::Example<>>,其中每個元素對應批次中的一個示例。
如果重新構建並執行此程式碼,您應該會看到類似以下內容
root@fa350df05ecf:/home/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
root@fa350df05ecf:/home/build# make
[100%] Built target dcgan
root@fa350df05ecf:/home/build# ./dcgan
Batch size: 64 | Labels: 5 2 6 7 2 1 6 7 0 1 6 2 3 6 9 1 8 4 0 6 5 3 3 0 4 6 6 6 4 0 8 6 0 6 9 2 4 0 2 8 6 3 3 2 9 2 0 1 4 2 3 4 8 2 9 9 3 5 8 0 0 7 9 9
Batch size: 64 | Labels: 2 2 4 7 1 2 8 8 6 9 0 2 2 9 3 6 1 3 8 0 4 4 8 8 8 9 2 6 4 7 1 5 0 9 7 5 4 3 5 4 1 2 8 0 7 1 9 6 1 6 5 3 4 4 1 2 3 2 3 5 0 1 6 2
Batch size: 64 | Labels: 4 5 4 2 1 4 8 3 8 3 6 1 5 4 3 6 2 2 5 1 3 1 5 0 8 2 1 5 3 2 4 4 5 9 7 2 8 9 2 0 6 7 4 3 8 3 5 8 8 3 0 5 8 0 8 7 8 5 5 6 1 7 8 0
Batch size: 64 | Labels: 3 3 7 1 4 1 6 1 0 3 6 4 0 2 5 4 0 4 2 8 1 9 6 5 1 6 3 2 8 9 2 3 8 7 4 5 9 6 0 8 3 0 0 6 4 8 2 5 4 1 8 3 7 8 0 0 8 9 6 7 2 1 4 7
Batch size: 64 | Labels: 3 0 5 5 9 8 3 9 8 9 5 9 5 0 4 1 2 7 7 2 0 0 5 4 8 7 7 6 1 0 7 9 3 0 6 3 2 6 2 7 6 3 3 4 0 5 8 8 9 1 9 2 1 9 4 4 9 2 4 6 2 9 4 0
Batch size: 64 | Labels: 9 6 7 5 3 5 9 0 8 6 6 7 8 2 1 9 8 8 1 1 8 2 0 7 1 4 1 6 7 5 1 7 7 4 0 3 2 9 0 6 6 3 4 4 8 1 2 8 6 9 2 0 3 1 2 8 5 6 4 8 5 8 6 2
Batch size: 64 | Labels: 9 3 0 3 6 5 1 8 6 0 1 9 9 1 6 1 7 7 4 4 4 7 8 8 6 7 8 2 6 0 4 6 8 2 5 3 9 8 4 0 9 9 3 7 0 5 8 2 4 5 6 2 8 2 5 3 7 1 9 1 8 2 2 7
Batch size: 64 | Labels: 9 1 9 2 7 2 6 0 8 6 8 7 7 4 8 6 1 1 6 8 5 7 9 1 3 2 0 5 1 7 3 1 6 1 0 8 6 0 8 1 0 5 4 9 3 8 5 8 4 8 0 1 2 6 2 4 2 7 7 3 7 4 5 3
Batch size: 64 | Labels: 8 8 3 1 8 6 4 2 9 5 8 0 2 8 6 6 7 0 9 8 3 8 7 1 6 6 2 7 7 4 5 5 2 1 7 9 5 4 9 1 0 3 1 9 3 9 8 8 5 3 7 5 3 6 8 9 4 2 0 1 2 5 4 7
Batch size: 64 | Labels: 9 2 7 0 8 4 4 2 7 5 0 0 6 2 0 5 9 5 9 8 8 9 3 5 7 5 4 7 3 0 5 7 6 5 7 1 6 2 8 7 6 3 2 6 5 6 1 2 7 7 0 0 5 9 0 0 9 1 7 8 3 2 9 4
Batch size: 64 | Labels: 7 6 5 7 7 5 2 2 4 9 9 4 8 7 4 8 9 4 5 7 1 2 6 9 8 5 1 2 3 6 7 8 1 1 3 9 8 7 9 5 0 8 5 1 8 7 2 6 5 1 2 0 9 7 4 0 9 0 4 6 0 0 8 6
...
這意味著我們能夠成功地從 MNIST 資料集載入資料。
編寫訓練迴圈#
現在讓我們完成示例的演算法部分,並實現生成器和判別器之間的微妙互動。首先,我們將建立兩個最佳化器,一個用於生成器,一個用於判別器。我們使用的最佳化器實現了 Adam 演算法
torch::optim::Adam generator_optimizer(
generator->parameters(), torch::optim::AdamOptions(2e-4).betas(std::make_tuple(0.5, 0.5)));
torch::optim::Adam discriminator_optimizer(
discriminator->parameters(), torch::optim::AdamOptions(5e-4).betas(std::make_tuple(0.5, 0.5)));
注意
截至本文撰寫時,C++ 前端提供了實現 Adagrad、Adam、LBFGS、RMSprop 和 SGD 的最佳化器。 文件 提供了最新列表。
接下來,我們需要更新我們的訓練迴圈。我們將新增一個外部迴圈來在每個 epoch 耗盡資料載入器,然後編寫 GAN 訓練程式碼
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();
torch::Tensor real_images = batch.data;
torch::Tensor real_labels = torch::empty(batch.data.size(0)).uniform_(0.8, 1.0);
torch::Tensor real_output = discriminator->forward(real_images).reshape(real_labels.sizes());
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});
torch::Tensor fake_images = generator->forward(noise);
torch::Tensor fake_labels = torch::zeros(batch.data.size(0));
torch::Tensor fake_output = discriminator->forward(fake_images.detach()).reshape(fake_labels.sizes());
torch::Tensor d_loss_fake = torch::binary_cross_entropy(fake_output, fake_labels);
d_loss_fake.backward();
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).reshape(fake_labels.sizes());
torch::Tensor g_loss = torch::binary_cross_entropy(fake_output, fake_labels);
g_loss.backward();
generator_optimizer.step();
std::printf(
"\r[%2ld/%2ld][%3ld/%3ld] D_loss: %.4f | G_loss: %.4f",
epoch,
kNumberOfEpochs,
++batch_index,
batches_per_epoch,
d_loss.item<float>(),
g_loss.item<float>());
}
}
上面,我們首先在真實影像上評估判別器,對於這些影像,它應該分配高機率。為此,我們使用 torch::empty(batch.data.size(0)).uniform_(0.8, 1.0) 作為目標機率。
注意
我們選擇介於 0.8 和 1.0 之間的均勻分佈的隨機值,而不是所有地方都用 1.0,以使判別器訓練更魯棒。這個技巧被稱為標籤平滑。
在評估判別器之前,我們將其引數的梯度清零。計算損失後,透過呼叫 d_loss.backward() 來反向傳播損失以計算新梯度。我們對虛假影像重複這個過程。我們不使用資料集中的影像,而是透過向生成器輸入一批隨機噪聲來讓生成器建立虛假影像。然後我們將這些虛假影像傳遞給判別器。這次,我們希望判別器發出低機率,理想情況下全部為零。一旦我們計算了真實影像批次和虛假影像批次的判別器損失,我們就可以透過一個步驟來推進判別器的最佳化器,以更新其引數。
要訓練生成器,我們再次先清零其梯度,然後重新評估判別器在虛假影像上的表現。但是,這次我們希望判別器分配非常接近於一的機率,這表明生成器可以生成欺騙判別器認為它們實際上是真實的(來自資料集)的影像。為此,我們將 fake_labels 張量填充為全一。最後,我們步進生成器的最佳化器以更新其引數。
我們現在應該準備好在 CPU 上訓練我們的模型了。我們還沒有捕獲狀態或採樣輸出的程式碼,但我們稍後會新增。現在,讓我們只觀察我們的模型正在做某事 — 我們稍後將根據生成的影像來驗證這個事情是否有意義。重新構建和執行應該會列印類似以下內容
root@3c0711f20896:/home/build# make && ./dcgan
Scanning dependencies of target dcgan
[ 50%] Building CXX object CMakeFiles/dcgan.dir/dcgan.cpp.o
[100%] Linking CXX executable dcgan
[100%] Built target dcga
[ 1/10][100/938] D_loss: 0.6876 | G_loss: 4.1304
[ 1/10][200/938] D_loss: 0.3776 | G_loss: 4.3101
[ 1/10][300/938] D_loss: 0.3652 | G_loss: 4.6626
[ 1/10][400/938] D_loss: 0.8057 | G_loss: 2.2795
[ 1/10][500/938] D_loss: 0.3531 | G_loss: 4.4452
[ 1/10][600/938] D_loss: 0.3501 | G_loss: 5.0811
[ 1/10][700/938] D_loss: 0.3581 | G_loss: 4.5623
[ 1/10][800/938] D_loss: 0.6423 | G_loss: 1.7385
[ 1/10][900/938] D_loss: 0.3592 | G_loss: 4.7333
[ 2/10][100/938] D_loss: 0.4660 | G_loss: 2.5242
[ 2/10][200/938] D_loss: 0.6364 | G_loss: 2.0886
[ 2/10][300/938] D_loss: 0.3717 | G_loss: 3.8103
[ 2/10][400/938] D_loss: 1.0201 | G_loss: 1.3544
[ 2/10][500/938] D_loss: 0.4522 | G_loss: 2.6545
...
遷移到 GPU#
雖然我們當前的指令碼可以在 CPU 上正常執行,但我們都知道卷積在 GPU 上速度更快。讓我們快速討論一下如何將我們的訓練遷移到 GPU。我們需要為此做兩件事:將 GPU 裝置規範傳遞給我們自己分配的張量,並透過 C++ 前端中所有張量和模組都具有的 to() 方法顯式地將其他張量複製到 GPU。實現這兩點的最簡單方法是在我們訓練指令碼的頂層建立一個 torch::Device 例項,然後將該裝置傳遞給張量工廠函式(如 torch::zeros)以及 to() 方法。我們可以從用 CPU 裝置這樣做開始
// Place this somewhere at the top of your training script.
torch::Device device(torch::kCPU);
新的張量分配,如
torch::Tensor fake_labels = torch::zeros(batch.data.size(0));
應該更新為將 device 作為最後一個引數
torch::Tensor fake_labels = torch::zeros(batch.data.size(0), device);
對於不是我們在手中建立的張量,例如來自 MNIST 資料集的張量,我們必須插入顯式的 to() 呼叫。這意味著
torch::Tensor real_images = batch.data;
變成
torch::Tensor real_images = batch.data.to(device);
並且我們的模型引數也應該被移動到正確的裝置
generator->to(device);
discriminator->to(device);
注意
如果一個張量已經存在於傳遞給 to() 的裝置上,則該呼叫無效。不會進行額外的複製。
此時,我們只是使我們之前的 CPU 程式碼更加明確。然而,現在也很容易將裝置更改為 CUDA 裝置
torch::Device device(torch::kCUDA)
現在所有張量都將駐留在 GPU 上,並透過所有操作呼叫快速的 CUDA 核心,而無需我們更改任何下游程式碼。如果我們想指定一個特定的裝置索引,可以將其作為 Device 建構函式的第二個引數傳遞。如果我們希望不同的張量駐留在不同的裝置上,我們可以傳遞單獨的裝置例項(例如,一個在 CUDA 裝置 0 上,另一個在 CUDA 裝置 1 上)。我們甚至可以動態進行此配置,這通常有助於使我們的訓練指令碼更具可移植性
torch::Device device = torch::kCPU;
if (torch::cuda::is_available()) {
std::cout << "CUDA is available! Training on GPU." << std::endl;
device = torch::kCUDA;
}
或者甚至
torch::Device device(torch::cuda::is_available() ? torch::kCUDA : torch::kCPU);
檢查點和恢復訓練狀態#
我們應該對訓練指令碼進行的最後一次增強是定期儲存模型引數的狀態、最佳化器狀態以及一些生成的影像樣本。如果我們的計算機在訓練過程中崩潰,前兩者將允許我們恢復訓練狀態。對於長時間的訓練會話,這絕對是必不可少的。幸運的是,C++ 前端提供了一個 API 來序列化和反序列化模型和最佳化器狀態以及單個張量。
這方面的核心 API 是 torch::save(thing,filename) 和 torch::load(thing,filename),其中 thing 可以是 torch::nn::Module 子類或像我們訓練指令碼中的 Adam 物件這樣的最佳化器例項。讓我們更新我們的訓練迴圈,以便在特定間隔儲存模型和最佳化器狀態
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({8, 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';
}
其中 kCheckpointEvery 是一個設定為類似 100 的整數,表示每 100 個批次進行一次檢查點,而 checkpoint_counter 是每次進行檢查點時遞增的計數器。
要恢復訓練狀態,您可以在建立所有模型和最佳化器之後,但在訓練迴圈之前,新增類似這些的行
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));
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");
}
int64_t checkpoint_counter = 0;
for (int64_t epoch = 1; epoch <= kNumberOfEpochs; ++epoch) {
int64_t batch_index = 0;
for (torch::data::Example<>& batch : *data_loader) {
檢查生成的影像#
我們的訓練指令碼現在已完成。我們已準備好訓練我們的 GAN,無論是在 CPU 還是 GPU 上。要檢查我們訓練過程的中間輸出(為此我們添加了程式碼以定期將影像樣本儲存到 "dcgan-sample-xxx.pt" 檔案),我們可以編寫一個小型 Python 指令碼來載入張量並使用 matplotlib 顯示它們
import argparse
import matplotlib.pyplot as plt
import torch
parser = argparse.ArgumentParser()
parser.add_argument("-i", "--sample-file", required=True)
parser.add_argument("-o", "--out-file", default="out.png")
parser.add_argument("-d", "--dimension", type=int, default=3)
options = parser.parse_args()
module = torch.jit.load(options.sample_file)
images = list(module.parameters())[0]
for index in range(options.dimension * options.dimension):
image = images[index].detach().cpu().reshape(28, 28).mul(255).to(torch.uint8)
array = image.numpy()
axis = plt.subplot(options.dimension, options.dimension, 1 + index)
plt.imshow(array, cmap="gray")
axis.get_xaxis().set_visible(False)
axis.get_yaxis().set_visible(False)
plt.savefig(options.out_file)
print("Saved ", options.out_file)
現在讓我們將模型訓練大約 30 個 epoch
root@3c0711f20896:/home/build# make && ./dcgan 10:17:57
Scanning dependencies of target dcgan
[ 50%] Building CXX object CMakeFiles/dcgan.dir/dcgan.cpp.o
[100%] Linking CXX executable dcgan
[100%] Built target dcgan
CUDA is available! Training on GPU.
[ 1/30][200/938] D_loss: 0.4953 | G_loss: 4.0195
-> checkpoint 1
[ 1/30][400/938] D_loss: 0.3610 | G_loss: 4.8148
-> checkpoint 2
[ 1/30][600/938] D_loss: 0.4072 | G_loss: 4.36760
-> checkpoint 3
[ 1/30][800/938] D_loss: 0.4444 | G_loss: 4.0250
-> checkpoint 4
[ 2/30][200/938] D_loss: 0.3761 | G_loss: 3.8790
-> checkpoint 5
[ 2/30][400/938] D_loss: 0.3977 | G_loss: 3.3315
...
-> checkpoint 120
[30/30][938/938] D_loss: 0.3610 | G_loss: 3.8084
並顯示圖中的影像
root@3c0711f20896:/home/build# python display.py -i dcgan-sample-100.pt
Saved out.png
這應該看起來像這樣
數字!萬歲!現在輪到您了:您能否改進模型,使數字看起來更好?
結論#
本教程希望為您提供了 PyTorch C++ 前端易於理解的摘要。像 PyTorch 這樣的機器學習庫必然擁有廣泛而豐富的 API。因此,有許多概念我們沒有時間或空間在這裡討論。但是,我鼓勵您嘗試使用該 API,並在遇到困難時查閱我們的文件,特別是庫 API 部分。另外,請記住,每當我們能夠做到時,您都可以期望 C++ 前端遵循 Python 前端的設計和語義,因此您可以利用這一點來提高您的學習速率。
提示
您可以在 此儲存庫 中找到本教程中提供的完整原始碼。
一如既往,如果您遇到任何問題或有疑問,可以使用我們的論壇或GitHub issue 進行聯絡。