Libtorch c++ 基本概念

上一節在VS 2019上配置了Libtorch c++,並進行了測試。有了基本的環境設置,可以進入更有序的學習。

首先,討論怎麼利用面向c++的接口定義模塊(module)並與之交互。從最基本、最小規模的模塊開始,然後利用面向c++接口內置的模塊搭建完整的對抗生成網絡模型。

1、libtorch的主要模塊的分類

Component

Description
torch::Tensor 可以自動微分,支持CPU和GPU的張量計算庫
torch::nn 用於神經網絡建模的一系列可組合模塊A collection of composable modules for neural network modeling
torch::optim Optimization algorithms like SGD, Adam or RMSprop to train your models
torch::data Datasets, data pipelines and multi-threaded, asynchronous data loader
torch::serialize A serialization API for storing and loading model checkpoints
torch::python Glue to bind your C++ models into Python
torch::jit Pure C++ access to the TorchScript JIT compiler

下面這個鏈接介紹了全部的Libtorch 類及其組織結構

https://pytorch.org/cppdocs/api/library_root.html

2、模塊接口的基礎

與python的接口一致,面向c++的Libtorch神經網絡接口也是由可重複使用的可組合模塊組成,稱之爲模塊modules。有一個模塊module的基類,所有其他模塊都繼承於它。在python中,這個基類是torch.nn.module,在c++接口下,這個基類是torch::nn::Module。另外,forward()方法執行模塊包含的算法,一個模塊通常有三個部分組成,分別是參數parameter,緩衝區buffers(相當於數據)和子模塊(submodules)。

參數parameter和緩衝區buffers以張量的形式存儲狀態,參數記錄梯度,但是緩衝區不保存。參數通常是神經網絡可訓練的權重,比如緩衝區包含了批量表轉化的均值和方差。爲了可以重複利用特定的邏輯塊和狀態,Libtorch的接口允許模塊可以相互嵌套。被嵌套的模塊成爲子模塊submodules。

由於python由很多反射功能,可以通過示例獲取關於類型的很多參數,而c++語言本身沒有這些功能,這增加了Libtorch的複雜性。一個顯著的特點是,參數parameter、緩衝區buffer和子模塊submodule必須顯式地註冊。註冊之後,纔可以通過類似parameters()和buffers()的方法獲取整個模塊及其子模塊的參數和緩衝區數據。類似地,像to(...)的方法,比如to(torch::kCUDA)可以把整個模塊和子模塊的所有參數和緩衝區從cpu轉移到CUDA內存。

3、定義模塊並註冊參數

註冊參數的方法是register_parameter(),比如下面的代碼通過註冊參數形成了線性神經網絡模塊/單元。

#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::nn::Module。在構造函數內,用隨機數函數torch::randn,與python中的torch.randn一樣,定義矩陣。一個有趣的不同是如何註冊參數。在python中,直接把矩陣用torch.nn.Parameter包括即可,但是在c++中,必須通過register_parameter方法,註冊爲參數。這麼做的原因是Python的接口能探測torch.nn.Parameter的屬性,並自動給矩陣註冊。但是,c++中,反射非常有限,所以提供了更傳統的方法。

4、註冊子模塊並遍歷模塊層次結構

按照類似的方法可以註冊模塊,方法爲register_module。下面的定義的網格Net中註冊的線性模塊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;
};

Libtorch提供了很多內置的神經網絡模塊,除了這裏用到的torch::nn::Linear,還有torch::nn::Dropout, torch::nn::Conv2d等,全部的內置神經網絡模塊可以在這個鏈接看到,這些內置的神經網絡模塊多達122種,通過這些模塊基本可以組合出大部分自己想要的複雜神經網絡模型。實際上,我感覺,深度學習主要任務由四部分(1)樣本準備/採集;(2)模型搭建/組合;(3)訓練調試;(4)部署應用。其中第二部分,神經網絡搭建,基本是最關鍵的部分(需要針對具體問題,搭建合適的模型),就是這些模塊的組合搭配。

https://pytorch.org/cppdocs/api/namespace_torch__nn.html

上述代碼比較微妙的地方在於爲什麼的模塊的創建需要構造函數參數列表,而參數在構造函數內創建。這麼做的主要原因,我們將在下面c++端所有權模型中觸及到。但是,最總結果是,我們可以像python那樣,遞歸地獲取所有模塊樹的參數。通過parameters()方法可以返回矩陣std::vectortorch::Tensor,它可以通過迭代方式訪問。

int main() {
  Net net(4, 5);
  for (const auto& p : net.parameters()) {
    std::cout << p << std::endl;
  }
}

運行結果

類似於python 中的三參數形式,c++接口也可以查看帶名字的參數named_parameters(),它的返回結果是OrderedDict,這個類型在python的接口中同樣存在。

int main()
{
    Net net(4, 5);
    for (const auto& pair : net.named_parameters()) {
        std::cout << pair.key() << ":\n" << pair.value() << std::endl;
    }

    return EXIT_SUCCESS;
}

5、模塊所有權模型

此時,我們已經知道如何用c++接口定義模塊,註冊參數,註冊模塊,通過類似parameters()的方法便利模塊體系,最後也能運行模塊的正演forward()方法。但是,在c++接口中,還有其他很多方法、類型和主題需要學習。其中很重要的一個概念是模塊所有權模型(ownership model),它涉及到torch::nn::Module的所有子類型。

可以這麼講,所有權模型表示模塊的存儲和傳遞方式,它決定了誰或什麼擁有特定的模塊實例。在python中,對象動態地在堆上創建,可能有多個引用語義,這使得python的對象使用起來很簡單直接。實際上,在python中,基本可以忘記對象在哪,如何引用,只關注如何完成要做的事情。

但是,c++作爲低級別的語言,提供了更多的可控選項。這增加了複雜度,嚴重影響了c++端口的設計和人工工程學改造。具體來說,對c++接口的模塊,我們有的選項是利用值語義和引用語義,前者最簡單,對象創建在棧上,當傳遞給函數時可以拷貝、移動完成,或通過引用或值傳遞。如下所示

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,它減少了模塊在函數傳遞過程中的過度考慮預計如何聲明。

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方式保存子模塊,而且具體類也需要這麼做。

但是,等等。上述代碼中並沒有看到共享指針share_ptr,爲什麼?因爲,std::shared_ptr<MyModule>相關的代碼太多了,這裏爲了給研究者保持高產出,和簡便性,我們想出了一個精心策劃的策略來隱藏對shared-ptr的提及,這通常是爲值語義保留的一個好處,同時保留了引用語義。實際上是做了很多封裝的工作,在文檔中可以具體看到。

總之,你會使用哪種所有權模型、哪種語義?面向C++的接口很好地支持模塊保持着提供的所有權模型。這種機制僅有的缺點是,在模塊聲明下面需要額外的一行引用(boilerplate)。也就是說,最簡單的模型仍是值語義模型。但是,遲早你會發現,由於技術原因,值語義模型不能適應全部場合。必須,序列化的接口,(torch::save和torch::load)僅支持模塊保持着(module holder),更直白地說就是共享指針(share_ptr)。這樣,模塊保持着的接口是推薦使用的c++接口的模塊定義方法,後續教程中會使用這種接口。

https://pytorch.org/tutorials/advanced/cpp_frontend.html

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章