使用 PyTorch C++ 前端¶
建立於:2019 年 1 月 15 日 | 最後更新:2025 年 1 月 23 日 | 最後驗證:2024 年 11 月 05 日
PyTorch C++ 前端是 PyTorch 機器學習框架的純 C++ 介面。 雖然 PyTorch 的主要介面自然是 Python,但此 Python API 位於一個重要的 C++ 程式碼庫之上,該程式碼庫提供基礎資料結構和功能,例如張量和自動微分。 C++ 前端公開了一個純 C++11 API,該 API 使用機器學習訓練和推論所需的工具擴充了此底層 C++ 程式碼庫。 這包括用於神經網路建模的內建常見元件集合;一個使用自訂模組擴充此集合的 API; 一個流行的最佳化演算法庫,例如隨機梯度下降; 一個具有 API 的平行資料載入器,用於定義和載入資料集; 序列化常式等等。
本教學課程將引導您完成使用 C++ 前端訓練模型的端對端範例。 具體來說,我們將訓練一個 DCGAN(一種生成模型),以生成 MNIST 數字的圖像。 雖然概念上是一個簡單的範例,但它應該足以讓您快速了解 PyTorch C++ 前端,並激發您訓練更複雜模型的興趣。 我們將從一些激勵性的話語開始,說明為什麼您一開始就想使用 C++ 前端,然後直接深入定義和訓練我們的模型。
提示
觀看 來自 CppCon 2018 的這個閃電講座,以快速(且幽默地)了解 C++ 前端。
提示
此筆記提供了對 C++ 前端元件和設計理念的全面概述。
提示
PyTorch C++ 生態系統的文件可在 https://pytorch.dev.org.tw/cppdocs 取得。 在那裡您可以找到高階描述以及 API 等級的文件。
動機¶
在我們開始 GAN 和 MNIST 數字的激動人心的旅程之前,讓我們退一步討論一下為什麼您一開始就想使用 C++ 前端而不是 Python 前端。 我們(PyTorch 團隊)建立 C++ 前端是為了在無法使用 Python 或 Python 根本不是正確工具的環境中進行研究。 此類環境的範例包括
低延遲系統:您可能希望在純 C++ 遊戲引擎中進行強化學習研究,該引擎具有高幀率和低延遲要求。 使用純 C++ 函式庫比 Python 函式庫更適合此類環境。 由於 Python 直譯器的速度慢,Python 可能根本不可行。
高度多執行緒環境:由於全域直譯器鎖 (GIL),Python 一次無法執行多個系統執行緒。 多重處理是一種替代方案,但可擴展性不高且存在重大缺點。 C++ 沒有此類限制,並且執行緒易於使用和建立。 需要大量平行化的模型(例如在 深度神經演化中使用的模型)可以從中受益。
現有的 C++ 程式碼庫:您可能擁有一個現有的 C++ 應用程式,用於執行任何操作,從在後端伺服器中提供網頁到在照片編輯軟體中呈現 3D 圖形,並且希望將機器學習方法整合到您的系統中。 C++ 前端允許您留在 C++ 中,並免去在 Python 和 C++ 之間來回綁定的麻煩,同時保留傳統 PyTorch(Python)體驗的大部分靈活性和直觀性。
C++ 前端並非旨在與 Python 前端競爭。 它的目的是補充它。 我們知道研究人員和工程師都喜歡 PyTorch 的簡潔性、靈活性和直觀的 API。 我們的目標是確保您可以在每個可能的環境中利用這些核心設計原則,包括上面描述的那些。 如果其中一種情境很好地描述了您的用例,或者如果您只是感興趣或好奇,請按照我們在以下段落中詳細探索 C++ 前端。
提示
C++ 前端試圖提供一個盡可能接近 Python 前端 API 的 API。 如果您有 Python 前端的經驗,並且曾經問自己「如何使用 C++ 前端執行 X?」,請以您在 Python 中編寫程式碼的方式編寫程式碼,並且通常在 C++ 中也會提供與 Python 中相同的功能和方法(只需記住將點替換為雙冒號)。
撰寫基礎應用程式¶
讓我們先從撰寫一個極簡的 C++ 應用程式開始,以確認我們對於設置和建置環境的認知一致。首先,你需要取得一份 LibTorch 發行版 – 這是一個預先建置好的壓縮檔,其中包含了所有相關的標頭檔、函式庫和 CMake 建置檔案,這些都是使用 C++ 前端所需的。LibTorch 發行版可以在 PyTorch 網站上下載,適用於 Linux、MacOS 和 Windows。本教學的其餘部分將假設一個基本的 Ubuntu Linux 環境,但你也可以在 MacOS 或 Windows 上跟著操作。
提示
關於安裝 PyTorch 的 C++ 發行版的說明更詳細地描述了以下步驟。
提示
在 Windows 上,偵錯 (debug) 和發佈 (release) 建置版本不具有 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.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 14)
注意
雖然 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()
方法之外,模組通常包含以下三種類型的子物件:參數、緩衝區和子模組。
參數和緩衝區以張量的形式儲存狀態。參數記錄梯度,而緩衝區則不記錄。參數通常是你的神經網路的可訓練權重。緩衝區的例子包括用於批次正規化的均值和變異數。為了重複使用特定的邏輯和狀態塊,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++ 來說更「原生」。 重要的是要注意,為了保持與 Python API 的人體工學接近,torch::nn::Module
的設計依賴於共享所有權。 例如,採用我們先前(在此處縮短的)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
又是什麼呢?這是一個棘手的選擇。我們可以說它應該是一個空的(null)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
的機率。對我們來說,最終結果是一台接收雜訊作為輸入並產生逼真數字圖像作為輸出的機器。
生成器模組¶
我們首先定義生成器模組,它由一系列轉置的 2D 卷積、批次正規化和 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
。超參數當然是透過研究生下降法找到的。
注意
在發現超參數的過程中,沒有研究生受到傷害。他們定期餵食 Soylent。
注意
關於將選項傳遞給 C++ 前端中的內建模組(例如 Conv2d
)的簡要說明:每個模組都有一些必需的選項,例如 BatchNorm2d
的特徵數量。如果你只需要配置必需的選項,你可以將它們直接傳遞給模組的建構子,例如 BatchNorm2d(128)
或 Dropout(0.5)
或 Conv2d(8, 4, 2)
(對於輸入通道數、輸出通道數和核心大小)。但是,如果你需要修改其他通常預設的選項,例如 Conv2d
的 bias
,你需要建構並傳遞一個選項物件。C++ 前端中的每個模組都有一個關聯的選項結構體,稱為 ModuleOptions
,其中 Module
是模組的名稱,例如 Linear
的 LinearOptions
。這就是我們對上面的 Conv2d
模組所做的事情。
判別器模組¶
鑑別器 (Discriminator) 類似地也是一系列的卷積、批次正規化 (batch normalization) 和激活函數 (activations)。然而,現在的卷積是常規的卷積,而不是轉置卷積 (transposed),並且我們使用 alpha 值為 0.2 的 leaky ReLU,而不是普通的 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
模組只是簡單地執行函數組合。 第一個子模組的輸出成為第二個子模組的輸入,第三個子模組的輸出成為第四個子模組的輸入,依此類推。
載入資料¶
現在我們已經定義了生成器 (generator) 和鑑別器模型,我們需要一些可以用來訓練這些模型的資料。 C++ 前端,就像 Python 前端一樣,帶有一個強大的平行資料載入器。 這個資料載入器可以從資料集中讀取批次的資料 (您可以自己定義),並提供許多配置選項。
注意
雖然 Python 資料載入器使用多重處理 (multi-processing),但 C++ 資料載入器是真正的多執行緒 (multi-threaded),並且不會啟動任何新的進程 (processes)。
資料載入器是 C++ 前端的 data
API 的一部分,包含在 torch::data::
命名空間中。 這個 API 由幾個不同的組件組成:
資料載入器類別,
用於定義資料集的 API,
用於定義 *轉換 (transforms)* 的 API,可以將其應用於資料集,
用於定義 *取樣器 (samplers)* 的 API,它們產生用於索引資料集的索引,
現有資料集、轉換和取樣器的函式庫。
在本教學中,我們可以使用 C++ 前端附帶的 MNIST
資料集。 讓我們為此實例化一個 torch::data::datasets::MNIST
,並應用兩個轉換:首先,我們正規化 (normalize) 圖像,使其範圍在 -1
到 +1
之間 (從原始範圍 0
到 1
)。其次,我們應用 Stack
*併排 (collation)*,它接受一批 tensors 並將它們沿第一個維度堆疊成一個單一的 tensor。
auto dataset = torch::data::datasets::MNIST("./mnist")
.map(torch::data::transforms::Normalize<>(0.5, 0.5))
.map(torch::data::transforms::Stack<>());
請注意,MNIST 資料集應該位於相對於您執行訓練二進制檔 (binary) 的 ./mnist
目錄中。 您可以使用 這個腳本 來下載 MNIST 資料集。
接下來,我們建立一個資料載入器並將此資料集傳遞給它。 為了建立一個新的資料載入器,我們使用 torch::data::make_data_loader
,它返回正確類型的 std::unique_ptr
(這取決於資料集的類型、取樣器的類型以及其他一些實作細節)
auto data_loader = torch::data::make_data_loader(std::move(dataset));
資料載入器確實帶有很多選項。 您可以在 這裡 檢查完整的集合。 例如,為了加速資料載入,我們可以增加 worker 的數量。 預設數量為零,這表示將使用主執行緒。 如果我們將 workers
設置為 2
,則將產生兩個執行緒並行載入資料。 我們還應該將批次大小 (batch size) 從預設的 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
。 此類型是一個簡單的結構 (struct),帶有一個用於資料的 data
欄位和一個用於標籤 (label) 的 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 資料集載入資料。
編寫訓練迴圈¶
現在讓我們完成範例的演算法部分,並實作生成器和鑑別器之間的微妙平衡。 首先,我們將建立兩個最佳化器 (optimizers),一個用於生成器,一個用於鑑別器。 我們使用的最佳化器實作了 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。 這個技巧稱為 *標籤平滑 (label smoothing)*。
在評估鑑別器之前,我們將其參數的梯度歸零。 計算損失 (loss) 後,我們透過呼叫 d_loss.backward()
將其反向傳播 (back-propagate) 到網路中,以計算新的梯度。 我們對假圖像重複此操作。 我們不是使用來自資料集的圖像,而是讓生成器透過將一批隨機雜訊 (noise) 輸入其中來建立假圖像。 然後,我們將這些假圖像轉發到鑑別器。 這次,我們希望鑑別器發出較低的機率,理想情況下全部為零。 一旦我們計算出真實圖像批次和假圖像批次的鑑別器損失,我們就可以使鑑別器的最佳化器前進一步,以便更新其參數。
為了訓練生成器,我們再次首先將其梯度歸零,然後在假圖像上重新評估鑑別器。 但是,這次我們希望鑑別器分配非常接近 1 的機率,這表示生成器可以產生欺騙鑑別器認為它們實際上是真實圖像 (來自資料集) 的圖像。 為此,我們將 fake_labels
tensor 填充為全 1。 我們最終使生成器的最佳化器前進一步,以也更新其參數。
現在我們應該可以準備好在 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 裝置規格傳遞給我們自己配置的 tensors,並透過 C++ 前端中所有 tensors 和 modules 都有的 to()
方法,明確地將任何其他 tensors 複製到 GPU 上。實現這兩者最簡單的方法是在訓練腳本的頂層建立一個 torch::Device
的實例,然後將該裝置傳遞給 tensor 工廠函數 (例如 torch::zeros
) 以及 to()
方法。我們可以先從 CPU 裝置開始
// Place this somewhere at the top of your training script.
torch::Device device(torch::kCPU);
像這樣的新 tensor 分配
torch::Tensor fake_labels = torch::zeros(batch.data.size(0));
應該更新以將 device
作為最後一個參數
torch::Tensor fake_labels = torch::zeros(batch.data.size(0), device);
對於那些不是由我們建立的 tensors,像是來自 MNIST 資料集的 tensors,我們必須插入明確的 to()
呼叫。這意味著
torch::Tensor real_images = batch.data;
變成
torch::Tensor real_images = batch.data.to(device);
而且我們的模型參數也應該被移動到正確的裝置上
generator->to(device);
discriminator->to(device);
注意
如果 tensor 已經存在於提供給 to()
的裝置上,則該呼叫將不會執行任何操作。不會進行額外的複製。
目前,我們只是讓我們先前的 CPU 程式碼更明確。然而,現在也很容易將裝置更改為 CUDA 裝置
torch::Device device(torch::kCUDA)
現在所有的 tensors 都將存在於 GPU 上,為所有操作呼叫快速 CUDA 核心,而我們無需更改任何下游程式碼。如果我們想要指定特定的裝置索引,則可以將其作為第二個參數傳遞給 Device
建構函式。如果我們希望不同的 tensors 存在於不同的裝置上,我們可以傳遞單獨的裝置實例 (例如,一個在 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);
檢查點和恢復訓練狀態¶
我們應該對訓練腳本做的最後一個增強是定期儲存我們的模型參數的狀態、我們的 optimizers 的狀態以及一些產生的圖像樣本。如果我們的電腦在訓練過程中崩潰,前兩者將允許我們恢復訓練狀態。對於長時間的訓練過程,這絕對是必要的。幸運的是,C++ 前端提供了一個 API 來序列化和反序列化模型和 optimizer 的狀態,以及個別的 tensors。
此 API 的核心是 torch::save(thing,filename)
和 torch::load(thing,filename)
,其中 thing
可以是 torch::nn::Module
的子類或 optimizer 實例,例如我們在訓練腳本中使用的 Adam
物件。讓我們更新我們的訓練迴圈,以便以一定的間隔檢查點模型和 optimizer 的狀態
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
是一個計數器,每次我們建立檢查點時都會增加。
要恢復訓練狀態,您可以在建立所有模型和 optimizers 之後,但在訓練迴圈之前,新增類似這樣的行
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 腳本來載入 tensors 並使用 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 個 epochs
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,並在遇到困難時查閱我們的文檔,尤其是Library API部分。此外,請記住,您可以期望 C++ 前端在我們盡可能的情況下遵循 Python 前端的設計和語義,因此您可以利用這一點來提高您的學習速度。
提示
你可以在這個儲存庫中找到本教程中提供的完整原始碼。
一如既往,如果您遇到任何問題或有任何疑問,可以使用我們的論壇或GitHub issues與我們聯繫。