英語を読める人は素直に公式のドキュメントおよびチュートリアルを読むべき。翻訳ソフトを使ってでもこれらを読んだ方が良いと思う。
以下の作業は少なくともcuda10.0,cudnn7という環境では動くと思われる。(Dockerでnvidia/cuda:10.0-cudnn7-devel-ubuntu18.04イメージから新しいコンテナを作ってそこで作業した)
簡単なサンプルのビルドまで
まずダウンロード。以下のページ
から適切にバージョンを指定して落としてくる。
以前はABIのバージョンがどうこうという問題があって自前でビルドしなければならない場合もあったが(※参考)、今見ると二つ用意されているようなので自分の環境に合わせた方をダウンロードすれば良いと思われる。
解凍するとlibtorchというディレクトリが得られる。LibTorchを使うときは基本的にcmakeでこのlibtorchディレクトリを指定してコンパイルすることになる。cmakeはCLionでもVisual Studioでも使えるのでそこまで使い勝手は悪くないと思う。
このページで示されているexample-app
というものをコンパイルしてみることとする。example-app
というディレクトリを作って、そこにexample-app.cpp
とCMakeLists.txt
という2つのファイルを用意する。
example-app/ CMakeLists.txt example-app.cpp
example-app.cpp
は例そのままのtensor
を作って表示するというものになる。
#include <torch/torch.h> #include <iostream> int main() { torch::Tensor tensor = torch::rand({2, 3}); std::cout << tensor << std::endl; }
CMakeLists.txt
の方も例とほぼ同じなのだが、一点だけ使いやすいように修正する。というのも、もとの例ではビルドするタイミングでlibtorchのパスを指定しているのだが、いちいちコマンドライン引数にパスを付け加えるのは嫌だし、IDE側から見つけるためにもlibtorchのパスをCMakeLists.txt
に書いてしまう。find_package(Torch REQUIRED)
の前の行にlist(APPEND CMAKE_PREFIX_PATH ~/libtorch)
としてcmakeがパッケージを探すパスを付け加えることができる。今はホーム直下にlibtorchを置いたのでこうしているが、各自ダウンロードしたところへ適当にパスを合わせて書けば良い。
cmake_minimum_required(VERSION 3.0 FATAL_ERROR) project(example-app) list(APPEND CMAKE_PREFIX_PATH ~/libtorch) find_package(Torch REQUIRED) add_executable(example-app example-app.cpp) target_link_libraries(example-app "${TORCH_LIBRARIES}") set_property(TARGET example-app PROPERTY CXX_STANDARD 11)
cmakeではソースコードを置いているディレクトリを汚さないようにbuildというビルド専用のディレクトリをその階層に作るのが普通らしいのでそのようにする。
mkdir build cd build cmake .. make
これでexample-app
を実行すると
0.8291 0.0013 0.0553 0.3590 0.1988 0.3392 [ Variable[CPUFloatType]{2,3} ]
と表示された。
ニューラルネットを書く
ニューラルネットなどの例は公式GitHubののexamplesを見るのが良いと思う。mnistを分類するような簡単なCNNだとmnist.cppにあり、学習の仕方も含めてだいたいはこれで理解できるはず。というわけでこの例で足りない部分についていくらか記述する。といっても正直なところ自分もよくわかっていないところが多いのでむしろ指摘をもらいたい……。
自作モジュールを作る
ニューラルネットの部分的モジュールなどを書く(たとえばMyModuleというモジュールを作るとする)場合はtorch::nn::Module
を継承してMyModuleImpl
というクラスを定義し、最後にTORCH_MODULE(MyModule)
というマクロを呼び出すという手順をする(チュートリアルの中程にある)。これによってMyModule
という名前のtorch::nn:ModuleHolder
が定義される。Linerについての例だと以下のような感じ。
struct LinearImpl : torch::nn::Module { LinearImpl(int64_t in, int64_t out); Tensor forward(const Tensor& input); Tensor weight, bias; }; TORCH_MODULE(Linear);
なぜこんな面倒なことをするかというと、torch::nn::Module
は大きいクラスになるので、たとえば関数の引数に与えるときに値渡しをすると非効率的になる。C++なのだから参照渡しをすることも容易なんだけど、Python的と同じ書き方で同じ効果を持った方がわかりやすい。Pythonの関数は参照の値渡しなので、C++でそれを実現するにはtorch::nn::Module
を直接渡すのではなくそれへのstd::shared_ptr
であるtorch::nn:ModuleHolder
を渡すようにするのが自然。英語があまり読めなかったので適当なことを言っているかもしれない。
モデルをセーブ・ロードする
torch::nn:ModuleHolder
であるnn
という変数があったとき、
torch::save(nn, "path_to_model_file"); torch::load(nn, "path_to_model_file");
とする。先のチュートリアル中に
the serialization API (torch::save and torch::load) only supports module holders (or plain shared_ptr).
とあるので、試したことはないがtorch::nn::Module
はセーブできないのだと思われる。サンプルのmnist.cppとかはtorch::nn:ModuleHolder
を使わず書いているのでセーブできなさそうなのだが、そんなものをexampleとして出すのはどうなんだという気もする……。
計算結果を取り出す
そもそもモデルを定義した後は一度model.to(device);
(mnist.cppの129行目)のようにしてGPUにモデルを転送するという作業が必要になる。入力、教師データも同様にauto data = batch.data.to(device), targets = batch.target.to(device);
としてGPUに飛ばしてから計算を走らせる。結果を取り出す際には計算結果を示すtorch::Tensor
を一度CPUに戻してくる。その後torch::Tensor
のメソッドdata<T>()
を使って、型T
に対するポインタとして先頭のポインタを得る。自分のコードでは、たとえば1バッチについてDATA_DIM
次元のベクトルを計算するデータを取り出す場合は以下のような感じにしている。
//計算結果を格納するstd::vector std::vector<std::vector<float>> results(batch_size); //計算してCPUに持ってくる torch::Tensor nn_result = model->forward(input).cpu(); //データの先頭へのポインタを取得してstd::vectorにバッチごとに突っ込む float* p = nn_result.data<float>(); for (uint64_t i = 0; i < batch_size; i++) { results[i].assign(p + i * DATA_DIM, p + (i + 1) * DATA_DIM); }
LibTorchどうこうという話ではなくC++としてデータをstd::vector
へと変換するところで非効率的なことをやっている気がする。C++よくわからん。
その他
一応拙作のMiacisという将棋ソフトでもAlphaZero的なResidual Blockを持つCNNを実装しているため、参考になれば。
ニューラルネット部分はだいたいsrc/neural_network.cppにある。