使用 PyTorch C++ 前端(上)
譯者:solerji
來源:官方文檔
轉載請告知
PyTorch C++ 前端 是PyTorch機器學習框架的一個純C++介面。PyTorch的主介面是Python,Python API位於一個基礎的C++代碼庫之上,提供了基本的數據結構和功能,例如張量和自動求導。C++前端暴露了一個純的C++11的API,在C++底層代碼庫之上擴展了機器學習訓練和推理所需的工具擴展。這包括用於神經網路建模的內置組件集合;擴展此集合的自定義模塊API;流行的優化演算法庫(如隨機梯度下降);使用API定義和載入數據集的並行數據載入程序;序列化例行程序等等。
本教程將為您介紹一個用C++ 前端對模型進行訓練的端到端示例。具體地說,我們將訓練一個 DCGAN——一種生成模型——來生成 MNIST數字的圖像。雖然看起來這是一個簡單的例子,但它足以讓你對 PyTorch C++ frontend有一個深刻的認識,並勾起你對訓練更複雜模型的興趣。我們將從設計它的原因開始,告訴你為什麼你應該使用C++前端,然後直接深入解釋和訓練我們的模型。
小貼士
可以在 this lightning talk from CppCon 2018 網站觀看有關C++前端的快速介紹。
小貼士
這份筆記提供了C++前端組件和設計理念的全面概述。
小貼士
在 https://pytorch.org/cppdocs你可以找到工作人員的API說明文檔,這些PyTorch C++ 生態系統的文檔是很有用的。
動機
在我們開始令人興奮的GANs和MNIST數字的旅程之前,讓我們往回看,討論一下為什麼我們一開始要使用C++前端而不是Python。我們(the PyTorch team)創建了C++前端,以便在不能使用Python的環境中或者是沒有適合該作業的工具的情況下進行研究。此類環境的示例包括:
- 低延遲系統:您可能希望在具有高幀/秒和低延遲的要求的純C++遊戲引擎中進行強化學習研究。由於Python解釋器的速度慢,Python可能根本無法被跟蹤,使用純C++庫這樣的環境比Python庫更合適。
- 高度多線程環境:由於全局解釋器鎖(GIL),一次不能運行多個系統線程。多道處理是另一種選擇,但它不具有可擴展性,並且有顯著的缺點。C++沒有這樣的約束,線程易於使用和創建。需要大量並行化的模型,像那些用於深度神經進化 Deep Neuroevolution的模型,可以從中受益。
- 現有的C++代碼庫:您可能是一個現有的C++應用程序的所有者,在後台伺服器上為Web頁面提供服務,以在照片編輯軟體中繪製3D圖形,並希望將機器學習方法集成到您的系統中。C++前端允許您保留在C++中,免除了在Python和C++之間來回綁定的麻煩,同時保留了傳統 PyTorch(Python)體驗的大部分靈活性和直觀性
C++前端不打算與Python前端競爭,它是為了補充Python前端。我們知道由於它簡單、靈活和直觀的API研究人員和工程師都喜歡PyTorch。我們的目標是確保您可以在每個可能的環境中利用這些核心設計原則,包括上面描述的那些。如果這些場景中的一個描述了你的用例,或者如果你只是感興趣的話,跟著我們在下面的文章中詳細探究C++前端。
小貼士
C++前端試圖提供儘可能接近Python前端的API。如果你對Python前端有經驗,並且想知道:「我如何用C++前端做這個東西?」你可以以Python的方式編寫代碼,在Python中,通常可以使用與C++相同的函數和方法(只要記住用雙冒號替換點)。
編寫基本應用程序
讓我們開始編寫一個小的C++應用程序,以驗證我們在安裝和構建環境上是一致的。首先,您需要獲取 LibTorch分發的副本——我們已經準備好了ZIP存檔,它封裝了使用C++前端所需的所有相關的頭文件、庫和 CMake 構建文件。Libtorch發行版可在Linux, MacOS 和 Windows的PyTorch website上下載。本教程的其餘部分將假設一個基本的Ubuntu Linux環境,您也可以在MacOS或Windows上繼續自由地學習。
小貼士
關於安裝PyTrac C++ 在 Installing C++ Distributions of PyTorch 的文檔更詳細地描述了以下步驟。
第一步是通過從PyTorch網站檢索到的鏈接在本地下載 LibTorch發行版。對於普通的Ubuntu Linux環境,這意味著運行:
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 11)
?
筆記
雖然CMake是LibTorch推薦的構建系統,但這並不是一個硬性要求。您還可以使用Visual Studio項目文件、Qmake、plain Makefiles或任何其他您覺得合適的構建環境。但是,我們不提供開箱即用的支持。
記下上述CMake文件中的第4行: find_package(Torch REQUIRED).。這將指示CMake查找LibTorch庫的構建配置。為了讓CMake知道在哪裡找到這些文件,我們必須在調用 cmake時設置 CMAKE_PREFIX_PATH 。在進行此操作之前,讓我們就 dcgan應用程序的以下目錄結構達成一致:
dcgan/
CMakeLists.txt
dcgan.cpp
此外,我將特別指出解壓LibTorch分發的路徑 /path/to/libtorch。請注意,這必須是絕對路徑。我們用編寫 $PWD/../../libtorch 的做法獲取相應的絕對路徑;如果將 CMAKE_PREFIX_PATH 設置為../../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# make -j
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 命令生成必要的build(Make)文件,最後通過運行 make -j.成功編譯了項目。現在,我們將項目設置為執行最小的二進位文件,基本項目配置這一部分就完成了:
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() or 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::admm(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++前端的 ownership model 。最終,我們可以像在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()參數方法,它像Python一樣返回 named_parameters():
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++更為「本土」。還需要注意的是,為了接近PythonAPI的人機工程學,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,具體的類也必須存儲。
等等!在上面的代碼中我沒有看到它提及共享資源!為什麼會這樣?因為std::shared_ptr<MyModule>是一個很難輸入的類型。為了保持研究人員的工作效率,我們提出了一個精心設計的方案來隱藏應該提及的共享資源——這是保留值語義的好處,它同時保留了引用語義。要了解這是如何工作的,我們可以查看核心庫中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>的封裝。它是一個封裝,而不是一個簡單的類型定義,因此,構造函數仍然可以按預期工作,即您仍然可以編寫 torch::nn::Linear(3, 4)而不需要寫 std::make_shared<LinearImpl>(3, 4)。我們將宏創建的類稱為模塊容器。與(共享)指針類似,您可以使用箭頭操作符(如 model->forward(...))訪問基礎對象。最終的結果是一個與PythonAPI非常相似的所有權模型。引用語義成為默認語義,但不需要額外輸入std::shared_ptr 或者 std::make_shared。對於我們的網路,使用模塊保持器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傳遞給容器的構造函數。
實際上,這意味著您可以使用前面所示的子模塊,其中模塊在初始值 initializer list中註冊和構造:
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;
};
?
或者,您可以先用一個空指針構造所有者,然後在構造函數中分配給它(對Pythonistas更熟悉):
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 aGAN?
GAN由兩個不同的神經網路模型組成:發生器和鑒別器。生成器接收來自雜訊分布的樣本,其目的是將每個雜訊樣本轉換為類似於目標分布的圖像——在我們的例子中是MNIST數據集。鑒別器反過來接收來自MNIST數據集的真實圖像或來自生成器的假圖像。它被要求發出一個概率來判斷一個特定圖像是真實的(接近 1))還是虛假的(接近 0))。從鑒別器上得到的生成器生成圖片的真實度的反饋被用來訓練生成器;鑒別器的辨識度的反饋已經被用來優化鑒別器。理論上,發生器和鑒別器之間的微妙平衡使它們串聯改進,導致發生器生成的圖像與目標分布不可區分,從而愚弄鑒別器的辨識,使真實和虛假圖像的概率均為 0.5 。對於我們來說,最終的結果是一台機器,它接收雜訊作為輸入,並生成數字的真實圖像作為輸出。
生成器模塊
我們首先定義生成器模塊,它由一系列轉置的二維卷積、批處理規範化和ReLU激活單元組成。與Python一樣,這裡的PyTorch為模型定義提供了兩個API:一個功能性的API,輸入通過連續的函數傳遞,另一個面向對象的API,我們在其中構建一個包含整個模型作為子模塊的 Sequential 模塊。讓我們看看我們的生成器如何使用這兩種API,您可以自己決定您喜歡哪一種。首先,使用 Sequential::
using namespace torch;
?
nn::Sequential generator(
// Layer 1
nn::Conv2d(nn::Conv2dOptions(kNoiseSize, 256, 4)
.with_bias(false)
.transposed(true)),
nn::BatchNorm(256),
nn::Functional(torch::relu),
// Layer 2
nn::Conv2d(nn::Conv2dOptions(256, 128, 3)
.stride(2)
.padding(1)
.with_bias(false)
.transposed(true)),
nn::BatchNorm(128),
nn::Functional(torch::relu),
// Layer 3
nn::Conv2d(nn::Conv2dOptions(128, 64, 4)
.stride(2)
.padding(1)
.with_bias(false)
.transposed(true)),
nn::BatchNorm(64),
nn::Functional(torch::relu),
// Layer 4
nn::Conv2d(nn::Conv2dOptions(64, 1, 4)
.stride(2)
.padding(1)
.with_bias(false)
.transposed(true)),
nn::Functional(torch::tanh));
?
小貼士
Sequential 模塊只執行函數組合。第一個子模塊的輸出成為第二個子模塊的輸入,第三個子模塊的輸出成為第四個子模塊的輸入,以此類推。
所選的特定模塊(如 nn::Conv2d 和nn::BatchNorm)遵循前面概述的結構。 kNoiseSize常量確定輸入雜訊矢量的大小,並設置為 100.。請注意,我們在激活函數中使用了torch::nn::Functional模塊,將內部層的torch::relu傳遞給它,最後激活的是 torch::tanh 。當然,超參數是通過梯度的下降發現的。
筆記
Python前端為每個激活功能都有一個模塊,比如 torch.nn.ReLU 或torch.nn.Tanh。在C++中,我們只提供 Functional 模塊,您可以通過 Functional的轉發forward()中調用的任何C++函數。
注意
對於第二種方法,我們在定義自己的模塊的forward()方法中顯式地在模塊之間傳遞輸入(以函數方式):
struct GeneratorImpl : nn::Module {
GeneratorImpl()
: conv1(nn::Conv2dOptions(kNoiseSize, 512, 4)
.with_bias(false)
.transposed(true)),
batch_norm1(512),
conv2(nn::Conv2dOptions(512, 256, 4)
.stride(2)
.padding(1)
.with_bias(false)
.transposed(true)),
batch_norm2(256),
conv3(nn::Conv2dOptions(256, 128, 4)
.stride(2)
.padding(1)
.with_bias(false)
.transposed(true)),
batch_norm3(128),
conv4(nn::Conv2dOptions(128, 64, 4)
.stride(2)
.padding(1)
.with_bias(false)
.transposed(true)),
batch_norm4(64),
conv5(nn::Conv2dOptions(64, 1, 4)
.stride(2)
.padding(1)
.with_bias(false)
.transposed(true)) {}
?
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::relu(batch_norm4(conv4(x)));
x = torch::tanh(conv5(x));
return x;
}
?
nn::Conv2d conv1, conv2, conv3, conv4, conv5;
nn::BatchNorm batch_norm1, batch_norm2, batch_norm3, batch_norm4;
};
TORCH_MODULE(Generator);
?
Generator generator;
?
無論使用哪種方法,我們現在都可以在生成器上調用 forward() 來將Generator雜訊樣本映射到圖像。
筆
一個簡短的關於路徑選擇的選項被傳遞到C++模塊中的像 Conv2d 這樣的內置模塊:每個模塊都有一些必需的選項,比如 BatchNorm.的特徵數。如果只需要配置所需的選項,則可以將它們直接傳遞給模塊的構造函數,如BatchNorm(128) 或 Dropout(0.5) 或 Conv2d(8, 4, 2) (用於輸入通道計數、輸出通道計數和內核大小)。但是,如果您需要修改其他選項(通常是默認的),例如使用 Conv2d的 with_bias ,則需要構造並傳遞一個選項對象。C++前端中的每個模塊都有一個相關的選項結構,稱為模塊選項,其中 Module 是ModuleOptions ,比如 Linear 的LinearOptions 。這是我們為上面的 Conv2d 模塊所做的。
鑒別器模塊
鑒別器類似於一系列卷積、批量規範化和激活。然而,現在卷積是常規的而不是轉置的,我們使用一個alpha值為0.2的leaky ReLU而不是vanilla ReLU。而且,最終的激活變成了一個Sigmoid,它將值壓縮到0到1之間的範圍。然後我們可以將這些壓縮值解釋為鑒別器分配給圖像真實的概率:
nn::Sequential discriminator(
// Layer 1
nn::Conv2d(
nn::Conv2dOptions(1, 64, 4).stride(2).padding(1).with_bias(false)),
nn::Functional(torch::leaky_relu, 0.2),
// Layer 2
nn::Conv2d(
nn::Conv2dOptions(64, 128, 4).stride(2).padding(1).with_bias(false)),
nn::BatchNorm(128),
nn::Functional(torch::leaky_relu, 0.2),
// Layer 3
nn::Conv2d(
nn::Conv2dOptions(128, 256, 4).stride(2).padding(1).with_bias(false)),
nn::BatchNorm(256),
nn::Functional(torch::leaky_relu, 0.2),
// Layer 4
nn::Conv2d(
nn::Conv2dOptions(256, 1, 3).stride(1).padding(0).with_bias(false)),
nn::Functional(torch::sigmoid));
?
筆記
當我們傳遞給 Functional 函數接受的參數多於一個tensor時,我們可以將它們傳遞給 Functional 構造函數,後者將把它們轉發給每個函數調用。對於上面的leaky ReLU,這意味著調用了torch::leaky_relu(previous_output_tensor, 0.2) 。
LLs Blog
推薦閱讀:
TAG:PyTorch | 生成對抗網路(GAN) |
